The article received an update on

To program the Micro:bit we first need to make sure we have the development environment set up correctly. Setting up the environment consists of two parts. We need to make sure Linux recognizes the device correctly and give it the correct tag. Without the correct tag, we can not write to the device as an unprivileged user. Then, we need to set up our development environment with the tools needed to compile and write our Rust project to the device.

Adding a udev rule

Linux recognizes the Micro:bit as a USB mass storage device when connected to the computer. This should work out of the box, but we cannot write to the device as an unprivileged user. For this to work we need to add a udev rule that adds the uaccess tag when the device connects to the computer. I’m using NixOS as my Linux distribution and declaring my configuration using Nix Flakes. The udev rule go in its own file that I import into the configuration of the host that the Micro:bit connects to:

{
  pkgs,
  ...
}: {
  config = let
    microbit-rules = pkgs.writeTextFile  {
      name = "69-microbit.rules";
      text = ''
        ACTION!="add|change", GOTO="microbit_rules_end"
        SUBSYSTEM=="usb", ATTR{idVendor}=="0d28", ATTR{idProduct}=="0204", TAG+="uaccess"
        SUBSYSTEM=="tty", KERNEL=="ttyACM*", TAG+="uaccess"
        LABEL="microbit_rules_end"
      '';
      destination = "/etc/udev/rules.d/69-microbit.rules";
    };
  in {
    services.udev.packages = [ microbit-rules ];
  };
}

In the snippet above, lines 9-12 show the udev rule. We declare the /etc/udev/rules.d/69-microbit.rules file as the destination for our rule on line 14. And lastly on line 17 we add the rules file to the udev packages to enable it.

The udev rule addresses two Linux subsystems, the USB and the TTY subsystem. We need this because the Micro:bit provides both a USB mass storage device endpoint and a serial interface. If we look at the electrical diagram of the Micro:bit we see that the board actually have two microcontrollers. The nRF52833 serves as the main microcontroller, but the board also features a NXP MKL27Z256VFM4 microcontroller that sit between the computer and the main microcontroller. This microcontroller provides the interface to the board and handles programming of the nRF52833. It also provides the debugging probe and the UART-to-USB interface. On line 10 we set the rule for the USB subsystem, matching on vendor and product ids of the NXP chip. Then on line 11 we set the rule for the serial interface. With these rules in place the user can program the device as well as access the debugging probe and the UART-to-USB interface.

After activating the configuration we can verify that the configuration works as intended. We first spawn a nix-shell with the usbutils package. After plugging in the device with the USB cable we list all the USB devices. We do this with lsusb and then grep for the Micro:bit which shows up with the name NXP ARM mbed:

> nix-shell -p usbutils

> lsusb | grep "NXP ARM mbed"
Bus 001 Device 024: ID 0d28:0204 NXP ARM mbed

This tells us the bus and device ids where we can find the device file. By looking up the access control list of the device file we can verify that the rules work as intended:

> ls -l /dev/bus/usb/001/024
crw-rw-r--@ 189,23 root  6 Jul 07:46 /dev/bus/usb/001/024

> getfacl /dev/bus/usb/001/024
getfacl: Removing leading '/' from absolute path names
# file: dev/bus/usb/001/024
# owner: root
# group: root
user::rw-
user:aj:rw-
group::rw-
mask::rw-
other::r--

With the correct permissions in place we can access the device and query the device with probe-rs:

> nix-shell -p probe-rs
> probe-rs list
The following debug probes were found:
[0]: BBC micro:bit CMSIS-DAP -- 0d28:0204:9904360273554e45004660070000001c000000009796990b (CMSIS-DAP)

> probe-rs info
Probing target via JTAG
-----------------------

Error while probing target: The protocol 'JTAG' could not be selected.

Caused by:
    The probe does not support the JTAG protocol.
Probing target via SWD
----------------------

 WARN probe_rs::architecture::arm::memory::romtable: Component at 0xe0001000: CIDR0 has invalid preamble (expected 0xd, got 0x0)
 WARN probe_rs::architecture::arm::memory::romtable: Component at 0xe0001000: CIDR2 has invalid preamble (expected 0x5, got 0x0)
 WARN probe_rs::architecture::arm::memory::romtable: Component at 0xe0001000: CIDR3 has invalid preamble (expected 0xb1, got 0x0)
 WARN probe_rs::architecture::arm::memory::romtable: Component at 0xe0000000: CIDR0 has invalid preamble (expected 0xd, got 0xb1)
 WARN probe_rs::architecture::arm::memory::romtable: Component at 0xe0000000: CIDR1 has invalid preamble (expected 0x0, got 0x1)
 WARN probe_rs::architecture::arm::memory::romtable: Component at 0xe0000000: CIDR2 has invalid preamble (expected 0x5, got 0xb1)
 WARN probe_rs::architecture::arm::memory::romtable: Component at 0xe0040000: CIDR0 has invalid preamble (expected 0xd, got 0x0)
 WARN probe_rs::architecture::arm::memory::romtable: Component at 0xe0040000: CIDR2 has invalid preamble (expected 0x5, got 0x0)
 WARN probe_rs::architecture::arm::memory::romtable: Component at 0xe0040000: CIDR3 has invalid preamble (expected 0xb1, got 0x0)
