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.
no_std
attributeRust'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)]
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());
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.
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
.#[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();
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"
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.
Recommended crates to use in firmware project:
🔗 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);
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
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());
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
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; }
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
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
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]]);
🔗 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 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 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.
Before releasing the crate/project go thought the following action items.
Cargo.toml
cargo update # Update dependencies cargo vet # Run audit check cargo build # Build project cargo test # Build and run tests cargo clippy # Run linter
use
only what's needed.const
variables for magic numbers.pub
, pub(crate)
, etc. modifiers.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).