在Arduino Due上设置正确的ADC预分频器,以定时器和中断驱动的多通道ADC采集

问题描述

我正在尝试针对Arduino Due:https://forum.arduino.cc/index.php?topic=589213.0遵循,适应,理解(并清理一点)围绕那里可用代码的变体。我不喜欢论坛格式,因为事情最终埋没得很深,所以请在这里询问。不幸的是,这意味着在问题之前有很多解释。如果您认为将其张贴在此处是错误的,请告诉我,我可以搬家。

基本上,该想法是使用基于计时器的触发将多个ADC通道记录在缓冲区中。有一些设置:

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

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

然后将计时器0的通道2设置为正确的频率以触发ADC转换:

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

最后可以用来触发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
}

并且ADC输出可以在相应的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
  }
}

我认为这是可以理解的,但是我有一个问题:预分频器的设置。

  • 如果我在线上理解的很好,应该将预分频器设置为frq_ADC >= sample_rate * nbr_channels,主要是因为该芯片只是通过多个通道对ADC进行多路复用

  • 如果我理解得很好,我们希望在给定先前约束的情况下将此类预分频器值设置得尽可能高,从而使ADC频率尽可能低,因为这会提高ADC的转换质量

是吗?

问题在于,我对如何设置预分频器以及与该值对应的值感到困惑,因为我在数据表中找到的内容与我阅读的其他一些在线回复不一致。

从数据表https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-11057-32-bit-Cortex-M3-Microcontroller-SAM3X-SAM3A_Datasheet.pdf:“如果PRESCAL为0,则ADC时钟范围在MCK / 2之间;如果PRESCAL设置为255(0xFF),则ADC时钟范围在MCK / 512之间。这与我在1334页上找到的内容一致:“ ADCClock = MCK /((PRESCAL + 1)* 2)”。但是在第1318页中,记录了转换速率为1MHz。那如何与Due上具有84MHz的MCK频率兼容呢? 84/2 = 48MHz,84/512 = 0.164MHz,高频率值太高。

然后让我感到困惑的是,我发现了这个问题:https://arduino.stackexchange.com/questions/12723/how-to-slow-adc-clock-speed-to-1mhz-on-arduino-due/21054#21054也似乎与1MHz的上限冲突。

有人知道我误会了什么吗? (以及有关该程序正常工作的更多评论吗?)。

解决方法

好吧,所以我对代码进行了一些测试,检查何时缺少某些转换,具体取决于计时器频率和预分频器值。代码有点长,因此我将其发布在答案的末尾。基本上:

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

我认为这表明:

  • 预分频器的值是一个8位int,介于0和255之间,因为它在256处结束

  • 我很难将结果与数据表中的公式进行匹配。我猜这是因为有一些开销切换通道等(?)。例如:

    • 在最高频率下,结果与```ADC_freq = 1MHz /(ps)一致,但是我想这是因为存在一些开销切换通道

    • 即使在使用最高预分频器的情况下,结果也与10kHz和1kHz的ADC_freq = 2MHz /(ps)一致。

我正在使用的代码如下,并且判定失败的标准是该代码报告了5个通道上有效采样频率的下降:

// -------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------
// 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,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();
      }
    }
  }
}

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...