- Getting the LoRa E5-mini board and testing it
- Flashing a blinky example with embassy-rs (and struggling with it)
- Debugging flash issues and getting it to run
Part 1 of the PAINFUL series (I know that)
Introduction
This is the first post in this series! Recently I became interested (at the same time!!) in embedded programming and radio stuffs. After doing some research I decided that I want to utilize microcontrollers with radio transceiver to create some kind of mesh communication system. Or something else. I'm not sure anymore!
I found a variety of LoRa radios on the market, but one of the products caught my attention - LoRa E5-mini board, based on STM32WLE5JC MCU. According to the ST's docs, it's a pretty powerful Cortex-M4 microcontroller with a built-in radio that's capable of LoRa, (G)FSK, (G)MSK and BPSK modulations! I instantly ordered it and looked at what's available in the Rust ecosystem for this MCU. (Un)fortunately, I found almost nothing! Only basic support in embassy-rs, no built-in functions for communicating with the radio. The only helpful resources I found were crates for using SX127x radios over SPI (eg. sx127x_lora), some crates for using the AT commands on the LoRa-E5 mini, and one singular crate which uses the integrated subGHz device - lora-rs - it might be really helpful in the future (though it only supports LoRa, not the other modulations, which I intend to implement)!
First cute steps
After getting the board, I tested if it even works.
It does!
The next step was to flash and test a simple blinky example. In the process, I encountered a major obstacle and it took waaaay longer than expected. I connected a Raspberry Pi Debug Probe to SWD pins (sry I was too lazy to solder the headers...) and looked up embassy-stm32 examples.
The first step was to see if probe-rs even sees this board. After going into bootloader mode by holding BOOT while clicking on RESET buttons, I ran this:
lusia@lusia-laptop ~/P/stm32wle5jc-radio (master)> probe-rs info --protocol swd
--connect-under-reset --chip STM32WLE5JC
Probing target via SWD
----------------------
ERROR probe_rs::architecture::arm::memory::romtable: Failed to read component information at 0xf0000000.
ARM Chip with debug port Default:
Debug Port: DPv2, Designer: STMicroelectronics, Part: 0x4970, Revision: 0x0, Instance: 0x00
├── V1(0) MemoryAP
│ └── 0 MemoryAP (AmbaAhb3)
│ ├── 0xe00ff000 ROM Table (Class 1), Designer: STMicroelectronics
│ ├── 0xe0001000 Generic
│ ├── 0xe0000000 Peripheral test block
│ ├── 0xe0040000 Generic
│ └── 0xe0043000 Coresight Component, Part: 0x0906, Devtype: 0x14, Archid: 0x0000, Designer: ARM Ltd
└── V1(1) MemoryAP
└── 1 MemoryAP (AmbaAhb3)
The bootloader mode and the default AT firmware were SOOOOO WONKY to work with, I had to reconnect the board manually because it'd softlock into some unresponsive states so often. I found the documentation of the board and saw that the factory firmware was protected with RDP (Read Protection) Level 1, so I tried to find a way to erase the firmware and unlock the flash. Thankfully, someone made a cool crate which made it easy and painless, without the need of installing STM32Cube software (I HAAATE IT SO MUCH OMG).
# flashing didn't work obviously
lusia@lusia-laptop ~/P/stm32wle5jc-radio (master)> cargo run --release
Finished `release` profile [optimized + debuginfo] target(s) in 0.11s
Running `probe-rs run --chip STM32WLE5JC target/thumbv7em-none-eabi/release/stm32wle5jc-radio`
Erasing ✔ 0% [--------------------] 0 B @ 0 B/s (ETA 0s) Error: An error with the flashing procedure has occurred.
Caused by:
0: Failed to erase flash sector at address 0x08000000.
1: The core entered an unexpected status: LockedUp.
# this did work :3
lusia@lusia-laptop ~/Projects> git clone https://github.com/newAM/stm32wl-unlock
Cloning into 'stm32wl-unlock'...
remote: Enumerating objects: 794, done.
remote: Counting objects: 100% (267/267), done.
remote: Compressing objects: 100% (144/144), done.
remote: Total 794 (delta 184), reused 175 (delta 113), pack-reused 527 (from 1)
Receiving objects: 100% (794/794), 332.57 KiB | 3.74 MiB/s, done.
Resolving deltas: 100% (546/546), done.
lusia@lusia-laptop ~/Projects> cd stm32wl-unlock/
lusia@lusia-laptop ~/P/stm32wl-unlock (main)> nix develop
[lusia@lusia-laptop:~/Projects/stm32wl-unlock]$ cargo run
...
Compiling stm32wl-unlock v0.1.0 (/home/lusia/Projects/stm32wl-unlock)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 22.58s
Running `target/debug/stm32wl-unlock`
Current readout protection is level 1, memories readout protection active
erase duration: 45.391177ms
flash protection set to level 0, readout protection not active
After clearing the RDP, I could finally flash!
Running the program??? CHECK OUT NOW FREE DOWNLOAD
The next step was to create a smol simple example program! It was hard to get started, but I followed the Embassy book and found two directories with examples in their repo - one for STM32WL and the other for STM32WLE5-LP. After some time, I finally crafted a minimal working example, which would compile. It's available on my git (final version, after many trials and errors) - it compiles well, and works! The main.rs itself is really minimal, it just blinks the integrated LED diode:
#![no_std]
#![no_main]
use defmt::*;
use embassy_executor::Spawner;
use embassy_stm32::{Config, gpio::{Level, Output, Speed}, rcc::{MSIRange, Sysclk, mux}};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let mut config = Config::default();
{
config.rcc.msi = Some(MSIRange::RANGE48M);
config.rcc.sys = Sysclk::MSI;
config.rcc.mux.rngsel = mux::Rngsel::MSI;
config.enable_debug_during_sleep = true;
}
let p = embassy_stm32::init(config);
info!("hiiiiiiii :3");
let mut led = Output::new(p.PB5, Level::High, Speed::Low);
loop {
info!("high");
led.set_high();
Timer::after_millis(50).await;
info!("low");
led.set_low();
Timer::after_millis(50).await;
}
}
But the road of getting to this state was SOOOO BUMPY!
Smol issue
The real problems started when I tried flashing the initial version of the example. When trying to get it working, the code used the default config in the init sequence:
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
It's a really important detail which caused a lot of pain in my cute ass. Running the example on the default compiler optimization level caused the MCU to throw this error:
lusia@lusia-laptop ~/P/stm32wle5jc-radio (master) [1]> cargo run --release
Finished `release` profile [optimized + debuginfo] target(s) in 0.12s
Running `probe-rs run --chip STM32WLE5JC target/thumbv7em-none-eabi/release/stm32wle5jc-radio`
Erasing ✔ 100% [####################] 18.00 KiB @ 48.92 KiB/s (took 0s)
Programming ✔ 100% [####################] 18.00 KiB @ 26.58 KiB/s (took 1s) Finished in 1.15s
Firmware exited unexpectedly: Multiple
Core 0
Frame 0: HardFault_ @ 0x08003dbe
/home/lusia/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cortex-m-rt-0.7.5/src/lib.rs:1103:1
Frame 1: HardFault <Cause: Escalated BusFault <Cause: Precise data access error at location: 0x58004f1e>> @ 0x08000c62
Frame 2: read_volatile<stm32_metapac::flash::regs::Acr> @ 0x08000c62 inline
/home/lusia/.rustup/toolchains/1.92-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:2118:9
Frame 3: read_volatile<stm32_metapac::flash::regs::Acr> @ 0x0000000008000c56 inline
/home/lusia/.rustup/toolchains/1.92-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mut_ptr.rs:1284:18
Frame 4: Reg<stm32_metapac::flash::regs::Acr, stm32_metapac::common::RW>::read @ 0x0000000008000c56 inline
/home/lusia/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/stm32-metapac-19.0.0/src/common.rs:58:39
Frame 5: init @ 0x0000000008000c56
/home/lusia/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-stm32-0.5.0/src/rcc/l.rs:362:23
Frame 6: _defmt_version_ = 4 @ 0x003d08fe> @ 0x00000000003d08fe
Error: Multiple
It was really puzzling at first and got frustrating really quick. I had no idea what was going on. Why would such a simple example like that crash so hard? Maybe I should be using config for another STM32WL chip? No, I double checked and the stm32wle5jc feature seems to be correct. Should I use static SHARED_DATA: MaybeUninit<SharedData> = MaybeUninit::uninit();? Nah, it's a single-core variant of WL family. So what happened?
The error points at something related to not being able to access flash on address 0x58004f1e. My first idea was that the memory layout was being generated in a wrong way, maybe for a wrong board, but no - I checked and it should match just correctly. The breakthrough came when I decided to test on debug mode with a simple cargo run, and to my surprise - it just worked! That's super weird. After a thorough investigation, it seems I've found the issue.
Tf happened???
It turns out that using Config::default() initializes the MSI (Multi-Speed Internal oscillator) to the default value of MSIRange::RANGE4M - 4MHz. This, in turn, causes several things. When the STM32WLE5JC boots up, it sets the initial flash latency to 2:
#[cfg(stm32wl)]
{
// Set max latency
FLASH.acr().modify(|w| w.set_prften(true));
FLASH.acr().modify(|w| w.set_latency(2));
}
Then this loop happens:
FLASH.acr().modify(|w| w.set_prften(true));
FLASH.acr().modify(|w| w.set_latency(latency));
while FLASH.acr().read().latency() != latency {}
Where latency is set to:
#[cfg(stm32wl)]
let latency = match hclk3.0 {
// VOS RANGE1, others TODO.
..=18_000_000 => 0,
..=36_000_000 => 1,
_ => 2,
};
On 4MHz clock, it's set to 0, and I'm guessing that the compiler optimizes this loop too aggressively, causing the read and write instructions to happen instantaneously after each other. This happens when the flash hasn't finished changing its latency settings yet - which causes the BusFault crash. I'm also guessing that opt-level = 0 or "s" just doesn't unroll this loop, which results in correct operations done on flash - there's just enough time for the hardware to do its stuff.
On 48MHz MSI clock the latency is set to 2, which doesn't change anything like on line 360 above, so the flash doesn't need to change its own settings, so the aggressively optimized loop causes no issues.
Conclusions
When the correct settings are applied, everything works well! The LED blinks, nothing crashes and I'm happy. This was a lot for one day and no embedded experience, and I'm happy I encountered these issues! I learned a lot, and it was just a simple blinky example. I can't wait what awaits next!
See ya :3