Questions about that awesome but super advanced ADC code

Status
Not open for further replies.

srtr

Member
Hi all, I'm playing around with some ADC code I found in another post in this forum (specifically, the DMA version). In another thread, PaulStoffregen gives multiple warnings that this code is too advanced for most programmers, and that editing it is a difficult task that requires deep knowledge of the source code, which is so advanced. So of course, I have to go around and mess with it :D

I tested it on my Teensy 3.6 and it seems to work very nicely. I read through the code several times and even tried changing a few parameters to see how it behaves. Although I don't understand the source code, I'd like to see if I can use it to turn the Teensy into an oscilloscope of sorts, writing the ADC data to USB serial.

But I'm a n00b, so I have questions...

First, here is the code, as originally posted there (* except for one edit, see below):

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

// connect out_pins to adc pins, PWM output on out pins will be measured.

const uint8_t adc0_pin0 = A14;  // digital pin 33, on ADC0
const uint8_t adc0_pin1 = A15;  // digital pin 34, on ADC0
const uint8_t adc1_pin0 = A12;  // digital pin 31, on ADC1
const uint8_t adc1_pin1 = A13;  // digital pin 32, on ADC1

constexpr std::array<uint8_t, 4> adc_pins = { adc0_pin0, adc0_pin1, adc1_pin0, adc1_pin1 };
constexpr std::array<uint8_t, 4> out_pins = { 5, 6, 9, 10 };

ADC adc;
std::array<ADC_Module*, 2> adc_modules;
static_assert(ADC_NUM_ADCS == 2, "Two ADCs expected.");

auto& serial = Serial;

const size_t buffer_size = 1000;
std::array<std::array<volatile uint16_t, buffer_size>,  4> buffers;

std::array<volatile uint32_t*, 4> adc_result_registers = { &ADC0_RA, &ADC0_RB, &ADC1_RA, &ADC1_RB };
std::array<DMAChannel, 4> dma_channels;
constexpr std::array<int, 4> dma_triggers = { DMAMUX_SOURCE_FTM3_CH0, DMAMUX_SOURCE_FTM3_CH1, DMAMUX_SOURCE_FTM3_CH2, DMAMUX_SOURCE_FTM3_CH3 };

// CMSIS PDB
#define PDB_C1_EN_MASK                           0xFFu
#define PDB_C1_EN_SHIFT                          0
#define PDB_C1_EN(x)                             (((uint32_t)(((uint32_t)(x))<<PDB_C1_EN_SHIFT))&PDB_C1_EN_MASK)
#define PDB_C1_TOS_MASK                          0xFF00u
#define PDB_C1_TOS_SHIFT                         8
#define PDB_C1_TOS(x)                            (((uint32_t)(((uint32_t)(x))<<PDB_C1_TOS_SHIFT))&PDB_C1_TOS_MASK)
#define PDB_C1_BB_MASK                           0xFF0000u
#define PDB_C1_BB_SHIFT                          16
#define PDB_C1_BB(x)                             (((uint32_t)(((uint32_t)(x))<<PDB_C1_BB_SHIFT))&PDB_C1_BB_MASK)

size_t bufferWriteIndex(size_t channel) {
    uintptr_t buffer_start = uintptr_t(buffers[channel].data());
    uintptr_t dma_pos = uintptr_t(dma_channels[channel].destinationAddress());
    return (dma_pos - buffer_start) / sizeof(uint16_t);
}

