目录
- 嵌入式 Rust 的定位
- no_std 环境搭建
- embedded-hal 硬件抽象
- ESP32 与 STM32 实战
- RTOS 与异步嵌入式
- FFI 基础:extern C
- C 调用 Rust
- Rust 调用 C:bindgen
- FFI 安全边界
- 速查表与最佳实践
1. 嵌入式 Rust 的定位
嵌入式是 Rust 的「原生战场」:no_std 环境无运行时、无 GC、无堆,所有权模型天然适合资源受限设备。
| 优势 | 说明 |
|---|---|
| 内存安全 | 编译期消除空指针/越界/数据竞争 |
| 零成本抽象 | 抽象不引入运行时开销 |
| 无运行时 | 可跑在 4KB RAM 的 MCU 上 |
| 生态成熟 | embedded-hal 统一硬件抽象 |
主流方案:
| 方案 | 目标 | 特点 |
|---|---|---|
| ESP32(Rust) | 乐鑫芯片 | 官方支持,wifi/bt 生态好 |
| STM32(Rust) | ST 芯片 | HAL 齐全,社区活跃 |
| RP2040(Raspberry Pi) | 树莓派 Pico | 便宜,适合入门 |
| nRF52 | Nordic BLE | 低功耗蓝牙首选 |
2. no_std 环境搭建
#![no_std] // 不用标准库
#![no_main] // 无 main(嵌入式入口由硬件启动代码决定)
Cargo.toml:
[package]
name = "my-firmware"
version = "0.1.0"
[profile.release]
debug = 1
opt-level = "s"
overflow-checks = true
[dependencies]
cortex-m-rt = "0.7"
cortex-m-semihosting = "0.5"
panic-halt = "0.5"
入口与中断:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _; // panic 时停机
#[entry]
fn main() -> ! {
// 无限主循环
loop {
// 业务逻辑
}
}
no_std 与 std 的差异:无
String/Vec(可用alloc手动接堆)、无文件系统、无网络(需外设)。
3. embedded-hal 硬件抽象
embedded-hal 定义统一的 GPIO、SPI、I2C、UART、PWM 等 trait,让驱动可跨芯片复用。
use embedded_hal::digital::OutputPin;
use embedded_hal::spi::SpiBus;
use embedded_hal::i2c::I2c;
// 任何实现了 OutputPin 的芯片都能跑这段代码
fn blink<LED>(mut led: LED) where LED: OutputPin {
led.set_high().ok();
// 延时
led.set_low().ok();
}
// 通用 I2C 读取温度传感器
fn read_temp<I>(i2c: &mut I) -> Result<u16, ()>
where I: I2c {
let mut buf = [0u8; 2];
i2c.write_read(0x48, &[0x00], &mut buf).map_err(|_| ())?;
Ok(u16::from_be_bytes([buf[0], buf[1]]))
}
HAL 生态分层:
| 层 | 例子 |
|---|---|
| 芯片访问 | stm32f4xx_hal / esp32_hal |
| 通用驱动 | embedded-hal trait |
| 具体外设驱动 | ssd1306(OLED)、bme280(传感器) |
4. ESP32 与 STM32 实战
4.1 ESP32:点亮 LED + 连接 WiFi
#![no_std]
#![no_main]
use esp_backtrace as _;
use esp_hal::{clock::ClockControl, peripherals::Peripherals, prelude::*, timer::TimerGroup};
use esp_wifi::{initialize, wifi::WifiAp};
#[entry]
fn main() -> ! {
let peripherals = Peripherals::take();
let system = peripherals.SYSTEM;
let clocks = ClockControl::boot_defaults().freeze();
// GPIO2 配置为输出
let mut led = esp_hal::gpio::IO::new(peripherals.GPIO, peripherals.IO_MUX)
.pins
.gpio2
.into_push_pull_output();
loop {
led.toggle();
esp_hal::time::delay(500u32.millis());
}
}
4.2 STM32:串口输出
use stm32f4xx_hal::{pac, prelude::*, serial::Serial};
fn main() {
let dp = pac::Peripherals::take().unwrap();
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze();
let mut gpioa = dp.GPIOA.split();
let tx = gpioa.pa2.into_alternate();
let rx = gpioa.pa3.into_alternate();
let serial = Serial::new(dp.USART2, (tx, rx), 115200.bps(), clocks);
let (mut tx, _rx) = serial.split();
loop {
tx.write_str("hello from STM32\r\n").ok();
delay();
}
}
5. RTOS 与异步嵌入式
5.1 RTOS 集成
| 方案 | 说明 |
|---|---|
embassy | Rust 原生异步嵌入式,无 RTOS |
RTIC | 实时中断驱动框架 |
FreeRTOS | 通过 FFI 使用 C RTOS |
5.2 Embassy 异步
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
loop {
// 异步定时任务,不阻塞
Timer::after(Duration::from_millis(1000)).await;
// 周期执行逻辑
}
}
// 多个并发任务
#[embassy_executor::task]
async fn blink_task() {
loop {
// LED 闪烁
Timer::after(Duration::from_millis(500)).await;
}
}
异步 vs RTOS:Embassy 用协作式调度,任务数量无上限、零内核开销,正在成为 Rust 嵌入式异步的标准。
6. FFI 基础:extern C
FFI(Foreign Function Interface)让 Rust 与 C 互调。基础是 extern "C" 块 + #[no_mangle]。
// 调用 C 库函数
extern "C" {
fn strlen(s: *const u8) -> usize;
fn malloc(size: usize) -> *mut u8;
}
fn c_len(s: &str) -> usize {
unsafe { strlen(s.as_ptr()) }
}
C ABI 内存布局:
#[repr(C)] // 保证字段布局与 C 一致
struct CPoint {
x: f64,
y: f64,
}
#[repr(C, u8)] // 带 tag 的枚举(类似 C union + tag)
enum CEvent {
Mouse(u32, u32),
Key(u8),
}
警告:普通 Rust enum 布局不定,FFI 必须用
#[repr(C)]或#[repr(C, u8)]。
7. C 调用 Rust
把 Rust 函数导出给 C 用:#[no_mangle] + extern "C" + 手动管理不安全接口。
// lib.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
// 导出内存操作(供 C 分配/释放)
#[no_mangle]
pub extern "C" fn my_alloc(size: usize) -> *mut u8 {
let mut v = Vec::with_capacity(size);
let ptr = v.as_mut_ptr();
std::mem::forget(v); // 交给 C 管理
ptr
}
#[no_mangle]
pub extern "C" fn my_free(ptr: *mut u8) {
unsafe {
let v = Vec::from_raw_parts(ptr, 0, 0); // 简化示例
drop(v);
}
}
C 侧调用:
// main.c
#include <stdio.h>
extern int add(int, int);
int main() {
printf("%d\n", add(2, 3));
return 0;
}
# 编译链接
rustc --crate-type=cdylib src/lib.rs -o librustlib.so
gcc main.c -L. -lrustlib -o app
8. Rust 调用 C:bindgen
用 bindgen 从 C 头文件自动生成 Rust 绑定。
# 安装
cargo install bindgen-cli
# 生成绑定
bindgen include/my_lib.h -o src/bindings.rs \
--allowlist-function "my_.*" \
--allowlist-type "MyStruct"
// my_lib.h
typedef struct {
int width;
int height;
} MyRect;
int my_area(const MyRect *rect);
void my_fill(MyRect *rect, int w, int h);
// 生成的 bindings.rs
#[repr(C)]
pub struct MyRect {
pub width: ::std::os::raw::c_int,
pub height: ::std::os::raw::c_int,
}
extern "C" {
pub fn my_area(rect: *const MyRect) -> ::std::os::raw::c_int;
pub fn my_fill(rect: *mut MyRect, w: ::std::os::raw::c_int, h: ::std::os::raw::c_int);
}
安全封装(调用方只看到 safe API):
mod cbindings; // 生成的绑定
pub struct Rect { width: i32, height: i32 }
impl Rect {
pub fn new(w: i32, h: i32) -> Self { Self { width: w, height: h } }
pub fn area(&self) -> i32 {
let r = cbindings::MyRect { width: self.width, height: self.height };
unsafe { cbindings::my_area(&r) }
}
}
跨语言场景:调用 C 库(OpenSSL、zlib、SQLite C API)、Python(PyO3)、Node(napi-rs)。
9. FFI 安全边界
FFI 是 unsafe 的最高危区域,必须遵守严格边界。
| 风险 | 规避 |
|---|---|
| 空指针 | 传入前检查 is_null() |
| 悬垂指针 | 生命周期由 C 管理时 mem::forget 防提前 drop |
| 数据竞争 | C 多线程访问 Rust 数据要加锁或原子 |
| 内存泄漏 | 配对 alloc/free 必须对称 |
| UB(未定义行为) | 不把 &mut 传给可能改数据的 C 函数 |
// 安全封装示例:确保指针生命周期
pub struct CStringGuard(*mut c_char);
impl CStringGuard {
pub fn from_raw(ptr: *mut c_char) -> Option<Self> {
(!ptr.is_null()).then(|| Self(ptr))
}
pub fn as_str(&self) -> &str {
unsafe { std::ffi::CStr::from_ptr(self.0) }
.to_str()
.unwrap_or("")
}
}
impl Drop for CStringGuard {
fn drop(&mut self) {
unsafe { free(self.0) } // 配套 C 的 free
}
}
FFI 设计原则:
- unsafe 代码最小化:只在薄薄一层 FFI 边界用 unsafe。
- 边界内做安全封装,暴露 safe API。
- 文档写明:谁拥有内存、谁负责释放、是否线程安全。
- 用
cargo geiger统计 unsafe 覆盖,保持受控。
10. 速查表与最佳实践
| 任务 | 工具 |
|---|---|
| no_std 项目 | #![no_std] + cortex-m-rt |
| 硬件抽象 | embedded-hal trait |
| ESP32 | esp-hal + esp-idf-hal |
| STM32 | stm32f4xx-hal |
| 异步嵌入式 | embassy |
| C 绑定生成 | bindgen |
| Rust 导出给 C | #[no_mangle] + extern "C" |
| Python 互操作 | pyo3 |
| Node 互操作 | napi-rs |
| unsafe 审计 | cargo geiger |
最佳实践清单:
- 嵌入式:优先
embassy异步,少写手写状态机。 - FFI:所有跨边界结构用
#[repr(C)]。 - 指针:谁分配谁释放,用 RAII 封装。
- 测试:FFI 层做集成测试,安全封装做单元测试。
- 文档:
# Safety段写清每个 unsafe 调用的前置条件。
一句话记忆:嵌入式 = no_std + embedded-hal + 异步运行时;FFI = extern “C” + repr(C) + 最小 unsafe 边界 + RAII 封装,把危险关进笼子里。
延伸阅读
- https://plumephp.com/rust-systems-programming/ — 系统级编程与性能
- https://plumephp.com/rust-desktop-wasm/ — WASM 与跨平台
- https://plumephp.com/rust-cli-development/ — 命令行工具(也常需 FFI)
- https://plumephp.com/posts/os/ — 操作系统与裸机编程
- https://plumephp.com/posts/cpp/ — C++ 与 Rust 互操作对比
- [[ai]] — Rust AI 推理中的 FFI 场景
从 MCU 上 4KB RAM 的 LED 闪烁,到通过 PyO3 把 Rust 库接入 Python 生态——嵌入式与 FFI 是把 Rust 的优势延伸到「更底层」和「更互联」两个方向的必经之路。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。