no_std Rust programming involves developing applications without relying on Rust’s standard library (std). This constraint is typical in embedded systems, where resources are limited or when direct control over hardware is required. Why use no_std Rust?

  • Bare-Metal Requirement: std assumes an operating system, and it has not been ported to many specialized systems.
  • Cross-Platform Compatibility: Enables development for various architectures and platforms without the overhead or assumptions of the standard library.
  • Resource Efficiency: Operates in environments with limited memory and storage.
  • Direct Hardware Control: Allows developers to write software that directly interacts with hardware.

This post introduces no_std Rust, providing the foundation you need to start your journey in the world of embedded development with Rust. After that, it covers how the no_std core and alloc functionality is exposed to std Rust programs before giving pointers to more in-depth information.

This post assumes a general familiarity with Rust, rustup and cargo. Readers who are not familiar with Rust and the ecosystem tooling should consider starting with “The Rust Programming Language Book”. (rustup and cargo also have extensive documentation.)

Software Versions

# Date (UTC)
$ date -u "+%Y-%m-%d %H:%M:%S +0000"
2026-01-16 21:40:17 +0000

# OS and Version
$ uname -vm
Darwin Kernel Version 23.6.0: Mon Jul 29 21:14:30 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T6000 arm64

$ sw_vers
ProductName:		macOS
ProductVersion:		14.6.1
BuildVersion:		23G93

# Hardware Information
$ system_profiler SPHardwareDataType | sed -n '8,10p'
      Chip: Apple M1 Max
      Total Number of Cores: 10 (8 performance and 2 efficiency)
      Memory: 32 GB

# Shell and Version
$ echo "${SHELL}"
/bin/bash

$ "${SHELL}" --version  | head -n 1
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)

# Rust Installation Versions
$ cargo --version
cargo 1.92.0 (344c4567c 2025-10-21)

Tutorial Objective

This tutorial aims to guide you through creating a no_std Rust library that performs fundamental rocket calculations. Once the library is developed, we will demonstrate its use through a simple binary application that utilizes these computations.

Thrust Equation

The thrust $T$, generated by a rocket, is calculated by the following equation:

\[T = \dot{m} \times v_e + (p_e - p_0) \times A_e\]

where:

  • $T$ is the thrust, measured in Newtons ($\text{N}$),
  • $\dot{m}$ is the mass flow rate of the propellant, measured in $\frac{\text{kg}}{\text{s}}$,
  • $v_e$ is the exhaust velocity, measured in $\frac{\text{m}}{\text{s}}$,
  • $p_e$ is the pressure at the nozzle exit, measured in Pascals ($\text{Pa}$),
  • $p_0$ is the ambient pressure, measured in Pascals ($\text{Pa}$),
  • $A_e$ is the exit area of the nozzle, measured in square meters ($\text{m}^2$).

Specific Impulse Equation

The specific impulse $I_{sp}$, which indicates the efficiency of rocket propellants, can be derived from the thrust and the mass flow rate as follows:

\[I_{sp} = \frac{T}{\dot{m} \times g_0}\]

where:

  • $I_{sp}$ is the specific impulse, measured in seconds ($\text{s}$),
  • $T$ is the thrust, measured in Newtons ($\text{N}$),
  • $\dot{m}$ is the mass flow rate of the propellant, measured in $\frac{\text{kg}}{\text{s}}$,
  • $g_0$ is the standard acceleration due to gravity, approximately $9.80665 \, \frac{\text{m}}{\text{s}^2}$.

Delta-v Equation

The delta-v $\Delta v$, or change in velocity that a rocket can achieve, is calculated using the Tsiolkovsky rocket equation:

\[\Delta v = I_{sp} \times g_0 \times \ln\left(\frac{m_0}{m_f}\right)\]

where:

  • $\Delta v$ is the change in velocity, measured in $\frac{\text{m}}{\text{s}}$,
  • $I_{sp}$ is the specific impulse, measured in seconds ($\text{s}$),
  • $m_0$ is the initial total mass of the rocket, including propellant, measured in kilograms ($\text{kg}$),
  • $m_f$ is the final mass of the rocket after the propellant has been expended, measured in kilograms ($\text{kg}$),
  • $\ln$ represents the natural logarithm.

