After having enjoyed working with the Arduino UNO R3 for several years, I recently acquired an UNO R4 and started experimenting with its added capabilities, one of which is a continuous scan mode of the ADC. I few weeks ago I posted on this Forum a question about how to use the continuous scan mode of the UNO R4, here. Getting no response, I started trying to find out on my own. I thought I had the answer when I found posts from @susan-parker that described how to install an interrupt vector by hijacking a pin interrupt. Unfortunately, I could not get this to work. I don't know what I was doing wrong; it may have been because Susan did her work on am R4 Minima and I have an R4 WiFi. For some reason, several of the pin assignments differ between the two.
At this point I gave up, for the time being, on continuous scan mode and started investigating what kind of speed I could get out of single scan mode, and got pretty impressive results, almost 360k samples / second in 12-bit mode and about 320k samples / sec in 14-bit mode. A reading in single scan mode is initiated by setting the ADST bit in the A/D Control Register (ADCSR) (see RA4M1 User's Manual section 35.2.3). Once the conversion is complete, the hardware clears the bit, and as soon as that happens, software can save the reading. This is done very quickly and very simply with code like this, adapted from Susan's code, in a tight loop:
*ADC140_ADCSR |= ADST; // start conversion
while(*ADC140_ADCSR & ADST); // bit clears when conversion has ended
reading = *ADC140_ADDR00; // using analog input pin A1
I was looking for a way to use a hardware timer to run this at various rates, when I stumbled on what I was looking for to make continuous mode work: the addGenericInterrupt() function in IRQManager.h. This did the trick, and I saw speeds of about 530k for 12-bits and about 470k for 14-bits in continuous scan mode. Here is a sketch that shows how:
#include "IRQManager.h"
// This code illustrates how to use the continuous scan mode of the Arduino UNO R4 ADC
// by Jack Short, 2 January 2025
// This is free software, and you may use it in any way you wish.
// No warranty is offered with regard to its usefulness to your application.
// #defines from Susan Parker's file: susan_ra4m1_minima_register_defines.h
#define SYSTEM 0x40010000 // ICU Base - See 13.2.6 page 233
#define SYSTEM_PRCR ((volatile unsigned short *)(SYSTEM + 0xE3FE)) // Protect Register
#define SYSTEM_SCKDIVCR ((volatile unsigned int *)(SYSTEM + 0xE020)) // System Clock Division Control Register
#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
// commands received from controlling program
#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, comment for 14-bit mode
#define BUFF_SIZE 1000 // number of samples per scan
unsigned short buffer[BUFF_SIZE]; // readings will be store here
uint8_t *pBuff8 = (uint8_t *)buffer; // buffer as an array of bytes
int nBuffIndex; // buffer position for next reading
volatile bool bRunning; // TRUE if currently reading
volatile bool bWaiting; // TRUE while waiting for the trigger
volatile bool bReadingReady; // TRUE when block of data ready to send
#ifdef _12BITS
#define ZERO_LVL 2048 // half of 2^12
#else
#define ZERO_LVL 8192 // half of 2^14
#endif
// 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
volatile unsigned short nTrigLevel = ZERO_LVL; // trigger level, default is to trigger when rising readings cross the zero value
volatile unsigned short nRt1, nRt2; // consecutive readings, used by trigger
// function forward decarations
void ADC_ISR(); // interrupt service routine for ADC interrupt
void StartScan(); // starts a scan
void StopScan(); // stops scanning
void SetRate(int nRateCode); // sets sampling rate
void SetTrigType(int nTrgTyp); // sets trigger type
// ADC clock rate settings
#define ADCLK_MAX 0 // maximum rate
#define ADCLK_1_2 1 // 1/2 maximum rate
#define ADCLK_1_4 2 // 1/4 maximum rate
#define ADCLK_1_8 3 // 1/8 maximum rate
#define ADCLK_1_16 4 // 1/16 maximum rate
#define ADCLK_1_32 5 // 1/32 maximum rate
#define ADCLK_1_64 6 // 1/64 maximum rate
GenericIrqCfg_t cfg; // needed for addGenericInterrupt()
void setup()
{
Serial.begin(115200);
while(!Serial);
SetRate(ADCLK_MAX); // set initial sampling rate - maximum
cfg.irq = FSP_INVALID_VECTOR; // set up structure
cfg.ipl = 12; // priority level
cfg.event = ELC_EVENT_ADC0_SCAN_END; // ADC interrupt
IRQManager::getInstance().addGenericInterrupt(cfg, ADC_ISR);// set our ISR
*MSTP_MSTPCRD &= (0xFFFFFFFF - (0x01 << MSTPD16)); // Enable 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_ADCSR &= 0x1FFF; // 0001111111111111b; sets single scan mode: bits14, 13 = 00
*ADC140_ADCSR |= 0x4000; // 0100000000000000b; sets continuous scan mode: bits 14, 13 = 10
*ADC140_ADANSA0 = 1; // using pin A1
nBuffIndex = 0; // initialize buffer index
bRunning = false; // not collecting data
nRt1 = 0xFFFF; // initialize trigger
}
void loop()
{
if(bReadingReady) { // true when the buffer is full and ready to transmit
Serial.write(pBuff8, sizeof(buffer)); // transmit data
bReadingReady = false; // wait for next read cycle to transmit again
}
if(!bRunning && Serial.available() > 0) { // if a command has come from the controlling program
unsigned char cCmdBuff[CMDLEN];
delay(2); // make sure all command bytes have been received, probably superfluous
Serial.readBytes(cCmdBuff, CMDLEN); // read command bytes, command is always CMDLEN bytes even if less are needed
switch(cCmdBuff[0]) { // 1st byte command code
case ARDCMD_STARTSCAN: // start a scan sequence
StartScan(); // do it
break;
case ARDCMD_SETRATE: // set the sampling rate
SetRate(cCmdBuff[1]); // 2nd byte is 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;
}
}
}
void ADC_ISR()
// ADC interrupt service routine (ISR)
{
R_ICU->IELSR[cfg.irq] &= ~(R_ICU_IELSR_IR_Msk); // reset interrupt controller
if(bRunning) { // if collecting data
if(bWaiting) { // if waiting for the trigger
if(nRt1 == 0xFFFF) // if this is the first interrupt of a read sequence
nRt1 = *ADC140_ADDR00; // initialize trigger first value
else { // not the first interrupt in a sequnce, nRt1 has been initialized
nRt2 = *ADC140_ADDR00; // get the current reading
if((nTrigType == TRIGTYPE_RISING && // if it is a rising-edge trigger
nRt1 < nTrigLevel && nRt2 >= nTrigLevel) || // and the trigger fires when nRt1 < trigger level while nR2 is >= the trigger level
(nTrigType == TRIGTYPE_FALLING && // else if it is a falling-edge trigger
nRt1 > nTrigLevel && nRt2 <= nTrigLevel)) { // and the trigger fires when nRt1 < trigger level while nR2 is <= the trigger level
bWaiting = false; // trigger has fired, not waiting any more
buffer[nBuffIndex++] = nRt2; // save current reading as first reading in scan
}
else // trigger condition not met
nRt1 = nRt2; // 2nd reading becomes new 1st reading
}
}
else if(nBuffIndex < BUFF_SIZE) { // not waiting, if buffer is not yet full
buffer[nBuffIndex] = *ADC140_ADDR00; // put current reading into buffer at current position
if(++nBuffIndex == BUFF_SIZE) { // advance buffer position, if buffer has filled
bReadingReady = true; // it's ready to be sentto th controlling program
StopScan(); // stop scanning until told to restart
}
}
}
}
void StartScan()
// start continuous scanning
{
*ADC140_ADCSR |= ADST; // stting this bit starts scanning
nBuffIndex = 0; // initialize buffer position
bReadingReady = false; // no data in buffer
bWaiting = nTrigType != TRIGTYPE_NONE; // wait for trigger if there is one
nRt1 = 0xFFFF; // initialize trigger
bRunning = true; // collecting data
}
void StopScan()
// stop continuous scanning
{
*ADC140_ADCSR &= 0x7FFF; // clearing the ADST bit stops scanning
bRunning = false; // not collecting data
}
void SetRate(int nRateCode)
// set the sampling rate
// the sampling rate is determined by the Periperal Module Clock C (PLCKC)
// its rate is set by the PCKC bits in the SCKDIVCR register
{
StopScan(); // stop scanning
if(nRateCode >= 0 && nRateCode <= 6) { // make sure code passed to this function is valid
*SYSTEM_PRCR = 0xA501; // Enable writing to the clock registers
*SYSTEM_SCKDIVCR &= 0xFFFFFF8F; // zero all PCKC bits
*SYSTEM_SCKDIVCR |= (nRateCode << 4); // put in new PCKC value
*SYSTEM_PRCR = 0xA500; // Disable writing to the clock registers
}
}
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 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 preceding 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)
}
I have also included a simple software trigger; I wanted a steady trace while I was working this up. You will note also that the loop() function has code for receiving commands from a controlling program to set the sampling rate or the trigger conditions.
The sampling rate is varied by setting the rate of the Peripheral Module Clock C (PCLKC) also referred to as the ADCLK in the RA4M1 User's Manual. Its rate is tied to the rate of the Peripheral Module Clock B (PCLKB). The rate of PCLKC can be the same as PCLKB, or 1/2, 1/4, 1/8, 1/16, 1/32 or 1/64 the rate of PCLKB. This means that a limited set of only 7 sampling rates is possible; I don't know yet if it is possible to fine-tune the rate by varying PCLKB or by some other means. The PCLKC rate is determined by the 3 PCKC bits in the System Clock Division Control Register (SCKDIVCR); see section 8.2.1 in the RA4M1 User's Manual.
This sketch was set up to implement an oscilloscope, but there are other uses for high-speed ADC. For instance, it is fast enough to use with audio signals, even dual-channel sampling for stereo sound.
For an oscilloscope, some external hardware and software are needed. A large signal must be reduced to have peak-to-peak voltages of <= 5 volts, and a DC offset of 2.5 volts must be added to read the negative portions of the signal. A circuit like this is needed:
Set the variable resistor to give 2.5 volts at the op amp input. The op amp can be any general-purpose quad op amp like LM324 or TL084. The 4th op amp can be set up to reduce or amplify the signal before it reaches the circuit shown.
Also needed is a computer program to control the Arduino and display traces. I have a simple Windows program that does this and will share it if anyone is interested.
On my system, that 2.5 volt "zero" offset actually gives readings significantly higher than the 8192 (14-bit mode, 2048 in 12-bit mode) expected. This is because the analog reference voltage is only 4.6 volts, not 5. For accurate readings, the external reference should be used and should be set to 5.0 volts using a Zener diode or other voltage reference.
