Pedvide's ADC library - multiple channel simultaneous & continuous acquisition

Status
Not open for further replies.

macaba

Well-known member
Hi all,

I've got my Teensy 3.6 acquiring from 2 channels, using the 2 ADCs, continuously using Pedvide's ADC library. (with the practical settings I've picked, I get adc0 interrupts at a jitter-free 156kHz [12-bit, averaging:4, No USB mode])

What's the ideal way to get continuous acquisition from 4 channels, (2 channels per ADC) at 78kHz?

In an ideal world I'd get an interrupt with the result from all 4 channels, though an interrupt per 2 channel pair wouldn't be an issue.

I've seen mention of the PDB module but no clear example and I suspect it wouldn't use the continuous running bit that makes the ADC run as fast as possible. I have a suspicion that the solution might involve getting the DMA to change the channel assignment bits before the next acquisition starts but I'm not sure this will be possible in continuous acquisition mode.

Any thoughts?
 
The Kinetis ADC multichannel support is poorly designed and tricky to use. And no, there is no library support.

If you want jitter-free conversions with minimal channel-switching overhead, your only option is using the primitive PDB multichannel support. The PDB is a very finicky and buggy beast. You can use the PDB to trigger conversions on 4 channels, 2 per ADC (and perform the ADC input mux switching). Each ADC has 2 conversion register sets (control, status, result). The channel is selected via ADC0_SC1A / ADC0_SC1B / ADC1_SC1A / ADC1_SC1B. The results end up in ADC0_RA / ADC0_RB / ADC1_RA / ADC1_RB.

The PDB is mis-designed and you can't continously trigger conversions when using both ADCs in parallel. Only back-to-back triggering between A / B channels works. So you need to figure out how long the ADC conversions take and use a slightly lower trigger frequency for the set of 4 ADC conversions. E.g., increase the PDB trigger frequency until things hang. When a ADC conversion is in progress and the PDB is triggered, it will detect an ADC sequencing error and stop.

The K66 manual chapters for ADC and PDB are mandatory reading.

Below is sample code that reads 4 channels. There is PWM output on 4 pins (to be connected to 4 input pins) as test signal.

(If something appears to be done in a strange way, there likely is a reason. The ADC library had/has various bugs and the hardware doesn't necessarily do what the documentation says.)

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

// 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 = { 7, 8, 9, 10 };

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

auto& serial = Serial;

struct Measurement {
    std::array<volatile uint16_t, 4> v;
};

std::array<Measurement, 1000> buffer;
volatile size_t write_pos = 0;

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


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.adc1->fail_flag) {
        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 pdb_trigger_frequency = 100000;
    constexpr uint32_t mod = (F_BUS / pdb_trigger_frequency);
    static_assert(mod <= 0x10000, "Prescaler required.");
    PDB0_MOD = (uint16_t)(mod-1);

    uint32_t pdb_ch_config = PDB_C1_EN (0b11) | // enable ADC A and B channel
//                             PDB_C1_TOS(0b00) | // trigger output select, no delay, bypass DLY --> doesn't work
                             PDB_C1_TOS(0b11) | // this enables the channel delay, which we don't really want, 
                                                // but the PDB appears to have a hardware bug and this 
                                                // needs to be set
                             PDB_C1_BB (0b10);  // back-to-back trigger; B triggered by A conversion complete
    PDB0_CH0C1 = pdb_ch_config; // ADC 0
    PDB0_CH1C1 = pdb_ch_config; // ADC 1

    // all channel delays are 0; ADC conversions are triggered as soon as possible
    PDB0_CH0DLY0 = 0;
    PDB0_CH0DLY1 = 0;
    PDB0_CH1DLY0 = 0;
    PDB0_CH1DLY1 = 0;

    // sync buffered registers
    PDB0_SC = ADC_PDB_CONFIG | PDB_SC_PRESCALER(0) | PDB_SC_MULT(0) | PDB_SC_LDOK;
   
    // enable interrupt for ADC1, second conversion (B-channel)
    ADC1_SC1B |= ADC_SC1_AIEN;
    NVIC_ENABLE_IRQ(IRQ_ADC1);

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

    // 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(write_pos >= 100) ;
    while(write_pos < 100) ;
   
    // Print first measurements in buffer.
    for(size_t i = 0; i < 100; i++) {
        auto v = buffer[i].v;
        serial.printf("%3u,%4u,%4u,%4u,%4u\n", i, v[0], v[1], v[2], v[3]);
    }
    serial.println();
    
    delay(5000);
}

void adc1_isr() {
    size_t write_pos_ = write_pos;
    // reading the result clears the interrupt flag
    buffer[write_pos_] = { ADC0_RA, ADC0_RB, ADC1_RA, ADC1_RB };
    write_pos_++;
    if(write_pos_ >= buffer.size()) write_pos_ = 0;
    write_pos = write_pos_;
}

Expected output (you can see that channel 1 / 3 start in parallel, 2 / 4 are sampled after their conversion is complete):
Code:
  0,   2,4095,   4,4095
  1,4095,4095,4095,4095
  2,4095,4095,4095,4095
  3,4095,4095,4095,4095
  4,4095,4095,4095,4095
  5,   1,4095,4095,4095
  6,   4,4095,4095,4095
  7,   2,4095,4095,4095
  8,   2,   1,4095,4095
  9,   1,   1,4095,4095
 10,   2,   1,4095,4095
 11,   1,   1,4095,4095
 12,   1,   1,   4,4095
 13,   2,   1,   2,4095
 14,   2,   1,   4,4095
 15,   2,   1,   5,4095
 16,   1,   1,   4,   1
 17,   1,   1,   4,   1
 18,   1,   1,   3,   1
 19,   3,   1,   5,   1
 20,   2,4095,   4,4095
 21,4095,4095,4095,4095
 22,4095,4095,4095,4095
 23,4095,4095,4095,4095
 24,4095,4095,4095,4095
 25,   3,4095,4095,4095
 26,   1,4095,4095,4095
 27,   4,4095,4095,4095
 28,   1,   1,4095,4095
 29,   1,   1,4095,4095
 30,   1,   1,4095,4095
 31,   1,   1,4095,4095
 32,   1,   1,   2,4095
 33,   1,   1,   4,4095
 34,   1,   1,   4,4095
 35,   2,   1,   5,4095
 36,   1,   1,   5,   1
 37,   1,   1,   3,   1
 38,   1,   1,   4,   1
 39,   1,   1,   5,   1
 ...
 
Thank you! Kudos from me.

I'll give this a go, your explanation is fantastic.
I've got a low impedance driving my ADC inputs so I should be able to set averaging to 1 to give me much more timing headroom between ADC conversion time and my desired PDB sample rate (about 100kHz for 4 channels would be ideal for my digital SMPS application). From looking through the sample code, I guess the key point is to read the ADC0_RA, ADC0_RB, ADC1_RA, ADC1_RB registers near the beginning of the interrupt before the PDB triggers the next set of conversions. (and obviously the PDB shouldn't trigger a new set of conversions until the 3rd/4th channel has finished converting, as you said.)

