The Event Link Controller (ELC) is a module in the RA4M1 device that links various modules within the device together so that one module can directly respond to an event in another module without going through software. There are 16 modules that can receive event notifications, and over 150 events that they can respond to. These include signals from the several timers and clocks, state changes of input pins and many more.
This topic is about using the ELC to supply a "synchronous" trigger to the ADC to set up an external trigger using one of the digital I/O pins other than the one that can be used as the "asynchronous" trigger. I have described that here.
The synchronous ELC trigger is enabled by setting the TRGE bit to 1 in the A/D Control Register (ADCSR); the EXTRG bit must be 0 (RA4M1 hardware manual section 35.2.3, page 1187). The ELC has 2 channels for triggering the ADC called ELD_AD00 and ELD_AD01. The TRSA bits in the A/D Conversion Start Trigger Select Register (ADSTRGR) must be set to select the one to use (RA4M1 manual section 35.2.12, page 1096). The second trigger channel is for double-trigger mode, which is not discussed here. We will be using ELD_AD00.
The pin to use is now selected. The pin must be one that can interrupt. The Arduino documentation says that only pins D2 and D3 are interrupt-capable, but that is with regard to digitalPinToInterrupt() with attachInterrupt(). A look at the "full-pinout" PDFs for both boards reveals that several more pins are available to "bare metal" programming. However, most of these pins are connected to serial communications of various types and some pins share an IRQ, so care must be taken not to select one that will interfere with something else you are doing. Indeed, IRQ 1, pin D2 on the WiFi (Minima D3), is also associated with serial communications. Of all digital pins with IRQ's, only 2 on each board have IRQ's that are exclusive to the pin: D3 on the WiFi, Minima D2, and D8 on both board. Analog pin A1 is also interrupt-capable, but it is being used for analog input hear. Pin D8 is used in this example; it is connected to RA4M1 pin P304.
The ELC is set up next. A code is put into register ELSR8 to set the event for ELD_AD00. This code specifies which event causes the ELD_AD00 signal. For the interrupt on pin D8, the code is IRQ_PORT_IRQ9 as defined by @susan-parker.
Last, the selected pin must be initialized. Set its pin function select register to input with pullup, event on falling edge, I/O port and interrupt enabled. The event can occur on the rising edge, too. Using the Susan Parker defines, the right register is PFS_PxxxPFS, where xxx is the RA4M1 pin number, so for D6 this is PFS_P304PFS. See the RA4M1 manual section 19.2.5 (page 357 and following). No interrupt service routine is needed; the interrupt is handled in the hardware.
This sketch, R4_ADC_SyncTrig.ino, illustrates how to set this up and use it. I tested it using a push-button connected between pin D8 and a ground pin.
#include "IRQManager.h"
/*
This sketch shows how to use the UNO R4 ADC in continuous mode with an internal
("synchronous") trigger from the Event Link Controller (ELC). The sketch uses a
digital I/O pin for an interrupt that triggers the event.
There are several pins that can be used as interrupt pins, not just the two
listed as interrupt-capable in the Arduino documentation (D2 and D3). Several
of the other digital I/O pins are interrupt capaple, but all but two have interrupts
that may be used serial communications of one for or another. Only 2 pins on
either WiFi or Minima have interrupts that are exclusive to the pins, and one
of them is one of the documented pins. These two pins are D3 on the WiFi
(D2 on the Minima) and D8 on both boards; D8 is used here.
There are many other ELC events that can be used, including various clocks
and timers, an analog comparator and more. The ELC can also start other
"peripherals" of the RA4M1 device, and the ADC interrupt can be the event that
does so.
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.
by Jack Short, February 2025
*/
// #defines from Susan Parker's file: susan_ra4m1_minima_register_defines.h
// access to the SCKDIVCR register which controls the rate of Periperal Module Clock C (PLCKC)
#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
// Module Stop Control Registers C and D
#define MSTP 0x40040000 // Module Registers
#define MSTP_MSTPCRC ((volatile unsigned int *)(MSTP + 0x7004)) // Module Stop Control Register C
#define MSTPC14 14 // ELC - Event Link Controller Module
#define MSTP_MSTPCRD ((volatile unsigned int *)(MSTP + 0x7008)) // Module Stop Control Register D
#define MSTPD16 16 // ADC140 - 14-Bit A/D Converter Module
// interrupt controller
#define ICUBASE 0x40000000 // ICU Base - See 13.2.6 page 233, 32 bits -
#define ICU_IELSR 0x6300 // ICU Event Link Setting Register n
#define ICU_IELSR00 ((volatile unsigned int *)(ICUBASE + ICU_IELSR)) // IELSR register 0
// ADC registers
#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
#define ADC140_ADANSA0 ((volatile unsigned short *)(ADCBASE + 0xC004)) // A/D Channel Select Register A0
#define ADC140_ADCER ((volatile unsigned short *)(ADCBASE + 0xC00E)) // A/D Control Extended Register
#define ADC140_ADSTRGR ((volatile unsigned short *)(ADCBASE + 0xC010)) // A/D Conversion Start Trigger Select Register
#define ADC140_ADDR00 ((volatile unsigned short *)(ADCBASE + 0xC020)) // A1 data register
// port registers for settng up the trigger pin
#define PORTBASE 0x40040000 // Port Base
#define PMISC_PWPR ((volatile unsigned char *)(PORTBASE + 0x0D03)) // Write-Protect Register - 19.2.6
#define P300PFS 0x08C0 // Port 3 Pin Function Select Register
#define PFS_P304PFS ((volatile unsigned int *)(PORTBASE + P300PFS + ( 4 * 4))) // D8 - IRQ9
// bit fields in Pin Function Select Registers
#define PFS_PODR 0 // Pin Output Data - 0: Low output; 1: High output
#define PFS_PIDR 1 // Pin Input State - Read 0: Low level; 1: High level
#define PFS_PDR 2 // Pin Direction - 0: Input (input pin); 1: Output (output pin)
#define PFS_PCR 4 // Pull-up Control - 1: Enable internal pull-up
#define PFS_NCODR 6 // N-Channel Open Drain Control - 1: NMOS open-drain output.
#define PFS_DSCR 10 // Port Drive Capability - 1: Middle drive; Default 0: Low drive
#define PFS_EOR 12 // Event on Rising - 1: Detect rising edge - Set EOR and EOF both to 1
#define PFS_EOF 13 // Event on Falling - 1: Detect falling edge - ... for Detect both edges
#define PFS_ISEL 14 // IRQ Input Enable - 1: Used as an IRQn input pin.
#define PFS_ASEL 15 // Analog Input Enable - 1: Used as an analog pin.
#define PFS_PMR 16 // Pin Mode Control - 1: Used as an I/O port for peripheral functions
#define PFS_PSEL_4_0 24 // Peripheral Function Select
// Event Link Contrioller registers
#define ELCBASE 0x40040000 // Event Link Controller
#define ELC_ELCR ((volatile unsigned char *)(ELCBASE + 0x1000)) // Event Link Controller Register
#define ELSR 0x1010 // Event Link Setting Registers
#define ELC_ELSR08 ((volatile unsigned short *)(ELCBASE + ELSR +( 8 * 4))) // ELC_AD00 - ADC14A
// The following are event-source codes for the ELC #defined in Susan Parker's code, the comments are by JWS
// See the "Advanced Section" drawings in the "full-pinout" PDF's for the two boards
#define IRQ_PORT_IRQ0 0x01 // IRQ 0 pin P105 - board pin D2 on Minima, D3 on WiFi; pin and IRQ not used by anything else on either board
#define IRQ_PORT_IRQ1 0x02 // IRQ 1 pin P104 - board pin D3 on Minima, D2 on WiFi; IRQ also used by IIC SDA and SCI TX on both boards, and by SPI COPI on the Minima
#define IRQ_PORT_IRQ2 0x03 // IRQ 2 pin P104 - SCL pin on both boards; used by IIC SCL and SCI RX on both boards, and by SPI CIPO on the Minima
#define IRQ_PORT_IRQ3 0x04 // IRQ 3 pin P110 - board pin D12 on Minima, not externally connected on WiFi; IRQ also used by SPI CIPO (Minima)
#define IRQ_PORT_IRQ4 0x05 // IRQ 4 pin P111 - Minima pin D13, WiFi pin D6; IRQ also used by RSPCKB on Minima, on WiFi also used by SPI COPI on pin P411 (D11)
#define IRQ_PORT_IRQ5 0x06 // IRQ 5 pin P302 - board pin D1 on both; used by serial TX
#define IRQ_PORT_IRQ6 0x07 // IRQ 6 pin P301 - board pin D0 on both; used by serial RX
#define IRQ_PORT_IRQ7 0x08 // IRQ 7 pin P000 - analog pin A1 on both; IRQ apparently unused otherwise
#define IRQ_PORT_IRQ8 0x09 // IRQ 8 - not associated with any pin on either board
#define IRQ_PORT_IRQ9 0x0A // IRQ 9 pin P304 - board pin D8 on both; pin and IRQ not used by anything else on either board
// end of Parker code
#define ELCON 0x80 // enable bit in the Event Link Controller Register (ELCR)
#define ADST 0x8000 // (1 << ADCSR_ADST), start-conversion bit in the ADCSR register
// pin function select bit positions as actual bits
#define _PODR 1 // Pin Output Data (bit position PODR is 0)
#define _PIDR (1 << PFS_PIDR) // Pin Input State
#define _PDR (1 << PFS_PDR) // Pin Direction
#define _PCR (1 << PFS_PCR) // Pull-up Control
#define _EOR (1 << PFS_EOR) // Event on Rising
#define _EOF (1 << PFS_EOF) // Event on Falling
#define EORF (_EOF | _EOR) // Event on either rising or falling edge
#define _ISEL (1 << PFS_ISEL) // IRQ Input Enable
#define _PMR (1 << PFS_PMR) // Pin Mode Control
#define CMDLEN 6 // length of a command sent to the Arduino from controller program
#define ARDCMD_NULL 0 // no command
#define ARDCMD_STARTSCAN 1 // start a scan
#define ARDCMD_SETRATE 2 // set the sampling rate
//#define _12BITS 1 // un-comment this for 12-bit mode
#define BUFF_SIZE 1000 // fills most of a computer screen
unsigned short buffer[BUFF_SIZE]; // readings go here
uint8_t *pBuff8 = (uint8_t *)buffer; // byte pointer for Serial.write()
int nBuffIndex; // buffer location for next reading
volatile bool bReadingReady; // TRUE when a block of data is ready to send
GenericIrqCfg_t cfg; // structure defined in IRQManager.h
// zero level for input that has a 2.5 volt offset added
#ifdef _12BITS
#define ZERO_LVL 2048 // half of 2^12
#else
#define ZERO_LVL 8192 // half of 2^14
#endif
// note: the above will need adjustment for the analog reference voltage being < 5.0v.
void ADC_ISR(); // ADC interrupt service routine
void StartScan(); // start a scan without the trigger
void StopScan(); // end the current scan
void SetRate(int nRateCode); // sets sampling rate
void setup()
{
Serial.begin(115200);
while(!Serial);
// install the ADC IRQ
cfg.irq = FSP_INVALID_VECTOR; // initialize structure
cfg.ipl = 12; // priority level
cfg.event = ELC_EVENT_ADC0_SCAN_END; // ADC140_ADI interrupt, code is defined in the core header elc_defines.h
IRQManager::getInstance().addGenericInterrupt(cfg, ADC_ISR);// attach the ADC ISR
// set up the ADC
*MSTP_MSTPCRD &= (0xFFFFFFFF - (0x01 << MSTPD16)); // clear MSTPD16 bit in Module Stop Control Register D to run the ADC 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; // using analog pin A1
*ADC140_ADCSR &= 0x1FFF; // 0001 1111 1111 1111b; clear scan mode bits: bits 14, 13 = 00 (they probably are zero anyway)
*ADC140_ADCSR |= 0x4000; // 0100 0000 0000 0000b; sets continuous scan mode: bits 14, 13 = 10
// set up the ELC trigger
*ADC140_ADCSR |= 0x0200; // 0000 0010 0000 0000b; enables ELC trigger
*ADC140_ADSTRGR &= 0xC0FF; // clear ADSTRGR:TRSA bits
*ADC140_ADSTRGR |= 0x0900; // TRSA bits 001001b (9): trigger from ELC_AD00
// set up the ELC module
*MSTP_MSTPCRC &= (0xFFFFFFFF - (0x01 << MSTPC14)); // clear MSTPC14 bit in Module Stop Control Register C to run the ELC module
*ELC_ELSR08 = IRQ_PORT_IRQ9; // set pin IRQ as the ADC event source; IRQ 9 is for RA4M1 pin P304, board pin D8 on both WiFi and Minima
*ELC_ELCR |= ELCON; // enable the ELC
// set pin D2 as input pullup, interrupt on falling edge, peripheral I/O port
*PMISC_PWPR &= 0x7F; // write protect register, clear BOWI bit to enable writing the PFSWE bit
*PMISC_PWPR |= 0x40; // set PFSWE bit, enables writing to the PmnPFS registers
*PFS_P304PFS = _PCR | _EOF | _PMR | _ISEL; // pin function select register for D8: input with pullup, falling edge, I/O port, interrupt
*PMISC_PWPR &= 0xBF; // clear PFSWE bit, disables writing to the PmnPFS registers
*PMISC_PWPR |= 0x80; // set BOWI bit, disables writing to the PFSWE bit
nBuffIndex = 0; // starting buffer index
}
void loop()
{
if(bReadingReady) { // set by the ISR when the buffer is full
Serial.write(pBuff8, sizeof(buffer)); // send out the data
bReadingReady = false; // wait for next trigger to read again
nBuffIndex = 0; // reset buffer pointer
delay(100);
}
else if(Serial.available() > 0) { // if there is a command from the controlling program
unsigned char cCmdBuff[CMDLEN]; // command buffer
delay(2); // make sure all command bytes have been received, probably superfluous
Serial.readBytes(cCmdBuff, CMDLEN); // read command, always CMDLEN bytes even if less are needed
switch(cCmdBuff[0]) { // first byte is the command code
case ARDCMD_STARTSCAN: // start a scan by operator command even though
StartScan(); // it's normally started by an ELC trigger
break;
case ARDCMD_SETRATE: // set the sampling rate
SetRate(cCmdBuff[1]); // 2nd byte is rate code
break;
}
}
}
void ADC_ISR()
// ISR for ADC140_ADI
{
// The Interrupt Status Flag bit (IR, bit 16), in the IELSRx register for the specific interrupt, is 1 on entry to the ISR.
// It must be cleared to 0 or the system will hang, see section 13.2.6 (page 233) of the RA4M1 hardware manual for a
// description of these registers.
*(ICU_IELSR00 + cfg.irq) &= ~(R_ICU_IELSR_IR_Msk); // reset interrupt controller using Susan Parker's #defines
// R_ICU->IELSR[cfg.irq] &= ~(R_ICU_IELSR_IR_Msk); // reset interrupt controller using core defines
if(nBuffIndex < BUFF_SIZE) { // if the buffer is not full
buffer[nBuffIndex] = *ADC140_ADDR00; // get a reading and save it
if(++nBuffIndex == BUFF_SIZE) { // advance buffer index, if the buffer is full
bReadingReady = true; // tell loop() to transmit data
StopScan(); // stop scanning
}
}
}
void StartScan()
// start a scan without the trigger
{
nBuffIndex = 0; // start of buffer for a new scan
*ADC140_ADCSR |= ADST; // set bit to start reading (still works even with ELC trigger enabled)
bReadingReady = false; // not ready to transmit
}
void StopScan()
// stop scanning
{
*ADC140_ADCSR &= 0x7FFF; // clearing ADST bit stops scanning
}
void SetRate(int nRateCode)
// set the sampling rate:
// set the PCKC for the Periperal Module Clock C (PLCKC) in the System Clock Division Control Register (SCKDIVCR),
// as described in the RA4M1 hardware manual section 8.2.1 (page 130)
{
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
}
}
I seem to be travelling in "uncharted territory" with my ADC work; I have seen nothing on this forum, except a few lines in Susan Parker's work, that addresses "bare metal" programming of the ADC, certainly nothing about getting the most out of the ADC. I am having great fun exploring this, and I want to find out as much as I can about the capabilities of the ADC; I will pass along what I learn. Next time will probably be about how to use timers to trigger the ADC, possibly followed by some other ELC stuff. There are many features of the ADC I haven't begun to explore, including group scan mode, double-trigger mode, reading the reference voltage or internal temperature, adding, averaging and the numerous compare-function registers. They are on my list, too.