The GPIO war: macro bunkers for typestate explosions
As most embedded developers I'm addled by a disease: code vertigo. I like my code flush to the earth, where I can take an ear to the ground and hear the fuzzy rumble of electrons and the crackle of dodgy solder joints. When code at that level collapses it makes a hell of a noise but it doesn't fall too far; digging through datasheets is often enough to put the pieces back together.
When code rests on layers I don't fully understand I get all nauseous. Suddenly all winds seem strong and I feel the need to inch away from the windows. They told me there's rebar in there but I don't know man, it seems like bare concrete to me. Hey, what's that little inscription in the beam... "Certified Misra C"? AHHH RUN! TAKE THE EMERGENCY EXIT!
Anyway. The consequence is often an inordinate amount of time spent understanding the layers beneath my code and, when not satisfied, building them from scratch. Working with Rust has done a lot to alleviate this sickness; it's the guarantee that beams are carbon steel and all asbestos is confined to tidy unsafe boxes. However the instinct prevails, not necessarily out of mistrust for these layers but the need to understand the techniques involved.
When I started work on the Loadstone
bootloader at Bluefruit
Software, I stood on the shoulders of giants even
at the relative shallows of bare metal code. To support our first target, an
STM32F412ZGT6 Discovery
Kit, we worked
on top of three major layers:
- 1-The Cortex-M minimal runtime.
- 2-The svd2rust generated stm32f4 PAC.
- 3-The stm32f4xx-hal.
Loadstone, along with other Rust components developed in-house at Bluefruit, will be open sourced soon! I'll link to them from this blog as soon as I have the green light.
These worked beautifully and didn't warrant any inquiry or turning up of anyone's nose. Foundations proved solid, but a bit of vertigo still kicked in. The claim that the PAC is fully machine-generated from a SVD file seemed too good to be true——seriously, peripheral access has killer ergonomics——and some of the macro-powered HAL modules were a tough sell for my team in terms of readability.
We recently had the chance to port Loadstone to a WGM160P. The brains on this thing are a Cortex-M4 powered EFM32GG (giant gecko). This chip is significantly less supported than the ubiquitous stm32 family, so I took the opportunity to dig a bit deeper on layers two and three.
There's not much to say about svd2rust
; it was painless to use and it did
generate a PAC crate nearly identical to the stm32 one, only requiring a quick
rename of a register named async
which the compiler didn't like for
obvious reasons.
Layer three, the hal
implementation, is where the fun began. I couldn't find
much out there for this particular microcontroller, so I took it to
progressively develop the parts that we'd need for the project, with the
intention of making them conform to the universal embedded hal and releasing them
publicly.
This article will take us on a journey through the process of writing a Rust module as low level as it gets. Brace for impact!
GPIO: The Borrow Checker's nemesis🔗
The first question a microcontroller hal needs to answer is how to drive GPIOs. GPIO stands for general purpose input/output, and it's the soul of most embedded applications. GPIOs are what lights the LEDs, what reads the button presses, what drives serial communications, what controls a motor's speed via pulse-width modulation. If a feature physically touches the external world a GPIO is probably involved. And like most foundational pieces, it's one you better get right.
GPIOs also happen to be Rust's worst nightmare.
If you show the borrow checker the GPIO section of a typical MCU datasheet, it will lunge for the closest corner, curl up in fetal position, and cry. The way GPIOs are driven in most MCUs I've worked with is anathema to the borrow checker, and the reason is the harsh contrast between how we perceive pin ownership and how it's actually implemented in hardware.
We like to assign pins to different software modules with
clear boundaries. PB6
is passed to led.c
, which is the LED pin.
button.c
gets PC12
, which is the button pin. All is good in our abstract
software model, but beneath the surface lurks horror, corruption and pain.
GPIOs are typically organized in ports. Notice the arbitrary pin names in the
previous paragraph; the port would be the B
in PB6
, and the C
in PC12
.
Each port is further subdivided in pins. How these pins are configured and
driven differs between specific vendors and chip families, but typically
involves writing and reading certain memory mapped port registers, which
aggregate a particular action for all of that port's pins.
Yes, I say "typically" a lot. We embedded developers are terrified of making sweeping architectural statements because there's very crazy stuff out there. Did you know not all bytes are 8 bits long?
Let's look at an example of such a port register, from the MCU's reference manual's section 32.5.4:
This is how your typical "data out" register looks like. This sits somewhere in your memory map, and you write 16-bit words to its lower half to dictate whether the pins in this port get all excited and full of electrons. Note a few things:
- There is a
x
in the name. That's a placeholder for the port, which means there is one of these for each GPIO port.GPIO_PA_DOUT
,GPIO_PD_DOUT
, and so forth. - The top half of the register is unused, as there are only 16 bits per port.
- Not reflected in the image, but writing to this register only makes sense for pins that are configured as output. Configuring pins happens through registers similar to the one above.
- Doing things to a pin configured in the wrong mode ranges from silently failing to venting the fabled smoke.
- An experienced embedded dev will see the
RW
access and notice an important implication: If you're only interested in setting a specific pin, you must respect the rest of the register! Clumsily slamming a0x1 << 5
on that register will light up pin five, but will also shut all the others down. I highlight this because vendor register naming can be inconsistent, and "data out" may have stood for a different style of set register where you simply write1
to each pin you want to raise, without disturbing the rest.
The specifics of how these registers work differ from MCU to MCU. Even something
as simple as setting the logic level of a pin can come in a few different
flavors. You may have to write a 1
to the appropriate bit of a "clear"
register to bring the pin low, or you may be offered the option to toggle a pin
regardless of the initial state.
Specifics aside, the attentive, paranoid and scarred reader probably notices the
problem already. These register "bit soups" care nothing about our abstract
separation of pin functions. All pins are claustrophobically close in the
hardware world. PB5
may drive a lowly LED, while PB6
controls your safety
critical, please-don't-toggle-or-everything-explodes vacuum valve relay, and they
are one bit apart in your memory map.
Thankfully, as the practice software development is built on pure, solid ground, a problem as old as this has long been solved by serious engineers and is unlikely to cause any trouble in practice... Right?
Why am I doing this to myself.
Yeah, no. Pin mishaps are an embedded classic and they come up in many forms,
with consequences ranging from the silent to the explosive. The
typical approach in the C world is to rely on vendor-provided libraries that
boil down to walls of #define __SOME_UNREADABLE_THING = (0b1 << _DAYS_IN_A_LEAP_YEAR &= !~WEIGHT_OF_A_HUMAN_SOUL | _MASK_OF_EL_ZORRO)
. These can
do the trick, but you're often one typo away from frobbing the wrong bit, or
frobbing the right bit wrong.
Surely we can do better.
Building GPIOs on Rust🔗
Last section probably gave you an idea why our anthropomorphic borrow checker wouldn't want to touch GPIOs with a ten foot pole. Tiny chunks of memory gating wide, potentially undefined, potentially catastrophic effects bound to entirely different software modules. So what can we do to appease it?
I mean... We could bypass it altogether. Children, please close your eyes:
unsafe fn set_gpio(port: char, index: u8) {
assert!(in_range(port, index));
let register = match port {
'A' => 0x0001000,
/*...*/
} as *mut u32;
*register |= 1 << index;
}
Ugh. Beyond answering the frequent question "Can Rust replace C word for word", this doesn't really help. Marking the function unsafe is almost a moral imperative given how many things can go wrong:
- Is the pin correctly configured?
- Is the read + write operation atomic?
- What if something else is writing to the same port register?
- Am I the only one writing to a specific pin?
What and what not to mark unsafe is an interesting question in embedded, since we're dealing with resources beyond raw memory. I like to mark as unsafe any functions that may, through misuse, lead to a MCU peripheral being left in an undefined state.
While an approach like the above "works", it shifts the burden of correctness to the layers above. In order to do better, we need to rescue our friend the borrow checker from its despair and teach it how to manage GPIOs. In the next section, we'll completely leave aside the mechanics of pin writes, reads and modes, and we'll instead focus solely on modeling pins in a way the borrow checker can understand.
A pin ownership model🔗
The borrow checker reasons about the ownership of variables. We want only a single pin of each index and port combination to exist, and thus we need to define a type to model those pins so the borrow checker can track them. Here's a first stab:
pub struct Pin {
port: char,
index: u8,
mode: Mode,
}
This... Eh. I mean, it's not wrong, but it isn't great. In the world of C, this kind of structure is what you'd reach for if you wanted to step away from relying solely on the preprocessor. The problem with a foundation like this is that we have to answer all of the important questions at runtime:
- Do I have the right pin? -> check the pin member variables at runtime.
- Am I on the right mode -> Again, I check the pin members. This also raises the
uncomfortable question of what to do when the user calls an invalid function,
like
set_high()
on an input pin. Does the program crash? Do we burden the API withResult
returns in all functions? - Is there a single owner of a
Pin
with a givenport
+index
combination? -> Probably rely on some runtime constructor logic that refuses to produce more than one of the same pin.
The above questions will typically require runtime computation, complication of the API——pin methods need to be fallible to account for being called in the wrong mode——and a memory footprint. Spending three bytes plus padding to earmark a MCU pin doesn't seem like much, but every stack byte counts when you're resource limited.
The borrow checker is not crying anymore, but is still brooding in the corner, judging us. We need to do better. And to do better we need typestates. Following our building analogy typestates are aerospace grade titanium alloy. A resource modeled properly via typestates just cannot be used wrong, and this is enforced at compile time.
When we use typestates a resource is represented by multiple related types, as opposed to a single type with fields reflecting its state. Methods are only defined for typestates where they make sense, which means invalid operations cannot be expressed. The typestate approach is what most popular Rust HAL crates use and it looks roughly like this:
struct Pin<MODE, PORT, PIN> {/* */}
Then, a set of marker structs specify pin characteristics and
classify behaviour. A block starting with impl<PORT, PIN> Pin<Output, PORT, PIN>
would only describe behaviour available to output pins, while one that
begins with impl<MODE> Pin<MODE, PortB, Pin1>
would only apply to PB1
,
regardless of mode.
In the ancient times of four months ago this was the only approach to describing pin typestates, and hence most popular embedded-hal crates look vaguely like the above. However, now we can make it even nicer thanks to the recent stabilization of min_const_generics, noting that port and index can be represented by scalars:
struct Pin<MODE, const PORT: char, const INDEX: u8> { /* ... */ }
And with this, we arrive to the pin representation that Loadstone
uses in its
hal
for the efm32gg
pins:
pub mod typestate {
pub struct NotConfigured;
pub struct Input;
pub struct Output;
}
use typestate::*;
pub struct Pin<MODE, const PORT: char, const INDEX: u8> {
_marker: PhantomData<MODE>,
}
Let's take inventory of where we are:
- I like giving typestates their own namespace——even though it's immediately pulled in——just to have a bit of hygiene when looking at this GPIO module from the outside world.
- If you haven't come across
PhantomData
before, it can be a bit of a head scratcher. Zero sized and weightless, it's a ghost that exists only to tell the type system "Hey, pretend there is something here of typeMODE
".PhantomData
is cool and shows up often in very good code, so don't get spooked. - Since the only field in the pin is zero-sized, we arrive at the first positive consequence of using typestates: our pin representations don't take any space in the stack (or the heap, if you're fancy enough to afford one, look at you with all that memory to spare!). Any decisions made around these types will ultimately be compiled down to direct writes and reads to the adequate registers, and all trace of the struct will vanish from the final binary.
Now that we have a pin representation, we can start to classify behaviour based on the typestates:
impl<const PORT: char, const INDEX: u8> OutputPin for Pin<Output, PORT, INDEX> {
fn set_low(&mut self) { unimplemented!() }
fn set_high(&mut self) { unimplemented!() }
}
impl<const PORT: char, const INDEX: u8> TogglePin for Pin<Output, PORT, INDEX> {
fn toggle(&mut self) { unimplemented!() }
}
impl<const PORT: char, const INDEX: u8> InputPin for Pin<Input, PORT, INDEX> {
fn is_high(&self) -> bool { unimplemented!() }
fn is_low(&self) -> bool { !self.is_high() }
}
OutputPin
, TogglePin
and InputPin
come from the layer above and are meant
to abstract away the board, manufacturer and MCU details. For now we have no
need to treat the pins any differently based on their port or index, though
we'll revisit that assumption in later blog entries. For now, all we've
expressed is that toggling and setting are only possible on pins that are
configured as output, and reading makes sense exclusively for pins configured as
input.
How do we transform these types? Skimming the datasheet shows us that there's no hidden gotcha when reconfiguring pins between different modes, so let's keep it simple and make a few universal conversion methods:
impl<MODE, const PORT: char, const INDEX: u8> Pin<MODE, PORT, INDEX> {
// Private constructor to ensure there only exists one of each pin.
fn new() -> Self { Self { _marker: Default::default() } }
pub fn as_input(self) -> Pin<Input, PORT, INDEX> { unimplemented!() }
pub fn as_output(self) -> Pin<Output, PORT, INDEX> { unimplemented!() }
pub fn as_disabled(self) -> Pin<NotConfigured, PORT, INDEX> { unimplemented!() }
}
The snippet above is pretty unassuming, but I'd like to spend a moment here as it encapsulates a lot of what makes Rust amazing. None of this will be all that exciting to Haskell veterans or anyone with functional programming background, but I come from a background of just banging bits together so this is all still pretty magical:
- A common first concern when dealing with unbounded typestates is that it's
very easy to conjure concrete types that don't make sense. Yes, you can name a
Pin<Potato, 'W', 42>
; the compiler will look at you funny but won't judge. Thankfully there's no way you can ever actually construct one, thanks to the private constructor. - This
impl
block exposes public methods capable of returning pins. In most other languages this would clash directly with the requirement of only holding one single pin of each port-index combination. However, the borrow checker comes to our rescue by enforcing that for everyA2
pin that gets generated, anA2
gets destroyed and forgotten——note theself
parameter. Thus, uniqueness is maintained in a Ship of Theseus kind of way. - There is, and there will only be, a single way to create a pin from nothing,
and that's the
new
function. We've made that function private so we have full control over when it is called. Notice how the user can't simply construct their own pins because of the privatePhantomData
field, which is doing a great job spooking type thieves away——another of its valuable roles in zero-sized types.
So thus far we have a typestate powered pin struct, a way to convert between
different configurations, and a few methods. I left them all as
unimplemented()
as we're only focusing on the rules of access. Later in this
blog series we'll put the spotlight on the mechanics of access, where we'll
have Fun with a capital F writing wrappers around the PAC crate.
We mentioned that the only way pins are created is through the new
associated
function. Well, we then have to figure out how and when this function gets
called. Our main requirement is that it must only be called once per pin and
port combination. Let's find a way to enforce that.
Rationing for the war: You only get one pin!🔗
The immediately obvious drawback of typestates is that no matter how closely related the pins, in the eyes of the compiler they are different types. This means you're not going to have an easy time collecting them into an array or iterating over them unless you want to bring dynamic dispatch into the mix, which at this level... Let's just say it's a bit of a bull in a china shop situation.
So what can we do? If you're looking to ration a limited resource, a very natural instinct is to write a list of the elements you have, and cross out the ones that have already been assigned:
impl Gpio {
pub fn claim<const PORT: char, const INDEX: u8>(&mut self)
-> Option<Pin<NotConfigured, PORT, INDEX>>
{
if !self.already_claimed(PORT, INDEX) {
self.mark_as_claimed(PORT, INDEX);
Some(Pin::new())
} else {
None
}
}
}
The Gpio
struct in this example would contain a "claimed" data
structure (imagine an array of bools, for simplicity) to
regulate construction. Thus, guaranteeing the uniqueness of our pins would be
reduced to guaranteeing the uniqueness of the Gpio
struct which dispenses them.
There's nothing fundamentally wrong about this——in fact, we will keep the idea
of guaranteeing pin uniqueness by enforcing a single Gpio
struct——but the
details are a bit off. There has been a recurring theme in this blog entry: It
is a good idea to ask ourselves if there's a way to pull decisions from runtime
to compile time. A runtime approach like the above is questionable for a few
reasons:
- Forces the library user to decide, also at runtime, how they want to deal with the possibility of a failed pin claim.
- A failed claim happens by definition only through API misuse, which is a compile time mistake. Therefore, by failing at runtime this approach fails later than it needs to. Fail often, fail early: words to live by.
- Even if you want to flex your embedded skills and write the
claimed
table as a compact bitset, using it still takes some bytes and some cycles when it could take zero bytes and zero cycles, which is infinitely fewer bytes and cycles!
So how do we go about it? Thankfully there's no need to reinvent the wheel; the language already comes with a system to enforce limited collections of heterogeneous types. Good old struct composition.
pub struct Gpio {
pub pa0: Pin<NotConfigured, 'A', 0>,
pub pa1: Pin<NotConfigured, 'A', 1>,
pub pb0: Pin<NotConfigured, 'B', 0>,
pub pb1: Pin<NotConfigured, 'B', 1>,
}
impl Gpio {
// We'll talk later about how to ensure this gets called only once.
pub fn new() -> Self {
Self {
pa0: Pin::new(),
pa1: Pin::new(),
pb0: Pin::new(),
pb1: Pin::new(),
}
}
}
Perfect! Just by constructing a Gpio struct, we get a public field for each pin that we can just break off. We know they're zero-sized, so we aren't even being wasteful. All done; pack it up and ship it!
... What?
What do you mean too long? They don't take any space on the stack, they don't impact the code size... Oh, the source code? Come on, don't tell me you are afraid of a bit of copy paste, how bad can it be? Here, let me help you...
pub struct Gpio {
pub pa0: Pin<NotConfigured, 'A', 0>,
pub pa1: Pin<NotConfigured, 'A', 1>,
pub pa2: Pin<NotConfigured, 'A', 2>,
pub pa3: Pin<NotConfigured, 'A', 3>,
pub pa4: Pin<NotConfigured, 'A', 4>,
pub pa5: Pin<NotConfigured, 'A', 5>,
pub pa6: Pin<NotConfigured, 'A', 6>,
pub pa7: Pin<NotConfigured, 'A', 7>,
pub pa8: Pin<NotConfigured, 'A', 8>,
pub pa9: Pin<NotConfigured, 'A', 9>,
pub pa10: Pin<NotConfigured, 'A', 10>,
pub pa11: Pin<NotConfigured, 'A', 11>,
pub pa12: Pin<NotConfigured, 'A', 12>,
pub pa13: Pin<NotConfigured, 'A', 13>,
pub pa14: Pin<NotConfigured, 'A', 14>,
pub pa15: Pin<NotConfigured, 'A', 15>,
pub pb0: Pin<NotConfigured, 'B', 0>,
pub pb1: Pin<NotConfigured, 'B', 1>,
pub pb2: Pin<NotConfigured, 'B', 2>,
pub pb3: Pin<NotConfigured, 'B', 3>,
pub pb4: Pin<NotConfigured, 'B', 4>,
pub pb5: Pin<NotConfigured, 'B', 5>,
pub pb6: Pin<NotConfigured, 'B', 6>,
pub pb7: Pin<NotConfigured, 'B', 7>,
pub pb8: Pin<NotConfigured, 'B', 8>,
pub pb9: Pin<NotConfigured, 'B', 9>,
pub pb10: Pin<NotConfigured, 'B', 10>,
pub pb11: Pin<NotConfigured, 'B', 11>,
pub pb12: Pin<NotConfigured, 'B', 12>,
pub pb13: Pin<NotConfigured, 'B', 13>,
pub pb14: Pin<NotConfigured, 'B', 14>,
pub pb15: Pin<NotConfigured, 'B', 15>,
pub pc0: Pin<NotConfigured, 'C', 0>,
pub pc1: Pin<NotConfigured, 'C', 1>,
pub pc2: Pin<NotConfigured, 'C', 2>,
pub pc3: Pin<NotConfigured, 'C', 3>,
pub pc4: Pin<NotConfigured, 'C', 4>,
pub pc5: Pin<NotConfigured, 'C', 5>,
pub pc6: Pin<NotConfigured, 'C', 6>,
pub pc7: Pin<NotConfigured, 'C', 7>,
pub pc8: Pin<NotConfigured, 'C', 8>,
pub pc9: Pin<NotConfigured, 'C', 9>,
pub pc10: Pin<NotConfigured, 'C', 10>,
pub pc11: Pin<NotConfigured, 'C', 11>,
pub pc12: Pin<NotConfigured, 'C', 12>,
pub pc13: Pin<NotConfigured, 'C', 13>,
pub pc14: Pin<NotConfigured, 'C', 14>,
pub pc15: Pin<NotConfigured, 'C', 15>,
}
Ugh, okay, point taken. I'm only down to the third port and my y
and p
keys
are worn out. Our typestates have exploded, and we need to take cover. Against
weapons of this caliber we have no recourse but to build a bunker, and for that
we need macros.
Unfortunately, walls of text like the above are pretty common sights in embedded development. I don't know what it is about low level coders but we seem to love these imposing code blocks. They feel... industrial? Anyway, be the change you want to see in the world!
I have to admit I was stuck at this spot for some time, trying to wrap my head around generating identifiers that expand two dimensions; in this case ports and indices. Fortunately, Yandros from the Rust community discord came to my aid with an amazing macro suggestion, that I later tweaked into a general purpose "matrix" generator. Shoutout to Yandros for their constant help and guidance!
#[macro_export]
macro_rules! matrix {
( $inner_macro:ident [$($n:tt)+] $ms:tt) => ( matrix! { $inner_macro $($n $ms)* });
( $inner_macro:ident $( $n:tt [$($m:tt)*] )* ) =>
( $inner_macro! { $( $( $n $m )* )* } );
}
I don't have enough blood in my caffeine system to walk through macro syntax
today, so I'll keep it simple: matrix
takes an inner macro as a
parameter, followed by two sequences of tokens——or token
trees to be
specific——and expands them in pairs. So, for example:
matrix!(my_macro, [a b c] [1 2 3]);
... expands into ...
my_macro!(a 1 a 2 a 3 b 1 b 2 b 3 c 1 c 2 c 3);
You can see how this can be pretty convenient for our GPIO module, as nearly
everything we do is expressed in port-index pairs we'd rather not spell out
manually. This reduces our Gpio
struct definition to this rather compact form:
macro_rules! gpio_struct {
($( ($letter:tt $character:tt) $number:tt )*) => { paste::item! {
pub struct Gpio {
$(
pub [<p $letter $number>]: Pin<NotConfigured, $character, $number>,
)+
}
}}
}
// Define a Gpio struct with 15 pins and ports A to L
matrix! {
gpio_struct
[(a 'A') (b 'B') (c 'C') (d 'D') (e 'E') (f 'F') (g 'G') (h 'H') (i 'I') (j 'J') (k 'K') (l 'L')]
[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
}
There's some secret, powerful sauce in the snippet above. Rust macros-by-example
don't like you messing with identifiers, so we need to borrow some
procedural magic from the paste crate in order
to synthesize the field names——pa0
, pa1
, etc. The wall of text in new()
can be tackled in a similar way:
macro_rules! construct_gpio {
($($letter:ident $number: literal)*) => { paste::item! {
Self {
$([<p $letter $number>]: Pin::new(),)*
}
}}
}
impl Gpio {
// We'll talk later about how to ensure this gets called only once.
pub fn new() -> Self {
matrix! { construct_gpio [a b c d e f g h i j k l] [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] }
}
}
Explosion averted! We safely ducked under our macro bunker, and while it is built out of alien materials and the inscriptions are a mix of cuneiform and ancient Egyptian, a roof is a roof.
As a neat bonus, here's an aliasing macro so we get less wordy analogues to our
pin types, so for example Pin<Output, 'B', 3>
can be exported as
Pb3<Output>
:
macro_rules! pin_aliases {
($( ($letter:tt $character:tt) $number:tt )*) => { paste::item! {
$(
pub type [<P $letter $number>]<MODE> = Pin<MODE, $character, $number>;
)*
} }
}
matrix! {
pin_aliases
[(a 'A') (b 'B') (c 'C') (d 'D') (e 'E') (f 'F') (g 'G') (h 'H') (i 'I') (j 'J') (k 'K') (l 'L')]
[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
}
Conclusions🔗
Writing drivers at this level may not be something you have any intention of doing, so I'd hope you at least take away something of general value as a consolation for sticking with me through awful analogies and meandering snippets. Here are some assorted thoughts that came up while writing this:
- When building a system for maximum safety, it helps to separate the rules and the mechanics of access, so you can focus on one thing at a time. Often the specifics of how resources must be managed get in the way of the design, and safety suffers as a consequence.
- Don't fear solutions that pull decisions to compile time at the expense of readability and source explosion. Source can always be transformed with macros and readability can be constantly improved. The risks of low readability are way smaller anyway when all your problems are caught at compile time!
- Typestates are plenty useful even if you have no way to restrict them to a
subset of "correct" types and values. Note how I didn't have to specify anywhere
that the
INDEX
constant must be lower than16
; it's enforced through our full control over instantiation. - The community discord is great. I bother people there for help all the time and I'm constantly humbled by the quality of code they can produce on a whim. Come over!
This is shaping up to be a three part entry. Next, we'll talk about how to actually drive these pins; the PAC crate exposes each register as a unique type with a fairly complex API, so we'll have break our brains mapping those types to our access model above. Last, we'll look at making our access rules even more powerful by managing which pins can be used by each peripheral to perform alternate functions, which is a common question during MCU driver development.
Thanks for reading!🔗
As always, I welcome feedback of all kinds and shapes, so I'll be around on the rust subreddit, community discord (I'm Corax over there) and over at my email.
Happy rusting!