setting right ADC prescaler on the Arduino Due in timer and interrupt driven multi-channel ADC acquisition

1.9k views Asked by At

I am trying to follow, adapt, understand (and clean up a bit) a variation around the code available there, for the Arduino Due: https://forum.arduino.cc/index.php?topic=589213.0 . I do not like the forum format, as things end up buried deep, so asking here instead. Unfortunately this means that there is quite a lot of explanations before the question. If you think this is wrong to post it here, let me know, and I can move.

Basically, the idea is to log several ADC channels in a buffer, using timer-based triggering. There is a bit of setup:

// sample rate in Hz
constexpr int sample_rate = 1000;

constexpr uint8_t channels[] = {7, 6, 5, 4, 3};
constexpr int nbr_channels = sizeof(channels);

Then time counter 0 channel 2 is set at the right frequency for triggering the ADC conversion:

// use time counter 0 channel 2 to generate the ADC start of conversion signal
// i.e. this sets a rising edge with the right frequency for triggering ADC conversions corresponding to sample_rate
// for more information about the timers: https://github.com/ivanseidel/DueTimer/blob/master/TimerCounter.md
// NOTE: TIOA2 should not be available on any due pin https://github.com/ivanseidel/DueTimer/issues/11
void tc_setup() {
  PMC->PMC_PCER0 |= PMC_PCER0_PID29;                       // TC2 power ON : Timer Counter 0 channel 2 IS TC2
  TC0->TC_CHANNEL[2].TC_CMR = TC_CMR_TCCLKS_TIMER_CLOCK2   // clock 2 has frequency MCK/8, clk on rising edge
                              | TC_CMR_WAVE                // Waveform mode
                              | TC_CMR_WAVSEL_UP_RC        // UP mode with automatic trigger on RC Compare
                              | TC_CMR_ACPA_CLEAR          // Clear TIOA2 on RA compare match
                              | TC_CMR_ACPC_SET;           // Set TIOA2 on RC compare match

  constexpr int ticks_per_sample = F_CPU / 8 / sample_rate; // F_CPU / 8 is the timer clock frequency, see MCK/8 setup
  constexpr int ticks_duty_cycle = ticks_per_sample / 2; // duty rate up vs down ticks over timer cycle; use 50%
  TC0->TC_CHANNEL[2].TC_RC = ticks_per_sample;
  TC0->TC_CHANNEL[2].TC_RA = ticks_duty_cycle;

  TC0->TC_CHANNEL[2].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN; // Software trigger TC2 counter and enable
}

Finally this can be used to trigger the ADC:

// start ADC conversion on rising edge on time counter 0 channel 2
// perform ADC conversion on several channels in a row one after the other
// report finished conversion using ADC interrupt
void adc_setup() {
  PMC->PMC_PCER1 |= PMC_PCER1_PID37;                     // ADC power on
  ADC->ADC_CR = ADC_CR_SWRST;                            // Reset ADC
  ADC->ADC_MR |=  ADC_MR_TRGEN_EN |                      // Hardware trigger select
                  ADC_MR_PRESCAL(1) |                    // the pre-scaler: as high as possible for better accuracy, while still fast enough to measure everything
                                                         // see: https://arduino.stackexchange.com/questions/12723/how-to-slow-adc-clock-speed-to-1mhz-on-arduino-due
                  ADC_MR_TRGSEL_ADC_TRIG3;               // Trigger by TIOA2 Rising edge

  ADC->ADC_IDR = ~(0ul);
  ADC->ADC_CHDR = ~(0ul);
  for (int i = 0; i < nbr_channels; i++)
  {
    ADC->ADC_CHER |= ADC_CHER_CH0 << channels[i];
  }
  ADC->ADC_IER |= ADC_IER_EOC0 << channels[nbr_channels - 1];
  ADC->ADC_PTCR |= ADC_PTCR_RXTDIS | ADC_PTCR_TXTDIS;    // Disable PDC DMA
  NVIC_EnableIRQ(ADC_IRQn);                              // Enable ADC interrupt
}

and the ADC output can be captured in the corresponding ISR:

void ADC_Handler() {
for (size_t i = 0; i < nbr_channels; i++)
  {
      SOME_BUFFER[i] = static_cast<volatile uint16_t>( * (ADC->ADC_CDR + channels[i]) & 0x0FFFF ); // get the output
  }
}

