Teensy 3.6 Datalogging at 10kHz

Status
Not open for further replies.

Ctsi

Member
Hello,

For my current work I am attempting to create a cheap datalogger using a Teensy 3.6 and would like a feasibility check. We need to sample 3 analog microphones at around 10kHz. I am not extremely knowledgeable with this functionality of micro-controllers so if I could find how realistic this requirement is before I delve too deep I would greatly appreciate it. The data will be saved onto an SD card using the Teensy built in SD module.
 
Last edited:
We need to sample 3 analog microphones at around 10kHz.

data logging up to 192 kHz (or 48 kHz for 4 channels) should be possible (I have done it). uSD filing is somewhat tricky, due to un-predictable uSD card behaviour (non-regular uSD internal latencies)

All libraries for uSD access give some example of data logging.
 
Last edited:
data logging up to 192 kHz (or 48 kHz for 4 channels) should be possible (I have done it). uSD filing is somewhat tricky, due to un-predictable uSD card behaviour (non-regular uSD internal latencies)

All libraries for uSD access give some example of data logging.

When you refer to uSD, is this different than the standard SD.h library? By any chance do you have example code for your datalogger? The low latency datalogger at 25kHz thread has code provided but it outputs in a binary file. Best case I would like to output in a text file but any output is doable as long as the sampling rate is consistent. Thank you for the response by the way, at least I know this is a possible project.
 
Sampling data is simple. You might want to try doing so from an interrupt handler to make sure you don't miss samples when other things are going on.

Formatting to text instead of binary is simple, too. The one thing I would recommend would be to buffer significant amounts of data (4-16 kilobytes) before you write to the SDCard, and make sure you write aligned blocks.
 
When you refer to uSD, is this different than the standard SD.h library? By any chance do you have example code for your datalogger? The low latency datalogger at 25kHz thread has code provided but it outputs in a binary file. Best case I would like to output in a text file but any output is doable as long as the sampling rate is consistent. Thank you for the response by the way, at least I know this is a possible project.

with uSD I mean microSD disk (sorry about that)
concerning examples. Paul's SD port has a datalogger in examples

