This is a demo project for the stm32wl-hal.
This runs on two NUCLEO-WL55JC2 boards.
What does it do? Well, it is a real fancy lightswitch. Press B3 on the board flashed with the "client" firmware, it will toggle its own LED, then the board with the "server" firmware will toggle its LED after authenticating the request.
Run the server
package and client
package on two separate targets at the
same time using probe-rs
.
$ probe-rs list
The following debug probes were found:
[0]: STLink V3 (VID: 0483, PID: 374e, Serial: 001600345553500A20393256, StLink)
[1]: STLink V3 (VID: 0483, PID: 374e, Serial: 001D00145553500A20393256, StLink)
$ DEFMT_LOG=trace cargo run -p server -- --probe 0483:374e:001600345553500A20393256
$ DEFMT_LOG=trace cargo run -p client -- --probe 0483:374e:001D00145553500A20393256
If you want to be fancy, I use tmux to launch these at the same time in one terminal session.
tmux new-session "DEFMT_LOG=trace cargo run -p server -- --probe 0483:374e:001600345553500A20393256" \; split-window "DEFMT_LOG=trace cargo run -p client -- --probe 0483:374e:001D00145553500A20393256" \; setw remain-on-exit on \;
End-to-end latency is about 31.3 ms.
The usual warnings apply, assume the code is full of security bugs.
The code is for demonstration purposes only and does not represent best security practices. Implementing a custom protocol is typically a bad idea when security is a requirement.
The code is not secure as-is:
A better nonce would be a non-volatile value stored in flash, but I do not want to wear out your flash for a demonstration.
/// Non-volatile u64 stored in the last page of flash
///
/// * Service lifetime is 30 years (flash data detention)
/// * Page endurance is 10k cycles
/// * Page size is 2048 bytes
///
/// service_days = 30 * 365 = 10950
/// u64_writes = 10k * (2048 / 8) = 2_560_000
/// writes_per_day = u64_writes / service_days = 233.79
///
/// You can use more pages or an external EEPROM if you need more writes/day.
pub struct NonVolatileU64 {
_priv: (),
}
impl NonVolatileU64 {
const U64_PER_PAGE: usize = Page::SIZE / size_of::<u64>();
const U64_PER_PAGE_ISIZE: isize = Self::U64_PER_PAGE as isize;
const PAGE: Page = unsafe { Page::from_index_unchecked(127) };
const FIRST: *const u64 = Self::PAGE.addr() as *const u64;
const LAST: *const u64 = (Self::PAGE.addr() + Page::SIZE - size_of::<u64>()) as *const u64;
#[inline]
pub fn fetch_increment(flash: &mut pac::FLASH) -> Result<u64, flash::Error> {
if let Some((val, offset)) = Self::fetch() {
let mut flash: Flash = Flash::unlock(flash);
let next: *mut u64 = if offset == 0 {
unsafe { flash.page_erase(Self::PAGE)? };
Self::FIRST as *mut u64
} else {
unsafe { Self::LAST.offset(-offset + 1) as *mut u64 }
};
// in theory this should be a checked add, panicing at overflow
// but the flash page will die before u64 wraps
let next_val: u64 = val.wrapping_add(1);
unsafe { flash.standard_program(&next_val, next)? };
Ok(next_val)
} else {
let mut flash: Flash = Flash::unlock(flash);
unsafe {
flash.page_erase(Self::PAGE)?;
flash.standard_program(&0, Self::FIRST as *mut u64)?
};
Ok(0)
}
}
fn fetch() -> Option<(u64, isize)> {
for offset in 0..Self::U64_PER_PAGE_ISIZE {
let ptr: *const u64 = unsafe { Self::LAST.offset(-offset) };
let value: u64 = unsafe { ptr.read_volatile() };
if value != u64::MAX {
return Some((value, offset));
}
}
None
}
}
Notes on what needs to be improved to extend this for a real-world application: