Skip to content

nRF52 DK Embassy Tutorial

This is a tutorial for building an Embassy application for the nRF52 DK, which features a nRF52832 SoC. The build is done in WSL (Windows Subsystem for Linux), but these instructions should work for any Linux distribution.

Installing Rust and Probe-RS

Before we can build the project, we need to install Rust. This is done with the rustup tool:

Terminal window
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Press enter when prompted to install it in the default location.

We’ll also need to install probe-rs. You can try this method:

Terminal window
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh

However, I got an error that my version of glibc was too old. So I used this method instead, which builds it from source:

Terminal window
cargo install probe-rs-tools --locked

This worked, and gave me the following result:

Terminal window
Installed package `probe-rs-tools v0.29.0` (executables `cargo-embed`, `cargo-flash`, `probe-rs`)

We’ll also need to install the thumbv7em-none-eabi target using rustup. Luckily if you try and cargo build without this is will tell you how to install it.

Terminal window
rustup target add thumbv7em-none-eabi

Creating a New Rust Project

Create a new Rust project:

Terminal window
cargo new nrf52-example

This will create a new directory called nrf52-example and populate it with a Cargo.toml file and a src directory containing a main.rs.

Opening the Project in VSCode

This tutorial assumes you are using VSCode, although most IDEs will work similarly. Open the newly created project folder ~/nrf52-example in VSCode.

Install the Rust Analyzer extension for VSCode. Without this, VS Code will give you syntax highlighting for Rust code, but not much else.

Screenshot of the Rust Analyzer extension for VSCode.

Updating the Files

Replace the contents of main.rs with the following, which is a basic blinky program for LED1 which is connected to pin P0.17.

#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_nrf::gpio::{Level, Output, OutputDrive};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_nrf::init(Default::default());
let mut led = Output::new(p.P0_17, Level::Low, OutputDrive::Standard);
loop {
led.set_high();
Timer::after_millis(300).await;
led.set_low();
Timer::after_millis(300).await;
}
}

no_std is used to tell the compiler not to add or link against the standard Rust library, instead just relying on the core library (which is a subset of the standard library).

no_main is used to tell the compiler not to look for a main function. A main function will still be defined, but this is taken care of us by Embassy.

At this point, rust-analyzer should give you the following error:

Screenshot of the rust-analyzer error before modifying the settings.

To fix this, add the following to you .vscode/settings.json file:

.vscode/settings.json
{
"rust-analyzer.check.allTargets": false
}

Next, create a file at the path .cargo/config.toml with the following contents (this is mostly copied from the Embassy example config.toml file, but with the --allow-erase-all flag added to the runner line):

.cargo/config.toml
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip nRF52832_xxAA --allow-erase-all"
[build]
target = "thumbv7em-none-eabi"
[env]
DEFMT_LOG = "trace"

The runner = line tells cargo what to do when you run cargo run. This will be used to program the nRF52 DK.

Also create a memory.x file in the project root directory with the following contents:

memory.x
MEMORY
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
FLASH : ORIGIN = 0x00000000, LENGTH = 512K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

memory.x is used to tell the linker where to put the code and data in the flash and RAM. This file will change depending on the microcontroller you are using.

Installing Dependencies and Building

Although you can add dependencies with cargo add, there are quite a few features for some of the dependencies that need adding. The easiest way to do this is to edit the Cargo.toml file.

I grabbed the Cargo.toml file from the Embassy nRF52840 example, and removed all the path = ... statements (it was using relative paths to the dependencies). It ended up looking like this:

Cargo.toml
[package]
edition = "2021"
name = "nrf52-example"
version = "0.1.0"
license = "MIT OR Apache-2.0"
[dependencies]
embassy-futures = { version = "0.1.0" }
embassy-sync = { version = "0.7.0", features = ["defmt"] }
embassy-executor = { version = "0.7.0", features = ["arch-cortex-m", "executor-thread", "executor-interrupt", "defmt"] }
embassy-time = { version = "0.4.0", features = ["defmt", "defmt-timestamp-uptime"] }
embassy-nrf = { version = "0.3.1", features = ["defmt", "nrf52832", "time-driver-rtc1", "gpiote", "unstable-pac", "time"] }
embassy-net = { version = "0.7.0", features = ["defmt", "tcp", "dhcpv4", "medium-ethernet"] }
embassy-usb = { version = "0.4.0", features = ["defmt"] }
embedded-io = { version = "0.6.0", features = ["defmt-03"] }
embedded-io-async = { version = "0.6.1", features = ["defmt-03"] }
embassy-net-esp-hosted = { version = "0.2.0", features = ["defmt"] }
embassy-net-enc28j60 = { version = "0.2.0", features = ["defmt"] }
defmt = "1.0.1"
defmt-rtt = "1.0.0"
fixed = "1.10.0"
static_cell = { version = "2" }
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
cortex-m-rt = "0.7.0"
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
rand = { version = "0.9.0", default-features = false }
embedded-storage = "0.3.1"
usbd-hid = "0.8.1"
serde = { version = "1.0.136", default-features = false }
embedded-hal = { version = "1.0" }
embedded-hal-async = { version = "1.0" }
embedded-hal-bus = { version = "0.1", features = ["async"] }
num-integer = { version = "0.1.45", default-features = false }
microfft = "0.5.0"
[profile.release]
debug = 2

Now that we have all the dependencies listed, we can build the project.

Terminal window
cargo build

Pass-through nRF52 DK USB device to WSL

Plugging the nRF52 DK into my Windows computer, the J-Link based debugger is detected. Using usbipd, I can pass-through the connected nRF52 DK from Windows into WSL. First you have to bind the USB device from a terminal on Windows with admin privileges:

Terminal window
usbipd bind -i 1366:1051

You only have to do the bind once. Then you use usbipd attach to pass it through. Run this next command from a standard terminal (not one with admin privileges).

Terminal window
usbipd attach -w -a -i 1366:1051

The -w flag is to attach to WSL, and -a is for auto-attach. If it passed through correctly, you should be able to detect it in WSL using probe-rs list.

Terminal window
gbmhunter@geoff-laptop:~/nrf52-example$ probe-rs list
The following debug probes were found:
[0]: J-Link -- 1366:1051:001050376416 (J-Link)

Flashing the nRF52 DK

Now that we have attached the debugger, we can now flash the nRF52 DK using cargo run:

Terminal window
cargo run
Screenshot of the cargo run command flashing the nRF52 DK.

If you have multiple debuggers attached, probe-rs will ask you to select which one to use.

Screenshot of probe-rs asking you to select which debugger to use.

The first time I tried to program the nRF52 DK, I got the following error:

WARN probe_rs::vendor::nordicsemi::sequences::nrf52: Core is locked. Erase procedure will be started to unlock it.
Error: Connecting to the chip was unsuccessful.
Caused by:
0: An ARM specific error occurred.
1: An operation could not be performed because it lacked the permission to do so: erase_all

I managed to fix this by adding the flag --allow-erase-all to the probe-rs run command in the .cargo/config.toml file.

.cargo/config.toml
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip nRF52832_xxAA --allow-erase-all"
Screenshot of probe-rs lacking permission to erase all.

Adding Logging

Logging is trivial to add to this project. First, we need to import defmt with:

use defmt;

And then we can log a info level message with:

defmt::info!("Hello, world!");
The output I get when using defmt to log a message.

A Slightly More Complex Example

The above example was trivially simple, so let’s expand on it a bit. Let’s add a second executor, and make that responsible for blinking the LED. We will re-purpose the primary executor to show how to use select to wait for multiple events (futures) at the same time.

The basic syntax for select is:

match select(future1, future2).await {
// ...
}

select will return as soon as one of the futures is ready. A match statement can then be used to work out what future is ready and handle it appropriately.

Update our main.rs file to look like this:

main.rs
#![no_std]
#![no_main]
use defmt;
use embassy_executor::Spawner;
use embassy_nrf::gpio::{AnyPin, Level, Pin, Output, OutputDrive};
use embassy_time::{Duration, Ticker, Timer};
use {defmt_rtt as _, panic_probe as _};
use embassy_futures::select::{select, Either};
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_nrf::init(Default::default());
spawner.spawn(blink(p.P0_17.degrade())).unwrap();
defmt::info!("Application has started.");
let mut ticker1 = Ticker::every(Duration::from_millis(1000));
let mut ticker2 = Ticker::every(Duration::from_millis(1200));
loop {
match select(ticker1.next(), ticker2.next()).await {
Either::First(_) => defmt::info!("1000ms timer"),
Either::Second(_) => defmt::info!("1200ms timer"),
}
}
}
#[embassy_executor::task]
async fn blink(pin: AnyPin) {
let mut led = Output::new(pin, Level::Low, OutputDrive::Standard);
loop {
led.set_high();
Timer::after_millis(150).await;
led.set_low();
Timer::after_millis(150).await;
}
}