在嵌入式开发中,不可避免需要操作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<u32>,
pub rvr: RW<u32>,
pub cvr: RW<u32>,
pub calib: RO<u32>,
}
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)