在嵌入式開發中,不可避免需要操作MCU的外設寄存器。在C/C++
中通常根據芯片手冊的外設寄存器清單來定義一系列的寄存器結構體和位域偏移宏、位域掩碼宏等。C/C++
的(de)寄存器(qi)(qi)操作接口雖然使得執行(xing)效率非(fei)(fei)常(chang)高(gao),且易于(yu)編寫,但(dan)通常(chang)需要非(fei)(fei)常(chang)小心(xin)核對寄存器(qi)(qi)偏(pian)移、位(wei)域偏(pian)移、位(wei)域值的(de)有效范(fan)圍,因此驅動工程師(shi)需要非(fei)(fei)常(chang)小心(xin)對照芯(xin)片手冊去定義大(da)量的(de)硬(ying)件相(xiang)關(guan)的(de)接口,非(fei)(fei)常(chang)考驗耐心(xin)和細心(xin)。
通(tong)常來說,外設寄存器(qi)的接(jie)口普遍一致,不同的是寄存器(qi)名字(zi)和(he)偏移值、位(wei)域定義等(deng),定義這(zhe)些接(jie)口需(xu)要實現大量重(zhong)復(fu)的工作。
typedef struct
{
uint32_t Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins */
uint32_t Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIO_mode */
uint32_t Pull; /*!< Specifies the Pull-Up or Pull-Down activation for the selected pins.
This parameter can be a value of @ref GPIO_pull */
uint32_t Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIO_speed */
uint32_t Alternate; /*!< Peripheral to be connected to the selected pins
This parameter can be a value of @ref GPIOEx_Alternate_function_selection */
} GPIO_InitTypeDef;
#define GPIO_MODE_INPUT (0x00000000u) /*!< Input Floating Mode */
#define GPIO_MODE_OUTPUT_PP (0x00000001u) /*!< Output Push Pull Mode */
#define GPIO_MODE_OUTPUT_OD (0x00000011u) /*!< Output Open Drain Mode */
#define GPIO_MODE_AF_PP (0x00000002u) /*!< Alternate Function Push Pull Mode */
#define GPIO_MODE_AF_OD (0x00000012u) /*!< Alternate Function Open Drain Mode */
#define GPIO_MODE_ANALOG (0x00000003u) /*!< Analog Mode */
那么在Rust
的MCU驅動開發中(zhong)是否也同樣(yang)需要(yao)定(ding)義大(da)量的結構(gou)體和(he)常量宏呢?當(dang)然(ran)不再需要(yao)!
Rust
是一門高級語言, 最大的特點是安全,同時也非常適合系統級的開發,能直接操作底層內存。Rust
可以像C/C++
一樣操作裸指針,如Systick
驅動可以定義如下
use volatile_register::{RW, RO};
pub struct SystemTimer {
p: &'static mut RegisterBlock
}
#[repr(C)]
struct RegisterBlock {
pub csr: RW,
pub rvr: RW,
pub cvr: RW,
pub calib: RO,
}
impl SystemTimer {
pub fn new() -> SystemTimer {
SystemTimer {
p: unsafe { &mut *(0xE000_E010 as *mut RegisterBlock) }
}
}
pub fn get_time(&self) -> u32 {
self.p.cvr.read()
}
pub fn set_reload(&mut self, reload_value: u32) {
unsafe { self.p.rvr.write(reload_value) }
}
}
pub fn example_usage() -> String {
let mut st = SystemTimer::new();
st.set_reload(0x00FF_FFFF);
format!("Time is now 0x{:08x}", st.get_time())
}
該方式非常類似于C/C++中直接操作外設寄存器地址的方式來編寫驅動,顯而易見可以看到充斥著大量的unsafe
標記的語句,表明該驅動可能調用了太多不安全的接口。
Rust 官方(fang)則推(tui)薦使用另外一種更(geng)加優雅(ya),便(bian)捷的方(fang)式(shi)去操作寄存器。
如上圖所示,Rust
在MCU寄存器與hal庫之間添加一個PAC
(Peripheral Access Crate
外設訪問庫),使用PAC
單獨描述MCU和外設寄存器抽象接口層,該層無需手動編寫代碼,通過工具svd2rust
自動生(sheng)成。pac將會(hui)提(ti)供(gong)所有寄存器的(de)(de)(de)操作接(jie)口(kou),根據寄存器的(de)(de)(de)只讀(du)(du)、只寫(xie)、讀(du)(du)寫(xie)權(quan)限(xian)自動生(sheng)成接(jie)口(kou),同時生(sheng)成位域(yu)的(de)(de)(de)屬性(xing)和相應的(de)(de)(de)讀(du)(du)或寫(xie)接(jie)口(kou)。保證軟件層不會(hui)超出(chu)硬件的(de)(de)(de)約束,因而可以避免產(chan)生(sheng)未定義的(de)(de)(de)行為。
以STM32的外設時鐘使能寄(ji)存器為例:
pac
提供(gong)的接口使(shi)用如下:
let dp = pac::Peripherals::take().unwrap();
dp.RCC
.ahb1enr
.write(|w| w.gpioaen().set_bit().gpiocen().set_bit());
loop {
// Read PC13 Input Value
if !dp.GPIOC.idr.read().idr13().bit() {
// Code if PC13 Low
} else {
// Code if PC13 High
}
}
這種方式能非常直接的被調用,無需關注寄存器操作中常用的移位、與、或、非等位操作。編寫hal
時只需要(yao)關(guan)注(zhu)寄(ji)存器名稱、位域名稱即可。
在提(ti)供便(bian)捷的(de)接口的(de)同(tong)時(shi),編(bian)譯出的(de)二進制代碼(ma)的(de)執行效率(lv)也幾乎與匯編(bian)代碼(ma)一致。
參考
STM32F4 Embedded Rust at the PAC: svd2rust (theembeddedrustacean.com)
PACs - Comprehensive Rust ???? (google.github.io)
Rust Embedded terminology - Discovery (rust-embedded.org)