随着 Rust 在开发更安全的系统软件方面越来越受欢迎,对微控制器硬件领域进行现实检验变得尤为必要。Rust 生态系统是否已为该领域做好准备?Rust 在实践中能否与 C 竞争?
此前,ST做了一个物联网工业案例研究,旨在解答这些问题。他们分析了两个团队在几个月的时间里同时开发相同功能的情况——一个团队使用 C,另一个团队使用 Rust。本文对他们的方法、结果和迭代工作进行了比较分析。
硬件分析和测量表明,基于内存占用或执行速度,没有充分的理由在微控制器固件方面优先选择 C 而不是 Rust。此外,Ariel OS 被证明提供了一个高效且可移植的 Rust 系统运行时,其占用空间小于传统上用于此领域的最先进的裸机 C 堆栈。
结论是,Rust 是目前该领域固件开发的可靠选择。

引言
在联网微控制器的嵌入式软件领域,一种趋势正在兴起:Rust 作为一种比 C/C++ 更安全的替代方案,正日益受到推崇。C/C++ 一直是该领域固件开发的主流语言。虽然一项针对 Android 固件的大规模研究已证实了在资源受限程度较低的硬件上从 C/C++ 过渡到 Rust 的可行性和优势,但微控制器嵌入式 Rust 的最新技术水平相对而言还不够成熟。
因此,本文探讨的核心问题是:嵌入式 Rust 在实践中作为传统 C/C++ 实现的替代方案,其成熟度如何?
为此,本文报告了一个物联网工业应用案例。该案例涉及一家大型供应商的产品线,涵盖用作传感器数据记录器的异构微控制器硬件。本文分析并比较了使用 C 和 Rust 并行开发的固件,这两种方法都能提供所需的功能。
在第一阶段,两个团队各自独立工作了约 6 周,最终各自交付了一个功能完善的固件——分别是 VDL-C 和 VDL-Rust。在第二阶段,两个团队又进行了约 4 周的合作,每周会面一次,审查性能并讨论对两个实现方案的改进。本文报告了这些结果以及在第二阶段所做的迭代工作。具体而言,本文描述了以下贡献:
我们提供了一个关于STAIoTCraft数据记录器的案例研究,这是一个面向异构微控制器和传感器的边缘AI工具包;
我们报告了VDL-C和VDL-Rust的并行设计、实现和优化工作——VDL-C和VDL-Rust分别是使用C语言和Rust语言实现的STAIoTCraft相同功能的固件;
我们对VDL-C和VDL-Rust的架构进行了比较分析;
我们提供了VDL-C和VDL-Rust在商用MCU硬件上运行的性能对比测量结果;
最后,我们评估了Rust语言在微控制器固件开发方面的成熟度。

