R4 UNO Fast ADC in Single Scan Mode

A couple of weeks ago I posted an item describing how to use the UNO R4 ADC in continuous-scan mode. I have also experimented with the single-scan mode and found it to be almost as fast. In a tight loop, not the loop() function, but one running within it, I got a bit over 355k samples per second in 12-bit mode and a bit over 315k in 14-bit mode.

The key to the single-scan mode is the way the ADC operates. Software sets the ADST bit to 1 in the ADCSR register to start a scan; the ADC starts reading the active channel or channels (there can be more than 1); see pages 1089 - 1090 in the R4M1 hardware manual. In single-scan mode, this bit stays set until all channels have been read once, and then the bit is cleared by the hardware. At this point, the values in the ADC channel data registers are valid and may be read by the software. This function will make a scan and return a result at top speed:

#define ADST (1 << ADCSR_ADST)         // start-conversion bit

unsigned short ReadSingle()
// get a single reading from the active analog port
{
  *ADC140_ADCSR |= ADST;               // start conversion
  while(*ADC140_ADCSR & ADST);         // bit clears when conversion has ended
  return *ADC140_ADDR00;               // return the result (pin A1 is being used)
}

This uses the macros from the work of @susan-parker.

Using this function to take 1000 readings from a single channel as fast as I can make it go looks like this:

#define BUFF_SIZE 1000
unsigned short buffer[BUFF_SIZE];      // readings will go here

unsigned short ReadSingle();           // forward reference to function below loop()

void loop()
{
int n;
// ...
  for(n = 0; n < BUFF_SIZE; n++)
    buffer[n] = ReadSingle();
// do something with the data
// delay() if needed
}

Run this with a 1 kHz sine wave on the input, and the 1000 data points will show a bit over 3 complete cycles of the 1 kHz sine wave.

I have observed an anomaly in the compiler (GCC) used by the Arduino IDE. If I remove the forward reference to ReadSingle(), the sketch will compile without error. The rules of C++ require a function definition or prototype before any call of a function, and as far as I know, there are no exceptions. That's the anomaly.

As I showed earlier, continuous-scan mode can be slowed down by setting the rate of the Peripheral Module Clock C (PCLKC) in the SCKDIVCR register. There may be some way to tweak it that I don't know about, but it looks like there are only seven rather specific sampling rates available. I don't know if it works in single-scan mode, anyway.

Single-scan mode sampling rates can be slowed down by using the AGT1 timer. Set the timer to interrupt at the desired sampling rate and take readings in the timer ISR until the buffer is full. I found a library that does that called Timer_AGT_One it establishes an IRQ for AGT1 and the rate can be set anywhere from megahertz to hours.

Unfortunately, I can't remember where I found the library; it's not in the libraries on Arduino.cc or on GitHub (as far as I can find out); I think it was on a post in this forum. Sorry, author, but you didn't put your name, or a copyright notice, in the code I have either. I will include a copy, with some changes I made, with this post.

When I found this is when I found the way to set an ISR for the ADC interrupt. Its author set the ISR with the function attachEventLinkInterrupt() a library called EventLinkInterrupt, which is on GitHub here. Go to GitHub and you will see that this library has been deprecated and using addGenericInterrupt() from IRQManager.h is recommended. I used it to install the ADC interrupt in my continuous-scan sketch.

The reason attachEventLinkInterrupt() is deprecated is that it attaches an ISR only to one particular RTC interrupt; only one ISR can be set up at any one time with it. An ISR can be attached to any interrupt, and more than one ISR can be set up. The changes I made to Timer_AGT_One were to use addGenericInterrupt().

Here is my sketch to do fast ADC in single-scan mode.

#include <Timer_AGT_One.h>
/*
R4_FastADC_Single.ino
This sketch runs the UNO R4 ADC in single-scan mode taking readings timed by the AGT1 timer  
and writes readings in groups of 1000 to be read by a program running on a computer.

by Jack Short

This is free software; no restrictions or limitations are placed on its use.
This software is provided "as is" without warranty of any kind with regard
to its usability or suitability for any purpose.
*/

// #defines from Susan Parker's file:  susan_ra4m1_minima_register_defines.h
#define MSTP           0x40040000                                         // Module Registers
#define MSTP_MSTPCRD   ((volatile unsigned int   *)(MSTP + 0x7008))       // Module Stop Control Register D
#define MSTPD16        16                                                 // setting bit 16 enables ADC140 - 14-Bit A/D Converter Module
#define ADCBASE        0x40050000                                         // ADC Base
#define ADC140_ADCSR   ((volatile unsigned short *)(ADCBASE + 0xC000))    // A/D Control Register
#define ADCSR_ADST     15                                                 // A/D Conversion Start bit (shift 1 by this)
#define ADC140_ADANSA0 ((volatile unsigned short *)(ADCBASE + 0xC004))    // A/D Channel Select Register A0
#define ADANSA0_ANSA00 0                                                  // bit 0 (== 1) selects A/D channel AN000, which is UNO in A1
#define ADC140_ADCER   ((volatile unsigned short *)(ADCBASE + 0xC00E))    // A/D Control Extended Register
#define ADC140_ADDR00  ((volatile unsigned short *)(ADCBASE + 0xC020))    // A1 data register

#define ADST (1 << ADCSR_ADST)                              // start-conversion bit

#define CMDLEN            4							// length of a command sent to the Arduino
#define ARDCMD_NULL       0                                 // Arduino command, no command
#define ARDCMD_STARTSCAN  1                                 // Arduino command, start a scan
#define ARDCMD_SETRATE    2                                 // Arduino command, set scan rate
#define ARDCMD_TRIGTYPE   3                                 // Arduino command, set trigger type
#define ARDCMD_TRIGLVL    4                                 // Arduino command, set trigger level


//#define _12BITS 1                                         // un-comment this for 12-bit mode
// The ADC can be set to either 14-bit or 12-bit resolution; the R4 starts up with the ADC in 12-bit mode.
// 12-bit mode is slightly faster than 14-bit.  With the code below, the fastest sampling rates seen were:
//  12-bit        355 kHz
//  14-bit        318 kHz
// The Arduino documentation says that the UNO R4 defaults to 10-bit mode for compatibility with R3 code
// This seems to be a software conversion of (probably) 12-bit readings, that occurs in the code
// for analogRead().  There seems to be no way to set it in the hardware.

#define BUFF_SIZE 1000                                      // number of samples / scan, fills most of a computer screen

// in order to read any AC waveform like a sine wave, the waveform must be no more than 5 volts peak to peak,
// and there must be a zero offset of +2.5 volts on the signal so that negative parts of the wave are made positive.
// With the offset only and no signal, "zero" readings should be half of the maximum reading:
// value of a reading that represents zero volts when using a +2.5-volt offset 
//  to see sine waves and other AC waveforms
#ifdef _12BITS
#define ZERO_LVL 2048                                       // half of 2^12
#else
#define ZERO_LVL 8192                                       // half of 2^14
#endif

int nSamplingInterval = 5;                                  // sampling interval, microseconds

unsigned short buffer[BUFF_SIZE],                           // readings will go here to be sent to the compute
        r1, r2;                                             // consecutive readings for trigger operation
uint8_t *pBuff8 = (uint8_t *)buffer;                        // pointer to the buffer as an array of bytes

// software trigger
#define TRIGTYPE_NONE    0                                  // no trigger, don't wait to start reading
#define TRIGTYPE_RISING  1                                  // trigger when readings go from below to above the trigger value
#define TRIGTYPE_FALLING 2                                  // trigger when readings go from above to below the trigger value
int nTrigType = TRIGTYPE_RISING;                            // trigger type, one of the above, default to rising
unsigned short nTrigLevel = ZERO_LVL;                       // trigger level, default is to trigger when readings cross the zero value

void SetRate(int nRateCode);                                // sets sampling rate
void SetTrigType(int nTrgTyp);                              // sets trigger type
void SetTrigLevel(unsigned char *pData);                    // sets trigger level

unsigned short ReadSingle();
void timerISR();

volatile bool bIdling, bReading, bDataReady;
volatile int nCount = 0;                                   // counter for readings

void setup()
{
  Serial.begin(115200);
  while(!Serial);
  r1 = 0xFFFF;                                              // initialize trigger
  bReading = false;                                         // flag for ISR, not reading
  bIdling = true;                                           // another flag, system is idlng
// set up the AGT1 timer
  Timer1.initialize(nSamplingInterval);                     // set initial timer interval
  Timer1.attachInterrupt(timerISR);                         // install the timer ISR
  *MSTP_MSTPCRD &= (0xFFFFFFFF - (0x01 << MSTPD16));        // clears bit 16, enables ADC140 module
#ifdef _12BITS
  *ADC140_ADCER = 0;                                        // 12-bit resolution, no self-diagnosis, flush right
#else
  *ADC140_ADCER = 6;                                        // 14-bit resolution, no self-diagnosis, flush right
#endif
  *ADC140_ADANSA0 = 1 << ADANSA0_ANSA00;                    // using channel A1
  *ADC140_ADCSR &= 0x1FFF;                                  // 0001111111111111b; bits 14, 13 = 00 - sets single scan mode
}

void loop()
{
  if(bDataReady) {                                          // true when the buffer has filled
    Serial.write(pBuff8, sizeof(buffer));                   // send data to controlling program
    nCount = 0;                                             // reset count
    bDataReady = false;                                     // get new data
    bReading = false;                                       // not reading until trigger fires
    r1 = 0xFFFF;                                            // for trigger initialization
    bIdling = true;                                         // idling until ARDCMD_STARTSCAN command is received from the controller
    }
   else if(Serial.available() > 0) {                        // buffer not full, check for command from controlling program; if there is one
   unsigned char cCmdBuff[CMDLEN];
    delay(2);                                               // make sure all command bytes have been received, probably superfluous
    Serial.readBytes(cCmdBuff, CMDLEN);                     // command is always CMDLEN bytes even if less are needed
    switch(cCmdBuff[0]) {                                   // first byte is command code
      case ARDCMD_STARTSCAN:                                // command:  start a scan
        bIdling = false;                                    // start the next scan
        break;
      case ARDCMD_SETRATE:                                  // command:  set sampling rate
        SetRate(cCmdBuff[1]);                               // 2nd byte is samplig rate code
        break;
      case ARDCMD_TRIGTYPE:                                 // set the trigger type
        SetTrigType(cCmdBuff[1]);                           // 2nd byte is trigger type code
        break;
      case ARDCMD_TRIGLVL:                                  // set trigger level
        SetTrigLevel(cCmdBuff + 1);                         // 2nd byte is LSB and 3rd byte is MSB of 2-byte trigger level
        break;
      }
    }
}

unsigned short ReadSingle()
// get a single reading from the active analog port
{
  *ADC140_ADCSR |= ADST;                                    // start conversion
  while(*ADC140_ADCSR & ADST);                              // bit clears when conversion has completed
  return *ADC140_ADDR00;                                    // return the result
}

void timerISR()
// interrupt service routine for the AGT1 timer
// called by ISR in the Timer_AGT_One code
{
  // ISR in the Timer_AGT_One code did the interrupt controller reset
  if(bIdling)                                               // if idling
    return;                                                 // do nothing
  if(!bReading) {                                           // if currently not making a scan
    if(nTrigType) {                                         // if there is a trigger
      if(r1 == 0xFFFF)                                      // if this is the first interrupt of a read sequence
        r1 = ReadSingle();                                  //  initialize trigger first value
       else  {                                              // not the first interrupt in a sequnce, nRt1 has been initialized
        r2 = ReadSingle();                                  // get a second reading
        if((nTrigType == TRIGTYPE_RISING &&                 // if trigger is rising-edge
                  (r1 < nTrigLevel && r2 >= nTrigLevel)) || //  the trigger fires when nRt1 < trigger level while nR2 is >= the trigger level
           (nTrigType == TRIGTYPE_FALLING &&                // else if it is a falling-edge trigger
                  (r1 > nTrigLevel && r2 <= nTrigLevel))) { //  the trigger fires when nRt1 < trigger level while nR2 is <= the trigger level
          buffer[nCount++] = r2;                            // trigger condition met, keep the reading and advance the count
          bReading = true;                                  // trigger has fired, start taking readings
          }
         else                                               // trigger condition not met yet
          r1 = r2;                                          // second reading is now initial reading
        }
      }
    }
   else if(nCount < BUFF_SIZE) {                            // trigger has fired, until the buffer is full
    buffer[nCount++] = ReadSingle();                        // take a reading and put it in the buffer
    if(nCount == BUFF_SIZE)                                 // if the buffer has filled
      bDataReady = true;                                    // signal loop() to send data to controlling program
    }
}

