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
- 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)
- 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