Clone this repo:

Branches

  1. 2883ea1 Initial version of the Firmware Development Guide by Imre Kis · 9 weeks ago main
  2. 894b767 Initial empty repository by Bálint Dobszay · 9 weeks ago

Firmware Development Guide

The goal of this document is to provide a set of best practices, design guidelines, and tooling recommendations for developing firmware in Rust, particularly in no_std environments.

Set no_std attribute

Rust's standard library relies on the presence of a rich OS. As a result, it isn’t available in firmware environments. Firmware projects or crates should therefore be marked with no_std to indicate that they do not use the std library.

Insert the following line at the top of main.rs/lib.rs.

#![no_std]

Tests might rely on the standard library since they are typically executed in a host environment. Therefore, the no_std attribute should be conditionally applied when the build is not intended for testing.

#![cfg_attr(not(test), no_std)]

Avoid using alloc

Using a heap in firmware components can lead to non-deterministic behavior and potential fragmentation issues. As a result, many firmware projects choose to avoid using a heap entirely.

Since a heap may not be available, relying on types from the alloc crate - such as Vec - can restrict the crate's applicability. Instead, prefer using slices for function inputs and outputs whenever possible. This approach allows the caller to manage memory allocation using the most appropriate storage mechanism for their context.

fn numbers_with_alloc(n: usize) -> Vec<usize> {
    let mut result = Vec::new();
    for i in 0..n {
        result.push(i);
    }
    result
}

fn numbers_without_alloc(n: usize, result: &mut [usize]) {
    for i in 0..n {
        result[i] = i;
    }
}

let result: Vec<usize> = numbers_with_alloc(10);

let mut result: [usize; 10] = [0; 10];
numbers_without_alloc(10, &mut result);

// But now it still works with Vec too!
let mut result: Vec<usize> = vec![0; 10];
numbers_without_alloc(10, result.as_mut_slice());

Derive or implement common traits for custom types

Rust provides a wide range of traits that allow custom types to integrate seamlessly with the core and standard library. For instance, a slice can be sorted if its element type implements the Ord trait, and if the element type implements PartialEq, it’s possible to check whether a slice contains a specific item. Implementing these traits for your custom types greatly increases their usability. Avoid creating ad-hoc methods for common operations, prefer standard traits like From or TryFrom for type conversions.

There are two main ways to implement a trait: by deriving it or by writing an explicit implementation. When a type's fields already implement a trait, using #[derive] is often enough to generate the trait implementation automatically. However, if the default behavior isn’t suitable, you can manually implement the trait to customize its logic.

Common traits

  • Debug - Enables printing the value using the "{:?}" formatter.
  • Display - Enables printing the value using the "{}" formatter. Display is similar to Debug, but Display is for user-facing output, and so cannot be derived.
  • Default - Provides a default() function for creating default instance.
  • Clone - Enables explicit clone()-ing of the type.
  • Copy - Enable implicit copying of the type. Do not use it for large types to avoid unnecessary copying.
  • PartialEq - Provides == and != operators.
  • Eq - The implementor promises that a == a, i.e. the type is reflexive. This is not true for floating point numbers where NaN != Nan.
  • PartialOrd - Provides <, <=, >, and >= operators.
  • Ord - Trait for types that form a total order.
  • From<T> - Used to do value-to-value conversions while consuming the input value. It is the reciprocal of Into. Implementing From over Into is preferred, because From automatically provides an Into implementation as well.
  • TryFrom<T>- Simple and safe type conversions that may fail in a controlled way under some circumstances. It is the reciprocal of TryInto.

Example

#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Address {
    city: String,
    street: String,
    house_number: u32
}

fn get_addresses() -> Vec<Address> {
    vec![Address {
        city: "Cambridge".to_string(),
        street: "Fulbourn Rd".to_string(),
        house_number: 110,
    }]
}

let mut addresses: Vec<Address> = get_addresses();

// Debug trait
println!("{:?}", addresses[0]);

// Default trait
let empty_address = Address::default();

// Clone trait
let empty_clone = empty_address.clone();

// PartialEq trait
let has_arm_address = addresses.contains(&Address {
    city: "Cambridge".to_string(),
    street: "Fulbourn Rd".to_string(),
    house_number: 110,
});

// Ord trait
addresses.sort();

Minimize and document unsafe code

Maintaining and reviewing unsafe code demands additional effort, so it's best to keep its usage to a minimum. In most cases unsafe code can be avoided by using an existing crate that provides a safe wrapper around the unsafe logic. For example, zerocopy enables the developer to convert between types, along all the necessary size and alignment checks.

During firmware development, writing some unsafe code is often unavoidable. Whenever you use unsafe features, clearly document how the necessary conditions for safety are satisfied to justify the use of unsafe blocks.

When defining an unsafe function, always specify the safety requirements that callers must uphold to use the function correctly.

# struct BootInfo(u32);

// SAFETY: The address is mapped statically and nothing else references it.
let boot_info = unsafe {  &*(0x8000_0000 as *const BootInfo) };

/// # Safety
///
/// Before calling this function, ensure that all pending writes have been
/// flushed and there are no references to the address.
unsafe fn unmap_memory(address: u64) {
    // [...]
}

Add the following lines to Cargo.toml in order to enforce these rules by Clippy.

[lints.clippy]
missing_safety_doc = "deny"
undocumented_unsafe_blocks = "deny"

Do not reinvent the wheel crate

Use popular crates instead of reimplementing a functionality. cargo provides great ways of reusing code and crates.io provides high quality crates. On the other hand, try to minimize the number of dependencies for supply chain, code size and build time reasons.

npm left-pad incident

Recommended crates to use in firmware project:

bitflags

🔗 bitflags

