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:

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.cgets 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:

Data Out Register
Data Out Register for port X

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 a 0x1 << 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 write 1 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 with Result returns in all functions?
  • Is there a single owner of a Pin with a given port+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 type MODE". 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 every A2 pin that gets generated, an A2 gets destroyed and forgotten——note the self 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 private PhantomData 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);
        } else {

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_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! {
    [(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! {
    [(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]


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 than 16; 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!