2026-07-16
Embedded Rust, maybe more so than regular Rust, is a fight with the borrow checker. In a regular, user-space Rust program, you can fall back on Copy and Clone to get another version of your data when the borrow checker gets complicated. However, embedded Rust takes these shortcuts off the table because many of the peripheral and hardware structs support neither copy nor clone and can't be instantiated multiple times! You can't clone a physical object, and Rust doesn't let your code pretend you can.
So what would happen if you could have a couple mutable references to a pin at the same time? Good embedded Rust rightfully won't let us do that, but we can build our own unsafe hardware abstraction layer (HAL) without those rules enforced.
Let's set up an embedded Rust project and start with the following program. This code uses an Arduino Uno, but you can use another microcontroller with the appropriate HAL.
#![no_std]
#![no_main] // Consequence of no_std
use panic_halt as _;
#[arduino_hal::entry]
fn main() -> ! {
let dp = arduino_hal::Peripherals::take().unwrap();
let pins = arduino_hal::pins!(dp);
let mut led = pins.d4.into_output();
loop {
arduino_hal::delay_ms(1_000);
led.set_high();
arduino_hal::delay_ms(1_000);
led.set_low();
}
}
Wire up an LED to pin 4 (using a resistor if necessary) then cargo run it. You should see the LED blink on and off for roughly one second at a time.
As an embedded program grows, the calls to the same pin get farther and farther apart. In another language I would expect to be able to define some function set_led that I could call from other functions or abstractions. Something like:
fn set_led(status: bool) {
let pins = todo!(); // This will not work
let mut led = pins.d4.into_output();
if status {
led.set_high();
} else {
led.set_low();
}
}
But Rust says no! You can't get led without passing it in as an argument. If you try to take the Peripherals, you'll just get None. That can be hard to debug here, since we have a halting panic, so let's go with a simpler example that the compiler will catch.
Let's say you modified the loop to include a second mutable reference to pin 4.
let mut led = pins.d4.into_output();
loop {
arduino_hal::delay_ms(1_000);
let mut second_var = pins.d4.into_output();
second_var.set_high();
arduino_hal::delay_ms(1_000);
led.set_low();
}
You'll get Rust's error E0382, which means you tried to use a value after it was moved.
error[E0382]: use of moved value: `pins.d4`
The pin 4 was moved into led, so pins.d4 is no longer valid. The pin's type doesn't implement the Copy trait, so you can't make a second. If you think about it, a pin shouldn't implement Copy: it's a physical object and can't be copied by software. There's exactly one pin 4 available to your program, and Rust forces your code to acknowledge it.
Good Rust code won't let us do stupid things with a pin abstraction, so we'll have to make it ourselves.
To make our own abstraction layer, we'll need to include some of the raw assembly. Here's a version of the working blink program where the use of pins.d4 is replaced by the AVR assembly instructions.
#![no_std]
#![no_main] // Consequence of no_std
#![feature(asm_experimental_arch)] // needed for asm!
use panic_halt as _;
#[arduino_hal::entry]
fn main() -> ! {
// Replaces: let mut led = pins.d4.into_output();
unsafe {
core::arch::asm!("sbi 0x0A, 4");
}
loop {
arduino_hal::delay_ms(1_000);
// Replaces: led.set_high();
unsafe {
core::arch::asm!("sbi 0x0B, 4");
}
arduino_hal::delay_ms(1_000);
// Replaces: led.set_low();
unsafe {
core::arch::asm!("cbi 0x0B, 4");
}
}
}
The asm! macro lets us write inline assembly in Rust (unsafe Rust, that is). You'll need to enable the asm_experimental_arch to use the inline assembly macro asm! for AVR. While the asm! macro can do more complicated things with variables (see the section in Rust by Example) we'll only need to use these simple instructions to flip a bit in the respective register.
We'll use the sbi instruction, aka Set Bit in I/O Register, and the cbi instruction, aka Clear Bit in I/O Register. We need two things to use one of these commands: the address of the I/O register and the bit corresponding to our pin. We'll need to look up the chip's datasheet, which in the case of an Arduino Uno is the ATMega328P.
There are two registers we need:
0x0A) to set the pin to be an output.0x0B) to write the value.The pin we're using is bit 4.
Let's take what we know to make a basic pin 4 HAL. We can just put our unsafe inline assembly into the methods of the struct, then use them without having to write assembly every time we want IO.
#![no_std]
#![no_main] // Consequence of no_std
#![feature(asm_experimental_arch)] // needed for asm!
use panic_halt as _;
struct Pin4;
impl Pin4 {
fn set_output(&mut self) {
unsafe {
core::arch::asm!("sbi 0x0A, 4");
}
}
fn set_high(&mut self) {
unsafe {
core::arch::asm!("sbi 0x0B, 4");
}
}
fn set_low(&mut self) {
unsafe {
core::arch::asm!("cbi 0x0B, 4");
}
}
}
#[arduino_hal::entry]
fn main() -> ! {
let mut led = Pin4;
led.set_output();
loop {
arduino_hal::delay_ms(1_000);
led.set_high();
arduino_hal::delay_ms(1_000);
led.set_low();
}
}
This code should run and give you the same blinking LED that you had initially.
Now we can see why there's only a singleton, no-copy pin 4 in the actual HAL. Let's add a set_input method for Pin4 that can make it into an input. We'll just do the opposite of set_output and clear the corresponding bit in DDRD.
impl Pin4 {
fn set_input(&mut self) {
unsafe {
core::arch::asm!("cbi 0x0A, 4");
}
}
}
Now we can get into trouble if there are two mutable Pin4's floating around. Let's use one as an input and the other as an output.
fn setup() {
let mut pin4 = Pin4;
pin4.set_input();
// Something
}
#[arduino_hal::entry]
fn main() -> ! {
let mut led = Pin4;
led.set_output();
setup();
loop {
arduino_hal::delay_ms(1_000);
led.set_high();
arduino_hal::delay_ms(1_000);
led.set_low();
}
}
You shouldn't see your blinking LED anymore! Calling setup switched the pin to an input, rather than an output, so flipping the bit in PORTD no longer does what you want. We had two mutable references/structs that would cover the pin, and the classic problem appeared: one reference changed something out from under the other.
Depending on your port or chip, you might have a blinking light but now it's dimmer. How weird! You could get this if you used pin 13 on the Arduino Uno's ATMega328P. The switch to input means that flipping the bit in PORTB, pin 13's port, now puts less through the pin. The AVR allows "pull-up" inputs where they're high voltage until something pulls them down, but it's not as high as the main voltage you get when setting an output pin high, hence the dimmer LED.
To wrap this up, let's fix our basic HAL and see what needs to happen for actual safety.
We can only have one reference to Pin4 floating around. We can enforce this with a singleton and a take method where the caller only gets a real Pin4 when no one else has taken it yet.
static mut PIN4TAKEN: bool = false;
impl Pin4 {
fn take() -> Option<Self> {
unsafe {
if PIN4TAKEN {
None
} else {
PIN4TAKEN = true;
Some(Pin4)
}
}
}
...
}
This code is unsafe since we're using a mutable static variable. If we were using multiple threads or interrupts, we'd have to worry about this, but we're fine to keep it simple in the blinking LED example without any data races.
Now let's use take in the program. We'll have to unwrap it or otherwise deal with the Option<Pin4>. Note that if you unwrap twice, one will fail and your program will halt thanks to the panic_halt panic handler.
fn setup() {
if let Some(mut pin4) = Pin4::take() {
pin4.set_input();
// Something
}
}
#[arduino_hal::entry]
fn main() -> ! {
let mut led = Pin4::take().unwrap();
led.set_output();
setup();
loop {
arduino_hal::delay_ms(1_000);
led.set_high();
arduino_hal::delay_ms(1_000);
led.set_low();
}
}
Run this and you should see blinking again! The body of setup never runs since it doesn't get a pin from take.
While we've created a safe way to get Pin4 with take, a caller could still just instantiate Pin4 itself. We need to be able to stop Pin4 from being a valid initialization.
The trick here is to use a module and private field. If Pin4 had a private field, then code outside the module can't instantiate it, even if the field is a zero-byte unit type. While we're at it, we'll also put PIN4TAKEN in there to stop any modifications outside take.
mod pins {
pub struct Pin4 {
_private: ()
}
static mut PIN4TAKEN: bool = false;
impl Pin4 {
pub fn take() -> Option<Self> {
unsafe {
if PIN4TAKEN {
None
} else {
PIN4TAKEN = true;
Some(Pin4 { _private: () })
}
}
}
// The rest of the impl as pub fn's
}
}
Now if you try to make a Pin4 from setup, which is outside the pins module, you should run into an error E0423. Now take is the only valid way to get a Pin4, and it won't let multiple be taken at once.