I think this is quite understandable, but I have one question: the setting of the pre-scaler.

  • if I understand well discussions online, the pre-scaler should be set so that frq_ADC >= sample_rate * nbr_channels, basically because the chip is just multiplexing the ADC through several channels

  • if I understand well, we want to set such pre-scaler value as high as possible given the previous constraint, so that the ADC frequency is as low as possible, because this improves ADC conversion quality

Is that right?

The problem is that I am confused about how to set the pre-scaler, and what value corresponds to what, because what I find in the datasheet disagree with some other online responses I read.

From the datasheet https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-11057-32-bit-Cortex-M3-Microcontroller-SAM3X-SAM3A_Datasheet.pdf : "The ADC clock range is between MCK/2, if PRESCAL is 0, and MCK/512, if PRESCAL is set to 255 (0xFF).". This is consistent with what I find on page 1334: "ADCClock = MCK / ( (PRESCAL+1) * 2 )". But page 1318, it is written that the conversion rate is 1MHz. Then how is that compatible with having a MCK frequency of 84MHz on the Due? 84/2 = 48MHz, 84/512 = 0.164MHz, the high freq value is too high.

Then to add to the confusion I have found this issue: https://arduino.stackexchange.com/questions/12723/how-to-slow-adc-clock-speed-to-1mhz-on-arduino-due/21054#21054 that also seem to conflict with the 1MHz upper bound.

Any idea where I misunderstand something? (and any more comments around the general working of the program?).

1

There are 1 answers

0
Zorglub29 On

Ok, so I did some tests with the code, checking when I was missing some conversions depending on the timer frequency and the prescaler value. The code is a bit long, so I post it at the end of the answer. Basically:

// pre-scalor analysis using 5 channels;
// quantities indicated are sampling frequency of the 5 channels
// i.e. necessary ADC sampling frequency is 5 x higher, and value
// of the prescaler ps
// --------------------
// 100kHz ps 1 ok
// 100kHz ps 2 ok
// 100kHz ps 3 fail
// 100kHz ps 255 fail
// 100kHz ps 256 ok
// this indicates: prescaler is 8 bits from 0 to 255, after this wraps up
// ADC frequency is max something like 1MHz in practice: 5 * 100 * 2 (may loose a bit
// due to other interrupts hitting ours?)
// --------------------
// 10kHz ps 38 ok
// 10kHz ps 39 fail
// 10 * 5 * 40 = 2000kHz: ADC is lower than 2MHz
// --------------------
// 1kHz ps 255 ok
// --------------------

I think this indicates that:

  • the pre-scaler value is well an 8 bits int, between 0 and 255, as it wraps up at 256

  • I have difficultie matching the results to the formula in the datasheet. I guess this is because there is some overhead switching channels etc (?). For example:

    • the results are consistent with ```ADC_freq = 1MHz / ( ps ) at the highest frequencies, but I suppose this is because there is a bit of overhead switching channels

    • the results are consistent with ```ADC_freq = 2MHz / ( ps ) at 10 kHz, and at 1kHz, even using the highest prescaler is fine.

The code I was using is the following, and the criterion for deciding that things fail is that the code reports a drop in the effective sample frequency over the 5 channels:

// -------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------
// timer driven ADC convertion captured by interrupt on n adc_channels for Arduino Due
//
// this is for Arduino Due only!
//
// the interrupt based ADC measurement is adapted from:
// https://forum.arduino.cc/index.php?topic=589213.0
// i.e. adc_setup(), tc_setup(), ADC_handler() are inspired from the discussion there.
//
// written with VSCode + Platformio and Due board setup
// -------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------

// make my linter happy
#include "Arduino.h"

//--------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------

// some vital ADC grabbing setup

// sample rate in Hz, should be able to go up to several 10s ok kHz at least
constexpr int adc_sample_rate = 1000;

// size of the data buffers "in time"
// i.e. how many consecutive measurements we buffer for each channel
constexpr size_t adc_buffer_nbr_consec_meas = 5;

// the adc_channels to read, in uC reference, NOT in Arduino Due pinout reference
// for a mapping, see: https://components101.com/microcontrollers/arduino-due
// i.e. A0 is AD7
//      A1    AD6
//      A2    AD5
//      A3    AD4
//      A4    AD3
//      A5    AD2
//      A6    AD1
//      A7    AD0
constexpr uint8_t adc_channels[] = {7, 6, 5, 4, 3};
constexpr int nbr_adc_channels = sizeof(adc_channels);

