嵌入式领域的Rust语言

作者:洛佳


k210芯片

Rust语言是二十一世纪的语言新星。Rust被人广泛承认的一点,就是因为它能运行在多样的目标上, 从桌面和服务器设备,到资源有限的嵌入式设备。

我们可以用适合来评价一门语言和技术。Rust非常适合开发嵌入式应用,它是一种和C相仿的、 能应用于嵌入式设备开发的编程语言。

操作系统都是从裸机设备开始运行的,Rust语言的这一点也意味着,它能很好地用于编写操作系统。 无论是应用层还是内核本身,Rust都是极富竞争力、值得投入时间的技术选项。

裸机上的Rust语言

开发裸机应用时,通常希望使用的语言速度快、可靠性强。此外们还希望语言的生态较好, 有利于提高生产效率,而且适用范围较广。Rust语言能满足以上的要求,适合裸机应用的开发。

运用在裸机场合时,Rust语言拥有许多优点。除了效率和安全,Rust还将传统上不用于裸机开发的编程技术引入到裸机, 让开发者有更多的选择,更灵活、高效地编写裸机应用代码。

二十一世纪的裸机编程语言

在这个互联网全面普及、性价比设备应用更广的时代,安全和可靠性成为一门语言必须考虑的因素。 Rust语言采用移动语义,拥有严格的代数类型系统以及生命周期、所有权模型; 相比传统的编程语言,这些模型能在合适的时候释放所用资源,减少漏洞的出现。 此外,通过语义检查,Rust能在编译期有效寻找内存和线程安全问题,降低开发和测试的负担。

Rust语言是的运行效率高、开发效率好、适用范围广。作为一门编译型语言,它直接编译输出到汇编代码, 通常公认裸机的Rust语言性能在C语言级别,拥有较高的运行效率。 Rust语言的开发效率很高,文档完善、编译器提示有帮助,能节省软件开发所需的时间。 它能应用在多个平台和指令集中,这包括裸机平台;处理核、操作系统厂家还可以提供自己的编译目标, 无需厂家自己重新开发、提供工具链。

Rust语言出彩的地方在于,它向嵌入式平台引入了大量新的编程技术。 这包括了闭包、过程宏等传统上用于函数式编程的技术,和多态、虚函数表等面向对象语言的技术。 新编程技术的引入,扩充了开发者的选择。即使彻底理解Rust的编程概念有一定难度,但这些易用的新技术, 让开发者只需阅读实例代码,便可快速进入开发状态。这些新技术的引入,是嵌入式平台从未有过的, Rust能提高开发者的工作效率,降低平台间迁移的学习时间和成本。

裸机上的过程宏

传统用于嵌入式平台的编程,我们加快开发速度使用的宏,常常基于语法字符串的替换和修改。 Rust语言扩充了宏的概念,提出了基于语法树的“过程宏”编程方法,让宏语法更容易使用、编写更方便。

“过程宏”是接收Rust代码作为输入,操作这些代码,然后产生另一些代码的过程。 它和字符串的替换不同,是从语法树到语法树的替换。开发一个过程宏,可以使用简单的定义过程, 或者有工作量的属性宏定义过程。简单的定义中,我们编写代码,给出宏的输入有哪些,要翻译到哪些输出代码, 这样就完成了一个宏的定义。属性宏定义则允许完成语法树分析、代码生成甚至代码优化的过程, 就需要编写专门的“属性宏库”,借用Rust编译器的一部分,完成宏代码的转化和输出。

过程宏是基于语法树的分析过程,借助“树”的结构我们能理解它的一些特点。因为Rust语法树的子树也是Rust代码, 所以宏的定义内也可以完成语法分析,这就为代码编辑器的提示和补全提供了便利。 一个语法项目不可能同时属于两颗不是亲子关系的子树,因为如果属于两颗子树,将和语法树的树根产生环, 就和语法树的定义相违背,所以语法项目都是独立的,宏内代码的解析不会影响外界代码的解析。

这样的独立性也就是“卫生宏”思想的提出,Rust的过程宏可以理解为代码的“内部展开”,不影响代码的上下文。 正因为Rust过程宏产生完整的语法子树,它的定义不需要额外的界符,因此只需要满足Rust语法就可以了。

