Getting Started with no_std Rust Programming
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:
stdassumes 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-rsfor 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
- NASA, Ideal Rocket Equation
- NASA, Rocket Specific Impulse Equation
- NASA, Rocket Thrust Equation
- OpenOCD, Debugging
- QEMU, Guide
- Rust, Cargo Book
- Rust, cargo-embed on crates.io
- Rust, Continuous Integration
- Rust, Discovery Book
- Rust, drone-os GitHub Repository
- Rust, Embassy GitHub Repository
- Rust, embedded-hal Repository
- Rust, Embedded Rust Setup Explained YouTube Video
- Rust, Embedonomicon
- Rust, GDB Debugging with cargo-embed
- Rust, heapless on crates.io
- Rust, Home Page
- Rust, probe-rs GitHub Repository
- Rust, probe-rs: A Modern, High-Performance Debugging Toolkit
- Rust, Real-Time Transfer (RTT) with cargo-embed
- Rust, rubble GitHub Repository
- Rust, Rust Book - Testing
- Rust, Rust by Example - Integration Testing
- Rust, Rust Embedded Book
- Rust, Rust Programming Language
- Rust, RTIC GitHub Repository
- Rust, rustup Book
- Rust, svd2rust on crates.io