最近看到一些有趣的東西:

用rust 來寫嵌入式系統,感覺相當生猛,正好最近在上傳說中的jserv 大神的嵌入式系統,就想把嵌入式系統作業用到東西,用rust 實作出來, 主要參考的內容包括上面的armboot,跟作業的mini-arm-os :

本篇相關的原始碼

跟armboot 類似,我用了libarm/stm32f4xx.rs(變體)跟 zero/std_types 這兩個lib,不使用rust std的lib:

mod zero {
    pub mod std_types;
    pub mod zero;
}
#[macro_use]
mod libarm {
#[macro_use]
pub mod stm32f1xx;
}

zero比較簡單,定義一些C 裡面用到的型態應該對應到rust 什麼型態,和rust 基本的trait 如Sized, Copy 等等,不寫的話rustc 會抱怨找不到這些trait; 就像在rust_hello_world這個實作裡面,也是把這些trait 寫在main裡面,我猜這些內容和Rust 的基本設計有關,目前還不是很清楚。

libarm就比較複雜,這感覺像是作者自己寫的,針對的是stm32f4, arm-cortex m4 的硬體。
理論上這裡應該是先研究stm32f4要怎麼初始化,不過我偷懶,先直接把部分的stm32f4 硬體位址修改成stm32f1 的,這樣舊的code 就可以直接搬過來XD。

Stm32f4xx.rs的內容,簡單來說就是大量的直接定義,例如裡面的RCCType,直接對應RCC register的位址,每個位址會是32 bits 的空間,可以和stm32的文件內容一一對應, 下面是我針對STM32f1 修改後的RCC內容:

pub struct RCCType {
    pub CR: uint32_t,
    pub CFGR: uint32_t,
    pub CIR: uint32_t,
    pub APB2RSTR: uint32_t,
    pub APB1RSTR: uint32_t,
    pub AHB1ENR: uint32_t,
    pub APB2ENR: uint32_t,
    pub APB1ENR: uint32_t,
    pub BDCR: uint32_t,
    pub CSR: uint32_t,
}

在最後面,它用函式宣告傳回對應的Type struct:

#[inline(always)]
pub fn RCC() -> &'static mut RCCType {
    unsafe {
        &mut *(RCC_BASE!() as *mut RCCType)
    }
}

RCC_BASE!() 這個Macro,又會照上一篇 提到的 Macro_rule 展開為:

AHB1PERIPH_BASE!() + 0x3000u32

一路展開,最後得到一個32 bits integer,再轉型成RCCType 的mutable pointer,我做的修改就是把RCC, USART2, GPIO的位址換成STM32f1的。
在main 裡面就可以使用 let rcc = RCC() 的方式,取得RCC的pointer,並用像C一樣的操作手法來操作對應的register位址。
例如要修改APB1ENR,啟動週邊的clock:

let rcc = RCC();
rcc.APB1ENR |= 0x00020000u32;

或是對usart2 的位址取值都沒問題:

let usart2 = USART2();
while(true) {
    while(usart2.SR & 0x0080u16 == 0) {}
    usart2.DR = 'x' as uint16_t;
}

這裡我們只輸出’x’,這是因為要把text 印出來,我們需要對str 作iterate,而這個東西是定義在rust 的core lib 裡面, 一般安裝後在 /usr/lib/rustlib 裡面只會有 x86_64_unknown_linux_gnu,如果要跑arm要先自己編譯arm的lib, 可見相關的內容
這步比較麻煩先跳過,之後研究出來再另文介紹。

另一個要解決的問題則是 isr_vector,這裡可以看到一種很謎樣的寫法,用 link_section 這個attribute,定義區段名為 .isr_vector, 並設定為一個array,內含一個extern “c” fn(),如果要需要其他的ISR,則可以在後面寫更多的function,並把1改為需要的數量。

#[link_section=".isr_vector"]
pub static ISRVECTORS: [unsafe extern "C" fn(); 1] = [
    main,
];

linker script裡面,先保留一個LONG 的寬度指向初始化stack pointer,接著放isr_vector的reset handler,再放其他的.text,這樣裝置一上電就會執行main裡的內容。

.text :
{
    LONG(_stackStart); /* Initial stack pointer */
    KEEP(*(.isr_vector)) /* ISR vector entry point to main */
    *(.text)
    *(.text*)
} > FLASH

如果我們把最終執行檔反組譯,會看到其中的位址配置,0x0指向stack start,0x4 reset_handler指向位在0x08 的main。:

Disassembly of section .text:
00000000 <_ZN10ISRVECTORS20h538ad2a8e3805addk6aE-0x4>:
0: 10010000 .word 0x10010000

00000004 <_ZN10ISRVECTORS20h538ad2a8e3805addk6aE>:
4: 00000009 ....

00000008 <main>:

執行結果,會印出滿坑滿谷的 ‘x’,我加一個條件讓它只print 100個:
xxx

這份rust code 裡面有用到大量的rust attribute,也就是function 前的#[attribute],這也可以另外寫一篇文……
啊感覺挖了自己一堆坑,要趕快填坑了OAO。

結語:

在這篇文我試著解釋如何用rust 撰寫嵌入式系統程式,結論當然是做得到的,但整體比C寫的原始碼複雜得多,也不像C這麼直覺,編輯libarm 也是非常麻煩的工作。
由於嵌入式系統的code 通常都不會複雜到哪裡去(唔…大概吧),發揮不出Rust的優勢,我認為比起來還是寫C會有效率得多。