案例研究:STAIoTCraft
STAIoTCraft 是意法半导体 (STMicroelectronics) 提供的一个在线平台和工具包,用于通过 Web 浏览器训练、部署和管理分布式人工智能物联网 (AIoT) 设备、网关和云基础设施。该平台和工具包已在之前的研究中有所描述,并可在线获取。该系统包含两个主要组件,如图 1 所示。一方面,传感器代理实时捕获传感器数据,并通过通信链路传输这些数据。另一方面,控制器接收传感器数据流,训练模型和/或在浏览器 GUI(运行于 PC)中显示传感器数据和传感器内推理结果。
传感器数据被上传到云端,以构建用于训练机器学习模型的分类数据集。在第二阶段,该模型被反馈给传感器代理,以执行传感器内数据推理和分类。
Sensor Agent Hardware – STAIoTCraft 工具包中用作传感器数据记录器的参考硬件是 SensorTile.box Pro,它集成了基于 Arm Cortex-M33 内核的 STM32U585AI 微控制器单元 (MCU) 和带有嵌入式机器学习内核的 LSM6DSV16X 六轴 MEMS 惯性测量单元 (IMU)。然而,该工具包支持使用各种不同的设备,包括微控制器、环境和运动 MEMS 传感器以及飞行时间 (ToF) 传感器。
Sensor Agent Software – 运行在传感器代理上的固件以下简称“Vanilla Data Logger (VDL)”固件。传感器代理用于将传感器数据传递给控制器的协议是 Vanilla Datalog Protocol (VDP),详情如下。
Vanilla Datalog Protocol (VDP) – 该协议用于传感器数据记录,是支持各种异构传感器代理硬件的事实标准。 VDP 实现了日志控制器和传感器代理之间的交互(参见图 1)。VDP 用于通过通信链路(例如 UART 外设)传输数据,它基于三层结构:
非对称串行数据包交换协议 (ASPEP:Asymmetric Serial Packet Exchange Protocol ):该层规定了数据包头部的语义和语法;
简单串行传输层 (SSTL:Simple Serial Transport Layer):该层规定了一种点对点的主/从协议,允许在串行链路(例如 UART 外设)上进行数据传输。SSTL 支持同步和异步传输,专为多通道数据流而设计;
数据层:该层承载命令-响应交换以及待传输的传感器数据。
方法与流程
下文将重点比较评估两种固件实现,一种使用 C 语言,另一种使用 Rust 语言。这两种固件都提供了便捷的硬件抽象,符合 STAIoTCraft 的要求,并且都实现了 VDP 协议。我们使用以下术语:VDL-C 是使用 C 语言开发的固件,而 VDL-Rust 是使用 Rust 语言开发的固件。
VDL-C 和 VDL-Rust 由不同的团队开发,开发时间相近(约 10 周)。每个团队的任务都是提供一个符合 VDP 协议的固件,能够将传感器数据从传感器代理传输到 STAIoTCraft 控制器,如前一节所述。两个团队都秉持着相同的“开箱即用”的理念:旨在提供一个简单易懂、易于使用且易于定制的 STAIoTCraft 数据记录器固件示例。简而言之,重点在于“一机通用”:代码的可移植性和可维护性。第四、五、七节侧重于具体的传感器和满足 STAIoTCraft 要求的特定微控制器,而第六、八、九节则旨在讨论更一般性的观察结果,这些观察结果不局限于此范围,适用于更多样化的微控制器和 MEMS。
C 固件概述:VDL-C
为了便于我们用 C 语言实现 VDP 协议,并确保其在各种不同的微控制器硬件上都能移植,我们广泛使用了最先进的代码自动生成工具,并集成了开源构建模块。VDL-C 固件栈如图 2 所示。
CubeMX 的使用 — 板级支持包 (BSP:board support package)、硬件抽象层 (HAL:hardware abstraction layer) 和硬件初始化代码均使用 STM32CubeMX自动生成。STM32CubeMX 是一款事实上的标准工具,拥有便捷的图形用户界面 (GUI)。首先,我们使用 STM32CubeMX 生成一个包含所有硬件初始化文件的初始项目(C 语言),然后可以从中选择目标板(例如 ST SensorTile.BoxPro),配置所需的外部设备,并添加所需的软件包,例如:
用于传感器支持和驱动程序的 CMSIS X-CUBE-MEMS1 功能包。第二步,生成图 2 所示即插即用 (PnP) 组件的 C 代码模板以及相应的中间件粘合代码。
有限状态机和中断事件 — VDL-C 固件采用裸机 C 语言编写,是一个事件驱动型应用程序。其架构遵循经典的有限状态机 (FSM),运行在一个主无限循环中,如图 3 所示。FSM 处理来自 LSM6DSV16X 传感器中断、UART/I²C 通信后端以及 MCU 定时器的事件。VDL-C 基于标准的 CubeMX 生成代码,实现了对通过 VDP 协议接收的命令的 FSM 响应,处理由传感器和定时器生成的中断事件,并通过 UART 串口公开 VDP 协议功能。
JSON 序列化/反序列化 — VDL-C 捆绑了用于 JSON 序列化/反序列化的开源 Parson 库。这是唯一需要动态堆内存分配(malloc 和 free)的固件组件,由 newlib 提供,newlib 是 ARM GCC 工具链中包含的 C 标准库。
Rust 固件概述:VDL-Rust
为了便于 VDP 协议的实现及其在异构微控制器硬件上的可移植性,VDL-Rust 广泛依赖于 Rust crate 生态系统中已有的构建模块,如下所述。基于此生态系统,VDL-Rust 被设计为一个业务逻辑层,利用了 Ariel OS 提供的 API。Ariel OS 是一个轻量级的开源库操作系统,专为使用 Rust 编写的微控制器而设计。
Ariel OS 的使用 — Ariel OS 提供了一个基于 Embassy 构建的硬件抽象层 (HAL),适用于无 std 和 alloc 约束的 Rust 实现——这意味着它不假定任何底层操作系统,并且只分配静态内存。Ariel OS 和嵌入式 Rust crate 生态系统的模块化设计使得在构建时可以进行细粒度的功能选择,从而最大限度地减少资源使用。因此,Ariel OS 为各种 MCU、传感器和通信总线(UART、I²C、SPI 等)提供了可移植的抽象和初始化。在此框架下,VDL-Rust 从 crates.io 导入所需的 ST 传感器驱动程序。图 4 展示了该架构的概览;有关 Ariel OS 的更多详细信息,请参阅相关文档。
使用静态内存进行序列化/反序列化 — VDP 逻辑的关键部分是 JSON 命令-响应处理。VDL-Rust 结合了提供序列化/反序列化框架的 serde crate 和提供与动态内存等效类型(例如 Vec 和 String)相对应的 heapless crate,但这些类型操作的是固定容量的静态内存。为了满足静态内存分配的要求,使用了 serde-json-core crate。它实现了一个嵌套结构体,用于匹配响应 get status 命令返回的设备配置,以及一个从传入的 JSON 命令反序列化的嵌套枚举。反序列化后,Rust 的模式匹配功能使得分发实现 VDP 的命令变体变得非常简单。
传感器数据流和进程间通信 (IPC) — 传感器读取由数据就绪中断触发。每个中断对应的任务会监控外部中断通道上的引脚状态变化。对于进程间通信 (IPC),任何等待中断的任务都可以向流任务发送数据就绪信号。收到信号后,流任务会检查每个标志位,以确定哪些传感器已触发,并读取相应的数据。在流传输期间,流任务还会检查是否已收到来自控制器应用程序的停止命令。IPC 和任务流程如图 5 所示。VDL-Rust 利用 embassy-sync crate 进行任务间通信,该 crate 提供线程安全的信号和通道等原语。对于简单的布尔标志,则使用原子类型,因为它们保证了原子访问,并满足编译器的内存安全要求。
固件设计比较
VDL-C 和 VDL-Rust 都采用了典型的事件驱动方法,其中传感器硬件中断触发 I²C 读取。虽然存在基于轮询的替代方案,但对这种权衡的讨论超出了本文的范围。相反,我们重点关注 VDL-Rust 与 VDL-C 设计中观察到的关键差异。
实时操作系统 (RTOS) 的使用 — VDL-C 架构由 STM32CubeMX 生成,它提供库功能来配置 MCU 并通过高级 C API 访问其外设。生成的固件包含自动生成的、特定于 MCU 的初始化代码,之后调用应用程序入口函数。虽然提供了用于外设访问的高级 C API,但没有线程、进程间通信 (IPC) 或同步原语可用。并发性是通过手动启用和禁用中断请求 (IRQ) 来管理的。
相比之下,VDL-Rust 应用程序代码不包含任何 MCU 特定的初始化或设置代码,而是依赖于 Ariel OS。Ariel OS 会启动 MCU、生成应用程序任务,并提供一套完整的进程间通信 (IPC) 和同步原语——这些原语由 Rust 编译器在类型级别强制执行。
像 VDL-C 那样指定 MCU 设置可以提供最大的控制权,但会阻碍可移植性。VDL-C 可以通过创建一个新的 STM32CubeIDE 项目并复制应用程序逻辑来进行移植,该逻辑使用条件编译来支持多种 MCU 类型。原则上,应用程序逻辑可以提取到一个可重用的模块中;然而,随着支持的硬件目标数量的增加,由于需要创建大量的不同的 STM32CubeIDE 项目,可维护性通常会变得脆弱。
相比之下,VDL-Rust 受益于 Ariel OS 的“逃生舱”机制,可以在必要时覆盖默认的 MCU 配置,并且由于 Ariel OS 的可移植构建系统,移植到其他开发板也变得非常简单。然而,这种程度的可移植性可能会以内存占用和/或执行速度为代价。以下章节将提供对比测量结果,以评估这种权衡。
动态内存的使用——VDL-Rust 完全依赖于静态内存分配,而 VDL-C 在其 JSON 解析器(Parson,PNPL 代码生成器默认使用)中使用动态堆内存(malloc 和 free)。由于最小化内存使用并非主要目标,我们保留了分配内存的 JSON 解析器,但需要注意的是,还有一些内存占用更少、完全不需要堆内存的方案。相应的内存占用测量结果将在以下章节中提供。
状态处理——事件驱动架构需要跨多个中断和输入源管理状态。VDL-C 实现了一个手工设计的状态机,而 VDLRust 则利用了 Rust 提供的 async/await 功能。尽管 Rust 在代码简洁性和正确性方面具有众所周知的优势,但 Rust 编译器使用 async/await 生成的二进制文件仍需进行性能验证。以下章节将讨论通过逻辑分析仪检测到的执行速度损失,该损失归因于 Rust 中使用 async/await。
crate 的使用——VDL-C 依赖于 STM32CubeMX 的原生打包机制来实现代码重用,而 VDL-Rust 则使用 Rust crate 生态系统。前者仅支持在 STM32Cube 生态系统内(仅限于 STM32 MCU 应用)进行代码重用,而后者则将重用范围扩展到包括嵌入式、WebAssembly 和桌面应用在内的多个应用领域。例如,用于 JSON 处理的 serde crate 是更广泛的 Rust 生态系统中的事实标准,因此成熟且久经考验。此外,VDL-Rust 将 PnP 协议代码提取到一个单独的 crate 中,从而便于进行独立于嵌入式协议栈的主机端测试,并支持在 WebAssembly 或桌面应用中重用。本文中用于生成测量结果的基准测试应用程序也使用了相同的 crate,该测试结果涵盖了 VDLC 和 VDL-Rust。
硬件性能测试
本节报告了 VDL-C 和 VDL-Rust 二进制文件在硬件上运行的性能测试结果。为简洁起见,并便于比较,此处仅展示 SensorTile.box Pro 的测试结果,但两种固件均支持多种 MEMS 硬件配置。SensorTile.box Pro 基于主频为 160 MHz 的 ARM Cortex-M33 微控制器 (STM32U585AII6Q),配备 2 MiB 闪存 (ROM) 和 786 KiB SRAM。测试仅针对 LSM6DSV16X 传感器,使用其加速度计、陀螺仪和机器学习核心 (MLC) 组件。
A. 内存占用
图 6 和图 7 分别展示了 VDL-C 和 VDL-Rust 固件的内存占用情况。我们对两种实现方案的代码大小和 RAM 使用情况进行了分析;结果汇总于表 I。
总代码大小(.text 段)。使用 bloaty 和 cargo-bloat 工具测量,VDL-C 的 .text 段总大小为 66,240 字节,VDL-Rust 为 69,764 字节(大 5%)。在 VDL-C 的总内存占用中,newlib 占用了 21,389 字节(32.3%),newlib 是工具链捆绑的 C 标准库,Parson 依赖它来实现 malloc 和 free 函数。VDL-Rust 的总 ROM 大小(包括 .rodata)为 84,100 字节,VDL-C 为 76,744 字节(10%),与 .text 段的比较结果一致。

