I haven’t had much experience with the STM32G0 family yet, but my course participants have. Recently, we were solving together a problem with interrupts not working on the STM32G031J6M6 known from the STM32G0316-DISCO board. It was an interesting problem, so I decided to share it more broadly.
Handling interrupts via NVIC in STM32
NVIC is an abbreviation – Nested Vector Interrupt Controller. It is the controller responsible for interrupts in STM32 microcontrollers. It is much more complex than what we used to have, for example in AVR. But there’s no need to be afraid of it 🙂
I’ll discuss only what is needed to describe the problem that I was dealing with together with a participant. We’re interested in the letter V in this abbreviation, i.e. Vector. In other words, it is an array of pointers. Why do we need it?
Several dozen interrupt lines go into the NVIC controller. Each of these lines triggers a different interrupt. Each interrupt has its own handler. And this is exactly where the interrupt vector comes in. It is nothing more than an array of pointers to functions that implement specific interrupts.
This vector is laid out according to how the chip manufacturer directly connected the interrupt lines to the NVIC. We really don’t want to change that order 🙂
What does such a vector defined in a program look like? It is written in assembly in the startup file for each microcontroller. For the discussed STM32G031J6M6 it looks as follows:
/****************************************************************************** * * The minimal vector table for a Cortex M0. Note that the proper constructs * must be placed on this to ensure that it ends up at physical address * 0x0000.0000. * ******************************************************************************/ .section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word SVC_Handler .word 0 .word 0 .word PendSV_Handler .word SysTick_Handler .word WWDG_IRQHandler /* Window WatchDog */ .word PVD_IRQHandler /* PVD through EXTI Line detect */ .word RTC_TAMP_IRQHandler /* RTC through the EXTI line */ .word FLASH_IRQHandler /* FLASH */ .word RCC_IRQHandler /* RCC */ .word EXTI0_1_IRQHandler /* EXTI Line 0 and 1 */ .word EXTI2_3_IRQHandler /* EXTI Line 2 and 3 */ .word EXTI4_15_IRQHandler /* EXTI Line 4 to 15 */ .word 0 /* reserved */ .word DMA1_Channel1_IRQHandler /* DMA1 Channel 1 */ .word DMA1_Channel2_3_IRQHandler /* DMA1 Channel 2 and Channel 3 */ .word DMA1_Ch4_5_DMAMUX1_OVR_IRQHandler /* DMA1 Channel 4 to Channel 5, DMAMUX1 overrun */ .word ADC1_IRQHandler /* ADC1 */ .word TIM1_BRK_UP_TRG_COM_IRQHandler /* TIM1 Break, Update, Trigger and Commutation */ .word TIM1_CC_IRQHandler /* TIM1 Capture Compare */ .word TIM2_IRQHandler /* TIM2 */ .word TIM3_IRQHandler /* TIM3 */ .word LPTIM1_IRQHandler /* LPTIM1 */ .word LPTIM2_IRQHandler /* LPTIM2 */ .word TIM14_IRQHandler /* TIM14 */ .word 0 /* reserved */ .word TIM16_IRQHandler /* TIM16 */ .word TIM17_IRQHandler /* TIM17 */ .word I2C1_IRQHandler /* I2C1 */ .word I2C2_IRQHandler /* I2C2 */ .word SPI1_IRQHandler /* SPI1 */ .word SPI2_IRQHandler /* SPI2 */ .word USART1_IRQHandler /* USART1 */ .word USART2_IRQHandler /* USART2 */ .word LPUART1_IRQHandler /* LPUART1 */ .word 0 /* reserved */
What we see here are function names, i.e. pointers to them. These functions are the interrupt handlers at the appropriate positions in the vector. You can notice that some positions are empty. That’s because ST did not connect any interrupt at those offsets. So there is no way to use that line.
We can compare this vector with what we have in the documentation. The interrupt vector together with positions and interrupt priorities can be found in the Reference Manual.

What happens when the NVIC detects an interrupt? It jumps to the address given in the interrupt vector. If the interrupt was on line #5, it jumps to the function in the vector at position 5. Simple, isn’t it?
VTOR
How does the NVIC know where in memory this vector is located? Is it permanently planned and assigned from the top down? Well, no.
This vector can be in different locations. This is used, for example, when writing bootloaders. A bootloader is a separate program on the microcontroller that calls the target application. That application is located elsewhere in memory and has its own interrupt vector.
So you have to tell the NVIC where it should jump in order to handle an interrupt. Now an important thing. The NVIC is part of the core, which is designed by ARM. It is not a module added by ST or another Cortex-M microcontroller manufacturer.
The core documentation (and NVIC) is a separate file from the Reference Manual. For Cortex-M0 and M0+ it is HERE.
Getting to the point: There is a special register in the microcontroller core that stores the address where the NVIC should look for the vector table. And that’s it 🙂
You have to fill this register. When? Before the main program starts.
BUG in STM32G0 HAL
If you use HAL and you’re not writing a bootloader, then you probably don’t care about setting this register. This is done in the function void SystemInit(void), which is called in the startup file.
You don’t care until… when VTOR is not being set.
Well, in the HAL for STM32G0 there is a bug that by default does not set the vector table. It is specifically for the example on the STM32G031J6M6, but I also found it in the HAL core itself.
Participants told me that other G0s work correctly. It’s quite possible that for specific microcontrollers something is different in their examples.
The bug is weird. This is what the SystemInit function looks like:
void SystemInit(void)
{
/* Configure the Vector Table location -------------------------------------*/
#if defined(USER_VECT_TAB_ADDRESS)
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation */
#endif /* USER_VECT_TAB_ADDRESS */
}
There is this clever define here: USER_VECT_TAB_ADDRESS
And unfortunately, it is not defined anywhere. This whole VTOR setting is disabled by default.
In HALs for older chips it looks a bit different. Here even that constant is named as if it were meant for user needs. But where is the default setting?
I have a theory.
They probably wanted to add a unified relocation of the interrupt vector. If we write a specific bootloader or a program completely without it, then we have the default setting.
If we want to write the main program that the bootloader jumps to, then we set our own user table.
Only… it’s implemented halfway. There is the user part, and the default one is missing 🙂 Something went wrong.
What will happen if VTOR is not set?
We have 2 options:
- No interrupts (mild)
- The program will end up in HardFault when trying to enter the interrupt handler (worse)
Solution
What to do? I submitted a fix on GitHub: https://github.com/STMicroelectronics/STM32CubeG0/pull/33
They accepted it, so we’ll see when they fix it. What to do as of today (May 23, 2022)? There are 3 options:
- Add, in the SystemInit() function, an assignment of the default base address: SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET;
Unfortunately, this is not a user section in CubeMX, so after regenerating it the solution will disappear. - Add a global definition of USER_VECT_TAB_ADDRESS. Quite a good idea. You can do this in the compiler settings, or in the compile command with the -D flag.
- Pull my fix into the HAL. That would be best, but maybe it’s worth waiting until ST makes the fix on their side 🙂
Summary
As you can see, HAL can still have annoying bugs. However, I still think it is a very good tool and it’s worth using.
Setting VTOR will be useful when we write a bootloader. Let me know in the comments if you’d like me to show how to write one.
If you would like to learn STM32 programming with me, I invite you to my course (the course content is conducted in Polish): https://kursstm32.pl
Do you have gaps or don’t know the C language at all? On June 1st I’ll tell you how to learn C for microcontrollers. Sign up for the free webinar (the webinar content is conducted in Polish) at https://cdlamikrokontrolerow.pl/webinar

0 Comments