My project is turning out to be a fun one with regards to timing - as it stands, I've got 2 channels at 156kHz with 2 PID loops inside the ADC interrupt - uses about 11% time in interrupt at 180MHz, so I expect 4 channels at 100kHz with 4 PID loops to be achievable with plenty of CPU time left in the main thread for background tasks like WIZ850io ethernet and I2C comms.
 
Last edited:
Yes, the interrupt is time-critical. You have to read the result / clear the COCO flag before the next PDB trigger happens.

The hardware is rather buggy when using DMA with A and B ADC channels. Triggering DMA transfers via PDB doesn't work correctly. De-asserting COCO is broken with chained DMA transfers.

Below is a DMA version. It uses 4 timer channels to work around the hardware bugs. The output pins are changed, since FTM3 is tied up for the DMA triggering.

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.adc1->fail_flag) {
        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);
}
 
A small update:

I've been testing the top end speed using the non-DMA sketch above. I can get upto 266kHz over 4 channels which is brilliant.
Things to be wary of during testing - sometimes you can go slightly higher than 266kHz but in actual fact, the ADC0 data is lost (without triggering a fault it seems). Go too high and it all falls over (to be expected).
With 4 PID loops updated in the ADC1 interrupt it takes 20% CPU (I need low latency between ADC and DAC so no buffering).

