Faster ADC sampling challenges (SPI, MCP3008)

I am attempting to sample analog values from a 5-channel microphone array at ideally ~40kHz per channel (200kHz total), then send that data off to a connected server, not unlike @rickkas7 excellent example 1! (Photon Audio streaming) To achieve those speeds I am using an MCP3008 which has a rated sample rate of 200ksps, but I can’t achieve those speeds listed on the ADC, nor with the onboard ADC.

ADC timing needs to be precise to within ~+/- 500Hz, so I am triggering off of an Interval Timer every 40kHz (although faster in the below code for debug purposes). The readings should also be within +/- 2% accuracy, which means I can’t overclock the SPI bus from 3.85MHz to 7.7MHz)

To communicate over SPI a bit faster I am using pinResetFast/pinSetFast to drive the SPI SS pin. Also to improve SPI performance I cut down one data frame by sampling on 9 bits of data instead of 10 - the MCP3008 requires 17bits (and thus 3x 8byte frames) for communication, but by cutting down one bit if fits nicely within 2 frames.

My two challenges are

  1. SPI performance
    I can only achieve about 150ksps with the MCP3008 vs it’s rated 200ksps, using SPI.transfer(). This can be seen by oscoping the 4 SPI pins (CLK, SS, MISO, MOSI in the below image) and seeing the timing from when I pull the SS pin LOW to start the ADC conversion, to pulling it HIGH to end it, being ~6.7uS = ~150kHz. This is even with the above optimizations.

When sampling all five channels I get the expected result of ~32.8uS (~30kHz per channel or ~152kHz total)

  1. IntervalTimer overhead
    While the ADC takes about 33uS to sample 5 channels, there is added overhead from waiting until the next IntervalTimer triggers.

With a single channel this is about 5uS delay

And when sampling 5 channels this is about an 11uS delay, bringing the actual sample rate to about 43.9uS or 22.7kHz per channel and total sampling speed of ~114kHz.

I understand there has to be a delay between triggers of the interrupt, to give other things a chance to run.

You’ll notice in the below code I also attempted to hookup a second ADC (an MCP3002) to capture samples in parallel, but made the mistake of thinking I could have two IntervalTimers triggering and not blocking each other. Additionally, I do not believe I can use the asynchronous DMA SPI.transfer(rx, tx, length, callback) because this function cannot run within an interrupt, and thus has to run in the main loop with nondeterministic timing. And lastly, I don’t understand enough of @rickkas7 DMA wizardy in his 3rd photonAudio example to hook SPI up to timers and DMA to run on its own.

Code

#include "SparkIntervalTimer.h"

#define CS_MCP3008 A2
#define CS_MCP3002 D5

byte mcp_3002_channel_0 = 0b11000000;
byte mcp_3002_channel_1 = 0b11100000;

byte mcp_3008_channel_0 = 0b11000000;
byte mcp_3008_channel_1 = 0b11001000;
byte mcp_3008_channel_2 = 0b11010000;
byte mcp_3008_channel_3 = 0b11011000;
byte mcp_3008_channel_4 = 0b11100000;
byte mcp_3008_channel_5 = 0b11101000;
byte mcp_3008_channel_6 = 0b11111000;
byte mcp_3008_channel_7 = 0b11111000;

volatile byte dataMSB =    0;
volatile byte dataLSB =    0;
volatile byte dataMSB2 =    0;
volatile byte dataLSB2 =    0;
volatile byte JUNK    = 0x00;
volatile int value1 = 0;
volatile int value2 = 0;
volatile int value3 = 0;
volatile int value4 = 0;
volatile int value5 = 0;

// The audio sample rate
const long SAMPLE_RATE = 95000;

IntervalTimer myTimer;
IntervalTimer myTimer2;

void capture_all_mics(void){
  adc_single_channel_read_mcp3008(mcp_3008_channel_0, mcp_3008_channel_1, mcp_3008_channel_2, mcp_3008_channel_3, mcp_3008_channel_4);
}

void capture_all_mics_mcp3002(void){
  adc_single_channel_read_mcp3002(mcp_3002_channel_0, mcp_3002_channel_1);
}

int adc_single_channel_read_mcp3002(byte adcConfig1, byte adcConfig2){
  // This configuration matches the datasheet, the MCP3002 is expecting 3 bits to tell it what to do
  // bit1 = start bit, setting this high with CS low starts operation
  // bit2 = single ended or pseudo differential, 1 = single ended, 0 = differential
  // bit3 = which channel to sample from, 1 = channel 1, 0 = channel 0
  // bit4 = wait clock cycle from ADC
  // bit5 = null bit from ADC
  // bit6, 7, 8, 9, 10, 11, 12, 13, 14, 15 = data bits

  pinResetFast(CS_MCP3002);
  dataMSB2 = SPI1.transfer(adcConfig1) & 0x07;
  dataLSB2 = SPI1.transfer(JUNK) & 0xFE;

  pinSetFast(CS_MCP3002);
  value4 = (dataMSB2 << 8 | dataLSB2) >> 2;

  pinResetFast(CS_MCP3002);
  dataMSB2 = SPI1.transfer(adcConfig2) & 0x07;
  dataLSB2 = SPI1.transfer(JUNK) & 0xFE;

  pinSetFast(CS_MCP3002);
  value5 = (dataMSB2 << 8 | dataLSB2) >> 2;
}

void adc_single_channel_read_mcp3008(byte adcConfig1, byte adcConfig2, byte adcConfig3, byte adcConfig4, byte adcConfig5){
  // This configuration matches the datasheet, the MCP3002 is expecting 3 bits to tell it what to do
  // bit1 = start bit, setting this high with CS low starts operation
  // bit2 = single ended or pseudo differential, 1 = single ended, 0 = differential
  // bit3, 4, 5 = which channel to sample from, 000 = channel 0, 001 = channel 1, 010 = channel 2,...
  // bit6 = wait clock cycle from ADC
  // bit7 = null bit from ADC
  // bit8, 9, 10, 11, 12, 13, 14, 15, 16, 17 = data bits

  noInterrupts();
  pinResetFast(CS_MCP3008);
  dataMSB = SPI.transfer(adcConfig1) & 0x01;
  dataLSB = SPI.transfer(JUNK);
  pinSetFast(CS_MCP3008);
  value1 = dataMSB << 8 | dataLSB;

  pinResetFast(CS_MCP3008);
  dataMSB = SPI.transfer(adcConfig2) & 0x01;
  dataLSB = SPI.transfer(JUNK);
  pinSetFast(CS_MCP3008);
  value2 = dataMSB << 8 | dataLSB;

  pinResetFast(CS_MCP3008);
  dataMSB = SPI.transfer(adcConfig3) & 0x01;
  dataLSB = SPI.transfer(JUNK);
  pinSetFast(CS_MCP3008);
  value3 = dataMSB << 8 | dataLSB;

  pinResetFast(CS_MCP3008);
  dataMSB = SPI.transfer(adcConfig4) & 0x01;
  dataLSB = SPI.transfer(JUNK);
  pinSetFast(CS_MCP3008);
  value4 = dataMSB << 8 | dataLSB;

  pinResetFast(CS_MCP3008);
  dataMSB = SPI.transfer(adcConfig5) & 0x01;
  dataLSB = SPI.transfer(JUNK);
  pinSetFast(CS_MCP3008);
  value5 = dataMSB << 8 | dataLSB;

  interrupts();
}

SYSTEM_THREAD(ENABLED);

void setup()
{
  //Serial.begin(115200);
  delay(200);

  // 1.2MHz for 3.3V input, which provides 75ksps total muxed across all pins
  SPI.setClockSpeed(4, MHZ);
  SPI.setBitOrder(MSBFIRST);
  SPI.setDataMode(SPI_MODE0);
  SPI.begin();

  SPI1.setClockSpeed(4, MHZ);
  SPI1.setBitOrder(MSBFIRST);
  SPI1.setDataMode(SPI_MODE0);
  SPI1.begin();

  digitalWrite(CS_MCP3008, LOW);
  digitalWrite(CS_MCP3008, HIGH);
  digitalWrite(CS_MCP3002, LOW);
  digitalWrite(CS_MCP3002, HIGH);

  myTimer.begin(capture_all_mics, 1000000 / SAMPLE_RATE, uSec, TIMER7);
  //myTimer2.begin(capture_all_mics_mcp3002, 1000000 / SAMPLE_RATE, uSec, TIMER5);
}

void loop()
{
  
}

Thanks for your time! Hopefully this gets others closer to sampling very fast on a Photon.

I’m a huge proponent of Particle (I own many Cores, Photons, and Electrons) and love what you guys do. Keep on rocking

-Garrett

I can't vouch for it to work, but I'd say it's worth a try :wink:
AFAICT all this esentially does is storing the three pointers and set up the execution "loop" and leave again (providing you do pass in a valid callback function). The callback will then be called from system.

1 Like

Thanks for the prompt reply @ScruffR! :smiley:

From reading other forum posts I was expecting a bit of delay on the DMA setup and teardown, but with that comes the ability to have the SPI transfer run outside of my main loop (yeah!).

I was able to get this method to work, albeit at a slower pace than SPI.transfer().

Here is a single 2-frame SPI.transfer - it has a conversion time of 14.16us (about 8uS longer than transferring the bytes individually), equaling ~71kHz total sample rate
SPI.transfer(tx0, rx0, 2, callbackFunctionPrint);

And here is the time until the next intervalTimer is triggered. Accounting for the intervalTimer overhead, this is about 18uS per channel, or ~55kHz total sample rate.

This scaled linearly as I added more channels, so 55kHz / 5 = ~11kHz sample rate.

From what I can tell, this delay is caused by the need to teardown the DMA/Timer, pull SS up (to stop the conversion), then pull SS down (to start a new conversion), then setup the DMA/Timer again for the transfer. This could be sped up if I could leave the DMA + Timer setup and running and also trigger SS as part of some new function - say something like:

SPI.transfer(tx, rx, ss, 2, callbackFunctionPrint);
where ss is a byte array defining when to pull the SS pin up or down.

Interestingly, the MISO data coming back is occasionally inverted, where it sometimes provides the correct values (0-512) and sometimes the inverse (65042 - 65556).

Attaching code…

#include "SparkIntervalTimer.h"

#define CS_MCP3008 A2

byte mcp_3008_channel_0 = 0b11000000;
byte mcp_3008_channel_1 = 0b11001000;
byte mcp_3008_channel_2 = 0b11010000;
byte mcp_3008_channel_3 = 0b11011000;
byte mcp_3008_channel_4 = 0b11100000;
byte mcp_3008_channel_5 = 0b11101000;
byte mcp_3008_channel_6 = 0b11111000;
byte mcp_3008_channel_7 = 0b11111000;

volatile byte dataMSB =    0;
volatile byte dataLSB =    0;
volatile byte dataMSB2 =    0;
volatile byte dataLSB2 =    0;
volatile byte JUNK    = 0x00;
volatile int value1 = 0;
volatile int value2 = 0;
volatile int value3 = 0;
volatile int value4 = 0;
volatile int value5 = 0;

// The audio sample rate
const long SAMPLE_RATE = 95000;

volatile int readFromChannel = 0;

uint8_t rx0[2];
uint8_t rx1[2];
uint8_t rx2[2];
uint8_t rx3[2];
uint8_t rx4[2];

uint8_t tx0[2] = {0b11000000, 0b00000000};
uint8_t tx1[2] = {0b11001000, 0b00000000};
uint8_t tx2[2] = {0b11010000, 0b00000000};
uint8_t tx3[2] = {0b11011000, 0b00000000};
uint8_t tx4[2] = {0b11100000, 0b00000000};

IntervalTimer myTimer;

void capture_all_mics(void){
  adc_single_channel_read_mcp3008(mcp_3008_channel_0, mcp_3008_channel_1, mcp_3008_channel_2, mcp_3008_channel_3, mcp_3008_channel_4);
}

