Teensy 3.6 Triggered Signal Averaging and Buffered Acquisition

Status
Not open for further replies.

machdisk

Member
Before explaining the steps I've taken to date, I'd like to first describe the technical scope of my challenge. In short, I would like to acquire data (ADC) at a rate of 250,000 kSps that is synchronized with a trigger that occurs every 10-40 Hz. Additionally, it is necessary to perform signal averaging on the data as the actual signal is quite noisy (typically a couple of hundred averages is needed) so I would like to average the readings from the respective data bins as defined by the trigger.

[TL : DR]

How to sample multiple ADCs continuously while simultaneously stream data through the USB?

From my experimentation using the links and discussions below, I gather that a single buffer collecting all of the data at 12-bit resolution is simply not possible and that a buffering approach may be best through a combination of PDB and DMA. However, this raises the question as to how to synchronize the buffer to the trigger especially if you need a circular/ring buffer to capture the data? Is there a way to tag/timestamp the values with respect to the trigger and still stream the information?

My initial thought was to simply acquire data on two channels with ADC0 sampling the input trigger and ADC1 sample the actual signal, send the data back to the PC, and fold the data appropriately using some other script based upon the rising edges recorded on the ADC0 channel. Is this a rational approach or is there a better option?

Digging through the forums the range of examples from tni have been extremely helpful: https://forum.pjrc.com/threads/43708-Teensy-3-6-Datalogging-at-10kHz In this example I can get the sampling rates needed but it is unclear to me how to trigger the byte stream back to the PC. My guess is that I need to detect when the buffer is full and transfer the last half before it is overwritten. This seems to be where I'm running into a lack of knowledge regarding how to trigger an interrupt and use a circular/ring buffer appropriately.


Other potential/partial solutions investigated:
 
There is an ADC library that does automated reads.

Teensy 3.x, LC ADC library https://github.com/pedvide/ADC Copyright (c) 2017 Pedro Villanueva

This included sample seems to have them on a DMA schedule: ...\hardware\teensy\avr\libraries\ADC\examples\ringBufferDMA\ringBufferDMA.ino
 
