I've just started learning STM32, and I'm learning it at low (register) level without libraries.

It is very easy to use GPIO: I #define memory addresses (like #define GPIOA_MODER *((volatile uint32_t*)(0x40020000 + 0x00))) of all the registers specified in STM32 docs and then configure them.

But there is a problem with interrupts. I believe I can write my own init function using registers (including the functions void NVIC_SetPriority (IRQn_t IRQn, uint32_t priority) and void NVIC_EnableIRQ(IRQn_t IRQn)). But I absolutely don't know how do I write an Interrupt Service Routine function using registers...

STM32 Reference Manual says that for each type of interrupt, an interrupt vector is specified. Do I understand correctly that this interrupt vector is a memory sector that should contain user code that is executed after the interrupt request is generated?

If so, how do I put my code into the memory sector? Something like #define GPIOA_MODER *((volatile uint32_t*)(0x40020000 + 0x00)) will not work for a block of code, obviously.

  • \$\begingroup\$ You need to do the same thing as the libraries already do, so read library source code. If you don't use libraries at all, you need your own startup code and interrupt vectors, like the libraries and existing startup codes define the vector table. \$\endgroup\$
    – Justme
    Commented Apr 2, 2023 at 14:31
  • \$\begingroup\$ "But there is a problem with interrupts." What is the problem? Also the STMIcro SDK includes a lot of HAL (Hardware Abstraction Layer) libraries. You should be able to avoid writing most lower level code. Most who get paid to do this type of work click on the features they want, let the SDK download and build the feature and go from there. \$\endgroup\$
    – st2000
    Commented Apr 2, 2023 at 15:04

2 Answers 2


ISR Handling without the help of librarys/CMSIS is an somewhat advanced topic. You need to take care about three different sides of the problem.

(1) How to place the Vector-Table

If your are using a "bare-metal" approach you need to place a vector-table at a specific address in FLASH. This commonly is placed at 0x00 and follows a - core or device - specific layout. I posted a ARM M4F MAX32660 example code below. Please check your Datasheet and other documentations for your specific part.

struct VecTab{
    U32 StackPointer;
    void (*Name1)(void);
    void (*Name2)(void);
    void (*NameX)(void);

__attribute__((interrupt("IRQ"))) __attribute__((optimize("-Ofast"))) 
void Handler1 (void) {

static const struct VecTab __attribute__((used,section("Section"),aligned(128))) VecTab = {
    .StackPointer = 0xABCDEDFG,
    .Name1 = Handler1,
    .Name2 = Handler2,
    .Name3 = Handler3

NOTE: As you are struggeling with ISRs in particular, i will go over them a little.

First, they are somewhat "normal" C-functions. There exist special macros, which "hide" the attributes required to make them work - but i tend to add them manually for better readability (I like seeing all attributes):

The attribute "IRQ" tells the GCC compiler, that it should generate a ISR appropriate Pro- and Epilogue. This means, that registers tampered with in the core register file, have to be restored. Also - depending on the compiler and core ISA, some feature like look-ahead and branch-prediction get disabled during ISRs (At least what i read - does not apply to ARM M4).

The attribute -OFast tells the compiler to use the "fast" optimization for this routine. This is not strictly necessary and can actually degrade performance in some cases. But generally i use it during developement and start optimizing the specific ISR after the software is "finished".

NOTE: On ARM, the first 4-Byte entry found on 0x00 in Flash is the initial StackPointer. The Value stored here, is the address of the the TOP of the stack at boot-time and must point into RAM. Make sure to use a properly configured RAM section (e.g. a bank migth be disabled at boot and must be enabled via software - this can not be used for the stack.) You can "pull" this value from your linker-script. Please see "Linker-Symbols". I like to give my self 4kB of stack as a start (When working on like 32kB RAM controllers or bigger) and place my stack at the bottom of the RAM region. This way, if i get a "stack-overflow" the controller will halt with a "mem-fault".

The memory section "Section" must be specified by your linker scipt (See linker-sections) and be placed at the correct address in flash (Commonly 0x00). THe order of your Vectors, the Name1...NameX function pointers, are specified by your specific IC. Please see your Datasheet and !include! required padding.

CAUTION If you are not implementing certain ISRs (writing the C-Routine), always assing the "RESET-Handler" to this vector in the table. This way, if the ISR should fire, the controller is "reset" - make sure to catch this case in your crt0 and take special care about the system vectors.

(2) crt0

Most controllers (still talking bare-metal) need some form of set-up. If your are doing "no-common startup", you have to write your own crt0 in the RESET-Vector (First vector after Stackpointer). This copies the .data section from FLASH to RAM and zerorizes the .bss contents.

You also have to make sure to configure certain peripherals and allow your programm to start from a "fresh-controller" every time. (E.g Watchdogs are commonly reset only by POR, but a software core-reset could also force the controller to reboot. Therefore, it has to be handled in the crt0 as well.

(3) Configure NVIC

Most arm controllers follow this layout:

Your have to enabled ISRs globally via __asm__ __volatile__ ("cpsie i"); and in the NVIC. Please see the NVIC programmers model provided by ARM.

You also have to enable the ISR source in the peripheral. E.g. SPI TX-DMA empty ISR in your SPI registers.

(1.1) There are, at least with ARM-Cores, some special core-ISRs. These ISRs are the first ones in the vector-table and are standardized accross devices. They are extremly helpfull during debugging and if you google a bit, you can find copy-paste ready code to make use of them!

And you are good to go

NOTE: Using a "bare-metal" approach can require a custom linker-script, some knowledge about ISRs in generall, a crt0 routine, and some fiddenling. Going with CMSIS is much easier and portable accross devices, as most ICs are supported.

BUT I still remember the first project i did my own vector-table, linker-script, crt0 and so on. The learning curve was steep and it was a good decision to go the extra mile and collect some new knowledge.

  • \$\begingroup\$ used attribute is noop. better do it in the linker script KEEP(.... \$\endgroup\$ Commented Apr 3, 2023 at 18:57
  • \$\begingroup\$ @0___________ KEEP() prevents overlays. So in some situations attributes are the way to go. Furthermore, keeping your "configuration" to a single file is easier to handle IMHO. Refering to someones actions as "beeing noop" implies the person "beeing a noop" - kind of an advanced, besides questionable, strategy to formulate a response to a person of unknown skill-level. \$\endgroup\$ Commented Apr 4, 2023 at 16:51
  • \$\begingroup\$ noop - means no op (no operation). It does not have anything in common with any person. \$\endgroup\$ Commented Apr 4, 2023 at 18:12
  • \$\begingroup\$ @0___________ Then i misunderstand. I am sorry. \$\endgroup\$ Commented Apr 4, 2023 at 22:17
  • \$\begingroup\$ And it is a very common word in the programmers slang \$\endgroup\$ Commented Apr 5, 2023 at 7:10

You need to place the address of your interrupt handler at the exact datasheet-specified offset from the vector table base. By default the vector table base is address zero.

For example on the STM32G474, the address of the LPTIM1 ISR must be placed at address 0x104. The "manual" way to do this is in an a assembly file which contains all your vectors. You probably already have one that contains your reset vector and starting stack pointer. An entry looks like this:

e.g. startup.S:

    .org 0x104          ; required offset from VTOR, found in datasheet
lptim1_vec:             ; arbitrary label name
    .word   lptim1_isr  ; name of a unit (asm label or c function) which should be jumped to in order to handle the LPTIM1 interrupt

And your handler:

e.g. handlers.c:

void lptim1_isr(void)
    // Do something

You need not do anything else in assembly before or after entering the handler C function, this is a specific advantage of Cortex devices.

If you did not create this file yourself, there is a good chance you are using a provided one that already contains a weak reference to a C function, which you can override by providing the linker with your own handler of the same name, you just need to find out what that name is.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.