diff options
-rw-r--r-- | notes/interrupts.txt | 348 |
1 files changed, 348 insertions, 0 deletions
diff --git a/notes/interrupts.txt b/notes/interrupts.txt new file mode 100644 index 0000000..631e018 --- /dev/null +++ b/notes/interrupts.txt @@ -0,0 +1,348 @@ +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. + +2. Interrupts in libmaple proper +-------------------------------- + +The libmaple proper interfaces all use functions named +foo_attach_interrupt(). So tehre'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 M3s book to +understand all the details; these are just the basics): + +- Each of the series of STM32 microcontroller 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 the 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 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. + +3. 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 keep these functions, what they're called, and +how you can write your own. + +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 (like STM32F1, STM32F2) 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 various vector_table.S must live somewhere +where nonportable code goes: somewhere 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 stack table 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. + +4. 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 +whether 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 an 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. |