Rust 为什么适合嵌入式开发

Rust 作为一门年轻的语言,聚焦与安全、并发、高性能等特点,号称能替代 C/C++,那么究竟有多少优点能值得我们来切换呢?本文将告诉你为什么 Rust 适合嵌入式开发。

Rust Embedded

工具链

  1. 轻松搭建各种不同类型的芯片编译环境,使用 cargo、rustup 等命令快速搭建新的环境。
  2. 统一的编译、调试工具、下载工具, 如cargo build,cargo falsh,probe-rs等
  3. 快速生成代码文档,cargo doc直接生成网页文档,能让新手快速了解整个项目的模块、接口框架。
  4. 内置代码格式化工具 fmt,轻松就各个代码文件统一格式,标准规范,团队作战无需在吐槽队友的代码风格,cargo fmt 后格式都会统一好,新手无需学习新公司的编码格式规范,公司也无需过多培训编码格式规范。

Rust Embedded

库的集成

  1. 移植优势

可移植性移植是嵌入式开发的一个大问题,每个工作可能或多或少会考虑一些模块的可移植性,想进来在不同框架不同平台上能够共有这些模块,避免重复造轮子。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"
  1. 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();

语言优势

  1. 内存安全优势。

C/C++嵌入式工程师肯定知道,经常在编码完成后,烧录程序到芯片测试运行,经常会出现莫名的内存泄露、异常退出甚至死机的现象,这种内存问题有时非常难怕查,也许编码十分钟调试两小时。当然目前也有一些先进的工具用来辅助调试,如 ASan,Valgrind、Memcheck 等工具,但这些工具本身就需要复杂的调试手段,需要仔细查看日志才能得出结果,但是对于某些资源受限嵌入式设备,很难使用这些工具来辅助排查。对于新手来说学习这些工具的使用就让人头疼。而 Rust 天生保证内存安全,没有丰富的 Rust 经验很难写出能让内存异常的代码,是的你没听错,Rust 对于新手保护特别友好,需要有经验的人才能故意写出不安全的代码。Rust 的生命周期的约束使得实现内存安全而且零成本,也就是无需在时间和空间上浪费资源。

  1. 语法优势:语法中新的枚举,闭包、异步、流控、变量生命周期控制、安全宏等,基于这些基础语法能最方便、便捷的表达问题的逻辑,无需使用太多的技巧。让编程更加简洁和优雅。

Rust 作为强类型的语言,但是在使用时无需过多指定变量类型,Rust 编译器会自动推导变量的类型,并基于生命周期的约束可以重复使用变量名,原理上保证使用这些语法就像 C 语言一样安全,但是用起来像 Python 一样方便。

  1. 线程安全,无畏并发
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等框架提供异步操作系统。

  1. 智能的编译提示,对于编译时的错误,给出详细的原因,对于有风险的代码段给出解决的意见,从代码编写阶段提高代码质量,无需在调试时去发现再优化,让程序员花给多的时间来考虑代码逻辑,业务逻辑,避免低效的调试过程。
  2. 轻松衔接 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");
}
  1. 底层控制能力

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());
}