在嵌入式開發中,不可避免需要操作MCU的外設寄存器。在C/C++
中通常根據芯片手冊的外設寄存器清單來定義一系列的寄存器結構體和位域偏移宏、位域掩碼宏等。C/C++
的寄存器操作接口雖然使得執行效率非常高,且易于編寫,但通常需要非常小心核對寄存器偏移、位域偏移、位域值的有效范圍,因此驅動工程師需要非常小心對照芯片手冊去定義大量的硬件相關的接口,非常考驗耐心和細心。
通常來說,外設寄存器的接口普遍一致,不同的是寄存器名字和偏移值、位域定義等,定義這些接口需要實現大量重復的工作。
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驅動開發中是否也同樣需要定義大量的結構體和常量宏呢?當然不再需要!
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 官方則推薦使用另外一種更加優雅,便捷的方式去操作寄存器。
如上圖所示,Rust
在MCU寄存器與hal庫之間添加一個PAC
(Peripheral Access Crate
外設訪問庫),使用PAC
單獨描述MCU和外設寄存器抽象接口層,該層無需手動編寫代碼,通過工具svd2rust
自動生成。pac將會提供所有寄存器的操作接口,根據寄存器的只讀、只寫、讀寫權限自動生成接口,同時生成位域的屬性和相應的讀或寫接口。保證軟件層不會超出硬件的約束,因而可以避免產生未定義的行為。
以STM32的外設時鐘使能寄存器為例:
pac
提供的接口使用如下:
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
時只需要關注寄存器名稱、位域名稱即可。
在提供便捷的接口的同時,編譯出的二進制代碼的執行效率也幾乎與匯編代碼一致。
參考
STM32F4 Embedded Rust at the PAC: svd2rust (theembeddedrustacean.com)
PACs - Comprehensive Rust ???? (google.github.io)
Rust Embedded terminology - Discovery (rust-embedded.org)