There are others uSD libraries (my own one called called uSDFS https://github.com/WMXZ-EU/uSDFS and Bill Greimans FAT32 derivative https://github.com/greiman/SdFat-beta) which may be useful in the future. Bill is very active in developing fast uSD libraries.
 
Thanks for the references. Now what kind of accuracy have you experienced at these high frequencies? Down to the millisecond? From my experience simply keeping time with micros() and delayMicroseconds(), the accuracy for sampling has a plus/minus of up to 10microseconds.
 
Thanks for the references. Now what kind of accuracy have you experienced at these high frequencies? Down to the millisecond? From my experience simply keeping time with micros() and delayMicroseconds(), the accuracy for sampling has a plus/minus of up to 10microseconds.

What do you mean by accuracy? for sampling?
You would not do sampling in the loop() together with the logger write operation.

in general you would either sample in an interrupt service routine, of use dma to get data to memory generating an interrupt to access data.
The procedure would depend on what type of mic you plan to use

digital (I2S) mic's or analog mic's ?
if analog mics, connected to Teensy ADC or to an audio card?
 
Analog mics connected to the Teensy ADC. They are relatively cheap microphones, Adafruit AGC MAX9814.
 
Yes, you must capture with DMA. Timing (and code) running in the main thread is subject to jitter caused by interrupt latencies and perhaps service done by other functions/libraries (depending on what you're using.)
 
The system can work with only 2 mics. As for the Direct memory access suggestion, I am very new to programming. How difficult is it to implement data capturing via DMA?
 
Here is DMA code for a single channel. It samples a 50kHz PWM signal at an ADC frequency of 200kHz. The DMA transfer runs in the background. When the end of the buffer is reached, it wraps around and starts at the beginning.

For a second channel, pick a pin on ADC1 and duplicate buffer / dma channel and the various setup stuff that references ADC0.
Code:
#include <ADC.h>
#include <DMAChannel.h>
#include <array>

// connect out_pin to adc_pin, PWM output on out_pin will be measured
// via adc_pin.

const uint8_t adc_pin = A9; // digital pin 23
const uint8_t out_pin = 2;

ADC adc;
DMAChannel dma;

std::array<volatile uint16_t, 4096> buffer;
volatile size_t write_pos = 0;

volatile uint16_t adc_val = 0;

void setup() {
    pinMode(adc_pin, INPUT);
    Serial.begin(9600);
    delay(2000);
    Serial.println("Starting");

    adc.setAveraging(1);
    adc.setResolution(12);

    adc.setConversionSpeed(ADC_CONVERSION_SPEED::MED_SPEED);
    adc.setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED);
    adc.adc0->analogRead(adc_pin); // performs various ADC setup stuff

    if(adc.adc0->fail_flag) {
        Serial.print("ADC error: ");
        Serial.println(adc.adc0->fail_flag, HEX);
    }

    dma.source(ADC0_RA); // ADC result register
    dma.transferSize(2);
    dma.triggerAtHardwareEvent(DMAMUX_SOURCE_ADC0);
    dma.destinationBuffer(buffer.data(), buffer.size() * sizeof(buffer[0]));
    dma.enable();

    adc.enableDMA(ADC_0);
    
    adc.adc0->stopPDB();
    // Sample at 200'000 Hz
    const uint32_t pdb_trigger_frequency = 200000;
    adc.adc0->startPDB(pdb_trigger_frequency);
    NVIC_DISABLE_IRQ(IRQ_PDB); // we don't want or need the PDB interrupt
    
    // PWM output on out_pin for testing purposes.
    analogWriteFrequency(out_pin, 50000);
    analogWrite(out_pin, 100);
}

void loop() {
    Serial.print("Current Buffer index DMA is writing to: ");
    size_t buffer_idx = ((uint16_t*) dma.destinationAddress()) - buffer.data();
    Serial.println(buffer_idx);
    
    // Print first 100 measurements in buffer.
    for(size_t i = 0; i < 100; i++) {
        Serial.print(buffer[i]);
        if(i % 20 == 19) Serial.println();
        else Serial.print(" ");
    }
    Serial.println();

    if(adc.adc0->fail_flag) {
        Serial.print("ADC error: ");
        Serial.println(adc.adc0->fail_flag, HEX);
    }
    delay(1000);
}

// ISR handler, since the ADC library enables the PDB interrupt we must acknowlege
// a couple of spurious interrupts
void pdb_isr(void) {
    PDB0_SC &=~PDB_SC_PDBIF; // clear interrupt
}

Expected output (4 captured values for each PWM period):

Current Buffer index DMA is writing to: 3421
4095 1 1 1 4095 1 1 2 4095 1 2 1 4095 1 1 1 4095 1 4 1
4095 4 1 2 4095 1 1 1 4095 1 1 1 4095 2 1 1 4095 1 1 1
4095 2 1 1 4095 1 1 1 4095 1 1 1 4095 1 1 1 4095 3 1 1
4095 1 1 1 4095 1 1 1 4095 1 1 1 4095 1 1 2 4095 1 1 1
4095 1 1 1 4095 1 1 1 4095 1 1 1 4095 1 1 2 4095 1 1 1

...
 
I tried 2 channels, the ADC library PDB setup doesn't work for kicking off 2 channels.

Here is working version for 2 channels, writing into the same buffer (with my own PDB setup):

Code:
#include <ADC.h>
#include <DMAChannel.h>
#include <array>

// connect out_pin to adc_pin0 and adc_pin1, PWM output on out_pin will be measured
// via adc_pin0 and adc_pin1.

const uint8_t adc_pin0 = A9;  // digital pin 23, on ADC0
const uint8_t adc_pin1 = A12; // digital pin 31, on ADC1
const uint8_t out_pin = 2;

ADC adc;
DMAChannel dma0;
DMAChannel dma1;

struct CapturePair {
    uint16_t v_adc0;
    uint16_t v_adc1;
};
std::array<volatile CapturePair, 4096> buffer;
volatile size_t write_pos = 0;

void setup() {
    pinMode(adc_pin0, INPUT);
    pinMode(adc_pin1, INPUT);
    Serial.begin(9600);
    delay(2000);
    Serial.println("Starting");

    adc.setAveraging(1);
    adc.setResolution(12);

    adc.setConversionSpeed(ADC_CONVERSION_SPEED::MED_SPEED);
    adc.setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED);
    adc.adc0->analogRead(adc_pin0); // performs various ADC setup stuff
    adc.adc1->analogRead(adc_pin1); // performs various ADC setup stuff

    if(adc.adc0->fail_flag || adc.adc1->fail_flag) {
        Serial.printf("ADC error, ADC0: %x ADC1: %x\n", adc.adc0->fail_flag, adc.adc1->fail_flag);
    }

    adc.adc0->stopPDB();
    adc.adc1->stopPDB();

    dma0.source((uint16_t&) ADC0_RA); // ADC result register
    dma0.triggerAtHardwareEvent(DMAMUX_SOURCE_ADC0);
    dma0.destinationBuffer(&buffer[0].v_adc0, buffer.size() * sizeof(buffer[0]));
    dma0.TCD->DOFF = 4;
    dma0.TCD->CITER = buffer.size();
    dma0.TCD->BITER = buffer.size();
    dma0.enable();
    adc.enableDMA(ADC_0);

    dma1.source((uint16_t&) ADC1_RA); // ADC result register
    dma1.triggerAtHardwareEvent(DMAMUX_SOURCE_ADC1);
    dma1.destinationBuffer(&buffer[0].v_adc1, buffer.size() * sizeof(buffer[0]));
    dma1.TCD->DOFF = 4;
    dma1.TCD->CITER = buffer.size();
    dma1.TCD->BITER = buffer.size();
    dma1.enable();
    adc.enableDMA(ADC_1);

    adc.adc0->setHardwareTrigger();
    adc.adc1->setHardwareTrigger();
    
    // enable PDB    
    SIM_SCGC6 |= SIM_SCGC6_PDB;
    // Sample at 10'000 Hz
    constexpr uint32_t pdb_trigger_frequency = 10000;
    constexpr uint32_t mod = (F_BUS / pdb_trigger_frequency);
    static_assert(mod <= 0x10000, "Prescaler required.");
    PDB0_MOD = (uint16_t)(mod-1);
    PDB0_CH0C1 = PDB_CHnC1_TOS_1 | PDB_CHnC1_EN_1; // PDB triggers ADC0 SC1A
    PDB0_CH1C1 = PDB_CHnC1_TOS_1 | PDB_CHnC1_EN_1; // PDB triggers ADC1 SC1A
    PDB0_SC = ADC_PDB_CONFIG | PDB_SC_PRESCALER(0) | PDB_SC_MULT(0) | PDB_SC_LDOK;
    
    // PWM output on out_pin for testing purposes.
    analogWriteFrequency(out_pin, 5000);
    analogWrite(out_pin, 120);

    // Kick off ADC conversion.
    PDB0_SC = ADC_PDB_CONFIG | PDB_SC_PRESCALER(0) | PDB_SC_MULT(0) | PDB_SC_SWTRIG; // start
}

void loop() {
    Serial.print("Current Buffer index DMA is writing to: ");
    noInterrupts();
    size_t buffer_idx0 = (((uint16_t*) dma0.destinationAddress()) - &buffer[0].v_adc0) / 2;
    size_t buffer_idx1 = (((uint16_t*) dma1.destinationAddress()) - &buffer[0].v_adc1) / 2;
    interrupts();
    Serial.print(buffer_idx0);
    Serial.print("    ");
    Serial.println(buffer_idx1);
    
    // Print first 64 measurements in buffer.
    for(size_t i = 0; i < 64; i++) {
        Serial.printf("[%4u, %4u]", buffer[i].v_adc0, buffer[i].v_adc1);
        if(i % 8 == 7) Serial.println();
        else Serial.print(" ");
    }
    Serial.println();
    
    if(adc.adc0->fail_flag || adc.adc1->fail_flag) {
        Serial.printf("ADC error, ADC0: %x ADC1: %x\n", adc.adc0->fail_flag, adc.adc1->fail_flag);
    }
    
    delay(1000);
}

Expected output (adc_pin0 is connected directly to out_pin, adc_pin1 is connected via a voltage divider):

Current Buffer index DMA is writing to: 1815 1815
[4095, 1021] [ 1, 1] [4095, 1022] [ 1, 1] [4094, 1021] [ 1, 1] [4095, 1021] [ 1, 1]
[4095, 1021] [ 1, 1] [4095, 1022] [ 1, 1] [4095, 1021] [ 1, 1] [4095, 1021] [ 1, 1]
[4095, 1021] [ 1, 1] [4095, 1021] [ 1, 1] [4095, 1021] [ 1, 1] [4095, 1021] [ 1, 1]
[4095, 1021] [ 1, 1] [4094, 1021] [ 1, 1] [4095, 1021] [ 1, 1] [4095, 1021] [ 1, 1]
[4095, 1021] [ 1, 1] [4095, 1021] [ 1, 1] [4095, 1021] [ 1, 1] [4094, 1021] [ 1, 1]
[4094, 1021] [ 1, 1] [4095, 1021] [ 1, 1] [4095, 1021] [ 1, 1] [4094, 1021] [ 1, 1]
[4095, 1021] [ 1, 1] [4095, 1021] [ 1, 1] [4094, 1021] [ 1, 1] [4095, 1021] [ 1, 1]
[4095, 1021] [ 1, 1] [4095, 1021] [ 1, 1] [4093, 1021] [ 1, 1] [4095, 1021] [ 1, 1]
 