ARM Chip with debug port Default:

Debug Port: DPv1, Designer: ARM Ltd
├── V1(0) MemoryAP
│   └── 0 MemoryAP (AmbaAhb3)
│       ├── 0xe00ff000 ROM Table (Class 1), Designer: Nordic VLSI ASA
│       ├── 0xe0001000 Generic
│       ├── 0xe0000000 Peripheral test block
│       ├── 0xe0040000 Generic
│       └── 0xe0041000 Cortex-M4 ETM   (Coresight Component)
└── V1(1) Unknown AP (Designer: Nordic VLSI ASA, Class: Undefined, Type: 0x0, Variant: 0x0, Revision: 0x0)


Debug port version DPv1 does not support SWD multidrop. Stopping here.

This verifies that we can communicate properly with the Micro:bit.

We have successfully completed the first part of the setup.

Development environment

Next we need to set up the Rust toolchain. The book describes which tools we need, so we need to create the Nix Flake that defines a devshell with the desired tools. If you do not know Nix and devshells, think of it as a virtual environment for your project. I prefer to use Numtide's devshell as it provides a clean and minimal environment to build upon. I also like to have everything defined in Nix and do not mix in TOML for the devshell configuration.

The initial template I created as a starting point for the project looks like this:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    utils.url = "github:numtide/flake-utils";
    utils.inputs.nixpkgs.follows = "nixpkgs";
    devshell.url = "github:numtide/devshell";
    devshell.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = { self, nixpkgs, utils, devshell }:
    utils.lib.eachDefaultSystem (system: {
      devShells = rec {
        default = let
          pkgs = import nixpkgs {
            inherit system;
            overlays = [ devshell.overlays.default ];
          };
        in pkgs.devshell.mkShell {
            devshell.packages = [ ];
        };
      };
    });
}

I add this flake.nix file to my project directory together with a basic .envrc for direnv:

use flake

Together these files make sure I have the development environment configured correctly every time I enter the project directory:

> cd rust-embedded-discovery
direnv: loading ~/Work/rust-embedded-discovery/.envrc
direnv: using flake
direnv: nix-direnv: Using cached dev shell
🔨 Welcome to devshell

Now we need to add the required tools to the devshell. Thus far I have always relied on the Nix community Fenix project to provide the Rust toolchain. I tried to use it yet again, but this time it did not work with the Micro:bit as the build target. Others who have tried also experienced the same issues, so without digging further into the issue I decided to try another approach. I decided to try to use Oxalica's rust-overlay. This approach got me all the way to success with just a few lines of Nix. The complete Nix Flake looks like this:

{
  inputs = {
    devshell = {
      url = "github:numtide/devshell";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, devshell, nixpkgs, rust-overlay, utils }:
    utils.lib.eachDefaultSystem (system: {
      devShells = rec {
        default = let
          pkgs = import nixpkgs {
            inherit system;
            config.allowUnfree = true;
            overlays = [
              devshell.overlays.default
              rust-overlay.overlays.default
            ];
          };
        in pkgs.devshell.mkShell {
            devshell.packages = with pkgs; [
              cargo-binutils
              gdb
              probe-rs-tools
              rust-analyzer
              (rust-bin.stable.latest.default.override {
                extensions = [ "rust-src" "llvm-tools-preview" ];
                targets = [ "thumbv7em-none-eabihf" ];
              })
            ];

            env = [
              {
                name = "RUST_SRC_PATH";
                value = pkgs.rust.packages.stable.rustPlatform.rustLibSrc;
              }
              {
                name = "RUST_LIB_SRC";
                value = pkgs.rust.packages.stable.rustPlatform.rustLibSrc;
              }
              {
                name = "CARGO_BUILD_TARGET";
                value = "thumbv7em-none-eabihf";
              }
            ];
        };
      };
    });
}

In the Nix Flake we specify our target architecture on line number 35.

Verifying the setup

We should now have all the tools needed to produce the correct binaries for our Micro:bit. We also made sure Linux correctly detects the board when connecting and sets the correct permissions.

Now we can put it all to the test!

By using the source code from the book to verify that we indeed can build and flash the device. Entering the src/03-setup directory, running cargo embed and the moment of truth:

Screenshot of terminal running `cargo embed`

The program successfully built and flashed on the device, giving us a nice Hello World in return:

*Hello World* from the micro:bit

We successfully set up the environment making us ready to explore the code!

Thank you for reading!

Resources