Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

no_std — Rust Without the Standard Library
no_std:不依赖标准库的 Rust

What you’ll learn: How to write Rust for bare-metal and embedded targets using #![no_std], how core and alloc split responsibilities, what panic handlers do, and how all this compares to embedded C without libc.
本章将学到什么: 如何用 #![no_std] 为裸机和嵌入式目标编写 Rust,core 与 alloc 分别负责什么,panic handler 是干什么的,以及这套模式和不依赖 libc 的嵌入式 C 有什么对应关系。

If the background is embedded C, working without libc or with a极小运行时本来就不陌生。Rust 也有一等公民级别的对应机制,那就是 #![no_std]。
如果本来就在写嵌入式 C,那么“不带 libc”或者“只带很小一层 runtime”这件事一点都不新鲜。Rust 对这类场景也有一套正统支持,就是 #![no_std]。

What is no_std?
no_std 到底是什么

When #![no_std] is added to the crate root, the compiler removes the implicit extern crate std; and links only against core,必要时再额外接上 alloc。
只要在 crate 根部加上 #![no_std],编译器就不会再偷偷帮忙引入 std,而是只链接 core,如果环境允许堆分配,再自行接上 alloc。

Layer
层级
What it provides
提供什么
Requires OS / heap?
需要操作系统或堆吗?
corePrimitive types, Option, Result, Iterator, math, slice, str, atomics, fmt
基础类型、Option、Result、Iterator、数学、切片、字符串切片、原子类型、格式化基础设施
No
不需要,裸机也能跑
allocVec, String, Box, Rc, Arc, BTreeMap
Vec、String、Box、Rc、Arc、BTreeMap
Needs allocator, but no OS
需要全局分配器,但不一定需要操作系统
stdHashMap, fs, net, thread, io, env, process
HashMap、文件系统、网络、线程、I/O、环境变量、进程控制
Yes
通常需要操作系统支持

Rule of thumb for embedded developers: if the C project links against -lc and uses malloc, then core + alloc is often可行;如果是纯裸机而且连 malloc 都没有,那就老老实实只用 core。
给嵌入式开发者的简单经验: 如果 C 项目会链接 -lc,还会用 malloc,那么很多时候 core + alloc 就够了;如果是纯裸机,连 malloc 都没有,那就尽量只用 core。

Declaring no_std
如何声明 no_std

#![allow(unused)]
fn main() {
// src/lib.rs  (or src/main.rs for a binary with #![no_main])
#![no_std]

// You still get everything in `core`
use core::fmt;
use core::result::Result;
use core::option::Option;

// If an allocator exists, opt in to heap-backed types
extern crate alloc;
use alloc::vec::Vec;
use alloc::string::String;
}

For bare-metal binaries, #![no_main] and a panic handler are usually needed too:
如果是裸机二进制,通常还得配上 #![no_main] 和 panic handler:

#![allow(unused)]
#![no_std]
#![no_main]

fn main() {
use core::panic::PanicInfo;

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

// Entry point depends on the HAL and linker script
}

What you lose and what replaces it
失去什么,以及拿什么替代

std featureno_std alternative
替代方案
println!core::write! to UART, or defmt
往 UART 写,或者用 defmt
HashMapheapless::FnvIndexMap or BTreeMap with alloc
heapless::FnvIndexMap,或者带 alloc 的 BTreeMap
Vecheapless::Vec
固定容量的 heapless::Vec
Stringheapless::String or &str
std::io::Read/Writeembedded_io::Read/Write
thread::spawnInterrupt handlers, RTIC tasks
中断处理或 RTIC 任务
std::timeHardware timer peripherals
硬件定时器外设
std::fsFlash / EEPROM drivers
Flash / EEPROM 驱动

Notable no_std crates for embedded
嵌入式里常见的 no_std crate

CratePurpose
用途
Notes
说明
heaplessFixed-capacity Vec, String, Queue, MapNo allocator needed — all stack or static storage
不需要分配器,适合固定容量场景
defmtEfficient embedded loggingDeferred formatting on host side
格式化推迟到主机端做,更省目标端资源
embedded-halHAL traits for SPI / I2C / GPIO / UARTWrite once, adapt to many MCUs
抽象一次,可适配多种 MCU
cortex-mARM Cortex-M low-level supportSimilar in spirit to CMSIS
cortex-m-rtRuntime and startup for Cortex-MReplaces handwritten startup code
rticReal-time interrupt-driven concurrencyCompile-time scheduled tasks
embassyAsync executor for embeddedBring async/await to bare metal
postcardno_std binary serializationUseful where serde_json is too heavy
thiserrorError derive macrosSince v2, works in no_std nicely
smoltcpno_std TCP/IP stackNetworking without a full OS

C vs Rust: bare-metal comparison
C 与 Rust 的裸机场景对比

A typical embedded C blinky:
一个典型的嵌入式 C 闪灯程序:

// C — bare metal, vendor HAL
#include "stm32f4xx_hal.h"

void SysTick_Handler(void) {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}

int main(void) {
    HAL_Init();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitTypeDef gpio = { .Pin = GPIO_PIN_5, .Mode = GPIO_MODE_OUTPUT_PP };
    HAL_GPIO_Init(GPIOA, &gpio);
    HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / 1000);
    while (1) {}
}

The Rust equivalent:
对应的 Rust 写法:

#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{pac, prelude::*};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    let gpioa = dp.GPIOA.split();
    let mut led = gpioa.pa5.into_push_pull_output();

    let rcc = dp.RCC.constrain();
    let clocks = rcc.cfgr.freeze();
    let mut delay = dp.TIM2.delay_ms(&clocks);

    loop {
        led.toggle();
        delay.delay_ms(500u32);
    }
}

Key differences for C developers:
对 C 开发者来说,几个关键差别是:

  • Peripherals::take() returns Option, which enforces the singleton pattern at compile time.
    Peripherals::take() 返回 Option,把“外设只能初始化一次”这件事收进了编译期约束里。
  • .split() transfers ownership of individual pins so two modules cannot accidentally drive the same pin.
    .split() 会把各个引脚的所有权拆开,避免两个模块同时控制同一根引脚。
  • Register access is type-checked, so写只读寄存器这种蠢事更难发生。
    寄存器访问是带类型检查的,写只读寄存器这类错误更不容易发生。
  • With frameworks such as RTIC, the borrow checker also helps prevent races between main and interrupt handlers.
    配合 RTIC 这类框架时,借用检查器还能顺手帮忙防住 main 和中断处理之间的数据竞争。

When to use no_std vs std
什么时候该用 no_std,什么时候该用 std

flowchart TD
    A["Does your target have an OS?<br/>目标环境有操作系统吗?"] -->|Yes<br/>有| B["Use std<br/>使用 std"]
    A -->|No<br/>没有| C["Do you have a heap allocator?<br/>有堆分配器吗?"]
    C -->|Yes<br/>有| D["Use #![no_std] + extern crate alloc"]
    C -->|No<br/>没有| E["Use #![no_std] with core only"]
    B --> F["Full Vec, HashMap, threads, fs, net<br/>完整容器、线程、文件系统、网络"]
    D --> G["Vec, String, Box, BTreeMap<br/>but no fs/net/threads"]
    E --> H["Fixed-size arrays, heapless collections<br/>no allocation"]

Exercise: no_std ring buffer
练习:no_std 环形缓冲区

🔴 Challenge — combines generics, MaybeUninit, and #[cfg(test)] in a no_std setting.
🔴 挑战题:在 no_std 环境下,把泛型、MaybeUninit 和 #[cfg(test)] 一起用起来。

In embedded systems, a fixed-size ring buffer is a very common building block. It never allocates, capacity is known in advance, and behavior under full load is explicit.
在嵌入式系统里,固定容量的环形缓冲区就是标准零件之一。它不分配内存,容量预先确定,写满时会怎么处理也完全可控。

