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
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):
* 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?
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:
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
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]);
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;
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: