Skip to content

Libmaple interrupt handlers

Roger Clark edited this page May 23, 2015 · 1 revision

(from https://github.com/leaflabs/libmaple/blob/master/notes/interrupts.txt )

Interrupt (IRQ) Handling in libmaple

There have been various threads asking about interrupt handling in libmaple. This file explains the libmaple interrupt handling interfaces, how libmaple organizes its interrupt handlers, and how to write new interrupt handlers.

If you know the Cortex M3 and the libmaple sources pretty well, you can skip to the end to read how to add a new interrupt handler. Otherwise, read on.

  1. Interrupts in Wirish

There are very few Wirish-level convenience functions for handling interrupts. The most obvious one is attachInterrupt(), which is used for external interrupt handlers:

http://leaflabs.com/docs/lang/api/attachinterrupt.html

Another example is HardwareTimer::attachInterrupt(); a usage example is here:

http://leaflabs.com/docs/lang/api/hardwaretimer.html#using-timer-interrupts

What these have in common is that they take a pointer to the function the user wants to use as an interrupt handler, and pass it down to the libmaple proper interface for the subsystem. For example, attachInterrupt() calls exti_attach_interrupt(), and HardwareTimer::attachInterrupt() calls timer_attach_interrupt().

So, as usual, the Wirish functions are just thin wrappers around the libmaple proper interfaces.

  1. Interrupts in libmaple proper

The libmaple proper interfaces all use functions named foo_attach_interrupt(). So there's the exti_attach_interrupt() and timer_attach_interrupt() routines that have already been mentioned, but there are also some others which (at time of writing) don't have Wirish equivalents, like dma_attach_interrupt().

These functions all behave the same way: they take a particular peripheral interrupt and a pointer to a user function, and they do whatever is necessary to turn on the interrupt line and ensure that the user's function gets called exactly when that interrupt occurs.

This in itself is a useful abstraction above the hardware. To understand why, here's a bullet-point primer on how interrupts work on STM32/Cortex M3 (read about the NVIC in a Cortex M3 book to understand all the details; these are just the basics):

  • Each series of STM32 microcontroller (STM32F1, STM32F2, etc.) specifies a certain number of IRQs (the libmaple type which enumerates the IRQs is nvic_irq_num; see the libmaple/nvic.h documentation for all the details).

  • Each IRQ has a number, which corresponds to a real, physical interrupt line inside the processor. When you talk about an "IRQ", you usually mean one of these interrupt lines.

  • The interrupt hardware can be configured to call a single function per IRQ line when an interrupt associated with the IRQ has happened (e.g. when a pin changes from low to high for an external interrupt).

  • However, sometimes, various interrupts share an IRQ line. For example, on Maple, external interrupts 5 through 9 all share a single IRQ line (which has nvic_irq_num NVIC_EXTI_9_5). That means that when any one (or any subset!) of those interrupts occurs, the same function (the IRQ handler for NVIC_EXTI_9_5) gets called.

    When that happens, your IRQ handler has to figure out which interrupt(s) it needs to handle (usually by looking at bitfields in some sort of status register), do the right thing to handle them, and then sometimes perform cleanup actions after finishing (e.g. external interrupts need to clear pending masks, or the interrupts will fire over and over again).

So now it should make sense why libmaple's foo_attach_interrupt() handlers are convenient: they let you pretend that each interrupt has its own IRQ line, even though that's often not true. They also take care of set-up and clean-up tasks for you. This means a performance hit, but the convenience is usually worth it.

  1. Where libmaple keeps its IRQ Handlers

As noted above, for each nvic_irq_num, there's an IRQ line, and for each IRQ line, you can set up a single function to call. This section explains where libmaple keeps these functions and what they're called.

You typically will only need the information in this section if there's no foo_attach_interrupt() routine for the kind of interrupt you're interested in. The discussion is at the hardware level, and assumes you know how the NVIC works. You can try looking in the (freely available) Cortex M3 Technical Reference Manual for the details, but Joseph Yiu's book, "The Definitive Guide to the Cortex M3" is a much more beginner-friendly resource, and covers everything you need to know.

3.1: The vector table files (vector_table.S)


While they don't contain interrupt handlers themselves, vector table
files are where to look for what they're named.

You can find the names libmaple expects for IRQ handlers by looking in
the vector table file for the microcontroller you're interested
in. This file is always named vector_table.S, but there are multiple
such files throughout the libmaple source tree. This is because the
different STM32 series and even lines and densities within a series
(like the value and performance lines and low/medium/high/XL-densities
for STM32F1) each have different sets of IRQs.

For portability, then, the vector table files must live somewhere
where nonportable code goes, namely, under libmaple/stm32f1/,
libmaple/stm32f2/, etc. as appropriate. The libmaple build system
knows which one to use for each board.

For example, the vector table file for the microcontroller on the
Maple (STM32F103RB, a medium-density performance line F1 -- whew!)  is
libmaple/stm32f1/performance/vector_table.S. Here's a snippet:

            .globl      __stm32_vector_table
            .type       __stm32_vector_table, %object

    __stm32_vector_table:
    /* CM3 core interrupts */
            .long       __msp_init
            .long       __exc_reset
            .long       __exc_nmi
            .long       __exc_hardfault
            .long       __exc_memmanage
            .long       __exc_busfault
            .long       __exc_usagefault
[...]
            .long       __irq_exti0
            .long       __irq_exti1
            .long       __irq_exti2
            .long       __irq_exti3
            .long       __irq_exti4

The names of the interrupt handlers appear one per line, after the
.long. The names are chosen to make it pretty obvious what IRQ line is
associated with the function. Additionally, since this is the actual
vector table for the chip, the names appear in NVIC order, so you can
check the interrupts and events chapter in the chip reference manual
to make sure which IRQ line a function is associated with.

3.2: Interrupts handled by libmaple
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The vector table file is just an assembly stub which defines the
actual vector table (i.e., the initial stack pointer and table of
function pointers that go at address 0x0), but it doesn't define the
interrupts themselves. It leaves that up to the rest of libmaple.

Though it doesn't handle them all, libmaple does provide many
interrupt handlers when it can provide some useful default
behavior. For example, it defines USART interrupt handlers that store
received bytes in a ring buffer. It defines EXTI interrupt handlers
that figure out which external interrupt actually fired, and call the
corresponding user interrupt handler (which was set either with
attachInterrupt() or exti_attach_interrupt()).

When there is a default IRQ handler, it lives in a .c file for the
peripheral the interrupt is related to. Again, usually for reasons of
portability, these usually live somewhere series-specific. For
instance, the USART IRQ handlers for Maple live in
libmaple/stm32f1/usart.c. More rarely, they'll be in some top-level
file under libmaple/ if the same interrupt is available on all
supported series (e.g. at time of writing, the EXTI interrupts in
libmaple/exti.c).

Use the vector table file and grep to find IRQ handlers for the MCU
you're interested in.

3.3: Interrupts not handled by libmaple (isrs.S)

Though libmaple does provide some IRQ handlers, it doesn't define one for every available interrupt. This is true for various reasons: maybe the peripheral or interrupt isn't supported yet, maybe there's no useful default behavior, etc.

In this case, it would be wasteful to have a separate function for each unhandled interrupt. To handle this, there's a single file that deals with all unhandled interrupts. Its name is isrs.S, and it lives in the same directory as the corresponding vector_table.S. For example, for Maple, the file is libmaple/stm32f1/performance/isrs.S.

These aren't complicated; read the source to see how they work.

  1. Adding your own interrupt handlers

When adding an interrupt handler (or overriding a default one), you need to decide whether you want it for a particular program, or if what you're writing is general-purpose enough that it should live in libmaple itself.

4.1 Adding a special-purpose interrupt handler


If you're just writing a one-off IRQ handler for your own use, your
job isn't too complicated, provided you know the peripheral you're
interested in well enough.

You need to:

1. Define an IRQ handler with the right name
2. Turn on the IRQ line with nvic_irq_enable()
3. Set any relevant interrupt enable bits in peripheral registers

You first need to define a function with the right name. Look up the
name in the vector table file for your board (see above). For example,
to define your own SDIO interrupt handler for Maple, define a function
named __irq_sdio():

    void __irq_sdio(void) {
        // Your handler goes here.
    }

The libmaple linker scripts are smart enough to notice that you've
done this and put a pointer to this function in the appropriate place
in the vector table.

IMPORTANT: the function you define MUST HAVE C LINKAGE.  C++ name
mangling will confuse the linker, and it won't find your function. So
if you're writing your IRQ handler in a C++ file, you need to define
it like this:

    extern "C" void __irq_sdio(void) {
        // etc.
    }

To enable the interrupt, you need to call nvic_irq_enable() with the
nvic_irq_num you want to enable. For SDIO, that looks like this:

    nvic_irq_enable(NVIC_SDIO);

This line typically goes in your setup code. Check the docs for
<libmaple/nvic.h> to find the nvic_irq_num you need.

Beyond that, you also sometimes need to set some interrupt enable bits
in a register associated with the peripheral. These bits vary by
peripheral; consult the reference manual for your chip for the
details. For example, SDIO interupts are enabled using bits in the
SDIO_MASK register.

4.2 Adding a general-purpose interrupt handler

Take this route only when you're sure your handler will be generally useful enough to ship with every copy of libmaple. Since the vector table is always present, your interrupt handler will consume every user's Flash. Normally, this is only worth it when defining some sort of foo_attach_interrupt() routine for a commonly used interrupt, though there are exceptions (e.g. the USART handlers).

To add an interrupt handler, you need to define interrupt handlers with the appropriate names as described in the previous section. These will live under the series directory for the microcontroller you're using. For example, for Maple, they'd live under libmaple/stm32f1.

DO NOT PUT THEM IN THE TOP-LEVEL LIBMAPLE DIRECTORY UNLESS THE INTERRUPT IS AVAILABLE ON ALL SUPPORTED SERIES, AND YOU CAN TEST IT ON MCUs FROM DIFFERENT SERIES.

Just because an IRQ is available and purports to work the same way on multiple STM32 series doesn't mean that it in fact does. For example, there are silicon bugs related to I2C interrupt handling on STM32F1 that require special-purpose workarounds. When in doubt, leave your handler in the series directory you can test. It can always be moved later.

After you've added the handler, you need to add IRQ enable and disable routines for the peripheral. At the very least, this needs to take a pointer to the peripheral's device and an argument specifying which IRQ or IRQs to enable. For example, here are some timer IRQ enable/disable routines present in <libmaple/timer.h>:

/**
 * @brief Enable a timer interrupt.
 * @param dev Timer device.
 * @param interrupt Interrupt number to enable; this may be any
 *                  timer_interrupt_id value appropriate for the timer.
 * @see timer_interrupt_id
 * @see timer_channel
 */
void timer_enable_irq(timer_dev *dev, uint8 interrupt);

/**
 * @brief Disable a timer interrupt.
 * @param dev Timer device.
 * @param interrupt Interrupt number to disable; this may be any
 *                  timer_interrupt_id value appropriate for the timer.
 * @see timer_interrupt_id
 * @see timer_channel
 */
void timer_disable_irq(timer_dev *dev, uint8 interrupt);

It's OK to take a flags argument for enabling/disabling multiple IRQs at once.

If you're adding a foo_attach_interrupt(), it needs to work similarly, except it will also take a pointer to the user function to call when the interrupt occurs. When called, it must enable the correct NVIC line (which is usually available via the device pointer), as well as set any interrupt-enable bits in the appropriate peripheral register necessary to turn the interrupt on. Here's a timer example:

/**
 * @brief Attach a timer interrupt.
 * @param dev Timer device
 * @param interrupt Interrupt number to attach to; this may be any
 *                  timer_interrupt_id or timer_channel value appropriate
 *                  for the timer.
 * @param handler Handler to attach to the given interrupt.
 * @see timer_interrupt_id
 * @see timer_channel
 */
void timer_attach_interrupt(timer_dev *dev,
                            uint8 interrupt,
                            voidFuncPtr handler) {
    dev->handlers[interrupt] = handler;
    timer_enable_irq(dev, interrupt);
    enable_irq(dev, interrupt);
}

You also need a corresponding foo_detach_interrupt() routine.

In the case of IRQs for which a foo_attach_interrupt() routine is available, the IRQ handler needs to do any register inspection necessary to ensure the user handler is called only when the corresponding interrupt has occurred (for example, don't call timer capture/compare interrupt handlers due to an update event). How this works will depend on the peripheral.

The IRQ handler must also perform any cleanup actions that are necessary. For example, various interrupts will cause the IRQ to fire until you clear some bits in a peripheral register. Users get confused and annoyed when their handlers get called forever. Clean up after them, so they don't need to worry about the details.