应用程序代码大小。排除系统运行时代码大小(仅考虑图 6 和图 7 中的应用程序、驱动程序和序列化代码),VDL-Rust 比 VDL-C 大 23%,这主要是由于应用程序逻辑(24,064 字节 vs. 18,084 字节)和序列化代码(11,433 字节 vs. 6,050 字节)的增加,后者是 serde 编译时单态化的结果。
系统运行时代码大小。观察系统运行时代码大小(图 6 和图 7 中的系统 + HAL),VDL-Rust 的代码量减少了 10%。对于 VDL-C,这主要归因于 newlib 库和启动代码。对于 VDL-Rust,这归因于 Ariel 操作系统和启动代码。这一观察结果令人惊讶,因为人们通常会认为,基于操作系统的方法(例如 VDL-Rust 基于 Ariel 操作系统)在占用空间方面会比更接近裸机的方法(例如 VDL-C)付出更大的代价。
RAM 和栈使用情况。表 I 显示了 RAM 的对比测量结果。VDL-C 使用 2,048 B 的栈、14,960 B 的静态内存,以及峰值 25,600 B 的堆内存,总计 44,656 B。VDL-Rust 使用 10,240 B 的栈、14,400 B 的静态内存,并且没有堆内存,总计 24,640 B(减少了 45%)。堆内存的增加完全归因于 Parson 对 JSON 树节点的动态分配;serde-json-core 通过直接反序列化为类型化的栈结构来消除动态分配,因此这种差异既反映了生态系统的惯例,也反映了有意的优化。更大的 VDL-Rust 栈并非语言本身固有的,而是源于第八节讨论的异步执行器模型。VDL-C 的总内存占用是取决于测试覆盖率的运行时峰值。 VDL-Rust 的内存限制是静态的,并且是编译时限定的。总体而言,从内存占用角度来看,VDL-Rust 比 VDL-C 更具优势。

