Basically, a timer is a clock, which is used to measure and controls time events. providing a precise time delay. Most of the microcontroller have inbuilt timers. The timers in microcontrollers not only used to generate time delays but also is used as a counter. This characteristic of the timer is used for many applications. The timers in microcontroller are controlled by special function registers that are assigned for timer operations.
Here the example showing how to configure the timer to periodically generate an interrupt and how to handle it. ESP32 has two timer groups, each one with two general purpose hardware timers. All the timers are based on 64 bits counters and 16-bit prescalers. The prescaler is used to divide the frequency of the base signal (usually 80 MHz), which is then used to increment or decrement the timer counter.
The counter variable will be shared amongst the main loop and the ISR, then it needs to be declared with the volatile keyword.
volatile int interruptCounter;
We will have an additional counter to track how many interrupts have already occurred.
int totalInterruptCounter;
In order to configure the timer, we will need a pointer to a variable of type hw_timer_t.
hw_timer_t * timer = NULL;
Finally, we will need to declare a variable of type portMUX_TYPE which we will use to take care of the synchronization between the main loop and the ISR.
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
To initialize the timer using a timerbegin function, this function receives the number of the timer we want to use (from 0 to 3, since we have 4 hardware timers), the value of the prescaler and a flag indicating if the counter should count up (true) or down (false).
timer = timerBegin(0, 80, true);
For this example we will use the first timer and will pass true to the last parameter, so the counter counts up the frequency of the base signal used by the ESP32 counters is 80 MHz.if we divide this value by 80 (using 80 as the prescaler value), we will get a signal with a 1 MHz frequency that will increment the timer counter 1 000 000 times per second.
Before enabling the timer, we need to bind it to a handling function, which will be executed when the interrupt is generated. This is done with a call to the timerAttachInterrupt function.
timerAttachInterrupt(timer, &onTimer, true);
This function receives as input a pointer to the initialized timer, which we stored in our global variable, the address of the function that will handle the interrupt and a flag indicating if the interrupt to be generated is edge (true) or level (false).
For this example we will pass our global timer variable as first input, as second the address of a function called onTimer that we will later specify, and as third the value true, so the interrupt generated is of edge type.
timerAlarmWrite(timer, 1000000, true);
timerAlarmWrite function to specify the counter value in which the timer interrupt generated. So, for this example, we assume that we want to generate an interrupt each second, and thus we pass the value of 1 000 000 microseconds, which is equal to 1 second.
The third argument we will pass true, so the counter will reload and thus the interrupt will be periodically generated.
To finish the setup function by enabling a call to timerAlarmEnable(timer);
Main Loop
The main loop will be where we actually handle the timer interrupt, after it being signaled by the ISR(Interrupt Service Routine also called Interrupt Handler).To check the value of interrupt counter, So we will check if the interrupt counter variable is greater than zero and if it is, we will enter the interrupt handling code. There, the first thing we will do is decrementing this counter, signaling that the interrupt has been acknowledged and will be handled.
if (interruptCounter > 0) { portENTER_CRITICAL(&timerMux); interruptCounter--; portEXIT_CRITICAL(&timerMux); totalInterruptCounter++; Serial.print("An interrupt as occurred. Total number: "); Serial.println(totalInterruptCounter); }
The ISR function needs to be a function that returns void and receives no arguments. The interrupt handling routine should have the IRAM_ATTR attribute, in order for the compiler to place the code in IRAM. Also, interrupt handling routines should only call functions also placed in IRAM.
void IRAM_ATTR onTimer() { portENTER_CRITICAL_ISR(&timerMux); interruptCounter++; portEXIT_CRITICAL_ISR(&timerMux); }
Since this variable is shared with the ISR, we will do it inside a critical section, which we specify by using a portENTER_CRITICAL and a portEXIT_CRITICAL macro. Both of these calls receive as argument the address of our global portMUX_TYPE variable.
The actual interrupt handling will simply consist on incrementing the counter with the total number of interrupts that occurred since the beginning of the program and printing it to the serial port.