Last edited:
I am currently getting the following errors, mostly about not declaring some ADC variables. I have the ADC library installed so I was wondering if you might have some insight. Thanks.


DMAPDB2Channel:33: error: 'ADC_CONVERSION_SPEED' has not been declared
adc.setConversionSpeed(ADC_CONVERSION_SPEED::MED_SPEED);

^

DMAPDB2Channel:34: error: 'ADC_SAMPLING_SPEED' has not been declared
adc.setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED);

^

DMAPDB2Channel:75: error: 'ADC_PDB_CONFIG' was not declared in this scope
PDB0_SC = ADC_PDB_CONFIG | PDB_SC_PRESCALER(0) | PDB_SC_MULT(0) | PDB_SC_LDOK;
 
Thanks everyone for the support. I ended up purchasing a couple audio boards and they are performing great. Now my issue comes with post processing the data. The audio boards record the audio into a ".RAW" format which I am not sure what to do with. If anyone has had success retrieving recorded audio through the board and importing into Excel please let me know.

Also I am wondering if anyone has been able to integrate a clock such as GPS time into the audio board. The application for my setup will be to leave the board recording for an extended period of time and to hopefully timeframe any events that are recorded in post-processing. I have an Adafruit GPS hooked up to a Teensy 3.6 reporting the time to serial but recording this into the .RAW datafile is probably a different story. Let me know, thanks.
 
Edit: It was pretty simple to import this .RAW audio file into Audacity and then exporting to a .csv from there.

I would still like to know if anyone has had success integrating a clock into the teensy audio board however. Recording the audio has been fantastic but I need to correlate this audio with the timeframe it happened.

Thanks
 
The Raw audio which is basically a .wav without any header information so that you have to know before post processing in which format your audio data had been stored, can be 'decoded' with audacity via File -> Import -> Raw Data.

I allow to guess that the audio shield records 16 bit pcm (almost for sure), now it's up to others to tell if it is big or little endian, signed or unsigned...

I know that this will not resolve your time problem, but it's a first step.
 
I would still like to know if anyone has had success integrating a clock into the teensy audio board however. Recording the audio has been fantastic but I need to correlate this audio with the timeframe it happened.

Thanks

Just add the time as first data to your file, before writing the audio. The sampling-freq is about 44117 Hz
 
I'm sorry can you clarify if you can, I didn't quite understand what you meant as "first data". Thanks
 
The file which you are writing consists of a series of bytes, always two of them representing one 16bit PCM audio sample.

Frank suggested that you add to your code a functionality which writes n bytes containing the current timestamp at the very beginning of your file (first data) before you continue filling the file with your audio samples.

When reading the file later, you'd need to write some code to extract the n bytes and to interpret these as a timestamp before further bytes will have to be seen as audio data.
 
I'm not sure if anyone will have the answer for this, but is the oscillator within the Teensy accurate enough to keep the sample rate exact for an extended period of time? The timestamp immediately before audio recording could be a solution. I would like for the system to run for upwards of 12 hours without any sampling issues.
 
Hello Tni,

I played with your " DMA code for a single channel ", on a teensy 3.6, and I tried to increase the buffer size. It seems to be limited to 64k bytes.
Is it true?
If yes, is it a limitation of the library or the hard?

Thank's
Yannick.
 
Status
Not open for further replies.
Back
Top