B. 缩小内存占用
虽然之前的测量结果展示了最终结果,但图 8 展示了在多次迭代(第二阶段)中,RAM 和 Flash 的占用情况变化。需要注意的是,VDLC 也需要进行一些改进,但为了简洁起见,本节不对此进行讨论,图 8 中仅展示了最终的 VDL-C 测量结果作为参考。
图 8 展示了为减少 VDLRust 的 ROM 和 RAM 占用而应用的十个增量优化步骤,并以使用 LTO 和 LTO-O 编译的 VDL-C(ROM:76,744 B,RAM:17,008 B)作为基准。所有后续比较均以此基准为参考。
到第 10 步,ROM 已从 118,236 B 减少到 84,100 B,与 VDL-C 参考值(76,744 B)的差距在 10% 以内。更详细地说,VDLRust 中导致代码膨胀的主要来源是单态化开销和异步状态机膨胀。
单态化的影响——单态化带来的开销在步骤 3、4 和 6 中得到解决。这些步骤包括:尽量减少不同的无堆字符串类型、合并按类别划分的枚举类型,以及在枚举变体上优先使用 匹配而不是派生的序列化/反序列化实现。每个步骤都在不改变逻辑的情况下减小了二进制文件的大小。
异步 Rust 状态机的大小——减小状态机的大小在步骤 2、5、7、9 和 10 中得到解决。这些步骤包括:通过引用传递大型缓冲区(步骤 2)以及在最终的 await 点之前反序列化为紧凑的中间枚举类型(步骤 5),从而减少了状态机状态中保留的数据量。一旦不再跨等待点持有大型变量,之前必需的 8,192 字节堆栈增加就被完全还原 ,从而回收了 8,192 字节的 RAM。用阻塞式 I²C 读取替换异步 I²C 读取,消除了应用程序层和驱动程序层中的多个等待点,并随后使得 PNPL 函数能够完全同步。
过度分配——ROM 减少幅度最大的一次发生在第 8 步,其中最坏情况下的固定数组大小被编译时可配置的大小所取代,类似于 C 预处理器定义,通过避免过度分配消除了大约 18,000 B。