void callbackFunctionPrint(void){
  if(readFromChannel==0){
    value1 = rx0[0] << 8 | rx0[1];
    pinSetFast(CS_MCP3008);
    readFromChannel=1;
  }
  else if(readFromChannel==1){
    value2 = rx1[0] << 8 | rx1[1];
    pinSetFast(CS_MCP3008);
    readFromChannel=0;
  }
  myTimer.interrupt_SIT(INT_ENABLE);
}

void adc_single_channel_read_mcp3008(byte adcConfig1, byte adcConfig2, byte adcConfig3, byte adcConfig4, byte adcConfig5){
  // This configuration matches the datasheet, the MCP3002 is expecting 3 bits to tell it what to do
  // bit1 = start bit, setting this high with CS low starts operation
  // bit2 = single ended or pseudo differential, 1 = single ended, 0 = differential
  // bit3, 4, 5 = which channel to sample from, 000 = channel 0, 001 = channel 1, 010 = channel 2,...
  // bit6 = wait clock cycle from ADC
  // bit7 = null bit from ADC
  // bit8, 9, 10, 11, 12, 13, 14, 15, 16, 17 = data bits
  
  if(readFromChannel==0){
    myTimer.interrupt_SIT(INT_DISABLE);
    pinResetFast(CS_MCP3008);
    SPI.transfer(tx0, rx0, 2, callbackFunctionPrint);
  }

  if(readFromChannel==1){
    myTimer.interrupt_SIT(INT_DISABLE);
    pinResetFast(CS_MCP3008);
    SPI.transfer(tx1, rx1, 2, callbackFunctionPrint);
  }

}

SYSTEM_THREAD(ENABLED);

void setup()
{
  //Serial.begin(115200);
  delay(200);

  // 1.2MHz for 3.3V input, which provides 75ksps total muxed across all pins
  SPI.setClockSpeed(4, MHZ);
  SPI.setBitOrder(MSBFIRST);
  SPI.setDataMode(SPI_MODE0);
  SPI.begin();

  SPI1.setClockSpeed(4, MHZ);
  SPI1.setBitOrder(MSBFIRST);
  SPI1.setDataMode(SPI_MODE0);
  SPI1.begin();

  digitalWrite(CS_MCP3008, LOW);
  digitalWrite(CS_MCP3008, HIGH);

  myTimer.begin(capture_all_mics, 1000000 / SAMPLE_RATE, uSec, TIMER7);
}

void loop()
{
  
}

Any other ideas on how to speed up either transfer?

Thanks again!
-Garrett

@Garrett, the SPI DMA version of SPI.transfer() is optimized for larger transfer than 2 bytes where the overhead of the setup becomes negligible. Part of your timing problem is that you have to setup each channel via SPI then sample the data from that channel. At 200KHz raw speed, you are interrupting the STM32 every 5us which is pushing the limits (if not exceeding based on my experience) of the interrupt response time. The 150KHz limit you are seeing may be due to this factor! The latest 0.8.0-rc.3 firmware has an new way of skipping the HAL for attaching interrupts, reducing latency below 2us. I will be updating SparkIntervalTimer to support this feature soon.

You might also get a bit more speed by accessing the SPI registers directly and avoiding the SPI.transfer() call overhead.

You may also want to consider a different ADC which auto-increments the sampling channel for example. I am not sure what ADC hardware is available to make this possible.

2 Likes

Thanks so much @peekay123! That definitely makes sense about transferring more bytes to mitigate the overhead costs, if I didn’t have to pull SS up/down to start conversions on a different channel this would be perfect! (or maybe I can look at an external MUX going into the ADC / auto-incrementing ADC)

Awesome! I can’t wait to check out the updated SparkIntervalTimer :smiley:

Do you have any examples on accessing the SPI registers directly to do a simple transfer?

Thanks again for your time, you guys rock.

I'd say go to the firmware repo and dig around for the SPI code. :wink:

1 Like

I added photonAudio sample 4. I should stress that this is a proof-of-concept only and only sort of works.

The main thing is that the code uses two ADCs (ADC1 and ADC2). They’re run in dual-simultaneous mode, so both are sampled at the same time.

They’re also run in scan mode so multiple pins are sample sequentially, and very rapidly, 5 cycles of the ADC clock (30 MHz).

ADC1 is set to read A0, A2, and A4. ADC2 is set to read A1, A3, and A5.

44100 times per second, controlled by a hardware timer, the ADCs are set off do do their thing, and store the data in a DMA buffer, so the inter-frame spacing is rock-solid.

The DMA buffer is 1020 bytes, because something around 1024 bytes is ideal with 0.6.x and TCP, but the buffer size should be a multiple of 12 (the number of channels * 2 bytes per channel).

There are two DMA buffers, and the interrupt flag is set when the buffer is half-full so it can be sent out from loop while the other half is being filled.

From the main loop we poll the IRQ flag to see if there’s a buffer ready to be transmitted. The good part is that this is not particularly timing-sensitive.

Because all of the sampling is done via hardware, there’s no interrupt latency to worry about.

Still, this is right at the edge of what the Photon can do, so I don’t really recommend trying to send 517 Kbytes/sec. off the Photon, but it sort of works.

5 Likes

This solution is phenomenal @rickkas7! :smiley: If there was a gold medaled Like I would give it to you.

I’m digesting the code now, but this absolutely looks like it will more than solve the problem. I’ll either downgrade to 0.6.x or look to reduce the bitdepth to 8bit (258kB/s) before sending out to the remote server.

Hopefully this helps others out doing very fast sampling on the Photon :+1:

Thanks again!

1 Like

Thanks again for your help @rickkas7

I may have found an issue with the ADC conversion on DMA2, with a half-workaround.

While running the above code I noticed sometimes ~10% of the time the DMA buffer would fail to get full, and data would stop being sent for that 30s sample. While digging into the DMA buffer (which indeed did stop getting filled), and the Timer (which was working properly), it seemed the ADC was overrunning.