I don't need 266kHz so I'll heading back down to 100-150kHz which satisfies all my design criteria and gives a little bit of headroom.
(Running at too high speed would make it very sensitive to faulting. I don't know the exact details of what higher priority interrupts exist in Teensyduino, systick & USB perhaps?)

Thank you tni.
 
As practical confirmation of the theory - when looking on the oscilloscope with infinite persistence turned on (150kHz PDB rate), the difference between 'USB Type: Serial' and 'USB Type: No USB' on the ADC1 interrupt jitter is clear.

With USB:
Jitter.png

Without USB:
No Jitter.png

My design uses a WIZ850io for offloaded network IO so no USB required - something to keep in mind to anyone looking to do this kind of acquisition in their design although the jitter only really matters if you're doing something like setting a DAC output in the interrupt (which I am)
 
Last edited:
FYI for anyone who comes across this thread - I wanted to use USB during development for debug output, so I used this macro to eliminate the interrupt jitter:
NVIC_SET_PRIORITY(IRQ_USBOTG, 200);
 
Modification for simultaneous sampling of 1 channel per ADC (instead of 2)

The Kinetis ADC multichannel support is poorly designed and tricky to use. And no, there is no library support.

If you want jitter-free conversions with minimal channel-switching overhead, your only option is using the primitive PDB multichannel support. The PDB is a very finicky and buggy beast. You can use the PDB to trigger conversions on 4 channels, 2 per ADC (and perform the ADC input mux switching). Each ADC has 2 conversion register sets (control, status, result). The channel is selected via ADC0_SC1A / ADC0_SC1B / ADC1_SC1A / ADC1_SC1B. The results end up in ADC0_RA / ADC0_RB / ADC1_RA / ADC1_RB.

The PDB is mis-designed and you can't continously trigger conversions when using both ADCs in parallel. Only back-to-back triggering between A / B channels works. So you need to figure out how long the ADC conversions take and use a slightly lower trigger frequency for the set of 4 ADC conversions. E.g., increase the PDB trigger frequency until things hang. When a ADC conversion is in progress and the PDB is triggered, it will detect an ADC sequencing error and stop.

The K66 manual chapters for ADC and PDB are mandatory reading.

Below is sample code that reads 4 channels. There is PWM output on 4 pins (to be connected to 4 input pins) as test signal.

(If something appears to be done in a strange way, there likely is a reason. The ADC library had/has various bugs and the hardware doesn't necessarily do what the documentation says.)

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

// 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 = { 7, 8, 9, 10 };

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

auto& serial = Serial;

struct Measurement {
    std::array<volatile uint16_t, 4> v;
};

std::array<Measurement, 1000> buffer;
volatile size_t write_pos = 0;

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


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.adc1->fail_flag) {
        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 pdb_trigger_frequency = 100000;
    constexpr uint32_t mod = (F_BUS / pdb_trigger_frequency);
    static_assert(mod <= 0x10000, "Prescaler required.");
    PDB0_MOD = (uint16_t)(mod-1);

    uint32_t pdb_ch_config = PDB_C1_EN (0b11) | // enable ADC A and B channel
//                             PDB_C1_TOS(0b00) | // trigger output select, no delay, bypass DLY --> doesn't work
                             PDB_C1_TOS(0b11) | // this enables the channel delay, which we don't really want, 
                                                // but the PDB appears to have a hardware bug and this 
                                                // needs to be set
                             PDB_C1_BB (0b10);  // back-to-back trigger; B triggered by A conversion complete
    PDB0_CH0C1 = pdb_ch_config; // ADC 0
    PDB0_CH1C1 = pdb_ch_config; // ADC 1

    // all channel delays are 0; ADC conversions are triggered as soon as possible
    PDB0_CH0DLY0 = 0;
    PDB0_CH0DLY1 = 0;
    PDB0_CH1DLY0 = 0;
    PDB0_CH1DLY1 = 0;

    // sync buffered registers
    PDB0_SC = ADC_PDB_CONFIG | PDB_SC_PRESCALER(0) | PDB_SC_MULT(0) | PDB_SC_LDOK;
   
    // enable interrupt for ADC1, second conversion (B-channel)
    ADC1_SC1B |= ADC_SC1_AIEN;
    NVIC_ENABLE_IRQ(IRQ_ADC1);

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

    // 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(write_pos >= 100) ;
    while(write_pos < 100) ;
   
    // Print first measurements in buffer.
    for(size_t i = 0; i < 100; i++) {
        auto v = buffer[i].v;
        serial.printf("%3u,%4u,%4u,%4u,%4u\n", i, v[0], v[1], v[2], v[3]);
    }
    serial.println();
    
    delay(5000);
}

void adc1_isr() {
    size_t write_pos_ = write_pos;
    // reading the result clears the interrupt flag
    buffer[write_pos_] = { ADC0_RA, ADC0_RB, ADC1_RA, ADC1_RB };
    write_pos_++;
    if(write_pos_ >= buffer.size()) write_pos_ = 0;
    write_pos = write_pos_;
}

Expected output (you can see that channel 1 / 3 start in parallel, 2 / 4 are sampled after their conversion is complete):
Code:
  0,   2,4095,   4,4095
  1,4095,4095,4095,4095
  2,4095,4095,4095,4095
  3,4095,4095,4095,4095
  4,4095,4095,4095,4095
  5,   1,4095,4095,4095
  6,   4,4095,4095,4095
  7,   2,4095,4095,4095
  8,   2,   1,4095,4095
  9,   1,   1,4095,4095
 10,   2,   1,4095,4095
 11,   1,   1,4095,4095
 12,   1,   1,   4,4095
 13,   2,   1,   2,4095
 14,   2,   1,   4,4095
 15,   2,   1,   5,4095
 16,   1,   1,   4,   1
 17,   1,   1,   4,   1
 18,   1,   1,   3,   1
 19,   3,   1,   5,   1
 20,   2,4095,   4,4095
 21,4095,4095,4095,4095
 22,4095,4095,4095,4095
 23,4095,4095,4095,4095
 24,4095,4095,4095,4095
 25,   3,4095,4095,4095
 26,   1,4095,4095,4095
 27,   4,4095,4095,4095
 28,   1,   1,4095,4095
 29,   1,   1,4095,4095
 30,   1,   1,4095,4095
 31,   1,   1,4095,4095
 32,   1,   1,   2,4095
 33,   1,   1,   4,4095
 34,   1,   1,   4,4095
 35,   2,   1,   5,4095
 36,   1,   1,   5,   1
 37,   1,   1,   3,   1
 38,   1,   1,   4,   1
 39,   1,   1,   5,   1
 ...

I need some help in modifying this example to trigger ADC0 and ADC1 simultaneously triggering the conversion by PDB as it is done here but I do not need to sample 4 channels, I need only two (i.e. one channel per ADC). Basically, I want each PDB to trigger conversion on both ADC0 and ADC1 channels simultaneously.

I started modifying the code in this way:

Code:
// 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;   // as I don't need the channel B
    //adc.adc0->analogRead(adc0_pin0);

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

I'm fully confused with this set of lines:

Code:
uint32_t pdb_ch_config = PDB_C1_EN (0b11) | // enable ADC A and B channel
//                             PDB_C1_TOS(0b00) | // trigger output select, no delay, bypass DLY --> doesn't work
                             PDB_C1_TOS(0b11) | // this enables the channel delay, which we don't really want, 
                                                // but the PDB appears to have a hardware bug and this 
                                                // needs to be set
                             PDB_C1_BB (0b10);  // back-to-back trigger; B triggered by A conversion complete
    PDB0_CH0C1 = pdb_ch_config; // ADC 0
    PDB0_CH1C1 = pdb_ch_config; // ADC 1

    // all channel delays are 0; ADC conversions are triggered as soon as possible
    PDB0_CH0DLY0 = 0;
    PDB0_CH0DLY1 = 0;
    PDB0_CH1DLY0 = 0;
    PDB0_CH1DLY1 = 0;

    // sync buffered registers
    PDB0_SC = ADC_PDB_CONFIG | PDB_SC_PRESCALER(0) | PDB_SC_MULT(0) | PDB_SC_LDOK;

Please suggest me the modifications I require for my purpose.

I think I don't need these lines as I'm not using the channel B. Am I right?

Code:
// enable interrupt for ADC1, second conversion (B-channel)
 //   ADC1_SC1B |= ADC_SC1_AIEN;
 //   NVIC_ENABLE_IRQ(IRQ_ADC1);

And the last obvious change is:

Code:
void adc1_isr() {
    size_t write_pos_ = write_pos;
    // reading the result clears the interrupt flag
    buffer[write_pos_] = { ADC0_RA, ADC1_RA };//{ ADC0_RA, ADC0_RB, ADC1_RA, ADC1_RB };
    write_pos_++;
    if(write_pos_ >= buffer.size()) write_pos_ = 0;
    write_pos = write_pos_;
}
 
Status
Not open for further replies.
Back
Top