C. 执行速度
为了评估执行速度,我们测量并比较了在选定的输出数据速率 (ODR) 下采集和传输传感器数据所需的时间。ODR 是一个硬件配置参数,用于定义传感器采样和输出数据的速率。每当采样准备就绪时,传感器都会触发一个硬件中断,该中断可由 MCU 处理。在这些实验中,我们使用了 LSM6DSV16X 加速度计,其 ODR 范围可在 15 Hz 至 7680 Hz 之间选择。
VDL-C 和 VDL-Rust 固件均成功实现了对配置为最大 ODR 7680 Hz 的传感器中断的处理。如图 10 和图 11 所示,两种实现方式的中断周期均为 133.75 µs,对应于 7477 Hz(参见图 10:P0,图 11:P0)。与目标频率 (7680Hz) 的偏差可以忽略不计,这可以归因于传感器端中断漂移,而不是固件限制。
异步 Rust 的影响——VDL-C 和 VDL-Rust 的一个关键区别在于异步 Rust 运行时固有的异步执行器开销。在 VDL-Rust 中,硬件中断引脚由 Embassy 中断服务例程 (ISR) 监控,触发后会唤醒 EXTI 处理程序任务,然后 EXTI 处理程序任务再向流式任务发出信号。这种两阶段调度会在每个周期的入口点引入调度延迟,并且这种延迟会传播到所有后续的任务转换中。这体现在 EXTI 到 Stream 的延迟增加(参见图 10:P1 = 1.958 µs,对比图 11:P1 = 3.458 µs)和 UART 转换延迟增加(参见图 10:P4 = 2.542 µs,对比图 11:P4 = 4.417 µs),以及 VDL-Rust 特有的额外 Stream-EXTI 转换(参见图 11:P6 = 4 µs),该转换代表执行器返回 EXTI 处理程序等待下一个中断的往返过程。这种分布式开销的累积效应反映在空闲窗口的减少上(参见图 10:P5 = 13.875 µs,对比图 11:P5 = 6.5 µs)。尽管存在这种开销,但两种实现方式都能满足中断服务截止时间,这证实了虽然 Rust 异步执行模型会产生一些开销,但它不会影响最大 ODR 下的实时传感器数据采集,尽管代价是减少了调度裕度。

D. 提升执行速度
第七部分D节的测量结果源于VDL-C和VDL-Rust团队在第二阶段进行的迭代改进,如图9所示。
最初预期VDL-C的执行速度会优于VDL-Rust,但初始测量结果却恰恰相反,如图9所示:VDL-Rust的峰值频率为2034 Hz,而VDL-C的峰值频率仅为1000 Hz。这种差异源于架构——VDL-Rust最初采用轮询方式,使用非阻塞UART,而VDL-C则采用中断驱动,使用阻塞UART写入。将VDL-C切换到非阻塞模式后,这一问题立即得到解决。
随后,VDL-Rust的架构也进行了重新设计,使其也采用中断驱动,但其性能仍然低于预期,直到禁用Ariel OS默认启用的日志记录功能(因为该应用程序不需要日志记录)。移除该问题并进行一些小的修复后,VDL-Rust 恢复了真正的基准线,并在 4985 Hz 时短暂领先,而 VDL-C 则稳定在 3738 Hz。VDL-C 通过采用 VDL-Rust 已使用的相同修复方法重新夺回领先地位——通过保存的配置而非运行时 DRDY 状态读取来解决 LSM6DSV16X 加速度计和陀螺仪之间的中断引脚歧义,
从而消除了额外的 I²C 事务——并从那时起可靠地维持了最大 ODR。
VDL-C 稳定后,注意力转向了 VDL-Rust 的剩余差距。即使是单次 I²C 查询,事务延迟仍然是瓶颈,而要解决这个问题,需要直接从 VDL-C 已配置的值移植每个调优步骤。STM32CubeMX 计算的 TIMINGR 值将 I²C 时序收紧到总线兼容的最小裕量,从而将事务延迟从 122 µs 降低到 112 µs。启用指令缓存 (I-Cache) 和闪存预取后,消除了指令获取停顿和流水线等待状态,将 UART 延迟从 17 微秒降低到 11 微秒,并将流间隔从 138 微秒缩短到 126 微秒。禁用 UART FIFO 进一步降低了 UART 延迟,降至 5 微秒。这些措施共同将流式传输任务持续时间从 140 微秒缩短到 120 微秒,端到端性能提升了 20 微秒。
到第 4 周(第二阶段结束),VDL-C 和 VDL-Rust 都达到了相同的吞吐量上限;观察到的 ODR 与标称 ODR 之间的残余差异是由于传感器侧 ODR 时间不准确造成的,而不是由于固件造成的。
经验教训与展望
C语言固件开发已发展数十年,而Rust语言在微控制器固件开发领域的应用则相对较新。尽管如此,C团队和Rust团队都在第一阶段规定的短时间内完成了各自的任务。两个团队在第二阶段也都需要付出额外的努力来提升性能。为了公平起见,必须将这些观察结果放在特定的背景下看待:Rust团队的经验远逊于C团队,而且C团队使用的是成熟的厂商技术栈,而Rust团队则完全依赖于相对较新的开源构建模块和工作流程。
以下内容总结了本研究的主要观察结果,重点关注嵌入式Rust新手容易犯的错误。
A. 代码可移植性方面
基于 Ariel OS(其本身基于 Embassy 构建)的嵌入式 Rust 代码,被证明能够轻松地移植到不同的硬件目标平台。VDL-Rust 已成功移植到配备 IKS4A1 MEMS 扩展板的 ST NUCLEO-F401RE 开发板上,仅需进行少量修改,其中大部分修改涉及特定于开发板的配置细节。
而等效的 VDL-C 移植则需要为目标开发板初始化一个新项目,复制可移植的应用程序代码,并通过预处理器 #define 指令定义引脚映射。基于 C 的项目需要为每个目标平台单独设置项目并手动复制代码,而基于 Ariel OS 的 Rust 则将所有内容整合到一个项目中——特定于开发板的细节通过 #[cfg] 特性门处理,切换目标平台只需更改构建命令即可。
通常,这种级别的可移植性会带来二进制文件大小或执行速度方面的显著缺陷。然而,正如我们在第七节中所示,在相同的硬件上对 VDL-C 进行的测量表明,与在相同上下文中使用的传统 C 运行时相比,这些惩罚并不显著。
B. Async/Await 的使用
VDL-Rust 的开发团队被要求使用 Embassy 和 Ariel 操作系统,因此使用 async/await 成为默认选择。虽然最初的迭代版本很快就开发到了可运行状态,但异步 Rust 的权衡取舍和潜在问题并未得到充分考虑。这导致代码量相对较大,却没有真正利用并发性。因此,如第七节所述,调整 async/await 需要额外的时间和迭代才能获得性能良好的代码。
事后看来,如果 Rust 团队之前对异步 Rust 进行更深入的研究,就能更早地使架构符合异步 Rust 的限制,从而避免项目后期的一些优化工作。
更激进的做法是:虽然没有考虑使用常规的同步代码代替异步 Rust 代码,但这原本也是一个可行的方案。
C. 避免 Rust 二进制文件大小陷阱
Rust async/await 的权衡——async/await 简化了代码,但每个 async 函数都会编译成一个状态机,其大小会随着在 await 点之间保持存活的局部变量数量的增加而增长——嵌套的 async 调用会加剧这种情况,悄无声息地增加静态 RAM 的使用量。
枚举陷阱——枚举会加剧我们刚才描述的内存膨胀。编译器会分配等于最大枚举值大小的空间,当枚举在挂起点之间保持存活时,所有在 await 点之间保持存活的枚举都会占用全部空间。
栈帧——为了保持栈帧的精简,应该使用 StaticCell 将大型对象放入 .bss 文件中,而不是分配为局部变量。对于大型对象,应该优先使用 init with() 而不是 init(),因为它会在 StaticCell 中就地构造值,避免中间的栈分配。
大型结构体的陷阱——将大型结构体按值传递给初始化函数可能会导致在值到达最终位置之前,栈上出现多个副本。类似地,持有一个大型局部变量并向其传递一个可变引用,会导致该变量的底层存储空间在变量的整个生命周期内都保留在栈上——引用本身开销很小,但它指向的栈分配开销却很大。
D. 避免 Rust 执行速度陷阱
Rust async/await 延迟 — 使用 async/await 进行任务切换的开销基于时钟周期,因此与时钟频率相关。在 SensorTile.box Pro (160 MHz) 上,一个最小的切换操作 — Timer::after micros(0).await — 耗时 1.7 µs(272 个时钟周期);2-3 次切换累计达到观察到的 3.5-4 µs。同样的 272 个时钟周期在 84 MHz (STM32F401RE) 上耗时 3.2 µs。这表明必须针对每个目标重新评估开销预算,尤其是在高 ODR 的情况下。
外设配置 — Embassy 的外设默认设置优先考虑跨 MCU 的兼容性而非性能。与 STM32CubeMX 生成特定于目标的初始化(最大外设时钟、优化的 I²C 时序、调整的闪存等待状态)不同,Embassy 不会自动复制此配置。因此,对于对性能要求较高的 Embassy/Ariel OS 端口,应交叉检查外围设备配置(例如,与 CubeMX 输出进行比对)。
相关工作
先前的工作对嵌入式Rust领域及其相关挑战进行了概述。其中一个挑战是Rust二进制文件的大小,Rust二进制文件的大小远超预期。另一方面,[针对Android设备研究了Rust固件开发相比C/C++固件开发在工作流程上的显著改进。关于微控制器领域,近年来开发了许多完全用Rust编写的嵌入式软件平台,为微控制器固件提供了基础。例如:TockOS 率先实现了裸机、自包含的嵌入式Rust编程。另一个例子是RTIC ,一个实时中断驱动的并发框架。最近,Embassy和Ariel OS 利用异步Rust和crates生态系统的兴起,提供了嵌入式Rust软件平台。然而,据我们所知,目前还没有针对微控制器Rust和C并行固件开发的比较研究,而这正是本文的重点。
结论
对于基于微控制器的嵌入式系统软件编程而言,Rust 的实际性能和技术成熟度至今仍是悬而未决的问题。在大型 C/C++ 代码库(目前主要使用这两种语言)中不断出现严重 bug,而系统软件又必须满足日益严格的要求(例如网络弹性法案的要求)的背景下,解答这些问题变得尤为迫切。本文通过研究两个团队分别使用 Rust 和 C 开发固件的并行工作,为物联网工业应用场景提供了一些答案。这两个团队开发固件的功能相同。我们的分析基于架构考量、对两个团队迭代工作的观察,以及我们在通用微控制器硬件上进行的对比性能测量。研究结果表明,Rust 目前确实是一个安全的选择。此外,我们还总结了经验教训,并概述了新一代 Rust 嵌入式开发人员在微控制器开发中需要避免的主要陷阱。

*免责声明:本文由作者原创。文章内容系作者个人观点,半导体行业观察转载仅为了传达一种不同的观点,不代表半导体行业观察对该观点赞同或支持,如果有任何异议,欢迎联系半导体行业观察。







