How to Manually Program an Interrupt Service Routine (ISR) for STM32F4 MCU Without Using Libraries?
Good day everyone,
I have gone through the Datasheet and User Manual for my STM32F4 Microcontroller Unit (MCU) in great detail, including PM0214 for STM32F4xx MCUs. Additionally, I have searched various online resources that provide information on programming interrupts without using a library but didn't find much guidance. My question is if NVIC's integration with hardware essentials makes it difficult to program an interrupt manually by specifying ISR address and function without relying on any libraries? Everywhere I look there are functions like:
Enable IRQn using NVIC.
Setting the priority for a specified IRQn type can be achieved by using
NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
.
What if someone desires to manually code an ISR for the purpose of learning?
Which steps do I need to take? Is there documentation available for reference? Would it be advisable or worthwhile to follow this approach?8 Replies
First things first - lets keep in mind there are multiple layers of abstraction in the ecosystem
You have the ST high level and low level HALs - it is very common to not use these, or just use them as reference / guidance when implementing your own drivers
Then there is CMSIS - this is a standard made by arm so we don't reinvent the wheel a million times in how some basic functions are handled, taking care of some of the core Cortex-XX things. In general, use CMSIS and don't throw it away - it is much less common to not use CMSIS than it is to not use whichever HAL
NVIC_SetPriority
comes from CMSIS - so typically we would always use it
(it's worth noting here, part of why Cortex-M3 was novel was ARM made not just the core, but a bunch of supporting stuff too, they mandated that there would be an NVIC and a Systick in all the systems and so these parts can be common even across different manufacturers, never mind parts - this is a huge step up from the older days. We can see some of the pain of the many interrupt handler implementations in the RISCV ecosystem today, though I suspect that will normalise over time)
If you look at it though, all it does is poke a couple of registers, there is absolutely nothing stopping you poking those registers directly - we can definitely replace NVIC_SetPriority
if we want to - I would go as far as to say this is easy.
One other note on things to consider throwing away and things to keep - keep the low level device headers i.e. the ones which give names to the memory map: https://github.com/STMicroelectronics/cmsis_device_f4/blob/master/Include/stm32f407xx.h - we generate them now from svd
To do an interrupt from scratch we need to go further though. At the end of the day, an interrupt is some function, with some specific calling convention, which exists at some explicit location in memory. We will see some systems let us point the NVIC to an interrupt table at various locations and know this can be somewhat configurable.GitHub
cmsis_device_f4/Include/stm32f407xx.h at master · STMicroelectronic...
Provides the STM32Cube MCU Component "cmsis_device_f4" of the STM32F4 series. - STMicroelectronics/cmsis_device_f4
But how do we define that table? Well we can see how ST do it here:
https://github.com/STMicroelectronics/cmsis_device_f4/blob/master/Source/Templates/arm/startup_stm32f401xc.s
This is startup code, we see the table of vectors being declared at line 60, but that's only half the story, we should also look at the linker file:
https://github.com/STMicroelectronics/STM32CubeF4/blob/master/Projects/STM32446E-Nucleo/Templates/STM32CubeIDE/STM32F446RETX_FLASH.ld
This is where the table actually gets placed at a specific location:
In English - put the symbol ".isr_vector" at the start of the ROM section - which is flash - which is where our table should be on startup
... we're still not done yet...
Lets go back to the startup code and look inside the table... what is this?
That's all the symbols for all the interrupt handlers being declared... we are about to have some fun though because what is this....
This is declaring a "weak linkage" handler for each interrupt. It's important to understand this weak linkage as it can be a source of confusion - it means "if no other thing provides this symbol, provide this value for it" - this lets the startup code install default handlers for every interrupt. When you declare the symbol in your code i.e. make a function
void RTC_WKUP_IRQHandler(void)...
you override that linkage, and now your function instead of the default handler will be there. I want to call this part out in particular. It is a common mistake in my experience for people to misname an interrupt vector - there are not really warnings when this happens because no symbol is missing, just the interrupt handler isn't called anywhere - this will make it "just not work" and it wont be clear why - if you have a debugger you will see when the interrupt fires you get stuck in the default handler - that is the clue you messed up there!
All of this is just a very long way to say - if you are using the ST startup code, you just need to define a function with a specific name and it will handle that interrupt - making your own interrupt handler for RTC wakeup events is as simple as creating the function RTC_WKUP_IRQHandler
and doing any setup to get the interruptsI think you scared him 🙂 for sure all the info though.
@Sterling if you look at the code around this commit https://github.com/zacck/stm32f4xx_device_drivers/commit/094f0bd64252e6b5323cbf0b62e137d8039e0453 you will see interrupts from scratch
GitHub
Interrupt work · zacck/stm32f4xx_device_drivers@094f0bd
- Added SYSCFG struct and its peripheral
- Added a macro to convert a base address to a port code
- Defined IRQ numbers for the EXTI Peripheral
- Added code to configure pin interrupts
This is really neat 🤓
Ah it's all just reading the reference manual and trying to code it up, mostly I fail.