Previously, we wrote some code that allowed us to sleep by waiting for a number of cycles to pass. However, we had to peek at the disassembly to know how many cycles we were spending and adapt our source code to match. While it got us started, it’s not a very elegant solution.
The Arduino Uno uses an ATmega328P processor. One of the features of this processor are 3 built-in timers that can trigger interrupts at certain periods. Interrupts are special bits of code that take over control of the processor when something important happens. These are often time-critical things that need to be handled quickly.
What would be ideal is if we could rely on the timer feature to
implement our sleep
method. To get started, we are going to need the
ability to specify the interrupt vector.
The interrupt vector is a table of 26 instructions that must be placed at a specific section in memory. Each element in the table corresponds to a specific interrupt, and should consist of one instruction that jumps to the appropriate interrupt handler.
To do this, we need to write a little bit of assembly:
1 2 3 4 5 |
|
In order to use this, we need to include it when linking all of our
code together. We also have to disable the existing interrupt vector
that would be added. This is done via the -nostartfiles
flag:
1
|
|
If you compile right now, you will get a whole bunch of errors of the form:
1 2 |
|
Our interrupt vector is trying to jump to a bunch of symbols that we
haven’t yet defined. We could do the simple thing and define a bunch
of _ivr_*
methods in Rust (and I did, to start with), but that’s
rather annoying. Instead, we can use weak linking to define a kind
of “fallback” symbol. We will have one simple handler that just
returns from the interrupt, and set each handler to use that unless it
is defined:
1 2 3 4 5 6 |
|
The only outlier is _ivr_reset
which we define to point to our
main
method, avoiding extraneous indirection. At this point, we
should be compiling again, but not using the interrupts yet. Let’s
change that.
Following this guide, we can see all the details of setting up the timer. At a high level it’s:
- Register an interrupt handler.
- Disable interrupts.
- Set a bunch of values as determined by the datasheet and math.
- Enable interrupts.
We will copy all of the values and registers from this article to
setup timer 0, but with a 1kHz rate instead of 2kHz. This matches
nicer with our sleep_ms
method which waits milliseconds.
Let’s use a little bit of nice Rust for a change. When we disable
interrupts, we really want to make sure we enable them again! In a
language like Rust, we can use a (misleadingly labeled) pattern known
as Resource Acquisition Is Initialization (RAII). We will create a
struct
that disables interrupts when it is created and enables them
when the struct is dropped. This means we can never forget to
re-enable interrupts as the compiler will ensure things are restored!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
We can bundle this into a nice wrapper:
1 2 3 4 5 6 |
|
And use it like so:
1 2 3 |
|
To define the interrupt handler, we simply create a method that matches the expected name from our assembly file. The method simply increments a global variable each time it is triggered:
1 2 3 4 5 6 |
|
And re-implement our sleep_ms
function to:
1 2 3 4 5 6 7 8 |
|
Compile this and load it onto the board, and we are greeted with the
sight of nothing blinking. It’s time to dig into more
disassembly. Here’s what _ivr_timer0_compare_a
looks like:
1 2 3 4 |
|
Checking the instruction set manual and the datasheet, we will notice a few problems:
- We use
ret
(Return from Subroutine) instead ofreti
(Return from Interrupt). - We do not save and restore the Status register.
- We do not save and restore the
r24
register.
Let’s modify our handler with a bit more assembly to address all three issues:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
That’s certainly a bit longer, but it compiles and works again! And it
will continue to work, so long as the compiler always decides to use
r24
for the incremented value, something we have no control over. As
you might guess, there’s a better solution.