Part 3: Improving Safety
Overview
Our goal in this post is to eliminate as much unsafe code from main
as possible. We will do this by using Rust’s type and lifetime systems
to enforce hardware invariants. This is similar to what we did in the
previous post for clock modes, but across all of our code.
We’re not going to end up with any new behaviors by the end of this post. We’re just laying a foundation of ways to think about using Rust’s safety features for hardware.
Hardware Allocation
The overall idea behind this post is that hardware should be allocated before a bit of code can use it. This implies the same things as a memory allocation in Rust:
-
The allocation itself is unsafe, but creation of a wrapper that handles deallocation is safe.
-
If user code attempts to allocation a resource that is unavailable, we must fail in some way.
Rust, of course, responds to a failure of memory allocation by
aborting. In this blog series, we’ll typically panic. I prefer panics
to an Option
or Error
type since they allow the example code to
follow the happy path without getting bogged down in
unwrapping. Panics also make it possible for us to include a useful
message indicating which hardware unit was incorrectly allocated.
The MCG and OSC
We will start with the easiest structs to update. The hardware modules for the MCG and OSC are not dependant on any other modules, so they do not reqire the complexity that some of our other structs will.
The update to the OSC looks like this:
There are lots of changes here. Let’s go through them all:
- We have a new import for
AtomicBool
, and some related bits. - The old
Osc
struct is now calledOscRegs
- There is a new struct called
Osc
, which holds a reference to the registers - There is a new global
AtomicBool
acting as a flag to indicate when there is an outstanding reference to theOsc
. - The
::new
function now returns an owned struct, instead of a reference. It will also panic if there is already an instance ofOsc
in use. - We implement
Drop
forOsc
, to indicate when there is no longer an active instance, and it is safe to create a new one.
It is important that we use an atomic boolean for this flag. Besides being friendly to Rust’s rules for global variables,an atomic protects us from race conditions when accessing the OSC.
The MCG struct is updated similarly, and uses an identical allocation
pattern. An important addition with the MCG is that each of the clock
state structs now own the MCG instance, and pass that ownership down
the chain as the clock state is changed. Here’s all of the MCG
code. I’ve left out the change to indirect all register accesses
through the regs
member, since that would balloon this example.
Remember Your Invariants
There is one more lingering piece of unsoundness in this code: The way we configure the MCG requires the OSC to be initialized, but nothing currently enforces this. We can fix this with a simple token type that indicates that the OSC is configured:
The SIM
Allocation for a Sim
instance can be handled in the same way as for
the MCG and OSC. We add an AtomicBool
to act as a lock, and
implement Drop
to clear that lock when the Sim
goes out of
scope. I trust you can do this on your own at this point.
Clock Gates
The next problem we face with the SIM is clock gates. Ideally, we want clock gating to be tied to the allocation of a hardware unit - it should be impossible to have an outstanding reference to a piece of hardware without its clock gate being enabled, and vice-versa. The first is necessary for safety reasons - accessing hardware that is disabled will cause a hardware fault. The second is for power-efficiency reasons. Any hardware we’re not using should be turned off if possible.
We’ll handle this by moving allocation of most hardware units to the SIM. Let’s start with Ports:
The new ClockGate
struct uses the bitband to enable or disable a
clock gate when it is created or dropped. The port allocation function
passes ownership of the ClockGate
to the Port
instance. Let’s take
a look at that now:
The move to a PortRegs
struct is the same pattern we’ve seen for
other hardware units so far. Wrapping the reference in UnsafeCell
is
due to how pins are handled - we’ll cover that shortly. The locks
member is also for safe pin handling. The _gate
member keeps the
appropriate clock gate enabled for as long as this Port
instance
exists. The ClockGate
will be dropped when the Port
is - precisely
what we want.
Safe Pins
Making pins safe requires some additional reasoning. Since a Port
being dropped will cause the associated clock gate to be disabled, we
know that a Port
must outlive any associated Pin
. This means that
each Pin
struct must borrow its parent Port
. In order for multiple
Pins
to borrow a single Port
, these must be immutable borrows.
This has an important implication: It must be possible to allocate a
Pin
from a borrowed Port
- possibly even simultaneously in
separate tasks or interrupt handlers. This is a form of internal
mutability, and is why the PortRegs
reference is wrapped in an
UnsafeCell
. Internal mutability often requires a bit more attention
to detail in order to ensure safety. I believe that this
implementation is safe, although I welcome any suggestions.
As before, each Pin
is really referencing only a subset of the
Port
struct - the particular pcr
register for that pin. This makes
our usage of UnsafeCell
in the set_pin_mode
function safe, as long
as the correct pin number is passed in. The various Pin::make_*
functions enforce this invariant, wrapping the unsafe set_pin_mode
in safe functions.
The Gpio
, Tx
, and Rx
structs now also hold on to their
associated Pin
struct. This is simply to ensure that the correct
Port
remains borrowed for as long as these structs are in scope.
Cleaning up the UART
We’re almost there! The UART has the most complex requirements, but
they build off of everything we’ve done so far. Just like a Port
, a
Uart
instance must hold on to a ClockGate
. It also must keep track
of the pins it is using, both to prevent the associated Port
from
being dropped, and to keep the pins themselves from being used
elsewhere.
We’ll start by adding a ::uart
method to the Sim
, to do the
allocation, and go from there by updating the Uart
struct itself to
own the various necessary structs. These are actually simpler changes
than were necessary for the Port
struct - we already did most of the
heavy lifting when we made that safe!
A New Segment
Several of the updated structs above use global AtomicBool
instances
to keep track of whether they are in use. These booleans are stored in
a new executable section called .bss
. We must initialize all data in
this section to zero at program startup.
We’ll add the new section to our linker script, then write a simple function to zero the data at startup.
SECTIONS
{
...
.bss : {
. = ORIGIN(RAM);
_bss_start = .;
*(.bss*)
_bss_end = .;
} > RAM
...
}
The new no_builtins
crate attribute prevents the compiler from
trying to convert our loop in setup_bss
into a call to memclr
,
which does not exist in our embedded world. The new setup_bss
function is also unsafe, since it directly manipulates memory that is
in use by other parts of the program.
Printing Panics
Our new code relies on panics quite frequently to indicate when something has gone wrong. Nothing should panic if you’re following this series closely, but in the future we’ll rely on panics in more complex code. We can use our UART to send panic messages back to a host computer, making it easier for us to track when something has gone wrong:
This code uses a couple of global variables to make a UART available to the panic printer. The printer also now resets the microcontroller, instead of hanging. This requires accessing a register we don’t have a struct for yet, so we cheat and write to its address directly.
Putting it Together
We’ve now expanded our types to use more of Rust’s features to ensure
safety. Let’s put it all together with a new main
that uses less
unsafe code:
Two of our remaining uses of unsafe
are clear: Clearing the .bss
segment, as we discussed earlier, writes to memory that is used by
other parts of the code. It must be unsafe. Modifying the globals used
by the panic printer is also unsafe.
But what about the watchdog? Any modification to the watchdog settings requires knowledge of how the program itself is structured. A misconfigured watchdog will reset the microcontroller at unexpected times - or fail to reset it when a program has failed. Since it is impossible to handle the coordination between watchdog settings and software design programmatically, it is better to leave it unsafe, to indicate that extreme caution is needed.
Next Time
Now that we have patterns for allocating hardware units, we’ll continue to expand our toolbox of hardware units. We’ll configure interrupts and a timer to interact with some external hardware - a WS2812B RGB LED strip.