// the buffer containing the measurements for all adc_channels over several measurements in time
volatile uint16_t adc_meas_buffer[adc_buffer_nbr_consec_meas][nbr_adc_channels];

// flag when a full vector of conversions is available
volatile bool adc_flag_conversion = false;

// time index of the current measurement in the adc reads buffer
volatile size_t crrt_adc_meas_buffer_idx = 0;

//--------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------

// some non-vital printing config

// a bit of time tracking, just to analyze how good performance
unsigned long current_us = 0;
unsigned long previous_us = 0;
unsigned long delta_us = 0;
float delta_us_as_s = 0;
float delta_us_as_ms = 0;

int nbr_readings_since_reduced_time_stats = 0;
unsigned long current_reduced_time_stats_us = 0;
unsigned long previous_reduced_time_stats_us = 0;
float delta_reduced_time_stats_us_as_s = 0;
float effective_logging_frequency = 0;

// decide what to print on serial
constexpr bool print_reduced_time_stats = true;
constexpr bool print_time_stats = false;
constexpr bool print_full_buffer = false;

//--------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------

// low level functions for setting clock and ADC

// start ADC conversion on rising edge on time counter 0 channel 2
// perform ADC conversion on several adc_channels in a row one after the other
// report finished conversion using ADC interrupt

// tests about pre-scaler: formula should be: 
// pre-scalor analysis using 5 channels;
// quantities indicated are sampling frequency of the 5 channels
// i.e. necessary ADC sampling frequency is 5 x higher, and value
// of the prescaler ps
// --------------------
// 100kHz ps 1 ok
// 100kHz ps 2 ok
// 100kHz ps 3 fail
// 100kHz ps 255 fail
// 100kHz ps 256 ok
// this indicates: prescaler is 8 bits from 0 to 255, after this wraps up
// ADC frequency is max something like 1MHz in practice: 5 * 100 * 2 (may loose a bit
// due to other interrupts hitting ours?)
// --------------------
// 10kHz ps 38 ok
// 10kHz ps 39 fail
// 10 * 5 * 40 = 2000kHz: ADC is lower than 2MHz
// --------------------
// 1kHz ps 255 ok
// --------------------
// CCL: use ps 2 at 100kHz with 5 channels,  20 at 10kHz, 200 at 1kHz
void adc_setup()
{
  PMC->PMC_PCER1 |= PMC_PCER1_PID37;      // ADC power on
  ADC->ADC_CR = ADC_CR_SWRST;             // Reset ADC
  ADC->ADC_MR |= ADC_MR_TRGEN_EN |        // Hardware trigger select
                 ADC_MR_PRESCAL(200) |    // the pre-scaler: as high as possible for better accuracy, while still fast enough to measure everything
                                          // see: https://arduino.stackexchange.com/questions/12723/how-to-slow-adc-clock-speed-to-1mhz-on-arduino-due
                                          // unclear, asked: https://stackoverflow.com/questions/64243073/setting-right-adc-prescaler-on-the-arduino-due-in-timer-and-interrupt-driven-mul
                 ADC_MR_TRGSEL_ADC_TRIG3; // Trigger by TIOA2 Rising edge

  ADC->ADC_IDR = ~(0ul);
  ADC->ADC_CHDR = ~(0ul);
  for (int i = 0; i < nbr_adc_channels; i++)
  {
    ADC->ADC_CHER |= ADC_CHER_CH0 << adc_channels[i];
  }
  ADC->ADC_IER |= ADC_IER_EOC0 << adc_channels[nbr_adc_channels - 1];
  ADC->ADC_PTCR |= ADC_PTCR_RXTDIS | ADC_PTCR_TXTDIS; // Disable PDC DMA
  NVIC_EnableIRQ(ADC_IRQn);                           // Enable ADC interrupt
}

// use time counter 0 channel 2 to generate the ADC start of conversion signal
// i.e. this sets a rising edge with the right frequency for triggering ADC conversions corresponding to adc_sample_rate
// for more information about the timers: https://github.com/ivanseidel/DueTimer/blob/master/TimerCounter.md
// NOTE: TIOA2 should not be available on any due pin https://github.com/ivanseidel/DueTimer/issues/11
void tc_setup()
{
  PMC->PMC_PCER0 |= PMC_PCER0_PID29;                     // TC2 power ON : Timer Counter 0 channel 2 IS TC2
  TC0->TC_CHANNEL[2].TC_CMR = TC_CMR_TCCLKS_TIMER_CLOCK2 // clock 2 has frequency MCK/8, clk on rising edge
                              | TC_CMR_WAVE              // Waveform mode
                              | TC_CMR_WAVSEL_UP_RC      // UP mode with automatic trigger on RC Compare
                              | TC_CMR_ACPA_CLEAR        // Clear TIOA2 on RA compare match
                              | TC_CMR_ACPC_SET;         // Set TIOA2 on RC compare match

  constexpr int ticks_per_sample = F_CPU / 8 / adc_sample_rate; // F_CPU / 8 is the timer clock frequency, see MCK/8 setup
  constexpr int ticks_duty_cycle = ticks_per_sample / 2;        // duty rate up vs down ticks over timer cycle; use 50%
  TC0->TC_CHANNEL[2].TC_RC = ticks_per_sample;
  TC0->TC_CHANNEL[2].TC_RA = ticks_duty_cycle;

  TC0->TC_CHANNEL[2].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN; // Software trigger TC2 counter and enable
}

// ISR for the ADC ready readout interrupt
// push the current ADC data on all adc_channels to the buffer
// update the time index
// set flag conversion ready
void ADC_Handler()
{
  for (size_t i = 0; i < nbr_adc_channels; i++)
  {
    adc_meas_buffer[crrt_adc_meas_buffer_idx][i] = static_cast<volatile uint16_t>(*(ADC->ADC_CDR + adc_channels[i]) & 0x0FFFF);
  }

  crrt_adc_meas_buffer_idx = (crrt_adc_meas_buffer_idx + 1) % adc_buffer_nbr_consec_meas;

  adc_flag_conversion = true;
}

//--------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------

// a simple script: setup and print information

void setup()
{
  Serial.begin(115200);
  delay(100);

  adc_setup();
  tc_setup();
}

void loop()
{
  if (adc_flag_conversion == true)
  {
    adc_flag_conversion = false;

    if (print_reduced_time_stats)
    {
      nbr_readings_since_reduced_time_stats += 1;

      if (nbr_readings_since_reduced_time_stats == adc_sample_rate)
      {
        current_reduced_time_stats_us = micros();
        delta_reduced_time_stats_us_as_s = static_cast<float>(current_reduced_time_stats_us - previous_reduced_time_stats_us) / 1000000.0;
        effective_logging_frequency = static_cast<float>(adc_sample_rate) / delta_reduced_time_stats_us_as_s;
        previous_reduced_time_stats_us = current_reduced_time_stats_us;

        Serial.print(F("Effective logging freq over nbr spls that should correspond to 1 second: "));
        Serial.println(effective_logging_frequency);

        nbr_readings_since_reduced_time_stats = 0;
      }
    }

    if (print_time_stats)
    {
      current_us = micros();

      delta_us = current_us - previous_us;
      delta_us_as_s = static_cast<float>(delta_us) / 1000000.0;
      delta_us_as_ms = static_cast<float>(delta_us) / 1000.0;

      Serial.println(F("ADC avail at uS"));
      Serial.println(micros());
      Serial.println(F("elapsed us"));
      Serial.println(delta_us);
      Serial.println(F("elapsed ms"));
      Serial.println(delta_us_as_ms);
      Serial.println(F("elapsed s"));
      Serial.println(delta_us_as_s);
      Serial.println(F("updated idx:"));
      size_t last_modified_buffer_idx;
      if (crrt_adc_meas_buffer_idx > 0){
        last_modified_buffer_idx = crrt_adc_meas_buffer_idx - 1;
      }
      else{
        last_modified_buffer_idx = nbr_adc_channels - 1;
      }
      Serial.println(last_modified_buffer_idx);

      previous_us = current_us;
    }

    if (print_full_buffer)
    {
      for (size_t i = 0; i < nbr_adc_channels; i++)
      {
        Serial.print(F(" ADC "));
        Serial.print(adc_channels[i]);
        Serial.println(F(" meas in time:"));

        for (size_t j = 0; j < adc_buffer_nbr_consec_meas; j++)
        {
          Serial.print(adc_meas_buffer[j][i]);
          Serial.print(F(" "));
        }
        Serial.println();
      }
    }
  }
}