Continuous ADC with the UNO R4

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.

4 Likes

@jwshort Awesome, and love that you have taken the time to put this all out in such detail.
Many thanks.

Thank you.

I'll soon have something about fast ADC in single-scan mode; it's almost as fast and I think it is possible to set a more precise sampling rate using the AGT1 timer.

I also want to figure out external hardware triggering (referred to as "asynchronous trigger ADTRG0" in the RA4M1 User's Manual). I am also curious about "synchronous trigger (ELC)", which I think has to do with the compare function. The latter will probably be tough to figure out since there are a number of registers involved, and it apparently involves group scan mode, another obscure feature of the chip.

Hi Susan,

I am posting here, hopefully not breaking any forum rules, just to thank you for putting the following on GitHub:

So far I haven't digested it fully but I have done a lot of realtime programming over the years and only bought an R4 Minima about a week ago, so still learning how it all works. Your example code will significantly help me to put the R4 Minima into low power mode to extend battery life.

Cheers, Peter

2 Likes

Thanks @downunder2 :slight_smile:
Don’t forget for low-power one has to modify the LEDs, particularly the power-on one as that drags 8 to 9 mA by itself!
I rewired mine so that it is powered from the USB Vin.

Thanks for the reminder. I did read that and took note. I'm waiting for my RTC to arrive before I borrow the low power mode code of yours and get the RTC working.

1 Like

jwshort and Susan
Where or how are the addresses ADC140_ADCSR etc defined please?

I found it here. She has several more items on GitHub.

1 Like

Thanks.