在过程宏的定义之外,Rust语言提供了大量便于嵌入式开发的标签。“align”标签定义内存对齐的方式, “link_section”标签给定代码要链接到的段或区。这样,过程宏可以包装各种各样的标签, Rust语言的用户可以方便地使用,而不需要深入宏了解代码的具体要求。 Rust语言定义的过程宏可以导出到包外,给其它的库使用,这有利于嵌入式Rust生态的搭建和共享。 Rust语言宏灵活的特性,让宏在更多的领域有可用之处,更好地服务嵌入式平台的开发工作。

嵌入式中的模块化编程

Rust语言拥有很好的模块化编程概念。传统平台的Rust语言中,社区总结出了“模块-包-项目”的模型。 这个模型也适用于嵌入式平台,增加协作开发的效率,更好地共享生态。

Rust的模块化编程分为模块、包、项目三级。模块是Rust语言可见性分划的最小单位, 语言中提供了专门的关键字,来区分不同模块的代码和可见性,是由Rust语言本身确定的。 在Rust语法中,“mod”是定义模块的关键字,“pub”是定义可见性的关键字。

包是Rust项目的二进制目标,这个等级是由Rust工具链给定的。每个包有版本号、作者和许可协议等元数据, 要依赖和使用的库也要登记到包中,以便共同编译。库的特性有点像传统语言的条件编译, 也是以包为单位规定的,每个包使用的库可以开启不同的特性,但库在同一个包中开启的特性是相同的。

“项目”这一层并非由Rust语言给定;人们开发软件时,发现一个解决方案中包含多个二进制目标是非常好的, 总结之后就出现了项目的抽象模型。项目由核心和外围包组成,或者是功能相近的一组包, 它通常由同一个团队组织和维护,可以在项目上添加扩展。项目在习惯上由核心包到功能包,以依赖的形式构成。 实践中,“项目”可以放在同一个工作空间里,以统一管理和发布编译版本。

Rust将模块化编程引入到嵌入式开发中,也可以方便地编写测试和性能检测代码。 模块化编程能提高Rust嵌入式开发者的工作效率,适应现代化嵌入式软件的需求。

搭建Rust嵌入式生态

生态是软件不可或缺的一部分。从编译器到软件支持,嵌入式Rust目前已经拥有良好的基础生态。 此外,操作系统内核也是嵌入式编程的重要部分,嵌入式Rust和内核开发也有较好的相容度。

截至2020年12月的嵌入式生态架构图

你的架构和指令集

嵌入式Rust的应用支持分为两个部分:一个是目标处理核的支持,一个是芯片外设的支持。

针对目标处理核,首先我们要编译Rust到这个指令集架构。Rust语言提供丰富的编译目标, 主流的编译目标都有很好的支持;此外,如果有自主研发的指令集架构,可以为Rust添加自己的编译目标。 编译完成后,还需要编写微架构支持库和微架构运行时。微架构运行时提供最小的启动代码实现, 能搭建一个适合Rust代码运行的环境。微架构支持库简单包装汇编代码,允许应用代码操作寄存器、运行特殊的指令, 作为编译器系统的补充。这之后,Rust对这个指令集架构的代码运行支持就完成了。

嵌入式应用定义了各有特点的中断控制器,有些是指令集架构定义的,有些是芯片设计厂家自己定义的。 嵌入式Rust要支持这些中断控制器,需要在微架构运行时中添加处理和封装部分,或者作为通用架构的补充, 在专用架构的支持库中添加专有架构的中断运行时。架构虽然定义了标准,但基地址、中断数量等配置可能相互不同。 这些元数据配置可以放在外设访问库的中断部分,和架构支持库共同构成中断控制器的支持。

目标的处理核定义了调试接口和闪存烧写算法,我们需要在调试器软件中编写这些算法。 社区通用的软件“probe-rs”是很好的调试器实现,可以替代OpenOCD,作为非常好的Rust语言调试软件。 如果自己的操作系统有软件调试接口,可以添加操作系统调试器的载荷,共同完成调试软件的部分。 只要处理器厂商实现了调试接口,提供相关的文档,配套的Rust软件可以尽快完成,方便各种技术的开发者调试和使用。

嵌入式生态的标准

