1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
|
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 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.
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 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.
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 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.
|