Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 25 of 25

Thread: Teensy 3.6 Datalogging at 10kHz

  1. #1
    Junior Member
    Join Date
    May 2017
    Posts
    15

    Teensy 3.6 Datalogging at 10kHz

    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 by Ctsi; 05-08-2017 at 05:35 PM.

  2. #2
    Senior Member+ defragster's Avatar
    Join Date
    Feb 2015
    Posts
    8,380
    This thread recently did something like that: Low-latency-data-logger

    25KHz - only one data value with lots of feedback

  3. #3
    Senior Member
    Join Date
    Jul 2014
    Posts
    2,157
    Quote Originally Posted by Ctsi View Post
    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 by WMXZ; 05-03-2017 at 05:16 PM.

  4. #4
    Junior Member
    Join Date
    May 2017
    Posts
    15
    Quote Originally Posted by WMXZ View Post
    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.

  5. #5
    Senior Member
    Join Date
    Dec 2014
    Posts
    253
    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.

  6. #6
    Senior Member
    Join Date
    Jul 2014
    Posts
    2,157
    Quote Originally Posted by Ctsi View Post
    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.

  7. #7
    Junior Member
    Join Date
    May 2017
    Posts
    15
    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.

  8. #8
    Senior Member
    Join Date
    Jul 2014
    Posts
    2,157
    Quote Originally Posted by Ctsi View Post
    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?

  9. #9
    Junior Member
    Join Date
    May 2017
    Posts
    15
    Analog mics connected to the Teensy ADC. They are relatively cheap microphones, Adafruit AGC MAX9814.

  10. #10
    Senior Member
    Join Date
    Jul 2014
    Posts
    2,157
    Quote Originally Posted by Ctsi View Post
    Analog mics connected to the Teensy ADC. They are relatively cheap microphones, Adafruit AGC MAX9814.
    You should use PDB timer code: see https://forum.pjrc.com/threads/42937...l=1#post138774
    for a hint by Paul
    or search forum for 'ADC PDB' for other PDB code or ADC related discussion

    definitely you should PDB + DMA to get your data into memory and use DMA interrupts to access data and save to disk

  11. #11
    Senior Member
    Join Date
    Dec 2014
    Posts
    253
    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.)

  12. #12
    Senior Member
    Join Date
    Jul 2014
    Posts
    2,157
    In addition, there are only two ADC on Teensy 3.6 so you can provide DMA for only 2 mics.
    Suggest to get 2 PJRC audio boards that allow you to interface up to 4 mics, or build your own multi channel audio interface (e.g. using Paul's latest toy: https://hackaday.io/project/2984-tee...annel-audio-io)

  13. #13
    Junior Member
    Join Date
    May 2017
    Posts
    15
    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?

  14. #14
    Senior Member
    Join Date
    Jan 2013
    Posts
    843
    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

    ...

  15. #15
    Senior Member
    Join Date
    Jan 2013
    Posts
    843
    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 by tni; 05-10-2017 at 10:19 PM.

  16. #16
    Junior Member
    Join Date
    May 2017
    Posts
    15
    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_S PEED);

    ^

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

    ^

    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;

  17. #17

  18. #18
    Junior Member
    Join Date
    May 2017
    Posts
    15
    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.

  19. #19
    Junior Member
    Join Date
    May 2017
    Posts
    15
    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

  20. #20
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,385
    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.

  21. #21
    Senior Member+ Frank B's Avatar
    Join Date
    Apr 2014
    Location
    Germany NRW
    Posts
    5,567
    Quote Originally Posted by Ctsi View Post
    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

  22. #22
    Junior Member
    Join Date
    May 2017
    Posts
    15
    I'm sorry can you clarify if you can, I didn't quite understand what you meant as "first data". Thanks

  23. #23
    Senior Member+ Theremingenieur's Avatar
    Join Date
    Feb 2014
    Location
    Colmar, France
    Posts
    2,385
    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.

  24. #24
    Junior Member
    Join Date
    May 2017
    Posts
    15
    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.

  25. #25
    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.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •