ENG | How Hard Could It Be to Make Blinky in Rust on nRF52840?
A realistic guide to setting up Rust for blinking an LED on the Seeed XIAO nRF52840 (Sense), including toolchain installation, memory layout, building, and UF2 upload. Covers common pitfalls, and comparisons to Zephyr, MicroPython, and other platforms.
Yesterday I asked myself how hard could be blinky in Rust when reviewing article about nRF52 dongle, because using nRF52840 was one of the most frustrating experince this year. These weird questions are emerging after week without sunlight on cold, rainy friday evening.
Answer is it depends, but first time, with often misleading answers from AI, it could be maybe two to four hours, depending on focus, sleep deprivation, luck and so on. Of you just get example code, go to TL;DR and paste write commands, it may be minutes, but things rarely go smoothly or without consequences. So parts of process was fixing firmware.
This article will deal with setting up tools for nRF52840 rather than writing program in Rust, because I haven’t used this programming language for years and I mostly made large language models to write it. Mostly, they did good job, but …
Make double sure, on which address bootloader expects your program. You may rewrite some important parts of flash. Such as SoftDevice firmware from Nordic Semiconductors. Maybe XIAO nRF52840 bootloader recovery is the most visited article on this blog for reason :-) If bootloader works (USB device is recognized), it’s enough to use ZIP file for recovery, you’ll find process later. Also, address information is lost when elf binary is converted to
binfile (nothex) and then touf2. uf2conv provided in examples does not produce validuf2file whenhexis input file. It just converts it to text and overwrites MBR with it.
This is not a fault of language models, five minutes tutorials on youtube show how to use embassy and do exactly that and they are not dealing with consequences or expecting you might want to go back to MicroPython ever.
Source code
Main program
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
use nrf52840_hal as hal;
use hal::gpio::Level;
use embedded_hal::digital::OutputPin;
use embedded_hal::delay::DelayNs;
#[entry]
fn main() -> ! {
// Get access to the device peripherals
let p = hal::pac::Peripherals::take().unwrap();
// Split the GPIO port into individual pins
let port0 = hal::gpio::p0::Parts::new(p.P0);
// Configure red=P0.26, gree=P0.30 blue=P.06 as a push-pull output
let mut led = port0.p0_06.into_push_pull_output(Level::High);
// Get access to the core peripherals for delay
let core = hal::pac::CorePeripherals::take().unwrap();
let mut delay = hal::delay::Delay::new(core.SYST);
// Blink loop
loop {
led.set_low().unwrap();
delay.delay_ms(100_u32);
led.set_high().unwrap();
delay.delay_ms(400_u32);
}
}
Memory layout for Seeed XIAO nRF52840 (Sense)
Source of this file is github repository
Whenever you change values, run
cargo cleanso changes take effect in linked code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MEMORY
{
/* Need to leave space for the SoftDevice
These values are confirmed working for S140 7.3.0
They were extracted from the Arduino IDE plugin linked indirectly at https://wiki.seeedstudio.com/XIAO_BLE/
*/
FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000
/* SRAM required by Softdevice depend on
* - Attribute Table Size (Number of Services and Characteristics)
* - Vendor UUID count
* - Max ATT MTU
* - Concurrent connection peripheral + central + secure links
* - Event Len, HVN queue, Write CMD queue
*/
RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
}
Firmware version of XIAO boards can be verified using
1
2
3
4
5
6
7
8
9
~$ udisksctl mount -b /dev/sdb
Mounted /dev/sdb at /run/media/pavel/XIAO-SENSE
~$ cat /run/media/pavel/XIAO-SENSE/INFO_UF2.TXT
UF2 Bootloader 0.6.1 lib/nrfx (v2.0.0) lib/tinyusb (0.10.1-293-gaf8e5a90) lib/uf2 (remotes/origin/configupdate-9-gadbb8c7)
Model: Seeed XIAO nRF52840
Board-ID: Seeed_XIAO_nRF52840_Sense
SoftDevice: S140 version 7.3.0
Date: Nov 12 2021
Zephyr also gives some hint, although it reports useable flash region to be 788kB long whereas example above suggest 792kB flash, but only 232kB RAM.
1
~/zephyrproject$ west build --pristine -b xiao_ble/nrf52840 ./zephyr/samples/basic/blinky_pwm
1
2
3
4
5
6
7
8
9
-- Zephyr version: 4.3.0 (/home/pavel/zephyrproject/zephyr), build: v4.3.0
[188/188] Linking C executable zephyr/zephyr.elf
Memory region Used Size Region Size %age Used
FLASH: 54372 B 788 KB 6.74%
RAM: 13624 B 256 KB 5.20%
IDT_LIST: 0 GB 32 KB 0.00%
Generating files from /home/pavel/zephyrproject/build/zephyr/zephyr.elf for board: xiao_ble
Converted to uf2, output size: 109056, start address: 0x27000
Wrote 109056 bytes to zephyr.uf2
But after some research, memory layout on github repository comes from Adafruit Feather
Zephyr RTOS have this:
1
2
3
4
5
6
7
8
9
10
11
/*
* Default flash layout for nrf52840 using UF2 and SoftDevice s140 v6
*
* 0x00000000 SoftDevice s140 v6 (152 kB)
* 0x00026000 Application partition (792 kB)
* 0x000ec000 Storage partition (32 kB)
* 0x000f4000 UF2 boot partition (48 kB)
*
* See https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather/hathach-memory-map
*/
1
2
3
4
5
6
7
8
/*
* Default flash layout for nrf52840 using UF2 and SoftDevice s140 v7
*
* 0x00000000 SoftDevice s140 v7 (156 kB)
* 0x00027000 Application partition (788 kB)
* 0x000ec000 Storage partition (32 kB)
* 0x000f4000 UF2 boot partition (48 kB)
*/
Weirdly, Zephyr RTOS does not respect memory map it cites.
Cargo.toml (modules)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[package]
name = "rust-nrf52-blinky"
version = "0.1.0"
edition = "2024"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
panic-halt = "1.0"
nrf52840-hal = { version = "0.19.0", features = ["rt"] }
embedded-hal = "1.0"
#probe-rs = "0.30.0"
[profile.release]
opt-level = "s"
lto = true
panic="abort"
[[bin]]
name = "rust-nrf52-blinky"
test = false
bench = false
Configuring the Build Target
1
2
3
4
5
6
7
[target.thumbv7em-none-eabihf]
rustflags = [
"-C", "link-arg=-Tlink.x",
]
[build]
target = "thumbv7em-none-eabihf"
| MCU | architecture-vendor-os-abi |
|---|---|
| nRF52 | thumbv7em-none-eabihf |
| RP2040 | thumbv6m-none-eabi |
| RP2350 | thumbv8m.main-none-eabihf |
| ESP32 | xtensa-esp32-none-elf (complicated) |
| ESP32-C3 | riscv32imc-unknown-none-elf |
Note:
1
2
3
thumbv7em = ARM Cortex-M4/M7 architecture
none = no operating system (bare metal)
eabihf = embedded ABI with hardware floating point
Installing Rust
The recommended way to install Rust is using rustup rather than your distribution’s packaged version. rustup is the official Rust toolchain installer that makes it easy to install Rust itself, add support for different target architectures (like ARM or RISC-V), and install additional development tools. Distribution packages are often outdated and lack the flexibility needed for embedded development.
So first commands are
1
2
3
4
# Install rustup script on Fedora
sudo dnf in rustup
# Guided installation of Rust
rustup-init
Installing Rust basically requires clicking on yes and it modifies profile files in your home to set environment variable. This require either restarting terminal or importing environment variables manually - just follow instructions.
1
2
# Import environment variables form file (bash, zsh, dash, ...)
. /home/pavel/.cargo/env
If source files are fine, all errors can be basically resolved by reading error message after cargo build --release command:
1
2
3
4
5
6
7
8
error[E0463]: can't find crate for `core`
|
= note: the `thumbv7em-none-eabihf` target may not be installed
= help: consider downloading the target with `rustup target add thumbv7em-none-eabihf`
For more information about this error, try `rustc --explain E0463`.
error: could not compile `nb` (lib) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
So proceed with installing toolset for target
1
2
3
4
pavel@thinkpad:~/devel/rust-nrf52-blinky$ rustup target add thumbv7em-none-eabihf
info: downloading component 'rust-std' for 'thumbv7em-none-eabihf'
info: installing component 'rust-std' for 'thumbv7em-none-eabihf'
12.1 MiB / 12.1 MiB (100 %) 8.9 MiB/s in 1s
and build it
1
2
3
4
pavel@thinkpad:~/devel/rust-nrf52-blinky$ cargo build --release
...
Compiling rust-nrf52-blinky v0.1.0 (/home/pavel/devel/rust-nrf52-blinky)
Finished `release` profile [optimized] target(s) in 39.07s
Now we may want to view disassembly, which will guide us through next steps
1
2
3
4
5
pavel@thinkpad:~/devel/rust-nrf52-blinky$ cargo objdump --release -- -d -C
error: no such command: `objdump`
help: view all installed commands with `cargo --list`
help: find a package to install `objdump` with `cargo search cargo-objdump`
Ok, we need to find objdump, which is part of cargo-binutils.
1
2
3
4
5
pavel@thinkpad:~/devel/rust-nrf52-blinky$ cargo install cargo-binutils
Updating crates.io index
Downloaded cargo-binutils v0.4.0
...
Installed package `cargo-binutils v0.4.0` (executables `cargo-cov`, `cargo-nm`, `cargo-objcopy`, `cargo-objdump`, `cargo-profdata`, `cargo-readobj`, `cargo-size`, `cargo-strip`, `rust-ar`, `rust-as`, `rust-cov`, `rust-ld`, `rust-lld`, `rust-nm`, `rust-objcopy`, `rust-objdump`, `rust-profdata`, `rust-readobj`, `rust-size`, `rust-strip`)
and retry:
1
2
3
4
5
pavel@thinkpad:~/devel/rust-nrf52-blinky$ cargo objdump --release -- -d -C
Finished `release` profile [optimized] target(s) in 0.06s
Could not find tool: objdump
at: /home/pavel/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-objdump
Consider `rustup component add llvm-tools`
wait, what? Let’s follow instructions:
1
2
3
4
5
6
7
8
9
10
11
12
pavel@thinkpad:~/devel/rust-nrf52-blinky$ rustup component add llvm-tools
info: downloading component 'llvm-tools'
info: installing component 'llvm-tools'
35.7 MiB / 35.7 MiB (100 %) 9.3 MiB/s in 3s
pavel@thinkpad:~/devel/rust-nrf52-blinky$ cargo objdump --release -- -d -C
Finished `release` profile [optimized] target(s) in 0.06s
rust-nrf52-blinky: file format elf32-littlearm
Disassembly of section .text:
00027100 <__stext>:
Finally, we can see disassembly and verify our code starts at address 0x27100
Now the goal is to create bin hex file and convert it to uf2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pavel@thinkpad:~/devel/rust-nrf52-blinky$ cargo objcopy --release -- -O binary rust-nrf52-blinky.bin
Finished `release` profile [optimized] target(s) in 0.08s
pavel@thinkpad:~/devel/rust-nrf52-blinky$ ls -la rust-nrf52-blinky.bin
-rwxr-xr-x. 1 pavel pavel 640 Dec 7 13:38 rust-nrf52-blinky.bin
pavel@thinkpad:~/devel/rust-nrf52-blinky$ cargo install uf2conv
Updating crates.io index
Downloaded uf2conv v0.1.0
...
Finished `release` profile [optimized] target(s) in 22.10s
Installing /home/pavel/.cargo/bin/uf2conv
Installed package `uf2conv v0.1.0` (executable `uf2conv`)
pavel@thinkpad:~/devel/rust-nrf52-blinky$ uf2conv
error: The following required arguments were not provided:
<INPUT>
USAGE:
uf2conv <INPUT> --base <base> --family <family> --output <output>
For more information try --help
pavel@thinkpad:~/devel/rust-nrf52-blinky$ uf2conv rust-nrf52-blinky.bin --family 0xADA52840 --base 0x27000 --output rust-nrf52-blinky.uf2
pavel@thinkpad:~/devel/rust-nrf52-blinky$ ls -la *.uf2
-rw-r--r--. 1 pavel pavel 1536 Dec 7 13:44 rust-nrf52-blinky.uf2
Now we can proceed by uploading it
1
2
pavel@thinkpad:~/devel/rust-nrf52-blinky$ udisksctl mount -b /dev/sdb && cp rust-nrf52-blinky.uf2 /run/media/pavel/XIAO-SENSE
Mounted /dev/sdb at /run/media/pavel/XIAO-SENSE
Victory!
TL;DR
1
2
3
4
5
# Install Rust
sudo dnf in rustup
# Agree on defaults
rustup-init
. /home/$USER/.cargo/env
1
2
3
4
# Install tools for target archicture(s)
rustup target add thumbv7em-none-eabihf
rustup component add llvm-tools
cargo install cargo-binutils uf2conv
1
2
3
4
5
6
7
# Compile and upload project (modify as needed)
# cargo clean (when you touch `memory.x`)
cargo build --release
cargo objcopy --release -- -O binary rust-nrf52-blinky.bin
uf2conv rust-nrf52-blinky.bin --base 0x27000 --family 0xADA52840 --output rust-nrf52-blinky.uf2
udisksctl mount -b /dev/sdb
cp rust-nrf52-blinky.uf2 /run/media/$USER/XIAO-SENSE
Here I tried to fix partially broken bootloader and/or SoftDevice. I just made it worse and erased softdevice. This possibly changed address where bootloader expects program and now I can’t upload anything. Hopefully I later unbricked it, by reuploading firmware via serial port.
I even found new version of firmware on Adafruit GitHub, but neither UF2, nor serial upload can overwrite bootloader area.
How to restore partially bricked Seeed XIAO
If USB is still recognized, just UF2 files seem ignored, get ZIP file with firmware from github.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
~$ pip3 install adafruit-nrfutil
Defaulting to user installation because normal site-packages is not writeable
Collecting adafruit-nrfutil
Downloading adafruit-nrfutil-0.5.3.post16.tar.gz (49 kB)
~$ adafruit-nrfutil dfu serial -pkg ~/Downloads/Seeed_XIAO_nRF52840_Sense_bootloader-0.6.1_s140_7.3.0.zip -p /dev/ttyACM0 -b 115200
Upgrading target on /dev/ttyACM0 with DFU package /home/pavel/Downloads/Seeed_XIAO_nRF52840_Sense_bootloader-0.6.1_s140_7.3.0.zip. Flow control is disabled, Dual bank, Touch disabled
########################################
########################################
########################################
########################################
########################################
########################################
########################################
########################################
########################################
###############
Activating new firmware
Device programmed.
Porting to nRF52840 Dongle
LED is on pin P0.06 just like blue LED on XIAO Sense, so source code can be untouched.
Memory map of dongle
Somehow, there is no softdevice installed. nRF Sniffer and Zephyr default to address 0x1000 just after MBR. Zephyr does not even take bootloader’s existence into account, I’ll be more conservative.
| Adress | Size | Description |
|---|---|---|
| 0x00000000-0x00000fff | 4096 | MBR |
| 0x00001000-0x0000e4e3 | 54516 | application |
| 0x000e0000-0x000fdfff | 122880 | bootloader |
Change memory map
1
2
3
4
5
MEMORY
{
FLASH (rx) : ORIGIN = 0x1000, LENGTH = 0xE0000 - 0x1000
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x40000
}
Compile and run
1
2
3
4
5
cargo clean # after changing memory.x file!
cargo build --release
cargo objcopy --release -- -O ihex rust-nrf52-blinky.hex
nrfutil pkg generate --hw-version 52 --sd-req 0x00 --application rust-nrf52-blinky.hex --application-version 1 rust-nrf52-blinky.zip
nrfutil dfu usb-serial --package rust-nrf52-blinky.zip -p /dev/ttyACM0
Summary
In the end it’s not that hard. Problem is you have to know which architecture to use, what is the memory layout (may differ between Seeed XIAO nRF52840, nRF52840 dongle, …), what is the upload firmware upload method (enter boot mode - how, XIAO uses UF2 upload to virtual flash driver, dongle uses nrfutil or some tool from Nordic SDK).
Article may be expanded in the future. Rust may seem as some alternative between Zephyr RTOS and MicroPython. Blinky is suprisingly tiny binary, only few hundred bytes.
But this microcontroller is mostly about BLE and low power, question is how well these are supported.
For Raspberry Pi Pico, MicroPython is nice and often sufficient. Zephyr seems useable so far (I still need some more complex project with WiFi, NTP, HTTPS, low power). Raspberry Pi Pico SDK is certainly worth exploring in the future.
For nRF52 MicroPython is basically useless, due to lack of BLE support as of December 2025 and Zephyr works very well, but it’s not simple initially.
To be fair, I learned some Rust basics years ago and I never found good use case this language - at least one that fits my needs. If there seems to be one, it depends on C libraries which completely defeats strenghts of Rust: dependency management and safety, so why not to use C++ then.
Lessons learned
memory.xfile importance andcargo cleanafter edit.- New method of uploading firmware using
adafruit-nrfutil(it seems to work for xiao, not for dongle and vice versa) - Adafruit made bootloader for many nRF52 family based boards, including Seeed XIAO nRF52840
- Don’t use
binfile or use--baseforuf2conv.
Links
- Official bootloader - always use one for XIAO Sense.
- Adafruit bootloader - likely needs jlink/openocd for update
- Adafruit nrfutil - tool to package and upload firmware to XIAO Sense via usb-serial
- nrfutil - tool to upload firmware, ble-sniffer, … read nRF52840 dongle article
- Wumpf’s Rust nRF52 examples
- Adafruit nRF52840 Adafruit memory map and actually very good article about this microcontroller