Creating a no_std Rust Library

Start by creating a new Rust project using cargo.

PROJECT="no_std_example"
cargo new --lib "${PROJECT}"
cd "${PROJECT}"

In your lib.rs, declare the project as no_std and start coding. In the following example, note that the ideal rocket equation includes a natural logarithm, so ln needs to be pulled in from libm. Neither the specific impulse equation nor the rocket thrust equation use anything beyond arithmetic, so they do not require the dependency.

// src/lib.rs

#![no_std]

extern crate libm;

/// Standard gravity (m/s^2)
pub const G0: f32 = 9.80665;

/// Calculates the thrust given the mass flow rate of the propellant (m dot),
/// the exhaust velocity (Ve), the pressure at the nozzle exit (Pe),
/// the ambient pressure (P0), and the exit area of the nozzle (Ae).
///
/// # Arguments
///
/// * `m_dot` - Mass flow rate of the propellant (kg/s).
/// * `ve` - Exhaust velocity (m/s).
/// * `pe` - Pressure at the nozzle exit (Pa).
/// * `p0` - Ambient pressure (Pa).
/// * `ae` - Exit area of the nozzle (m^2).
///
/// # Returns
///
/// Thrust in Newtons.
pub fn calculate_thrust(m_dot: f32, ve: f32, pe: f32, p0: f32, ae: f32) -> f32 {
  m_dot * ve + (pe - p0) * ae
}

/// Calculates the specific impulse given the thrust and the mass flow rate.
///
/// # Arguments
///
/// * `thrust` - The thrust in Newtons (N).
/// * `m_dot` - The mass flow rate in kilograms per second (kg/s).
///
/// # Returns
///
/// * Specific impulse in seconds (s).
pub fn calculate_specific_impulse(thrust: f32, m_dot: f32) -> f32 {
  thrust / (m_dot * G0)
}

/// Calculates the delta-v of a rocket using the Tsiolkovsky rocket equation.
///
/// # Arguments
///
/// * `isp` - The specific impulse in seconds (s).
/// * `m0` - The initial total mass of the rocket (including propellant) in kilograms (kg).
/// * `mf` - The final mass of the rocket (without propellant) in kilograms (kg).
///
/// # Returns
///
/// * Delta-v in meters per second (m/s).
pub fn calculate_delta_v(isp: f32, m0: f32, mf: f32) -> f32 {
  isp * G0 * libm::logf(m0 / mf)
}

#[cfg(test)]
mod tests {
    use super::*;

    const ACCEPTABLE_ERROR: f32 = 1e-3;

    #[test]
    fn test_calculate_thrust() {
        let m_dot = 5.0; // Mass flow rate (kg/s)
        let ve = 2500.0; // Exhaust velocity (m/s)
        let pe = 101325.0; // Pressure at nozzle exit (Pa) - Atmospheric pressure
        let p0 = 101325.0; // Ambient pressure (Pa) - Atmospheric pressure
        let ae = 0.1; // Exit area of the nozzle (m^2)
        let expected_thrust = 12500.0; // Expected thrust (N)

        let thrust = calculate_thrust(m_dot, ve, pe, p0, ae);
        // Ensure the calculated thrust is as expected
        assert!( (thrust - expected_thrust).abs() < ACCEPTABLE_ERROR);
    }

    #[test]
    fn test_calculate_specific_impulse() {
        let thrust = 12500.0; // Thrust (N)
        let m_dot = 5.0; // Mass flow rate (kg/s)
        let expected_isp = 254.929; // Expected specific impulse (s)

        let isp = calculate_specific_impulse(thrust, m_dot);
        // Ensure the calculated thrust is as expected
        assert!( (isp - expected_isp).abs() < ACCEPTABLE_ERROR);
    }

    #[test]
    fn test_calculate_delta_v() {
        let isp = 254.6479; // Specific impulse (s)
        let m0 = 1000.0; // Initial mass of the rocket (kg)
        let mf = 100.0; // Final mass of the rocket (kg)
        let expected_delta_v = 5750.114; // Expected delta-v (m/s)

        let delta_v = calculate_delta_v(isp, m0, mf);
        // Ensure the calculated delta-v is as expected
        assert!( (delta_v - expected_delta_v).abs() < ACCEPTABLE_ERROR);
    }
}