void SetRate(int nRateCode)
// Set sampling rate.  Controlling program in this demo sends a code
// to select 1 of 7 "standard" sampling rates.  Alternatively, the 
// controlling program could specify a rate in microseconds; the TimerAGTOne
// code allows the rate to be set to as low as more than 250 seconds 
// (about 4 min 16 sec) per sample.  A 32-bit number is required to set a rate
// that low (250,000,000 microseconds).  Using a 16-bit rate would be
// useful most of the time, allowing as much as 65535 microseconds 
// (65.6 milliseconds) per reading.  Multi-byte values would need to be 
// sent and received as described below for SetTrigLevel()
{
static unsigned long nRates[] = { 5, 10, 20, 50, 
                                          100, 200, 500 };  // sampling rates in microseconds per sample
// samples / second:  200k, 100k, 50k, 20k, 10k, 5k, 2k
  if(nRateCode >= 0 && nRateCode <= 6) {                    // many more are possible, but only 7 rates in this demo
    Timer1.stop();
    Timer1.setPeriod(nRates[nRateCode]);
    }
}

void StopScan()
// stop scanning
{
    bIdling = true;                                         // idling, no action until until ARDCMD_STARTSCAN command is received from the controller
    bReading = false;                                       // not reading until trigger fires
    bDataReady = false;                                     // get new data
    nCount = 0;                                             // reset count
    r1 = 0xFFFF;                                            // trigger initialization
}

void SetTrigType(int nTrgTyp)
// set the trigger type
{
  StopScan();                                               // stop scanning
  if(nTrgTyp >= TRIGTYPE_NONE && 
                              nTrgTyp <= TRIGTYPE_FALLING)  // make sure code passed to this function is valid
    nTrigType = nTrgTyp;                                    // if it is, save it
}

void SetTrigLevel(unsigned char *pData)
// set trigger level, a 12- or 14-bit, 2-byte number
// From looking at the register addresses in the chip manual, it is apparent
// that the Renesas chip uses the "big endian" byte order with more significant
// bytes preceeding less significant ones in multi-byte words.  All PC's 
// (and recent Macs, I think) use the "little endian" byte order with less 
// significant bytes first.  For this reason, the controller program explicitly 
// puts the low byte of the trigger level before the high byte when sending the 
// command, and the following code will put them back together with the proper 
// byte order, no matter what the byte order is.
{
  StopScan();                                               // stop scanning
  nTrigLevel = *pData | ((unsigned short)pData[1] << 8);    // trigger level is (first byte) | (second byte << 8)
}

Like my continuous-scan sketch, this one also implements the software trigger. Note that the sampling rate is set in microseconds instead of a rate code.

Note: I always try to line all my end-of-line comments (//) line up to enhance readability. For some reason this gets fouled up when I put my code in one of the code blocks.

Here is the modified Timer_AGT_One.h file.

#ifndef TIMER_AGT_ONE_H
#define TIMER_AGT_ONE_H

#include "Arduino.h"
#include "IRQManager.h

// origin of this file is forgotten
// modified by J. W. Short Feb. 8 2025
// changed ISR setup from using the deprecated attachEventLinkInterrupt()
// to using IRQManager::getInstance().addGenericInterrupt()

#define AGT1_RESOLUTION 0xFFFF                              // AGT1 is a 16-bit timer

typedef enum agt_opmode_t {                                 // timer operating mode, values defind on page 514
  TIMER,                                                    // timer mode
  PULSE_OUTPUT,                                             // pulse output mode
  EVENT_COUNTER,                                            // event counter mode
  PULSE_WIDTH,                                              // pulse width measurement mode
  PULSE_PERIOD                                              // pulse period measurement mode
};

typedef enum agt_cntsrc_t {                                 // source clock for timer
  PCLKB,                                                    // PCLKB
  PCLKB_8,                                                  // PCLKB / 8
  PCLKB_2 = 3,                                              // PCLKB / 2
  AGTLCLK,                                                  // divided clock specified by AGTLCLK
  AGT0_UF,                                                  // AGT0 underflow
  AGTSCLK                                                   // divided clock specified by AGTSCLK
};

// most of the comments below are by the original author

class TimerAGTOne {
private:
  static agt_cntsrc_t srcbits;
  static uint8_t divbits;
  static uint16_t reloadSetting;
  static uint8_t eventLinkIndex;
  static GenericIrqCfg_t cfgAgtIrq;

  static void internalCallback();                           // added by JWS

public:

  static void (*isrCallback)();                             // ISR in Timer_AGT_One.cpp, comment by JWS
  static void isrDefaultUnused(){};                         // default ISR callback, does nothing, replaced by internalCallback()  comment by JWS

  void initialize(unsigned long microseconds = 1000000) __attribute__((always_inline)) {
    // enable the timer in Module Stop Control Register D
    R_MSTP->MSTPCRD &= ~(1 << R_MSTP_MSTPCRD_MSTPD2_Pos);
    // We're using R_AGT1, but all the positions and bitmasks are defined as R_AGT0
    // set mode register 1
    //(-) (TCK[2:0]) (TEDGPL) (TMOD[2:0])
    //  Use TIMER mode with the LOCO clock (best we can do since Arduino doesn't have crystal for SOSC)
    R_AGT1->AGTMR1 = (AGTLCLK << R_AGT0_AGTMR1_TCK_Pos) | (TIMER << R_AGT0_AGTMR1_TMOD_Pos);
    // mode register 2
    // (LPM) (----) (CKS[2:0])
    R_AGT1->AGTMR2 = 0;
    // AGT I/O Control Register
    // (TIOGT[1:0]) (TIPF[1:0]) (-) (TOE) (-) (TEDGSEL)
    R_AGT1->AGTIOC = 0;
    // Event Pin Select Register
    // (-----) (EEPS) (--)
    R_AGT1->AGTISR = 0;
    // AGT Compare Match Function Select Register
    // (-) (TOPOLB) (TOEB) (TCMEB) (-) (TOPOLA) (TOEA) (TCMEA)
    R_AGT1->AGTCMSR = 0;
    // AGT Pin Select Register
    // (---) (TIES) (--) (SEL[1:0])
    R_AGT1->AGTIOSEL = 0;

    // AGT1_AGTI is the underflow event
    //  The event code is 0x21

// change made here - JWS
    cfgAgtIrq.irq = FSP_INVALID_VECTOR;                     // set up structure
    cfgAgtIrq.ipl = 12;                                     // priority level
    cfgAgtIrq.event = ELC_EVENT_AGT1_INT;                   // AGT1 interrupt
    IRQManager::getInstance().addGenericInterrupt(cfgAgtIrq, 
                                        internalCallback);  // attach our ISR
    setPeriod(microseconds);                                // set rate
  }

  void setPeriod(unsigned long microseconds) __attribute__((always_inline)) {

    // for smal periods we can use PCLKB instead of LOCO
    divbits = 0;  // No divider bits on PCLKB
    // PCLKB is running at SYSCLK/2 or 24MHz or 24 ticks per microsecond
    unsigned long ticks = (24 * microseconds);
    if (ticks < AGT1_RESOLUTION) {
      srcbits = PCLKB;
      reloadSetting = ticks;
    } else if (ticks < AGT1_RESOLUTION * 2) {
      srcbits = PCLKB_2;
      reloadSetting = ticks / 2;
    } else if (ticks < AGT1_RESOLUTION * 8) {
      srcbits = PCLKB_8;
      reloadSetting = ticks / 8;
    } else {
      //  Period is too long for PCLKB, use AGTLCLK (LOCO)
      // LOCO is 32.768KHz  is (1/32768) = 30.518us/tick
      srcbits = AGTLCLK;
      // recalculate ticks at new clock speed
      ticks = microseconds / (1000000.0 / 32768.0);
      if (ticks < AGT1_RESOLUTION) {
        divbits = 0;
        reloadSetting = ticks;
      } else if (ticks < AGT1_RESOLUTION * 2) {
        divbits = 1;
        reloadSetting = ticks / 2;
      } else if (ticks < AGT1_RESOLUTION * 4) {
        divbits = 2;
        reloadSetting = ticks / 4;
      } else if (ticks < AGT1_RESOLUTION * 8) {
        divbits = 3;
        reloadSetting = ticks / 8;
      } else if (ticks < AGT1_RESOLUTION * 16) {
        divbits = 4;
        reloadSetting = ticks / 16;
      } else if (ticks < AGT1_RESOLUTION * 32) {
        divbits = 5;
        reloadSetting = ticks / 32;
      } else if (ticks < AGT1_RESOLUTION * 64) {
        divbits = 6;
        reloadSetting = ticks / 64;
      } else if (ticks < AGT1_RESOLUTION * 128) {
        divbits = 7;
        reloadSetting = ticks / 128;
      }
    }
    //  Use TIMER mode with the LOCO clock (best we can do since Arduino doesn't have crystal for SOSC)
    R_AGT1->AGTMR1 = (srcbits << R_AGT0_AGTMR1_TCK_Pos) | (TIMER << R_AGT0_AGTMR1_TMOD_Pos);
    R_AGT1->AGTMR2 = divbits;
    R_AGT1->AGT = reloadSetting;
    start();
  }

  void start() __attribute__((always_inline)) {
    resume();
  }
  void stop() __attribute__((always_inline)) {
    R_AGT1->AGTCR = 0;
  }
  void restart() __attribute__((always_inline)) {
    start();
  }
  void resume() __attribute__((always_inline)) {
    R_AGT1->AGTCR = 1;
  }

  void attachInterrupt(void (*isr)()) __attribute__((always_inline)) {
    isrCallback = isr;
  }
  void attachInterrupt(void (*isr)(), unsigned long microseconds) __attribute__((always_inline)) {
    if (microseconds > 0) setPeriod(microseconds);
    attachInterrupt(isr);
  }
  void detachInterrupt() __attribute__((always_inline)){
    isrCallback = isrDefaultUnused;
  }
};

extern TimerAGTOne Timer1;

#endif  //TIMER_AGT1_H

The ISR it installs is in Timer_AGT_One.cpp.

#include "Timer_AGT_One.h"

// modified by J. W. Short Feb. 8 2025
// changed ISR setup from using the deprecated attachEventLinkInterrupt()
// to using IRQManager::getInstance().addGenericInterrupt()

TimerAGTOne Timer1;   // preinstantiate.

uint16_t TimerAGTOne::reloadSetting = 0xFFFF;
agt_cntsrc_t TimerAGTOne::srcbits = PCLKB;
uint8_t TimerAGTOne::divbits = 0;
uint8_t TimerAGTOne::eventLinkIndex = 0;
void (*TimerAGTOne::isrCallback)() = TimerAGTOne::isrDefaultUnused;

GenericIrqCfg_t TimerAGTOne::cfgAgtIrq = 
    { FSP_INVALID_VECTOR, 0, ELC_EVENT_NONE };                    // needed initialization - compiler complains without it

void TimerAGTOne::internalCallback()
// installed ISR
{
// Reset the interrupt link and the flag for the timer
  R_ICU->IELSR[cfgAgtIrq.irq] &= ~(R_ICU_IELSR_IR_Msk);           // reset interrupt controller - JWS
  R_AGT1->AGTCR &= ~(R_AGT0_AGTCR_TUNDF_Msk);                     // reset the flag for the timer
  isrCallback();                                                  // callback in sketch, set in initialize()
}

I have figured out how to use the hardware trigger (called as "asynchronous trigger ADTRG0" in the RA4M1 hardware manual). I'll pass that along when I get it fleshed out. I've had less luck with what the manual calls "synchronous trigger (ELC)", which will trigger on a compare condition and can apparently be used to make any digital input pin a hardware trigger (ADTRG0 is tied to a specific pin). I haven't started trying to understand group-scan mode, yet.

1 Like

Hi @jwshort. Thanks for sharing this!

The Arduino development tools automatically generate function prototypes for all functions that are defined in a .ino ("Arduino language") file:

https://arduino.github.io/arduino-cli/latest/sketch-build-process/#pre-processing

The "sketch preprocessing" produces the valid C++ code that is passed to the GCC compiler.

Awesome, thank you! :slight_smile:

Thanks for the kudos, Susan. Your work is pretty awesome, too. It certainly has been a great help to me.

Hello, @ptillisch. Thanks for the info. As a senior citizen who has been doing C++ for many years, I was mightily surprised when I discovered this. I guess I need to go to that site describing preprocessing and find out what else is different!

1 Like

2 posts were split to a new topic: What is "attribute((always_inline))"?