My main goal is to reduce the bitrate to half (~250 kB/s) and aimed to do so by reducing the bitdepth from 12 > 8bits, by changing
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
ADC_InitStructure.ADC_Resolution = ADC_Resolution_8b;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;

this exacerbated the ADC overrun issue such that it happens very frequently.

This issue may have been hit awhile ago on DMA2 from this forum post

I mention a half-workaround, as resetting the overrun flag then causes all 3 channels on that ADC to be sampled in a different order (I think). I read on some other posts that you may need to clear the overrun flag, restart the ADC conversion, and restart the DMA copy to fix everything (which seems like it would cause delays / be drastic).

And lastly, even though the data is 8bits, the measured datarate on the node.js side is still around ~500kB/s

Any ideas?

#include "Particle.h"

// particle compile photon audio4.cpp --target 0.6.3 --saveTo firmware.bin


//
// ADCDMA - Class to use Photon ADC in DMA Mode
//
#include "adc_hal.h"
#include "gpio_hal.h"
#include "pinmap_hal.h"
#include "pinmap_impl.h"


void buttonHandler(system_event_t event, int data); // forward declaration

// The buffer should be around 1024 bytes. However, it also should be a multiple of
// 2 * number of channels = 12, so 1020 is a good choice. The total memory usage is 2x this because
// of double buffers.
//const size_t SAMPLE_BUF_SIZE = 1020;
const size_t SAMPLE_BUF_SIZE = 1008;

// The audio sample rate. The minimum is probably 8000 for minimally acceptable audio quality.
// Not sure what the maximum rate is, but it's pretty high.
//const long SAMPLE_RATE = 10000;
const long SAMPLE_RATE = 44100;

// If you don't hit the setup button to stop recording, this is how long to go before turning it
// off automatically. The limit really is only the disk space available to receive the file.
const unsigned long MAX_RECORDING_LENGTH_MS = 30000;

// This is the IP Address and port that the audioServer.js node server is running on.
IPAddress serverAddr = IPAddress(192,168,2,228);
int serverPort = 7123;


uint8_t samples[SAMPLE_BUF_SIZE];

TCPClient client;
unsigned long recordingStart;
unsigned long pollAndResetDMA;

enum State { STATE_WAITING, STATE_CONNECT, STATE_RUNNING, STATE_FINISH };
State state = STATE_WAITING;

//
//
//
class ADCDMA {
public:
	//ADCDMA(uint16_t *buf, size_t bufSize);
	ADCDMA(uint8_t *buf, size_t bufSize);
	virtual ~ADCDMA();

	void start(size_t freqHZ);
	void stop();

private:
	int pin;
	//uint16_t *buf;
	uint8_t *buf;
	size_t bufSize;
};

// Helpful post:
// https://my.st.com/public/STe2ecommunities/mcu/Lists/cortex_mx_stm32/Flat.aspx?RootFolder=https%3a%2f%2fmy%2est%2ecom%2fpublic%2fSTe2ecommunities%2fmcu%2fLists%2fcortex%5fmx%5fstm32%2fstm32f207%20ADC%2bTIMER%2bDMA%20%20Poor%20Peripheral%20Library%20Examples&FolderCTID=0x01200200770978C69A1141439FE559EB459D7580009C4E14902C3CDE46A77F0FFD06506F5B&currentviews=6249

//ADCDMA::ADCDMA(uint16_t *buf, size_t bufSize) : pin(pin), buf(buf), bufSize(bufSize) {
ADCDMA::ADCDMA(uint8_t *buf, size_t bufSize) : pin(pin), buf(buf), bufSize(bufSize) {
}

ADCDMA::~ADCDMA() {

}

void ADCDMA::start(size_t freqHZ) {

    // Using Dual ADC Regular Simultaneous DMA Mode 1

	// Using Timer3. To change timers, make sure you edit all of:
	// RCC_APB1Periph_TIM3, TIM3, ADC_ExternalTrigConv_T3_TRGO

	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC2, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

	// Set the pin as analog input
	// GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
	// GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
	for(int pin = A0; pin <= A5; pin++) {
		HAL_Pin_Mode(pin, AN_INPUT);
	}


	// Enable the DMA Stream IRQ Channel
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

	// 60000000UL = 60 MHz Timer Clock = HCLK / 2
	// Even low audio rates like 8000 Hz will fit in a 16-bit counter with no prescaler (period = 7500)
	TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
	TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
	TIM_TimeBaseStructure.TIM_Period = (60000000UL / freqHZ) - 1;
	TIM_TimeBaseStructure.TIM_Prescaler = 0;
	TIM_TimeBaseStructure.TIM_ClockDivision = 0;
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
	TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); // ADC_ExternalTrigConv_T3_TRGO
	TIM_Cmd(TIM3, ENABLE);



	ADC_CommonInitTypeDef ADC_CommonInitStructure;
	ADC_InitTypeDef ADC_InitStructure;
	DMA_InitTypeDef DMA_InitStructure;

	// DMA2 Stream0 channel0 configuration
	DMA_InitStructure.DMA_Channel = DMA_Channel_0;
	DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buf;
	DMA_InitStructure.DMA_PeripheralBaseAddr =  0x40012308; // CDR_ADDRESS; Packed ADC1, ADC2;
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
	DMA_InitStructure.DMA_BufferSize = bufSize;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
	//DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	//DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;
	DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
	DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
	//DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull; // possible alternate
	//DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; // possible alternate
	//DMA_InitStructure.DMA_FIFOThreshold = 0; // possible alternate
	DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
	DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
	DMA_Init(DMA2_Stream0, &DMA_InitStructure);



	// Don't enable DMA Stream Half / Transfer Complete interrupt
	// Since we want to write out of loop anyway, there's no real advantage to using the interrupt, and as
	// far as I can tell, you can't set the interrupt handler for DMA2_Stream0 without modifying
	// system firmware because there's no built-in handler for it.
	// DMA_ITConfig(DMA2_Stream0, DMA_IT_TC | DMA_IT_HT, ENABLE);

	DMA_Cmd(DMA2_Stream0, ENABLE);

	// ADC Common Init
	ADC_CommonInitStructure.ADC_Mode = ADC_DualMode_RegSimult;
	ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
	//ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div8; // possible alternate
	ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_1;
	ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
	ADC_CommonInit(&ADC_CommonInitStructure);



	// ADC1 configuration
	//ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
	ADC_InitStructure.ADC_Resolution = ADC_Resolution_8b;
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;
	//ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Left;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // possible alternate for 8bit data
	ADC_InitStructure.ADC_NbrOfConversion = 3;
	ADC_Init(ADC1, &ADC_InitStructure);



	ADC_RegularChannelConfig(ADC1, PIN_MAP[A0].adc_channel, 1, ADC_SampleTime_3Cycles);
	ADC_RegularChannelConfig(ADC1, PIN_MAP[A2].adc_channel, 2, ADC_SampleTime_3Cycles);
	ADC_RegularChannelConfig(ADC1, PIN_MAP[A4].adc_channel, 3, ADC_SampleTime_3Cycles);

	// ADC2 configuration - same
	ADC_Init(ADC2, &ADC_InitStructure);

	//
	ADC_RegularChannelConfig(ADC2, PIN_MAP[A1].adc_channel, 1, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig(ADC2, PIN_MAP[A3].adc_channel, 2, ADC_SampleTime_3Cycles);
    ADC_RegularChannelConfig(ADC2, PIN_MAP[A5].adc_channel, 3, ADC_SampleTime_3Cycles);

	// Enable DMA request after last transfer (Multi-ADC mode)
	ADC_MultiModeDMARequestAfterLastTransferCmd(ENABLE);

	// Enable ADCs
	ADC_Cmd(ADC1, ENABLE);
	ADC_Cmd(ADC2, ENABLE);

	ADC_SoftwareStartConv(ADC1);

}