Provides a macro to define typesafe bitflags for managing sets of binary flags (single-bit). Not suitable for multi-bit fields.

bitflags::bitflags! {
    struct Permissions: u32 {
        const READ    = 0b001;
        const WRITE   = 0b010;
        const EXECUTE = 0b100;
    }
}

let perms = Permissions::READ | Permissions::WRITE;
let is_executable = perms.contains(Permissions::EXECUTE);

embedded-hal

🔗 embedded-hal

Provides hardware abstraction layers for embedded system as a family of crates (embedded-hal, embedded-hal-async, embedded-hal-nb, embedded-io, etc.). Peripheral drivers should implement the suitable traits of these crates in order to increase interoperability.

num_enum

🔗 num_enum

Rust does not have a built-in way of converting from integers to enums. The num_enum crate provides utilities for defining enums with explicit integer representations and conversions between them safely.

use num_enum::{TryFromPrimitive, TryFromPrimitiveError};
use core::convert::TryFrom;

#[derive(Debug, TryFromPrimitive, PartialEq, Eq)]
#[repr(u8)]
enum Command {
    Start = 1,
    Stop = 2,
}

assert_eq!(Ok(Command::Start), Command::try_from(1u8));
assert_eq!(Ok(Command::Stop), Command::try_from(2u8));
assert!(Command::try_from(3u8).is_err());

safe-mmio

🔗 safe-mmio

For MMIO peripheral access use the safe-mmio crate. This crates provides primitives for tracking the lifetime and ownership of an MMIO peripheral and for defining different types of peripherals registers.

use core::ptr::NonNull;
use safe_mmio::{
    field,
    fields::{ReadOnly, ReadPure, ReadWrite, WriteOnly},
    UniqueMmioPointer,
};

#[repr(C)]
struct UartRegisters {
    data: ReadWrite<u8>,
    status: ReadPure<u8>,
    pending_interrupt: ReadOnly<u8>,
}

// SAFETY:
// The UART peripherals at 0x900_0000 is statically mapped for the lifetime of
// the application and nothing else references it.
let mut uart_registers: UniqueMmioPointer<UartRegisters> =
    unsafe { UniqueMmioPointer::new(NonNull::new(0x900_0000 as _).unwrap()) };

field!(uart_registers, data).write(b'x');

spin

🔗 spin

Implements simple spinlocks for no_std environments without relying on OS primitives. Use spin::Once for global/static variable lazy initialization.

use spin::Mutex;

static DATA: Mutex<u32> = Mutex::new(0);

fn increment() {
    let mut data = DATA.lock();
    *data += 1;
}

thiserror

🔗 thiserror

This library provides a convenient derive macro for the standard library’s core::error::Error trait. It makes creating custom error types easier by

use thiserror::Error;

#[derive(Debug, Error)]
enum CustomError {
    #[error("generic error")]
    GenericError,
    #[error("invalid capacity: {0}")]
    InvalidCapacity(usize),
    #[error("unknown command index: {0}")]
    UnknownCommandIndex(u32),
}

uuid

🔗 uuid

Use the uuid crate for parsing/handling UUID/GUID values. It provides functions for converting between the Uuid type and strings, bytes and other representations.

use uuid::{uuid, Uuid};

const ID: Uuid = uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8");
let my_uuid =
    Uuid::parse_str("67e55044-10b1-426f-9247-bb680e5fe0c8").expect("Failed to parse UUID");

let bytes: &[u8; 16] = my_uuid.as_bytes();

zerocopy

🔗 zerocopy

Enables zero-copy parsing and serialization of fixed-layout types using safe abstractions. Zerocopy ensures that the source and destination types are suitable for conversion and they have a matching size and alignment.

use zerocopy::{FromBytes, IntoBytes, KnownLayout, Immutable, transmute};

#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
#[repr(C, packed)]
struct Packet {
    header: u16,
    payload: u32,
}

let raw: [u8; 6] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
let packet = Packet::read_from(&raw[..]).unwrap();
let raw_serialized = packet.as_bytes();
assert_eq!(raw_serialized, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);

let one_dimensional: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
let two_dimensional: [[u8; 4]; 2] = transmute!(one_dimensional);
assert_eq!(two_dimensional, [[0, 1, 2, 3], [4, 5, 6, 7]]);

Recommended tools

rustfmt

🔗 rustfmt

Rust has an official style guide that ensures consistent formatting across the Rust ecosystem. cargo fmt automatically formats the code according to the official style guide. Most editors can integrate rustfmt to format on save.

Clippy

🔗 Clippy

Clippy is the official Rust linter tool. It can emit various style, performance, etc. issues. Always address the issues pointed out by Clippy to improve code quality.

Cargo Vet

🔗 Cargo Vet

Cargo provides an easy way for using third party crates, however this introduces the possibility of supply chain attacks. An attacker can take over a dependency or push a malicious change into it. Cargo Vet is a tool for supply chain auditing and it only allows the use of audited crate versions.

Pre-release checklist

Before releasing the crate/project go thought the following action items.

  • Update version number in Cargo.toml
  • Update readme/changelog
  • Run the following commands
cargo update # Update dependencies
cargo vet    # Run audit check
cargo build  # Build project
cargo test   # Build and run tests
cargo clippy # Run linter

General advice

  • use only what's needed.
  • Create const variables for magic numbers.
  • Try to limit the visibility of types and functions using pub, pub(crate), etc. modifiers.
  • The fields of enum variants inherit the visibility of the type, however this is not true for struct field. Make sure that struct fields have the correct visibility if they are intended to be accessed outside of the defining module.

Copyright 2025 Arm Limited and/or its affiliates open-source-office@arm.com

Arm is a registered trademark of Arm Limited (or its subsidiaries or affiliates).