Rust 为什么适合嵌入式开发
Rust 作为一门年轻的语言,聚焦与安全、并发、高性能等特点,号称能替代 C/C++,那么究竟有多少优点能值得我们来切换呢?本文将告诉你为什么 Rust 适合嵌入式开发。
工具链
- 轻松搭建各种不同类型的芯片编译环境,使用 cargo、rustup 等命令快速搭建新的环境。
- 统一的编译、调试工具、下载工具, 如cargo build,cargo falsh,probe-rs等
- 快速生成代码文档,cargo doc直接生成网页文档,能让新手快速了解整个项目的模块、接口框架。
- 内置代码格式化工具 fmt,轻松就各个代码文件统一格式,标准规范,团队作战无需在吐槽队友的代码风格,cargo fmt 后格式都会统一好,新手无需学习新公司的编码格式规范,公司也无需过多培训编码格式规范。
库的集成
- 移植优势
可移植性移植是嵌入式开发的一个大问题,每个工作可能或多或少会考虑一些模块的可移植性,想进来在不同框架不同平台上能够共有这些模块,避免重复造轮子。Rust 则提供了高效的方法来保证库能够轻松移植,避免库接口域业务接口杂糅到一起。trait 等特性让 Rust 的库能在不同的 CPU 如 ARM 或 RISV-V 甚至操作系统上方便得使用,无需过多关注库的文件数量、无需手动添加库的每个文件,仅仅只需在toml文件中添加库的名字、版本、开启所需的featues即可。在 Rust 中你可以轻松得将各种 IIC 的传感器库添加到自己的工程,很少花时间在适配上。
[dependencies]
panic-halt = "0.2.0"
ufmt = "0.2.0"
nb = "1.1.0"
embedded-hal = "1.0"
pwm-pca9685 = "1.0.0"
infrared = "0.14.1"
embedded-storage = "0.2"
[dependencies.embedded-hal-v0]
version = "0.2.3"
package = "embedded-hal"
- Rust 官方发布了许多标准的库,基于这些库能简化开发、指导用户完成统一的适配接口。Rust 社区也非常活跃,发布了大量的开源库。
As part of the Rust open source project, support for embedded systems is driven by a best-in-class open source community with support from commercial partners.
调试
Rust 生成的固件能使用 openocd 来轻松 gdb 调试,与 C/C++ 完全一样,单步、断电、查看等操作都支持。无需担心调试障碍。
(gdb) break main
Breakpoint 1 at 0x8000d18: file examples/hello.rs, line 15.
(gdb) continue
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 1, main () at examples/hello.rs:15
15 let mut stdout = hio::hstdout().unwrap();
语言优势
- 内存安全优势。
C/C++嵌入式工程师肯定知道,经常在编码完成后,烧录程序到芯片测试运行,经常会出现莫名的内存泄露、异常退出甚至死机的现象,这种内存问题有时非常难怕查,也许编码十分钟调试两小时。当然目前也有一些先进的工具用来辅助调试,如 ASan,Valgrind、Memcheck 等工具,但这些工具本身就需要复杂的调试手段,需要仔细查看日志才能得出结果,但是对于某些资源受限嵌入式设备,很难使用这些工具来辅助排查。对于新手来说学习这些工具的使用就让人头疼。而 Rust 天生保证内存安全,没有丰富的 Rust 经验很难写出能让内存异常的代码,是的你没听错,Rust 对于新手保护特别友好,需要有经验的人才能故意写出不安全的代码。Rust 的生命周期的约束使得实现内存安全而且零成本,也就是无需在时间和空间上浪费资源。
- 语法优势:语法中新的枚举,闭包、异步、流控、变量生命周期控制、安全宏等,基于这些基础语法能最方便、便捷的表达问题的逻辑,无需使用太多的技巧。让编程更加简洁和优雅。
Rust 作为强类型的语言,但是在使用时无需过多指定变量类型,Rust 编译器会自动推导变量的类型,并基于生命周期的约束可以重复使用变量名,原理上保证使用这些语法就像 C 语言一样安全,但是用起来像 Python 一样方便。
- 线程安全,无畏并发
Rust makes it impossible to accidentally share state between threads. Use any concurrency approach you like and you’ll still get Rust’s strong guarantees
一般来说 Rust 与其他语言也会面临同一样的并发问题。对于嵌入式软件环境,包括:
- 多线程
- 多核处理器
- 中断处理
对于以上三种常见并发的问题,Rust 也提供了高效的解决方案,如定义原子类型、临界类型、互斥体防止被中断影响。同时也在编译期间检测多线程引起死锁风险,让风险扼杀在编译期间。目前已经有优秀的embassy,rtic等框架提供异步操作系统。
- 智能的编译提示,对于编译时的错误,给出详细的原因,对于有风险的代码段给出解决的意见,从代码编写阶段提高代码质量,无需在调试时去发现再优化,让程序员花给多的时间来考虑代码逻辑,业务逻辑,避免低效的调试过程。
- 轻松衔接 C/C++的代码,零成本接口绑定
Integrate Rust into your existing C codebase or leverage an existing SDK to write a Rust application.
如果目前你的项目无法使用 Rust 来完成所有的模块,你也可以使用 FFF 机制来轻松绑定原项目的 C/C++接口,能够轻松与 C/C++互相操作。可以使用bindgen命令来轻松构建外部接口,也可在build.rs中编译 C/C++ 文件,也能 C/C++ 调用库文件如*.a。轻松集成。
/* File: cool_bindings.rs */
#[repr(C)]
pub struct CoolStruct {
pub x: cty::c_int,
pub y: cty::c_int,
}
pub extern "C" fn cool_function(
i: cty::c_int,
c: cty::c_char,
cs: *mut CoolStruct
);
pub extern "C" fn cool_function( ... );
extern crate cc;
fn main() {
cc::Build::new()
.file("foo.c")
.compile("libfoo.a");
}
- 底层控制能力
Rust 也提供了接口能尽可能安全访问地访问底层接口,如 PAC 包用来抽象微控制器的外设寄存器的访问,能编译成高效的二进制代码且接口容易使用,用户无需太多关注寄存器各域的位置,只需聚焦于芯片手册来操作各域的值,不会出现移位错或写错的低级错误。
#![no_std]
#![no_main]
extern crate panic_halt; // panic handler
use cortex_m_rt::entry;
use tm4c123x;
#[entry]
pub fn init() -> (Delay, Leds) {
let cp = cortex_m::Peripherals::take().unwrap();
let p = tm4c123x::Peripherals::take().unwrap();
let pwm = p.PWM0;
pwm.ctl.write(|w| w.globalsync0().clear_bit());
// Mode = 1 => Count up/down mode
pwm._2_ctl.write(|w| w.enable().set_bit().mode().set_bit());
pwm._2_gena.write(|w| w.actcmpau().zero().actcmpad().one());
// 528 cycles (264 up and down) = 4 loops per video line (2112 cycles)
pwm._2_load.write(|w| unsafe { w.load().bits(263) });
pwm._2_cmpa.write(|w| unsafe { w.compa().bits(64) });
pwm.enable.write(|w| w.pwm4en().set_bit());
}