void ADCDMA::stop() {
	// Stop the ADC
	ADC_Cmd(ADC1, DISABLE);
	ADC_Cmd(ADC2, DISABLE);

	DMA_Cmd(DMA2_Stream0, DISABLE);

	// Stop the timer
	TIM_Cmd(TIM3, DISABLE);
}

ADCDMA adcDMA(samples, SAMPLE_BUF_SIZE);

// End ADCDMA

STARTUP(WiFi.selectAntenna(ANT_EXTERNAL));
//SYSTEM_THREAD(ENABLED); // maybe enable this for increased loop() speed?

void setup() {
	Serial.begin(115200);

	// Register handler to handle clicking on the SETUP button
	System.on(button_click, buttonHandler);
	pinMode(D7, OUTPUT);

}

void loop() {
	//uint16_t *sendBuf = NULL;
	uint8_t *sendBuf = NULL;

	switch(state) {
	case STATE_WAITING:
		// Waiting for the user to press the SETUP button. The setup button handler
		// will bump the state into STATE_CONNECT
		break;

	case STATE_CONNECT:
		// Ready to connect to the server via TCP
		if (client.connect(serverAddr, serverPort)) {
			// Connected
			adcDMA.start(SAMPLE_RATE);

			Serial.println("starting");

			recordingStart = millis();
			pollAndResetDMA = millis();
			digitalWrite(D7, HIGH);

			state = STATE_RUNNING;
		}
		else {
			Serial.println("failed to connect to server");
			state = STATE_WAITING;
		}
		break;

	case STATE_RUNNING:

		// for debugging the amount of data left in the stream buffer`
		//Serial.println(DMA2_Stream0->NDTR);

		if(ADC_GetFlagStatus(ADC1, ADC_FLAG_OVR)){
			if(ADC_GetFlagStatus(ADC2, ADC_FLAG_OVR)){
				ADC_ClearFlag(ADC2, ADC_FLAG_OVR);
				ADC_ClearFlag(ADC1, ADC_FLAG_OVR);
			}
			else{
				ADC_ClearFlag(ADC1, ADC_FLAG_OVR);
			}
		}


		//Serial.println("HTIF status:" + String(DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_HTIF0)));
		if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_HTIF0)) {
		    DMA_ClearFlag(DMA2_Stream0, DMA_FLAG_HTIF0);
		    sendBuf = samples;
		}
		//Serial.println("TCIF status:" + String(DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TCIF0)));
		if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TCIF0)) {
		    DMA_ClearFlag(DMA2_Stream0, DMA_FLAG_TCIF0);
		    sendBuf = &samples[SAMPLE_BUF_SIZE / 2];
		}

		if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TEIF0)) {
			Serial.println("Transfer Error interrupt fired");
		}

		if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_FEIF0)) {
			Serial.println("FIFO Mode Transfer Error interrupt fired");
		}

		if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_DMEIF0)) {
			Serial.println("Direct Mode Transfer Error interrupt fired");
		}

		if (sendBuf != NULL) {
			// There is a sample buffer to send

			/*
			int i=0;
			while (i < SAMPLE_BUF_SIZE){
				smallSendBuf[i] = sendBuf[i] >> 8;
				i++;
			}
			*/


			//Serial.println(String(sendBuf[5] >> 8));
			Serial.println(String(sendBuf[0]));

			// Send here. We're actually sending 1/2 of the samples buffer, but the samples
			// are 16 bits and client.write() takes bytes, so there's no division
			//int count = client.write((uint8_t *)sendBuf, SAMPLE_BUF_SIZE);
			int count = client.write(sendBuf, SAMPLE_BUF_SIZE);
			//int count = client.write(smallSendBuf, SAMPLE_BUF_SIZE);
			if (count == SAMPLE_BUF_SIZE) {
				// Success
			}
			else
			if (count == -16) {
				// TCP Buffer full
				Serial.printlnf("buffer full, discarding");
			}
			else {
				// Error
				Serial.printlnf("error writing %d", count);
				state = STATE_FINISH;
			}



		}		

		if (millis() - recordingStart >= MAX_RECORDING_LENGTH_MS) {
			state = STATE_FINISH;
		}
		break;

	case STATE_FINISH:
		digitalWrite(D7, LOW);
		adcDMA.stop();
		client.stop();
		Serial.println("stopping");
		state = STATE_WAITING;
		break;
	}
}