When you say automated reads, do you mean with respect to the DMA? This surely looks like a key library to consider (the non-functioning example I'm working on uses it) but I'm not sure how exactly and when the ringBufferDMA calls its isr. Is it when it is full? If that's the case then I need to assume at least two things: 1.) that both buffers have been filled equally and 2.) that I need to transfer 1/2 the buffer via serial within the dma's isr?

Finally, is it appropriate to use Serial in the isr or just set a flag that gets caught in the loop to then print out the values?
 
I meant automated by the ADC library - yes, in this case it seems it is by DMA.

No, serial print from an _isr is not good practice. Just found that example by name as an example to start with. I see it only buffers 8 at a time - USB packets are 64 bytes of data - so working up to that to pass a full buffer would be most efficient.

Not sure the next step to get continuous run and offloading data ahead of it being overwritten … but that should be possible which seems to be the goal here.
 
I'm still not entirely sure how to effectively send the binary data in chunks once the buffer is full. Should this be done in chunks or as soon as they become available?

After banging my forehead for a while I saw that there is an issue associated with the RingBufferDMA and ADC_1 (https://github.com/pedvide/ADC/issues/34). Basically, the correct DMA/ADC pair was not being assigned in the pedvide code. In case this serves someone else I'll pass it on:

Cheers.

Code:
/*
*   It doesn't work for Teensy LC yet!
*   Added example for two pins on ADC_0 and ADC_1 
*   Added a couple of lines to ensure the correct DMA setup on ADC_1
*/

#include "ADC_Module.h"
#include "ADC.h"
#include "RingBufferDMA.h"


const uint8_t readPin = A9;
const uint8_t readPin1 = A12; // digital pin 31, on ADC1

ADC *adc = new ADC(); // adc object


// Define the array that holds the conversions here.
// buffer_size must be a power of two.
// The buffer is stored with the correct alignment in the DMAMEM section
// the +0 in the aligned attribute is necessary b/c of a bug in gcc.
const uint8_t buffer_size = 8;
DMAMEM static volatile int16_t __attribute__((aligned(buffer_size+0))) buffer[buffer_size];

// use dma with ADC0
RingBufferDMA *dmaBuffer = new RingBufferDMA(buffer, buffer_size, ADC_0);

#if ADC_NUM_ADCS>1
const int buffer_size1 = 8;
DMAMEM static volatile int16_t __attribute__((aligned(buffer_size1+0))) buffer1[buffer_size1];
// use dma with ADC1

RingBufferDMA *dmaBuffer1 = new RingBufferDMA(buffer1, buffer_size1, ADC_1);
#endif // defined

void setup() {

    pinMode(LED_BUILTIN, OUTPUT);
    pinMode(readPin, INPUT); //pin 23 single ended
    pinMode(readPin1, INPUT); //pin on ADC_1

    Serial.begin(9600);

    // reference can be ADC_REFERENCE::REF_3V3, ADC_REFERENCE::REF_1V2 (not for Teensy LC) or ADC_REF_EXT.
    //adc->setReference(ADC_REFERENCE::REF_1V2, ADC_0); // change all 3.3 to 1.2 if you change the reference to 1V2

    adc->setAveraging(8); // set number of averages
    adc->setResolution(12); // set bits of resolution

    // always call the compare functions after changing the resolution!
    //adc->enableCompare(1.0/3.3*adc->getMaxValue(ADC_0), 0, ADC_0); // measurement will be ready if value < 1.0V
    //adc->enableCompareRange(1.0*adc->getMaxValue(ADC_1)/3.3, 2.0*adc->getMaxValue(ADC_1)/3.3, 0, 1, ADC_1); // ready if value lies out of [1.0,2.0] V

    // enable DMA and interrupts
    adc->enableDMA(ADC_0);
    // ADC interrupt enabled isn't mandatory for DMA to work.
    adc->enableInterrupts(ADC_0);

    #if ADC_NUM_ADCS>1
    adc->setAveraging(8, ADC_1); // set number of averages
    adc->setResolution(12, ADC_1); // set bits of resolution
    adc->enableDMA(ADC_1);
    adc->enableInterrupts(ADC_1);
    #endif
    
}

char c=0;


void loop() {

     if (Serial.available()) {
      c = Serial.read();
      if(c=='s') { // start dma
            Serial.println("Start DMA");
            dmaBuffer->start(&dmaBuffer_isr);
      } else if(c=='d') { // start dma
            Serial.println("Start DMA 1");
            dmaBuffer1->start(&dmaBuffer1_isr);            
            dmaBuffer1->dmaChannel->disable();//added to get around the ring buffer issue: https://github.com/pedvide/ADC/issues/34
            dmaBuffer1->dmaChannel->triggerAtHardwareEvent(DMAMUX_SOURCE_ADC1); // start DMA channel when ADC finishes a conversion
            dmaBuffer1->dmaChannel->enable();

            

      } else if(c=='c') { // start conversion
          Serial.print("Conversion: ");
          adc->analogRead(readPin, ADC_0);
      } else if(c=='v') { // start conversion
          Serial.print("Conversion: ");
          adc->analogRead(readPin1, ADC_1);
      } else if(c=='p') { // print buffer
          printBuffer();
      } else if(c=='l') { // toggle led
          digitalWriteFast(LED_BUILTIN, !digitalReadFast(LED_BUILTIN));
      } else if(c=='r') { // read
          Serial.print("read(): ");
          Serial.println(dmaBuffer->read());
      } else if(c=='f') { // full?
          Serial.print("isFull(): ");
          Serial.println(dmaBuffer->isFull());
      } else if(c=='e') { // empty?
          Serial.print("isEmpty(): ");
          Serial.println(dmaBuffer->isEmpty());
      }
  }


    //digitalWriteFast(LED_BUILTIN, !digitalReadFast(LED_BUILTIN));
    delay(100);
}

void dmaBuffer_isr() {
    digitalWriteFast(LED_BUILTIN, !digitalReadFast(LED_BUILTIN));
    Serial.println("******dmaBuffer_isr");
    // update the internal buffer positions
    dmaBuffer->dmaChannel->clearInterrupt();
}


void dmaBuffer1_isr() {
    digitalWriteFast(LED_BUILTIN, !digitalReadFast(LED_BUILTIN));
    Serial.println("******dmaBuffer1_isr");
    // update the internal buffer positions
    dmaBuffer1->dmaChannel->clearInterrupt();
}


// it can be called everytime a new value is converted. The DMA isr is called first
void adc0_isr(void) {
    //int t = micros();
    Serial.println("ADC0_ISR"); //Serial.println(t);
    adc->adc0->readSingle(); // clear interrupt
}


// it can be called everytime a new value is converted. The DMA isr is called first
#if ADC_NUM_ADCS>1
void adc1_isr() {
    //int t = micros();
    Serial.println("ADC1_ISR"); //Serial.println(t);
    adc->adc1->readSingle();
    //digitalWriteFast(LED_BUILTIN, !digitalReadFast(LED_BUILTIN) );
}
#endif


void printBuffer() {
    Serial.println("Buffer: Address, Value");

    uint8_t i = 0;
    // we can get this info from the dmaBuffer object, even though we should have it already
    volatile int16_t* buffer = dmaBuffer->buffer();
    for (i = 0; i < dmaBuffer->size(); i++) {
        Serial.print(uint32_t(&buffer[i]), HEX);
        Serial.print(", ");
        Serial.println(buffer[i]);
    }

}
 
Keeping with the signal averaging thread, I've been exploring different ideas on how to realize that goal over the course of multiple seconds all with a data acquisition rate approaching 250 kSps across more than one ADC. After some exploration with the USB and different logging strategies, I seem to be maxing out the capacity of the USB transfer rate (https://forum.pjrc.com/threads/54647-Fast-Data-Logging-over-USB?highlight=Serial.write). The data acquisition strategy using SdFs is one potential solution but the file sizes generated there are quite large and I'd like to find a way to be more efficient as my ultimate goal is signal averaging synced with an external trigger--in many ways like a deep memory scope but I have no idea how one of those works.

Towards that end, I obtained one of the flash memory expansions (https://bolderflight.com/products/teensy/flash/). I've managed to get the examples from SerialFlash working (https://github.com/PaulStoffregen/SerialFlash) but wonder if there is a way to append values to an array already written to memory? I've looked through the examples but I'm not sure whether this is possible.

As a though experiment, let's suppose I need to acquire data for 1 second at 250 kSps. An input trigger arrives and resets the counter, writing the data from the DMA/PDB acquisition using the ringbuffer to the flash. Then a second trigger comes in 1 second later and the index we care about resent. The question is how do I add the next value to the correct index on the file saved to the flash? Just acquiring the data in a stream and folding it seems inefficient.

Would the best way to accomplish this goal be to read the data from flash, add the two numbers (data just acquired and that previously saved to flash), and then write it back to flash? Will that be fast enough even with a ring buffer?

Any advice would be appreciated.
 
Status
Not open for further replies.
Back
Top