Rust 語言在嵌入式領域的應用

Rust 語言是二十一世紀的語言新星。Rust 被人廣泛承認的一點,就是因爲它能運行在多樣的目標上,從桌面和服務器設備,到資源有限的嵌入式設備。

我們可以用適合來評價一門語言和技術。Rust 非常適合開發嵌入式應用,它是一種和 C 相仿的、能應用於嵌入式設備開發的編程語言。

操作系統都是從裸機設備開始運行的,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 和內核開發也有較好的相容度。

Rust 語言的嵌入式生態

你的架構和指令集

嵌入式 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 語言的建設,希望這些微小的技術更新和迭代,最終能回饋到未來物聯網行業更輕便、更安全的開發體驗中去。(本文作者:洛佳)

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JlV6vDZTjXL7Ml8xUdtqCw