Modify your Cargo.toml to pull in the libm dependency. Also, specify the panic = "abort" strategy.

$ cargo add libm
$ cat <<EOF >> Cargo.toml

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"
EOF
# Cargo.toml

[package]
name = "no_std_example"
version = "0.1.0"
edition = "2021"

[dependencies]
libm = "0.2.15"

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

Test the library.

$ cargo test

Using the Library

Next, add a src/main.rs file that uses the library.

// src/main.rs

#![no_std]
#![no_main]

// Change the following line to use your crate name.
use no_std_example as crate_library;
use libc_print;

#[unsafe(no_mangle)]
pub extern "C" fn main(_argc: i32, _argv: *const *const u8) -> i32 {
    // Example values for thrust calculation
    let m_dot = 5.0; // Mass flow rate in kg/s
    let ve = 2500.0; // Exhaust velocity in m/s
    let pe = 101325.0; // Pressure at nozzle exit in Pa
    let p0 = 101325.0; // Ambient pressure in Pa
    let ae = 0.1; // Exit area of the nozzle in m^2

    // Calculating thrust
    let thrust = crate_library::calculate_thrust(m_dot, ve, pe, p0, ae);
    libc_print::libc_println!("Thrust: {} N", thrust);

    // Calculating specific impulse
    let isp = crate_library::calculate_specific_impulse(thrust, m_dot);
    libc_print::libc_println!("Specific Impulse: {} s", isp);

    // Example values for delta-v calculation
    let m0 = 1000.0; // Initial total mass in kg
    let mf = 100.0; // Final mass in kg

    // Calculating delta-v
    let delta_v = crate_library::calculate_delta_v(isp, m0, mf);
    libc_print::libc_println!("Delta-v: {} m/s", delta_v);

    // Return success
    0
}

// Panic handling in module for easy conditional exclusion when testing.
// Testing pulls in std, which provides panic handling.
mod panic_handling {
    use core::panic::PanicInfo;

    #[panic_handler]
    fn panic(_info: &PanicInfo) -> ! {
        loop {}
    }

    // dummy symbol hack for my `aarch64-apple-darwin` host
    #[unsafe(no_mangle)]
    pub extern "C" fn rust_eh_personality() {}
}

We are using libc_print for output in the above binary, so add it as a dependency in Cargo.toml.

$ cargo add libc-print
# Cargo.toml, partial listing
[dependencies]
libc-print = "0.1.23"

Generally speaking, libc can used in conjunction with no_std code to test its functionality on the development host machine. Note that libc is not always available on development targets. Some systems have a proprietary library that implements key functionality generally found in libc, while others have a quirky proprietary libc. In these cases, it is safe to assume that libc related crates cannot be used.

You can run the no_std binary on the host machine with the following command.

cargo run

Assuming you are using a modern Stable toolchain and the panic = "abort" profile is set, the output should look something like this.

Thrust: 12500 N
Specific Impulse: 254.92906 s
Delta-v: 5756.4634 m/s

This concludes the tutorial section of the post.

Debugging and Testing

As illustrated above, standard Rust unit and integration testing can be used to validate no_std code. You might need to simulate or mock the hardware interactions. Continuous integration can also be used to ensure that every change is validated on both host and target platforms.

Real Time Transfer (RTT) is supported by probe-rs and it can be used to log the state of code running on an embedded device. cargo-embed, another probe-rs tool, even supports on device GDB debugging.

Emulators like QEMU can be used to simulate your target hardware environment, which is especially useful for early development stages. Tools like JTAG, SWD, and hardware debuggers can be used to directly interact with applications running on actual hardware.

Moving From std to no_std Rust

Most crates are written assuming std Rust. These crates can simply not be used in a no_std environment. no_std alternatives will need to be identified, and you may need to write your own implemention if your desired functionality is unavailable.

When a program is declard no_std, it automatically pulls in the core crate. There is no way around this. no_std core is Rust’s minimum feature set. Everything that relies on heap allocated memory is located in alloc. Note that you need to supply your own memory allocation strategy when using alloc in a no_std environment, but there are crates like embedded_alloc for this.

