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.