Microprocessors and microcontrollers use asynchronous event notifications called interrupts. As the name suggests, when particular event happens (e.g. timer overflow, DMA transfer complete) currently executing code gets interrupted and the particular interrupt handler function gets called. Interrupts may be generated by most of the peripherals, by the core (e.g. SYSTICK) or by the software.
In order to use the interrupts user must set the interrupt handler function address in the interrupt vector table, end enable the interrupt. Most peripherals require additional configuration.
Our previous LED blink program implementation used busy-loop to add delay between LED blinks.
We now attempt to modify program to use timer interrupts.
SYSTICK
interruptSTM32 provides a number of peripheral timers as well as internal SYSTICK
timer. We begin with SYSTICK
timer as it is slightly easier to configure.
Defined in previous chapters interrupt vector already contains systick_handler
at the appropriate position of the table
void systick_handler() __attribute__((weak, alias("default_handler")));
void (*const __isr_vector[])() __attribute__((section(".isr_vector"))) = {
...
systick_handler,
...
}
It is defined as weak symbol so all we have to do is the redefine systick_handler
function. This function will periodically be evaluated, so it should contain only the code we need to be periodically evaluated. All configuration should be done prior to enabling the timer.
void
systick_handler(void)
{
// toggle LED
*gpioc_odr ^= (1 << 13);
}
STM32 chip has a high precise oscillator which is not strictly necessary in this case but we are going to use it anyway. Enabling internal oscillator is a matter of waiting for it becoming available and switching it on.
volatile unsigned long *const rcc_cr = (unsigned long *) (RCC_BASE + RCC_CR);
// wait for 8MHz oscillator to be ready
while (!(*rcc_cr & (1 << 1))) {}
// enable 8MHz oscillator
*rcc_cr |= (1 << 0);
SYSTICK
counter counts downwards from the SYST_RVR
register value to zero, decrementing each system clock tick. Thus interrupt call period may be calculated from the equation
SYSTICK
is 24 bit width, so the maximum call period in our case is about 2 seconds. We will set SYST_RVR
to 0x7a1200
in order to get a one second call period.
// set systick reload value
volatile unsigned long *const syst_rvr = (unsigned long *) (SYST_RVR);
*syst_rvr = 0x7a1200UL;
We finish SYSTICK
configuration by enabling internal clock source and interrupt on underflow bits and starting the counter.
// enable interrupt and start systick
volatile unsigned long *const syst_csr = (unsigned long *) (SYST_CSR);
*syst_csr |= 0b111;
TIM2
interruptUsing a peripheral interrupt is a bit more involved. First of all, we need to enable timer clock. In this case we will use general purpose timer TIM2
.
// enable TIM2 clock
volatile unsigned long *const rcc_apb1enr = (unsigned long *) (RCC_BASE + RCC_APB1ENR);
*rcc_apb1enr |= (1 << 0);
TIM2
is a 16 bit counter with 16 bit prescaler which gives us about 9 minutes maximum call period.
For one second delay we use 0x3e8
prescaler value TIMx_PSC
and 0x1F40
counter value TIMx_ARR
.
// set TIM2 prescaler
volatile unsigned long *const tim2_psc = (unsigned long *) (TIM2 + TIMx_PSC);
*tim2_psc = 0x3e8UL;
// set TIM2 auto-reload register
volatile unsigned long *const tim2_arr = (unsigned long *) (TIM2 + TIMx_ARR);
*tim2_arr = 0x1F40UL;
Timer needs to be instructed to always use TIMx_ARR
value instead of reloading to default.
// auto-reload TIM2 preload register
volatile unsigned long *const tim2_cr1 = (unsigned long *) (TIM2 + TIMx_CR1);
*tim2_cr1 |= (1 << 7);
Tricky part about peripheral interrupts is that they need to be enabled twice: in peripheral registers and in Nested Vectored Interrupt Controller NVIC
registers. Enabling interrupt in timer registers is straightforward
volatile unsigned long *const tim2_dier = (unsigned long *) (TIM2 + TIMx_DIER);
*tim2_dier |= (1 << 0);
In order to enable interrupt in NVIC
registers one needs to know its (interrupt) position. For TIM2
global interrupt position value is 28
. First we find NVIC_ISER
register out interrupt belongs to.
volatile unsigned long *const nvic_iser_tim2 = (unsigned long *) (NVIC_ISER + NVIC_REG_OFFSET(28));
After that setting interrupt bit to enable it.
*nvic_iser_tim2 |= (1 << NVIC_BIT(28));
In these two lines of code macros NVIC_REG_OFFSET
and NVIC_BIT
are quite trivial and defined to make code more readable.
#define NVIC_REG_OFFSET(N) (N>>5)
#define NVIC_BIT(N) (N & 0x1FUL)
To start times we set the register bit
// enable TIM2 timer
*tim2_cr1 |= (1 << 0);
Big difference between TIM2
and SYSTICK
implementations lies in interrupt handle implementation.
void
tim2_irqhandler(void)
{
// clear pending interrupt bit
*tim2_sr = 0b0000UL;
// toggle LED
*gpioc_odr ^= (1 << 13);
}
When using peripheral timer we need to unset the pending interrupt bits in the interrupt handler. Otherwise it will be called continuously.
https://deepbluembedded.com/stm32-interrupts-tutorial-nvic-exti/
https://www.codeinsideout.com/blog/stm32/interrupt/