Now that we can turn an LED on, let’s see if we can do something more exciting: make the LED blink. Surprisingly, this is more difficult than you might expect!
Blinking boils down to “turn the light on, wait a while, turn the light off, wait a while” and repeat forever. We already know how to turn the light on and off, as well as repeating forever. The trick lies in “wait a while”.
In a conventional Rust application, we’d probably call something like
std::thread::sleep
, but we don’t have access to libstd
on an
Arduino as that library is too high-level. We will have to implement
it ourselves!
It’s easy enough, all we have to do is loop a bunch of times. If the
Arduino processor runs at 16MHz, we can waste 16000 cycles to take one
millisecond. We will execute a nop
instruction to waste the time:
1 2 3 4 5 6 7 8 9 10 |
|
Just compile this and away we go! Or not…
1 2 3 4 5 |
|
Right, we haven’t actually defined any of the Iterator
logic; that’s
in libcore
which we don’t have yet. Let’s skip that and do something
a little more C-like. We can just loop and increment integers and
compare them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
And… that fails too:
1 2 3 |
|
Ok, no division, even if it is just a constant and should be computed at compile time. Well, we can hard code it for the moment…
1 2 3 4 5 6 7 |
|
OK, wow, no addition or comparison either. There’s no way around
this – we really need libcore
or else we are stuck with a pretty
primitive environment. Since we know we have issues compiling all of
libcore, let’s try a smaller part, just enough to compile this
example.
Previously, we had copied in some small snippets from libcore, but let’s replace those excerpts with the complete files and drag in a few more. After some trial-and-error, this small set compiles:
clone
cmp
intrinsics
marker
ops
option
With it compiling, let’s actually call sleep_ms
in our main
and
load the program onto the board:
1 2 3 4 5 6 |
|
Look at that nice, steady blinking. Blinking at a rate that is nothing like 500 milliseconds. Let’s take a look at the disassembly for the inner loop to understand why:
1 2 3 4 |
|
We increment our counter and check to see if we’ve exceeded our
limit. In all cases except the last iteration we will branch back to
the beginning of the loop, bringing the total cycle count of the loop
to six. Compare that to the naive calculation that the nop
would
take one cycle and the rest of the loop would be free. Dividing the
inner loop constant by six gets us much closer to the appropriate
duration.
The outer loop and the function call itself also have some overhead, but these only add up to a few cycles per inner loop. Since the inner loop corresponds to many thousands of cycles, a few cycles is a small error and I think can be safely ignored.
An interesting aside is that I have no idea why the nop
does not
occur inside the loop. The compiler has reordered the code such that
the nop
occurs in the variable initialization of the function. You
can change the code to just asm!("")
and accomplish the same goal of
preventing the loop from being optimized away.
Next time, we will see if we can do something a little more structured than counting cycles to sleep. As before, check out the repository for the code up to this point.