How to safely operate MCU registers in Rust
In embedded development, it is inevitable to operate the peripheral registers of the MCU. In ,
C/C++
a series of register structures and bit field offset macros, bit field mask macros, etc. are usually defined according to the peripheral register list in the chip manual.
C/C++
Although the register operation interface of makes the execution efficiency very high and easy to write, it is usually necessary to carefully check the valid range of register offset, bit field offset, and bit field value. Therefore, driver engineers need to be very careful to define a large number of hardware-related interfaces by comparing with the chip manual, which is a test of patience and carefulness.
Generally speaking, the interfaces of peripheral registers are generally consistent, and the only differences are the register names, offset values, bit field definitions, etc. Defining these interfaces requires a lot of repetitive work.
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 */
So,
Rust
is it also necessary to define a large number of structures and constant macros in the development of MCU drivers? Of course not!
Rust
It is a high-level language. Its biggest feature is security. It is also very suitable for system-level development and can directly operate the underlying memory.
Rust
It can
C/C++
operate raw pointers like , such as
Systick
The driver can be defined as follows
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())
}
This method is very similar to the way of writing drivers in C/C++ by directly operating the peripheral register address. Obviously, it can be seen that there are a lot of
unsafe
marked statements, indicating that the driver may call too many unsafe interfaces.
Rust officially recommends using another more elegant and convenient way to operate registers.
As shown in the figure above,
add a
(
peripheral access library)
Rust
between the MCU register and the hal library
, and use to
describe the MCU and peripheral register abstract interface layer separately. This layer does not need to be manually written, and
is automatically generated by the tool . PAC will provide the operation interface of all registers, automatically generate interfaces according to the read-only, write-only, and read-write permissions of the registers, and generate the attributes of the bit field and the corresponding read or write interface. Ensure that the software layer does not exceed the constraints of the hardware, thereby avoiding undefined behavior.
PAC
Peripheral Access Crate
PAC
svd2rust
Take the STM32 peripheral clock enable register as an example:
pac
The provided interfaces are used as follows:
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
}
}
This method can be called very directly without paying attention to the commonly used shift, AND, OR, and non-equal operations in register operations.
hal
When writing , you only need to pay attention to the register name and bit field name.
While providing a convenient interface, the execution efficiency of the compiled binary code is almost the same as that of the assembly code.
refer to
STM32F4 Embedded Rust at the PAC: svd2rust (theembeddedrustacean.com)
PACs - Comprehensive Rust ???? (google.github.io)
Rust Embedded terminology - Discovery (rust-embedded.org)