Requirements:
要求:

  • Generic over T: Copy
    元素类型是 T: Copy
  • Fixed capacity N via const generics
    容量 N 用 const generics 表示
  • push(&mut self, item: T) overwrites the oldest element when full
    push(&mut self, item: T) 在满了时覆盖最旧元素
  • pop(&mut self) -> Option<T> returns the oldest element
    pop(&mut self) -> Option<T> 返回最旧元素
  • len(&self) -> usize
    提供 len(&self) -> usize
  • is_empty(&self) -> bool
    提供 is_empty(&self) -> bool
  • Must compile with #![no_std]
    必须能在 #![no_std] 下编译
#![allow(unused)]
#![no_std]

fn main() {
use core::mem::MaybeUninit;

pub struct RingBuffer<T: Copy, const N: usize> {
    buf: [MaybeUninit<T>; N],
    head: usize,
    tail: usize,
    count: usize,
}

impl<T: Copy, const N: usize> RingBuffer<T, N> {
    pub const fn new() -> Self {
        todo!()
    }
    pub fn push(&mut self, item: T) {
        todo!()
    }
    pub fn pop(&mut self) -> Option<T> {
        todo!()
    }
    pub fn len(&self) -> usize {
        todo!()
    }
    pub fn is_empty(&self) -> bool {
        todo!()
    }
}
}
Solution 参考答案
#![allow(unused)]
#![no_std]

fn main() {
use core::mem::MaybeUninit;

pub struct RingBuffer<T: Copy, const N: usize> {
    buf: [MaybeUninit<T>; N],
    head: usize,
    tail: usize,
    count: usize,
}

impl<T: Copy, const N: usize> RingBuffer<T, N> {
    pub const fn new() -> Self {
        Self {
            // SAFETY: MaybeUninit does not require initialization
            buf: unsafe { MaybeUninit::uninit().assume_init() },
            head: 0,
            tail: 0,
            count: 0,
        }
    }

    pub fn push(&mut self, item: T) {
        self.buf[self.head] = MaybeUninit::new(item);
        self.head = (self.head + 1) % N;
        if self.count == N {
            self.tail = (self.tail + 1) % N;
        } else {
            self.count += 1;
        }
    }

    pub fn pop(&mut self) -> Option<T> {
        if self.count == 0 {
            return None;
        }
        let item = unsafe { self.buf[self.tail].assume_init() };
        self.tail = (self.tail + 1) % N;
        self.count -= 1;
        Some(item)
    }

    pub fn len(&self) -> usize {
        self.count
    }

    pub fn is_empty(&self) -> bool {
        self.count == 0
    }
}

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

    #[test]
    fn basic_push_pop() {
        let mut rb = RingBuffer::<u32, 4>::new();
        assert!(rb.is_empty());

        rb.push(10);
        rb.push(20);
        rb.push(30);
        assert_eq!(rb.len(), 3);

        assert_eq!(rb.pop(), Some(10));
        assert_eq!(rb.pop(), Some(20));
        assert_eq!(rb.pop(), Some(30));
        assert_eq!(rb.pop(), None);
    }

    #[test]
    fn overwrite_on_full() {
        let mut rb = RingBuffer::<u8, 3>::new();
        rb.push(1);
        rb.push(2);
        rb.push(3);

        rb.push(4);
        assert_eq!(rb.len(), 3);
        assert_eq!(rb.pop(), Some(2));
        assert_eq!(rb.pop(), Some(3));
        assert_eq!(rb.pop(), Some(4));
        assert_eq!(rb.pop(), None);
    }
}
}

Why this matters for embedded C developers:
这道题对嵌入式 C 开发者有价值的地方在于:

  • MaybeUninit is Rust’s way to represent uninitialized memory explicitly.
    MaybeUninit 是 Rust 里显式表达“这块内存还没初始化”的正规方式。
  • The unsafe scope is tiny and each use can be单独解释清楚。
    unsafe 范围很小,而且每一处都能给出明确理由。
  • const fn new() means the buffer can be created in static storage without runtime construction.
    const fn new() 说明这个缓冲区可以直接放进 static,不需要运行时构造。
  • Even though the code is no_std, tests can still run on the host with cargo test.
    虽然代码本身是 no_std,但测试照样可以在主机上通过 cargo test 执行。