// button handler for the SETUP button, used to toggle recording on and off
void buttonHandler(system_event_t event, int data) {
	switch(state) {
	case STATE_WAITING:
		state = STATE_CONNECT;
		break;

	case STATE_RUNNING:
		state = STATE_FINISH;
		break;
	}
}

Additionally, I attempted to reduce the datarate by copying (uint16_t) *sendBuf into a new 8bit buffer, then sending that out with client.write. Oddly the datarate was the same.

The first thing I’d do is change the sample time to be larger than 3 cycles. I’d set it to 28. Even 144 is fast enough to sample 3 pins per ADC at 44100 Hz.

#define ADC_SampleTime_3Cycles                    ((uint8_t)0x00)
#define ADC_SampleTime_15Cycles                   ((uint8_t)0x01)
#define ADC_SampleTime_28Cycles                   ((uint8_t)0x02)
#define ADC_SampleTime_56Cycles                   ((uint8_t)0x03)
#define ADC_SampleTime_84Cycles                   ((uint8_t)0x04)
#define ADC_SampleTime_112Cycles                  ((uint8_t)0x05)
#define ADC_SampleTime_144Cycles                  ((uint8_t)0x06)
#define ADC_SampleTime_480Cycles                  ((uint8_t)0x07)

The other thing is to increase the phase delay between ADC reads. I’d set it to 14, so the ADC DMA writes are exactly out of phase with each other, maximizing the time between DMA writes.

#define ADC_TwoSamplingDelay_5Cycles               ((uint32_t)0x00000000)
#define ADC_TwoSamplingDelay_6Cycles               ((uint32_t)0x00000100)
#define ADC_TwoSamplingDelay_7Cycles               ((uint32_t)0x00000200)
#define ADC_TwoSamplingDelay_8Cycles               ((uint32_t)0x00000300)
#define ADC_TwoSamplingDelay_9Cycles               ((uint32_t)0x00000400)
#define ADC_TwoSamplingDelay_10Cycles              ((uint32_t)0x00000500)
#define ADC_TwoSamplingDelay_11Cycles              ((uint32_t)0x00000600)
#define ADC_TwoSamplingDelay_12Cycles              ((uint32_t)0x00000700)
#define ADC_TwoSamplingDelay_13Cycles              ((uint32_t)0x00000800)
#define ADC_TwoSamplingDelay_14Cycles              ((uint32_t)0x00000900)
#define ADC_TwoSamplingDelay_15Cycles              ((uint32_t)0x00000A00)
#define ADC_TwoSamplingDelay_16Cycles              ((uint32_t)0x00000B00)
#define ADC_TwoSamplingDelay_17Cycles              ((uint32_t)0x00000C00)
#define ADC_TwoSamplingDelay_18Cycles              ((uint32_t)0x00000D00)
#define ADC_TwoSamplingDelay_19Cycles              ((uint32_t)0x00000E00)
#define ADC_TwoSamplingDelay_20Cycles              ((uint32_t)0x00000F00)

I’m pretty sure the bottleneck you’re running into is the ADC writing to RAM using DMA, and this is happening because the sampling is too fast.

Reducing the sample size on the other side (before writing to the network) may be a bottleneck as well, but that’s not the one causing the ADC overflow.

It’s possible to reduce the data to 8 bits in the ADC itself, but that’s more complicated. I’d try just increasing the sampling time and moving them out of phase with each other first.

3 Likes

You’re correct again @rickkas7! There are no more ADC Overruns now that I moved to ADC_SampleTime_28Cycles and ADC_TwoSamplingDelay_14Cycles over 10minutes of sampling :smiley:

The last challenge I have is reducing the datarate to ~250kB/s (1byte * 6 channels * 44100) - as a bit depth of 8 bits is all I need per sample, and I didn’t want to push the network and the post processing needed with the extra bits per sample.

I believe I did everything I needed to change the ADC to sample 8bits instead of 12 with these four parameters, as I can print out the results

DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
ADC_InitStructure.ADC_Resolution = ADC_Resolution_8b;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;

And I am receiving 1byte samples (0-255), yet my datarate remains around 500kB/s, as if I hadn’t changed from 2byte samples to 1byte samples.

On the node.js side I added this to benchmark:

socket.on('end', function () {
		var totalTime = (process.hrtime()[0]*1000000 + process.hrtime()[1] / 1000) - startTime;
		console.log('transmission complete, saved to ' + outPath);
		console.log('bytes read:' + socket.bytesRead);
		console.log('totalTime (us):' + totalTime);
		console.log('average data rate (bytes/sec):' + (socket.bytesRead / (totalTime / 1000000)));
		writer.end();
	});

Terminal output

transmission complete, saved to C:\Users\Kale\Documents\audioStreaming\photonAudio-master\audio4\out\00010.wav
bytes read:15104880
totalTime (us):30156459.588989258
average data rate (bytes/sec):500883.7312426125

Any ideas on why the sample rate hasn’t dropped to ~250kB/s from 500kB/s since I dropped the bit depth to 8bit?

Thanks again for your time! :smiley:

Complete code

#include "Particle.h"

// particle compile photon audio4.cpp --target 0.6.3 --saveTo firmware.bin


//
// ADCDMA - Class to use Photon ADC in DMA Mode
//
#include "adc_hal.h"
#include "gpio_hal.h"
#include "pinmap_hal.h"
#include "pinmap_impl.h"


void buttonHandler(system_event_t event, int data); // forward declaration

// The buffer should be around 1024 bytes. However, it also should be a multiple of
// 2 * number of channels = 12, so 1020 is a good choice. The total memory usage is 2x this because
// of double buffers.
//const size_t SAMPLE_BUF_SIZE = 1020;
const size_t SAMPLE_BUF_SIZE = 1008;

