初探 STM32 嵌入式 Rust
这段时间在实习和个人项目中学习 STM32 上嵌入式 Rust 的一些总结
资料
- The Book: 熟悉 Rust 语法,其中并发部分的 channel 和 Mutex, Cell, RefCell 等,在嵌入式中有类似的用法
- Discovery: 写博客时发现这本书有了使用 micro:bit 的新版, 我读的是使用 STM32F3Discovery 的旧版
- The Embedded Rust Book: 有嵌入式开发经验可以跳过 discovery 直接看这本。我买了 F3 板子所以直接跑在板子上没有用 QEMU
- RTIC: 一个裸机多任务框架,更好地共享 Rust 变量。我看的时候是 v0.5,现在已经出到 1.0 了,可喜可贺。
- ferrous-systems’s blog: 这家公司的博客介绍了许多 Rust 在嵌入式开发中的技巧,获益匪浅。尤其是实现了一个 async/await executor
环境搭建
Rust 工具链
这里使用 rustup 安装了 msvc 工具链的 rust
按需下载对应平台的 core,否则无法编译:
|
|
调试工具
我使用包管理器 scoop, 以下工具scoop install
即可
- arm-none-eabi-gdb
- openocd
此外,如果使用 ST-LINK,需要手动安装驱动
这篇文章末尾提供了使用 vscode 调试的方法,我觉得 gdb 命令行就够用了,因此没有尝试
Hello World
仅限 cortex-m 内核,其他平台未研究
|
|
- 修改 .cargo/config.toml
- 修改 memory.x 一般 STM32 FLASH 起始地址在 0x08000000, RAM 在 0x20000000. 芯片手册里如果有多段 RAM,取第一段的大小
- 修改 openocd.cfg,可用的配置文件可以在 openocd 的安装目录下找到
- 在项目目录下执行
openocd
, 不要关闭这个终端 - 另起一个终端,执行
cargo run
, 观察是否收到输出
PAC & HAL
PAC 一般由 svd2rust 根据 ARM 厂商提供的 SVD 文件自动生成,提供了寄存器操作的基本包装,API 用法如:
|
|
HAL 在 PAC 基础上遵循 embedded-hal 编写。但至少 STM32 各系列的实现略有区别,导致同样的外设在 f1, f4 系列上的代码可能大不一样,各芯片的 driver crate 也不一定都能使用。部分外设如 FSMC,因为 hal 没有编写相关部分,几乎只能靠手动配置寄存器并引入 unsafe 块才能使用
起手式 (裸机)
|
|
safe 全局变量
我们都知道操作 static mut
是 unsafe 的,大量的编程规范要求尽量减少使用全局变量。但嵌入式环境下往往无法避免。一个更好的方法是规划好变量的作用范围后使用 RTIC. 不过这里先讲一下简单的做法和原理
-
Atomic
是平台支持下最优雅的做法,简洁,安全。缺点是不能进行有复杂逻辑的操作,且仅支持几种基本数据类型,不过一般需求下够用了。 -
Mutex<RefCell<T>>
配合临界区使用。需要use cortex_m::interrupt::{self, Mutex};
. 可以包装更复杂的数据类型,包括外设。但会引入大量语法噪音。使用方法大致为:- 声明一个
static FOO:Mutex<RefCell<Option<T>>> = Mutex::new(RefCell::new(None));
- 在 main 中初始化外设并在临界区中使用
interrupt::free(|cs| FOO.borrow(cs).replace(Some(T)));
移动所有权给全局变量FOO
- 使用时同样需要进入临界区后,使用
FOO.borrow(cs).borrow()
获取RefCell
后再as_ref()
才能得到内部的 T
我几乎从不使用这种方法,心累手也累,复杂项目直接上 RTIC 完事儿
- 声明一个
这部分相当让人抓狂,许多在 C 语言中可以直接写的部分要包上好多层,即便我知道它是安全的。
RTIC
我很想管 RTIC 叫抢占式调度框架,如果搭配内存分配器,用起来和抢占式的 RTOS 没啥区别。然而它其实只是个前后台系统,靠设置中断优先级来管理任务,并不具有上下文切换的能力。用它的原因就在于它包装了上述复杂的 Mutex<RefCell<Option<T>>>
, 并可以将空闲的硬中断注册为可以有参数和容量的多个软中断。
然而,缺点来自于它使用了大量的宏,导致无论 RLS 还是 RA 都不能很好的支持自动补全和类型。有些报错会一直显示却不影响正常编译… 真正编不过时又找不到报错的原因。因此我经常先在裸机上搭好一些外设驱动框架,调试好类型后再复制进 RTIC 项目。
内存分配器
alloc-cortex-m: 需要 nightly 工具链,用法
使用内存分配器后,可以像有 std 一样使用方便地使用 vec!
等。可以先通过 FSMC 配置好外部 RAM 后将其内存起始地址指向 RAM
RTOS
我尝试过drone, WSL 和 Linux 物理机都试过,然而连 hello world 都没能跑起来… 它似乎也没在更新了
另外还有Tock OS, 但外设驱动需要自行编写,官方的 ST demo 只有 f3disco 和两个 f4nucleo,并没有尝试过
RTOS 方面估计很难超越 μCOS 和 FreeRTOS,大量芯片驱动都有现成的 C 代码,Rust 这边还只能用爱发电. OS 能否和现有的 hal 框架兼容还是个问题
常用 crate
- heapless: 提供了静态内存分配的常用数据类型
- HistoryBuffer: 可用于平滑滤波
- spsc::Queue: 消息队列
- String: 方便输出调试信息
- bitbang-hal: 提供了软件模拟的 I2C, USART, SPI
- nb: 虽然名字叫做 non-block 但更多用 block!宏来等待外设工作完成,例如:
1
block!(Serial.write(byte))?;
- micromath: 提供了嵌入式环境下可能缺失的某些 F32 操作
总结
个人项目的话,Embedded Rust 只能说差强人意。小芯片 debug 编译二进制太大,没法调试。语法噪音也是相当烦人。我利用几个芯片的 crate 写了个平衡车玩,过段时间会专门介绍
实习中用 rust 写了一些小板子的验证 demo,逻辑简单的话用 hal 分分钟就能起个项目。也用 RTIC 尝试过复杂项目,有这么几个问题:
- 本质仍然是前后台,复杂任务调度比较烧脑
- 运行速度比 μCOSⅢ 慢不少,写了个简单的串口环回,能慢将近一半。或许是我时钟没配好…也可能人家商业公司在关中断这块儿确实优化的好。尝试了两天,还是改用 μCOS 了,因为即便我这个实习生写出来也以后没人接手维护…
Rust 合理的 trait 抽象加上 embedded-hal 这套统一的规范,使得各种库的编写成为可能。我的平衡车项目几乎纯靠调库就能完成,可见这套抽象的威力。这也是 STM32 的标准库和 HAL 库 之所以火遍中国。然而现实很骨感,一是性能损失,这种抽象多少会带来一些冗余代码,当然 LLVM 能不能优化我就不懂了; 二是虽然有 embedded-hal 规定了一些 trait,但 trait 之外的部分各不相同,就比如 ST 系列,FSMC 几乎没有,F1XXHAL 和 F4XXHAL 许多 api 完全不同… 最后一点纯属猜测,C++ 也有虚函数,也完全可以定义一套接口规范,为何 ARM 厂商不用 C++ 呢?虚表实现有性能损失,那编译器也可以用其他实现呀,反正芯片厂的编译器都是魔改过的…这么多年 C++ 都没在嵌入式铺开,Rust 只能说悬
Rust 是个好语言,但在嵌入式方面生态似乎是更主要的问题。没有解决大痛点的话业界根本没有动力抛弃多年积累重新开始。