The good news is that std effectively reexports everything core and alloc. Therefore, unlike popular third party crates, a lot of the types you are accustomed to using are actually available in a no_std environment, especially if core and alloc are available. There are generally drop in replacements for the types defined in std. The following table broadly illustrates what is reexported by std and from where.

Type Category Located In std Re-export Notes
Primitives (u8, f32, etc.) core std Always available.
Option/Result core std Always available.
Iterators/Markers core std Copy, Send, Sync, Iterator.
String / Vec alloc std Requires a #[global_allocator].
Smart Pointers alloc std Box, Arc, Rc.
BTreeMap/BTreeSet alloc std HashMap is NOT in alloc (needs OS entropy).
Networking/Files std N/A Not available in no_std.

BTreeMap and BTreeSet are the standard functional alternatives to HashMap and HashSet when working in a core and alloc environment. The hashbrown crate is a commonly used no_std HashMap alternative. It is actually the underlying implementation of std::collections::HashMap. Note that you must provide your own non-random hasher, like FxHash because an OS to provide random seeds is unavailable.

Nightly Rust

In the past, nightly Rust was required to build no_std binaries. It is still required for tier 3 targets where the -Z build-std flag needs to be used to build core and alloc from scratch for your chip.

Outside of this toy example, you might need to do other things that require nightly Rust. For example, it was required in the past because a proper custom implemention of eh_personality needed to be supplied, as opposed to the symbol hack I used in this post. You might need to use other unstable features, or advanced asm! feature..

The following commands can be used to install the nightly toolchain for the host system and switch to it.

$ rustup toolchain install nightly
$ rustup default nightly

The following command can be used to switch back to stable.

$ rustup default stable

Next Steps

The Embedded Rust Book is an excellent resource for those looking to deepen their understanding of embedded systems programming with Rust. It includes a variety of examples and extensive discussions on interfacing with hardware. Additionally, this book provides a list of useful resources that can help further your learning.

For those eager to purchase hardware to learn on, it’s wise to identify a learning resource and specifically purchase the hardware it recommends. For example, the Discovery Book is highly recommended for beginners. It provides step-by-step instructions and ready-to-run examples that are extremely helpful when you are just starting out. These examples are tailored for specific development boards, ensuring that you can follow along without compatibility issues given the right hardware. Alternatively, the embedded Rust setup explained YouTube video is a good option for those who prefer video tutorials.

Additional Tools and Libraries

The embedded Rust ecosystem is enriched by a variety of libraries that enhance functionality and ease development across different hardware platforms. Below is an overview of some essential libraries, listed alphabetically:

  • cargo-embed: Facilitates flashing and debugging of embedded applications, leveraging probe-rs for its backend, and includes support for RTT (Real-Time Transfer).

  • drone-os: An embedded operating system written in Rust that runs on ARM Cortex-M processors, suitable for high-performance real-time applications.

  • Embassy: An asynchronous runtime for embedded systems that aims to make using async/await feasible in no_std environments, with support for a variety of embedded platforms.

  • embedded-hal: Provides hardware abstraction layers for interfacing with peripherals across different microcontrollers, facilitating portable and reusable code.

  • heapless: Enables the use of static memory allocation for data structures like vectors, strings, and hash maps, crucial for systems where dynamic memory allocation cannot be used.

  • probe-rs: A modern debugging and flashing tool that supports a wide range of ARM Cortex-M microcontrollers.

  • RTIC (Real-Time Interrupt-driven Concurrency): Offers a concurrency framework for building real-time systems in Rust, which is ideal for applications needing high reliability and performance.

  • rubble: A Bluetooth stack implemented in pure Rust, designed for embedded systems using Bluetooth Low Energy.

  • svd2rust: Generates Rust API from SVD (System View Description) files, allowing for safe, ergonomic interaction with peripheral registers.

Conclusion

no_std Rust programming opens up a new realm of possibilities for Rust developers in the embedded world. By understanding the basics and progressively exploring more complex scenarios, you can leverage Rust’s safety and performance on platforms where the standard library is not an option.

References