void setup() {
    for(size_t i = 0; i < adc_modules.size(); i++) adc_modules[i] = adc.adc[i];
    for(auto pin : adc_pins) pinMode(pin, INPUT);

    serial.begin(9600);
    delay(2000);
    serial.println("Starting");

    for(auto adc_module : adc_modules) {
        adc_module->setAveraging(1);
        adc_module->setResolution(12);
        adc_module->setConversionSpeed(ADC_CONVERSION_SPEED::MED_SPEED);
        adc_module->setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED);
    }
    
    // perform ADC input mux setup; the ADC library doesn't handle the B-set of registers
    // so we copy the config over
    adc.adc0->analogRead(adc0_pin1);
    ADC0_SC1B = ADC0_SC1A;
    adc.adc0->analogRead(adc0_pin0);

    adc.adc1->analogRead(adc1_pin1);
    ADC1_SC1B = ADC1_SC1A;
    adc.adc1->analogRead(adc1_pin0);

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

    for(auto adc_module : adc_modules) adc_module->stopPDB();
    // conversion will be triggered by PDB
    for(auto adc_module : adc_modules) adc_module->setHardwareTrigger();

    // enable PDB clock
    SIM_SCGC6 |= SIM_SCGC6_PDB;

    // Sample at 100'000 Hz 
    constexpr uint32_t trigger_frequency = 100000;
    constexpr uint32_t mod = (F_BUS / trigger_frequency);
    static_assert(mod <= 0x10000, "Prescaler required.");

    // The K66 ADC conversion complete DMA triggering is completely broken for multi-channel conversions.
    // PDB DMA triggering doesn't work correctly, DMA channel linking for FTM-triggered DMA transfers
    // is also broken and doesn't correctly deassert COCO.
    // Thus, 4 timer channels are used to trigger DMA. The FTM counter at half point triggers DMA
    // for ADC A channel, the counter at MOD triggers DMA for ADC channel B.

    FTM3_SC = 0;
    FTM3_CNT = 0;
    FTM3_MOD = uint16_t(mod - 1);

    // output compare mode, trigger DMA when counter reaches FTM3_CxV
    uint32_t ftm_channel_config = FTM_CSC_CHIE | FTM_CSC_DMA | FTM_CSC_MSA | FTM_CSC_ELSA;
    FTM3_C0SC = ftm_channel_config;
    FTM3_C1SC = ftm_channel_config;
    FTM3_C2SC = ftm_channel_config;
    FTM3_C3SC = ftm_channel_config;

    FTM3_C0V = uint16_t(mod / 2 - 1);      // DMA trigger for ADC0 A
    FTM3_C1V = FTM3_MOD;                   // DMA trigger for ADC0 B
    FTM3_C2V = uint16_t(mod / 2 - 1);      // DMA trigger for ADC1 A
    FTM3_C3V = FTM3_MOD;                   // DMA trigger for ADC1 B
    FTM3_EXTTRIG = FTM_EXTTRIG_INITTRIGEN; // external trigger at counter overflow --> trigger PDB

    PDB0_MOD = (uint16_t)(mod-1);

    uint32_t pdb_ch_config = PDB_C1_EN (0b11) | // enable ADC A and B channel
                             PDB_C1_TOS(0b11) | // enables the channel delay
                             PDB_C1_BB (0b00);  // back-to-back trigger disabled
    PDB0_CH0C1 = pdb_ch_config; // ADC 0
    PDB0_CH1C1 = pdb_ch_config; // ADC 1

    // ADC0 A and ADC1 A conversions are triggered immediately, ADC0 B and ADC1 B at the half point
    PDB0_CH0DLY0 = 0;
    PDB0_CH0DLY1 = uint16_t(mod / 2 - 1);
    PDB0_CH1DLY0 = 0;
    PDB0_CH1DLY1 = uint16_t(mod / 2 - 1);

    const uint32_t pdb_base_conf = PDB_SC_TRGSEL(0b1011) |               // triggered by FTM3
                                   PDB_SC_PDBEN |                        // enable
                                   PDB_SC_PRESCALER(0) | PDB_SC_MULT(0); // count at F_BUS
                                   
    // sync buffered registers
    PDB0_SC = pdb_base_conf | PDB_SC_LDOK;
    
    for(size_t i = 0; i < 4; i++) {
        DMAChannel& dma = dma_channels[i];
        dma.source(*(uint16_t*) adc_result_registers[i]);
        dma.destinationBuffer(buffers[i].data(), sizeof(buffers[i]));
        dma.triggerAtHardwareEvent(dma_triggers[i]);
        dma.enable();
    }

    FTM3_SC = (FTM_SC_CLKS(1) | FTM_SC_PS(0));  // start FTM, run at F_BUS 

    // PWM output on out pins for testing purposes.
    for(auto out_pin : out_pins) analogWriteFrequency(out_pin, 5000);
    for(size_t i = 0; i < out_pins.size(); i++) analogWrite(out_pins[i], (i + 1) * 50);

    delay(500);
}

void loop() {
    // print buffer right after the first 100 elements have been updated
    while(bufferWriteIndex(1) >= 100) ;
    while(bufferWriteIndex(1) < 100) ;

    // Print first measurements in buffer.
    for(size_t i = 0; i < 100; i++) {
        serial.printf("%3u,%4u,%4u,%4u,%4u\n", i, buffers[0][i], buffers[1][i], buffers[2][i], buffers[3][i]);
    }
    serial.println();

    delay(5000);
}

* Edit I changed the if statement handling ADC errors according to Paul's suggestion, since the original code doesn't compile.

(In my tests I removed the last two lines, serial.println() and delay(5000) in order to have the program run continuously without pausing. I also removed the PWM output parts, since I can use an AWG to generate a signal for the ADCs to read.)

I have the following questions:

1. That Serial.printf() in loop() ouputs the first 100 samples in the buffer filled up by the ADCs. However, does the ADC sampling pause while Serial.printf() is running, or is the buffer continuously updated independent of what is going on inside loop()?

2. I want to print the entire 1000 samples from the buffer (actually, 4x1000 since it's sampling 4 pins) instead of the first 100 samples. Is it just a matter of changing the index in the for-loop to tick up to 1000? What about the while loops?
Code:
    // print buffer right after the first 100 elements have been updated
    while(bufferWriteIndex(1) >= 100) ;
    while(bufferWriteIndex(1) < 100) ;

    // Print first measurements in buffer.
    for(size_t i = 0; i < 100; i++) {
        serial.printf("%3u,%4u,%4u,%4u,%4u\n", i, buffers[0][i], buffers[1][i], buffers[2][i], buffers[3][i]);
In my experiments, I also changed the limit in the while loop to 999 instead of 100, but I admit I'm not entirely sure how the while loops regulate the whole affair. (Changing them to 1000 doesn't seem to work.)

3. I put one of the digital pins to HIGH then LOW during the loop, and then hooked that pin to an oscilloscope. This allows me to measure the program cycle time, which seems to be locked into executing at 100 Hz. Looking at the following line:
Code:
// Sample at 100'000 Hz 
constexpr uint32_t trigger_frequency = 100000;
If I change trigger_frequency to 200000 my oscilloscope indicates that the program now executes 200 times per second. Is that all it takes to increase the sampling frequency in this program? Or does this break something else in some sneaky way? Actually 100 kHz is perfect for my project, I'm just curious to know if this program can be easily made to run at 200 kHz.. :)

4. As I increase the number of samples the serial.printf() is writing to above ~250, the for loop that contains it starts to take longer than 10 ms to execute (10 ms = 1 / 100 Hz, the "fixed" program cycle time). I can see this effect clearly in my oscilloscope. If the ADCs are sampling continuously (Question 1 above), then that means the buffer is overwritten by the ADCS before serial.printf() writes it to USB serial - correct? What would be the best way to avoid this? Right now I'm playing with Serial.write(), which executes MUCH faster. However, with Serial.write() I need to format the data first, and I haven't found a good/fast way to do that yet.

I think that's all for now. I would be very grateful if anyone could help me answer these questions and dissect this code a little bit :)
 
Last edited:
Status
Not open for further replies.
Back
Top