// The audio sample rate. The minimum is probably 8000 for minimally acceptable audio quality.
// Not sure what the maximum rate is, but it's pretty high.
//const long SAMPLE_RATE = 10000;
const long SAMPLE_RATE = 44100;

// If you don't hit the setup button to stop recording, this is how long to go before turning it
// off automatically. The limit really is only the disk space available to receive the file.
const unsigned long MAX_RECORDING_LENGTH_MS = 30000;

// This is the IP Address and port that the audioServer.js node server is running on.
IPAddress serverAddr = IPAddress(192,168,2,228);
int serverPort = 7123;

int overruns = 0;

uint8_t samples[SAMPLE_BUF_SIZE];

TCPClient client;
unsigned long recordingStart;
unsigned long pollAndResetDMA;

enum State { STATE_WAITING, STATE_CONNECT, STATE_RUNNING, STATE_FINISH };
State state = STATE_WAITING;

//
//
//
class ADCDMA {
public:
	//ADCDMA(uint16_t *buf, size_t bufSize);
	ADCDMA(uint8_t *buf, size_t bufSize);
	virtual ~ADCDMA();

	void start(size_t freqHZ);
	void stop();

private:
	int pin;
	//uint16_t *buf;
	uint8_t *buf;
	size_t bufSize;
};

// Helpful post:
// https://my.st.com/public/STe2ecommunities/mcu/Lists/cortex_mx_stm32/Flat.aspx?RootFolder=https%3a%2f%2fmy%2est%2ecom%2fpublic%2fSTe2ecommunities%2fmcu%2fLists%2fcortex%5fmx%5fstm32%2fstm32f207%20ADC%2bTIMER%2bDMA%20%20Poor%20Peripheral%20Library%20Examples&FolderCTID=0x01200200770978C69A1141439FE559EB459D7580009C4E14902C3CDE46A77F0FFD06506F5B&currentviews=6249

//ADCDMA::ADCDMA(uint16_t *buf, size_t bufSize) : pin(pin), buf(buf), bufSize(bufSize) {
ADCDMA::ADCDMA(uint8_t *buf, size_t bufSize) : pin(pin), buf(buf), bufSize(bufSize) {
}

ADCDMA::~ADCDMA() {

}

void ADCDMA::start(size_t freqHZ) {

    // Using Dual ADC Regular Simultaneous DMA Mode 1

	// Using Timer3. To change timers, make sure you edit all of:
	// RCC_APB1Periph_TIM3, TIM3, ADC_ExternalTrigConv_T3_TRGO

	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC2, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

	// Set the pin as analog input
	// GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
	// GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
	for(int pin = A0; pin <= A5; pin++) {
		HAL_Pin_Mode(pin, AN_INPUT);
	}


	// Enable the DMA Stream IRQ Channel
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

	// 60000000UL = 60 MHz Timer Clock = HCLK / 2
	// Even low audio rates like 8000 Hz will fit in a 16-bit counter with no prescaler (period = 7500)
	TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
	TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
	TIM_TimeBaseStructure.TIM_Period = (60000000UL / freqHZ) - 1;
	TIM_TimeBaseStructure.TIM_Prescaler = 0;
	TIM_TimeBaseStructure.TIM_ClockDivision = 0;
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
	TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); // ADC_ExternalTrigConv_T3_TRGO
	TIM_Cmd(TIM3, ENABLE);



	ADC_CommonInitTypeDef ADC_CommonInitStructure;
	ADC_InitTypeDef ADC_InitStructure;
	DMA_InitTypeDef DMA_InitStructure;

	// DMA2 Stream0 channel0 configuration
	DMA_InitStructure.DMA_Channel = DMA_Channel_0;
	DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buf;
	DMA_InitStructure.DMA_PeripheralBaseAddr =  0x40012308; // CDR_ADDRESS; Packed ADC1, ADC2;
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
	DMA_InitStructure.DMA_BufferSize = bufSize;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
	//DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
	//DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;
	DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
	DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
	//DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_1QuarterFull; // possible alternate
	//DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; // possible alternate
	//DMA_InitStructure.DMA_FIFOThreshold = 0; // possible alternate
	DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
	DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
	DMA_Init(DMA2_Stream0, &DMA_InitStructure);



	// Don't enable DMA Stream Half / Transfer Complete interrupt
	// Since we want to write out of loop anyway, there's no real advantage to using the interrupt, and as
	// far as I can tell, you can't set the interrupt handler for DMA2_Stream0 without modifying
	// system firmware because there's no built-in handler for it.
	// DMA_ITConfig(DMA2_Stream0, DMA_IT_TC | DMA_IT_HT, ENABLE);

	DMA_Cmd(DMA2_Stream0, ENABLE);

	// ADC Common Init
	ADC_CommonInitStructure.ADC_Mode = ADC_DualMode_RegSimult;
	ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
	//ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div8; // possible alternate
	ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_1;
	ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_14Cycles;
	ADC_CommonInit(&ADC_CommonInitStructure);



	// ADC1 configuration
	//ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
	ADC_InitStructure.ADC_Resolution = ADC_Resolution_8b;
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO;
	//ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Left;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // possible alternate for 8bit data
	ADC_InitStructure.ADC_NbrOfConversion = 3;
	ADC_Init(ADC1, &ADC_InitStructure);



	//ADC_RegularChannelConfig(ADC1, PIN_MAP[A0].adc_channel, 1, ADC_SampleTime_3Cycles); // changed from 3Cycles
	ADC_RegularChannelConfig(ADC1, PIN_MAP[A0].adc_channel, 1, ADC_SampleTime_28Cycles);
	ADC_RegularChannelConfig(ADC1, PIN_MAP[A2].adc_channel, 2, ADC_SampleTime_28Cycles);
	ADC_RegularChannelConfig(ADC1, PIN_MAP[A4].adc_channel, 3, ADC_SampleTime_28Cycles);

	// ADC2 configuration - same
	ADC_Init(ADC2, &ADC_InitStructure);

	//
	ADC_RegularChannelConfig(ADC2, PIN_MAP[A1].adc_channel, 1, ADC_SampleTime_28Cycles);
    ADC_RegularChannelConfig(ADC2, PIN_MAP[A3].adc_channel, 2, ADC_SampleTime_28Cycles);
    ADC_RegularChannelConfig(ADC2, PIN_MAP[A5].adc_channel, 3, ADC_SampleTime_28Cycles);

	// Enable DMA request after last transfer (Multi-ADC mode)
	ADC_MultiModeDMARequestAfterLastTransferCmd(ENABLE);

	// Enable ADCs
	ADC_Cmd(ADC1, ENABLE);
	ADC_Cmd(ADC2, ENABLE);

	ADC_SoftwareStartConv(ADC1);

}

void ADCDMA::stop() {
	// Stop the ADC
	ADC_Cmd(ADC1, DISABLE);
	ADC_Cmd(ADC2, DISABLE);

	DMA_Cmd(DMA2_Stream0, DISABLE);

	// Stop the timer
	TIM_Cmd(TIM3, DISABLE);
}

ADCDMA adcDMA(samples, SAMPLE_BUF_SIZE);

// End ADCDMA

STARTUP(WiFi.selectAntenna(ANT_EXTERNAL));
//SYSTEM_THREAD(ENABLED); // maybe enable this for increased loop() speed?

void setup() {
	Serial.begin(115200);

	// Register handler to handle clicking on the SETUP button
	System.on(button_click, buttonHandler);
	pinMode(D7, OUTPUT);

}

void loop() {
	//uint16_t *sendBuf = NULL;
	uint8_t *sendBuf = NULL;

	switch(state) {
	case STATE_WAITING:
		// Waiting for the user to press the SETUP button. The setup button handler
		// will bump the state into STATE_CONNECT
		break;

	case STATE_CONNECT:
		// Ready to connect to the server via TCP
		if (client.connect(serverAddr, serverPort)) {
			// Connected
			adcDMA.start(SAMPLE_RATE);

			Serial.println("starting");

			recordingStart = millis();
			pollAndResetDMA = millis();
			digitalWrite(D7, HIGH);

			state = STATE_RUNNING;
		}
		else {
			Serial.println("failed to connect to server");
			state = STATE_WAITING;
		}
		break;

	case STATE_RUNNING:

		// for debugging the amount of data left in the stream buffer`
		//Serial.println(DMA2_Stream0->NDTR);

		if(ADC_GetFlagStatus(ADC1, ADC_FLAG_OVR)){
			Serial.println("Overrun");
			overruns++;
			ADC_ClearFlag(ADC2, ADC_FLAG_OVR);
			ADC_ClearFlag(ADC1, ADC_FLAG_OVR);
			adcDMA.stop();
			adcDMA.start(SAMPLE_RATE);
		}


		//Serial.println("HTIF status:" + String(DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_HTIF0)));
		if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_HTIF0)) {
		    DMA_ClearFlag(DMA2_Stream0, DMA_FLAG_HTIF0);
		    sendBuf = samples;
		}
		//Serial.println("TCIF status:" + String(DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TCIF0)));
		if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TCIF0)) {
		    DMA_ClearFlag(DMA2_Stream0, DMA_FLAG_TCIF0);
		    sendBuf = &samples[SAMPLE_BUF_SIZE / 2];
		}

		if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_TEIF0)) {
			Serial.println("Transfer Error interrupt fired");
		}

		if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_FEIF0)) {
			Serial.println("FIFO Mode Transfer Error interrupt fired");
		}

		if (DMA_GetFlagStatus(DMA2_Stream0, DMA_FLAG_DMEIF0)) {
			Serial.println("Direct Mode Transfer Error interrupt fired");
		}

		if (sendBuf != NULL) {
			// There is a sample buffer to send

			/*
			int i=0;
			while (i < SAMPLE_BUF_SIZE){
				smallSendBuf[i] = sendBuf[i] >> 8;
				i++;
			}
			*/


			//Serial.println(String(sendBuf[5] >> 8));
			Serial.println(String(sendBuf[0]));

			// Send here. We're actually sending 1/2 of the samples buffer, but the samples
			// are 16 bits and client.write() takes bytes, so there's no division
			//int count = client.write((uint8_t *)sendBuf, SAMPLE_BUF_SIZE);
			int count = client.write(sendBuf, SAMPLE_BUF_SIZE);
			//int count = client.write(smallSendBuf, SAMPLE_BUF_SIZE);
			if (count == SAMPLE_BUF_SIZE) {
				// Success
			}
			else
			if (count == -16) {
				// TCP Buffer full
				Serial.printlnf("buffer full, discarding");
			}
			else {
				// Error
				Serial.printlnf("error writing %d", count);
				state = STATE_FINISH;
			}



		}

		if (millis() - recordingStart >= MAX_RECORDING_LENGTH_MS) {
			state = STATE_FINISH;
		}
		break;

	case STATE_FINISH:
		digitalWrite(D7, LOW);
		adcDMA.stop();
		client.stop();
		Serial.println("stopping. ADC overruns# " + String(overruns));
		state = STATE_WAITING;
		break;
	}
}

// button handler for the SETUP button, used to toggle recording on and off
void buttonHandler(system_event_t event, int data) {
	switch(state) {
	case STATE_WAITING:
		state = STATE_CONNECT;
		break;

	case STATE_RUNNING:
		state = STATE_FINISH;
		break;
	}
}
1 Like

Ah I figured it out.

The DMA_FLAG_HTIF0 going high triggers a bottom-half buffer copy, and in your 2byte transfer SAMPLE_BUF_SIZE is appropriate because it is actually half the samples due to the buffer being 2040 in size.

Then when ‘DMA_FLAG_TCIF0’ goes high, it triggers a top-half buffer copy, and the same scenario writing the data out.

The solution was to simply divide the SAMPLE_BUF_SIZE by 2, as you alluded to in your code comments, and reconstruct it appropriately on the other end.

Once again, thanks!

4 Likes