起初嵌入式开发者会为每个芯片都编写一次代码。随着生态的发展,大家认识到,需要提供一个基本的抽象, 大家都围绕着抽象去编写,就能剩下大量外设反复操作的时间。embedded-hal就是这样的标准, 它是Rust语言的嵌入式外设抽象,支持大量的片内和片外外设,包括传感器等,很好地扩充了嵌入式的生态。

embedded-hal是统一的Rust语言标准,它是针对外设功能本身的抽象,是抽象的集合,具体实现由实现库去完成。 它的扩展性很好,比如“SPI-GPIO扩展器”外设输入SPI接口抽象,输出GPIO的抽象,很多模块都是抽象到抽象的过程, 就可以方便的极联、衔接和嵌套,整合更多的项目;这就非常容易为新的芯片编写支持库。

市场上海量的芯片都支持embedded-hal标准。K210、GD32V和BL602系列的芯片都提供很好的embedded-hal实现库。 要编写embedded-hal标准的支持库,只需要机器生成外设库,然后编写中间层库,就能完成对此标准的原厂支持。

Rust与操作系统内核

操作系统也是嵌入式应用。常见的操作系统如按是否包含虚拟内存区分,有不含虚拟内存的实时系统, 和包含虚拟内存传统操作系统。基于微架构的支持库和运行时库,操作系统内核可以很方便地编写。

社区中提供了大量成熟的操作系统运行时。 如rCore系列操作系统是第一个基于RISC-V架构的完整Rust操作系统,尤其适合教学使用。 RTIC框架是中断驱动的异步实时系统,完全针对应用使用Rust的宏语法生成,拥有极高的效率。 Tock系统是针对微处理器的安全实时系统,已经用于手表、智能路标和加密狗等产品。

针对操作系统和应用程序开发,Rust是适合编写硬件驱动的语言。 如果使用有产权的代码,可以以混合链接的形式,与Rust代码联合编译为二进制使用。 系统模块、插件和动态链接库等等都能受益于Rust语言内存安全的特性,适合现在对安全敏感的开发需求。

物联网系统要求嵌入式的操作系统能够连上网络。Rust嵌入式社区也在探索射频连接的技术标准, 包括蓝牙、WiFi等硬件标准。smoltcp是社区提供的非常好的TCP协议栈实现,它可以代替lwip, 在嵌入式系统领域高效、安全地完成网络传输。搭配缓冲区和协议库,物联网操作系统就可以连上网了。

RustSBI:新型操作系统引导软件

我们在开发操作系统内核时,有的内核直接运行在裸机上,有的还依托于一个运行环境。 在RISC-V上,“SBI”就是这样的运行环境。它除了引导启动内核,还将常驻后台,提供操作系统需要的实用功能。

RISC-V标准中,“SBI”意味着“操作系统二进制接口”,运行在其上的操作系统会通过环境调用“ecall”指令, 陷入到二进制接口的实现中,由其调用具体硬件的实现功能。这种实现被称作“SBI实现”,社区常用的实现有开源的OpenSBI。 RustSBI是鹏城实验室“rCore代码之夏-2020”活动提出的SBI实现,它是全新的操作系统引导软件。

实现与模块组成

RustSBI由几个功能模块组成。硬件环境接口实现了RISC-V SBI v0.2版本的接口,能运行支持此版本的操作系统。 硬件运行时则是SBI实现运行在裸机环境的必要模块,它将由硬件启动,开始运行所有的RustSBI模块。 SBI的初始化完成后,将进入引导启动模块,这里将发挥SBI标准“引导启动”的功能,最终启动操作系统内核。 另外,兼容性模块能完成硬件到硬件间的支持,能模拟旧版硬件不存在的指令、寄存器,进一步延长操作系统的生命周期。

去年12月,RustSBI的0.1版本在深圳的Rust中国社区2020年年会上发布。使用目前最新的0.1.1版本, RustSBI已经支持大量SBI标准提出的功能,支持大量自定义的扩展功能;完全使用安全的Rust语言编写,提高开发效率。 开发Rust语言的操作系统内核,可以统一编译工具链。另外,RustSBI已经被RISC-V组织收录入RISC-V SBI标准, 它的实现编号为4。

RustSBI是一个库,它以库的形式设计的初衷是,便于平台开发者“积木”式地引入库的模块,为自己的硬件目标开发SBI支持。 虽然RustSBI提供了QEMU、K210平台的参考实现,但应用开发者不应当将自己的目标也加入参考实现中, 而是在自己的仓库里引用RustSBI的模块,可以选择参考这些实现的内容,最终完成完全可控的开发过程。 这两个平台的使用范围较广,参考实现也会长期维护,以发现RustSBI本身可能的少量问题,并及时修补完善。

为什么用Rust开发RustSBI呢?我们认为,相比使用C语言,嵌入式Rust的生态圈在协调发展阶段,它容易支持新硬件, Rust语言较强的编译约束也提高了硬件代码的安全性。

硬件到硬件的兼容性

RISC-V是快速更迭的指令集规范。我们为新版RISC-V硬件编写软件,会遇到与旧版硬件不兼容的情况。 硬件和硬件之间的兼容性,也能通过软件完成——这是RustSBI提供的功能与亮点之一。

RustSBI实现的硬件兼容性,是靠捕获指令异常完成的。例如,K210平台实现的是1.9.1版本的RISC-V特权级标准, 它规定了旧版的页表刷新指令;而目前最新的1.11版标准,规定的是新版的刷新指令。为新标准编写的操作系统内核, 使用新版刷新指令,会因为K210硬件无法找到新版指令,抛出非法指令异常。这个非法指令异常被RustSBI捕获, 它解析后,发现是新版的页表刷新指令,便直接在硬件上运行旧版的指令,完成指令的页表刷新功能。

这种硬件兼容性,目前能支持新增的指令和寄存器。一切情况下,指令、寄存器在仍然存在,但新版中修改了它们的功能和意义。 只靠RustSBI软件本身,就不足以提供兼容性支持了。如果RISC-V芯片实现提供特定的兼容性外设, 比如这个外设能拦截特定CSR寄存器的访问指令,就可以在功能修改的寄存器访问时,产生一个可供软件捕获的中断。 这样的外设设计之后,使用RustSBI软件,将能支持功能修改的指令和寄存器,将进一步提升操作系统内核的硬件兼容性。

兼容旧硬件,也是兼容未来新硬件的过程。未来的RISC-V标准快速发展,将与目前的硬件标准产生一定的差异; 在硬件不变的前提下,未来软件能对当前的硬件兼容,就能延长软件的生命周期。 或许,我们未来升级RISC-V上的操作系统,只需要更换硬件中的RustSBI固件,就能完美兼容最新标准的操作系统了。 升级原有系统的硬件也非常容易,替换RustSBI固件就能达到升级效果。

另外,硬件兼容性也意味着实现硬件上缺少的指令集。当这些指令集运行时,就会陷入到软件中,由RustSBI软件模拟这些指令, 最终返回,这个过程应用软件不会有感知。当然,这种软件模拟过程可以满足正确性,效率不如新版的硬件, 但临时运行一个新版的软件、体验新版的指令集还是足够的。当模拟指令的过程多到影响性能时,也就是硬件该升级的时候了。

RustSBI与嵌入式Rust生态

在RustSBI的实现中,多次使用“embedded-hal”的实现完成编写过程。“embedded-hal”是Rust嵌入式的外设规范, 它对大量厂家的外设提供了软件支持。只要厂家的硬件支持“embedded-hal”,只需要编写部分抽象接口代码, RustSBI支持就可以快速地开发完成。

硬件处理核和SoC系统的开发也受益于设计好的RustSBI软件架构。“RustSBI很快速地实现了仿真环境的双核测试,” 华中科技大学的社区贡献者车春池说,“这能为处理核提供丰富的测试环境,在开发高性能RISC-V处理核中非常重要。”

无论硬件和软件,我们都乐于看到各个应用领域积极互动,嵌入式Rust生态的发展过程得到加快。 “embedded-hal”本是裸机外设的标准,RustSBI将这个标准运用在引导软件上,能加速裸机外设的开发和建设, 也能更快适配SBI标准到平台上。

借这个项目,我们很高兴能参与嵌入式领域Rust语言的建设,希望这些微小的技术更新和迭代,最终能回馈到我们美好的生活中去。


作者简介:

洛佳

华中科技大学网络空间安全学院本科生,热爱操作系统、嵌入式开发。RustSBI项目作者,3年Rust语言开发经验,社区活跃贡献者。目前致力于向产业、教学等更多的领域推广Rust语言。