Building and flashing Rust-firmware to an STM MCU with Nix
The goal is to make a reproducible environment for both my laptop and desktop. You can assume that both my systems are x86-64 based and both are running NixOS. I will be developing for the STM32L0 target which is programmed by a ST-Link V2/1.
I don't like C++ and C requires too much typing, so i settled on developing mainly on Rust. This may bite me in the ass later because i don't know how Rust can be configured to compile outside of the std library.
We will be reading the following guide: https://docs.rust-embedded.org/
By the end of this, I will have the following commands
$ nix run .#firmware // builds the firmware
$ nix run .#flash // flashes the MCU
$ nix run // default target (builds + flash) The tools
Just to be clear, i don't have any experience creating my own tool chains so this will be a first, but I gather I will need a compiler/linker, and a flasher/debugger.
The compiler
For the compiler i don't have too much of a choice, i will be using rusc to target thumbv6m-none-eabi. https://doc.rust-lang.org/rustc/platform-support/thumbv6m-none-eabi.html. This specific compiler targets the arm-m0 (actually thumb) architecture . While looking at the documentation of this platform I noticed that it's considered a Tier-2 target. I have never heard of that before, but it just means that the rustc team guarantees building/compilation but binaries may not pass tests. As opposed to Tier-3 which does not guarantee compilation.
The flasher/debugger
For flashing and debugging I will use stlink and openocd.
flake.nix
Note the following
{
description = "Simple embedded development for STM32L0538";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, rust-overlay }:
let
lib = nixpkgs.lib;
systems = [ "x86_64-linux" ];
forAllSystems = lib.genAttrs systems;
rustTarget = "thumbv6m-none-eabi";
mkPkgs = system:
import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
mkFirmwareBundle = system:
let
pkgs = mkPkgs system;
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [
"clippy"
"llvm-tools-preview"
"rust-src"
"rustfmt"
];
targets = [ rustTarget ];
};
rustLinker = "${rustToolchain}/bin/rust-lld";
rustPlatform = pkgs.makeRustPlatform {
cargo = rustToolchain;
rustc = rustToolchain;
};
hasCargoToml = builtins.pathExists ./Cargo.toml;
hasCargoLock = builtins.pathExists ./Cargo.lock;
missingFirmware = pkgs.runCommand "stm32l0-firmware-missing" { } ''
echo "Firmware source is incomplete." >&2
${lib.optionalString (!hasCargoToml) "echo 'Missing Cargo.toml at the repository root.' >&2"}
${lib.optionalString (!hasCargoLock) "echo 'Missing Cargo.lock at the repository root.' >&2"}
echo "Add the firmware crate, then run: nix build .#firmware" >&2
exit 1
'';
firmware =
if hasCargoToml && hasCargoLock then
rustPlatform.buildRustPackage {
pname = "stm32l0-firmware";
version = "0.1.0";
src = lib.cleanSource ./.;
cargoLock.lockFile = ./Cargo.lock;
strictDeps = true;
doCheck = false;
CARGO_BUILD_TARGET = rustTarget;
CARGO_TARGET_THUMBV6M_NONE_EABI_LINKER = rustLinker;
nativeBuildInputs = with pkgs; [
findutils
llvmPackages.bintools
];
installPhase = ''
runHook preInstall
mkdir -p "$out"
artifact_count="$(find "target/${rustTarget}/release" -maxdepth 1 -type f -executable | wc -l)"
if [ "$artifact_count" -eq 0 ]; then
echo "No firmware artifact found under target/${rustTarget}/release" >&2
exit 1
fi
if [ "$artifact_count" -gt 1 ]; then
echo "Multiple firmware artifacts found under target/${rustTarget}/release" >&2
echo "Set up a single binary target or specialize the flake install phase." >&2
find "target/${rustTarget}/release" -maxdepth 1 -type f -executable >&2
exit 1
fi
artifact="$(find "target/${rustTarget}/release" -maxdepth 1 -type f -executable | head -n 1)"
cp "$artifact" "$out/firmware.elf"
llvm-objcopy -O binary "$artifact" "$out/firmware.bin"
runHook postInstall
'';
}
else
missingFirmware;
flashTool = pkgs.writeShellApplication {
name = "flash-firmware";
runtimeInputs = with pkgs; [
probe-rs-tools
stlink
];
text = ''
tool="''${FLASH_TOOL:-probe-rs}"
case "$tool" in
probe-rs)
chip="''${PROBE_RS_CHIP:-STM32L053C8Tx}"
exec probe-rs download --chip "$chip" ${firmware}/firmware.elf
;;
st-flash)
address="''${FLASH_ADDRESS:-0x08000000}"
exec st-flash write ${firmware}/firmware.bin "$address"
;;
*)
echo "Unsupported FLASH_TOOL: $tool" >&2
echo "Use FLASH_TOOL=probe-rs or FLASH_TOOL=st-flash." >&2
exit 1
;;
esac
'';
};
in
{
packages = {
default = firmware;
firmware = firmware;
flash = flashTool;
};
apps = {
flash = {
type = "app";
program = "${flashTool}/bin/flash-firmware";
};
};
devShells = {
default = pkgs.mkShell {
packages = with pkgs; [
rustToolchain
llvmPackages.bintools
probe-rs-tools
stlink
];
CARGO = "${rustToolchain}/bin/cargo";
RUSTC = "${rustToolchain}/bin/rustc";
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
CARGO_TARGET_THUMBV6M_NONE_EABI_LINKER = rustLinker;
shellHook = ''
echo "STM32L0 Rust development shell"
echo "Target: ${rustTarget}"
echo "Cargo: $CARGO"
echo "Rustc: $RUSTC"
echo "Linker: $CARGO_TARGET_THUMBV6M_NONE_EABI_LINKER"
echo "Init: cargo init --bin ."
echo "Build: nix build .#firmware"
echo "Flash: nix run .#flash"
'';
};
};
};
in
{
packages = forAllSystems (system: (mkFirmwareBundle system).packages);
apps = forAllSystems (system: (mkFirmwareBundle system).apps);
devShells = forAllSystems (system: (mkFirmwareBundle system).devShells);
};
}
Reference
See the HAL I will be using: