在嵌入式开发中,为新芯片构建开发环境总是一个充满挑战的过程. 本文将详细介绍如何使用 Rust 语言在 GD32VW55x 上实现LED闪烁和串口通信功能. 整个过程涉及从 SEGGER 寄存器描述文件到 SVD 文件的转换,使用 `svd2rust` 生成外设库,以及最终编写硬件驱动代码.
大多数芯片厂商提供各种格式的寄存器描述文件,SEGGER Embedded Studio 的寄存器描述 XML 格式(下简称 SEGGER 寄存器描述)是其中一种常见格式. 然而,Rust 嵌入式生态系统通常使用最主流的 ARM 的 SVD 格式来描述微控制器的外设和寄存器. 虽然 SVD 格式是 ARM 的标准,但其内容本身架构中立,可以描述各类不同架构的嵌入式设备.
GigaDevice 官方提供的 SEGGER 工程中,都附带了一个 XML 文件,其内容就是 SEGGER 的寄存器描述文件,主要用于调试目的.
- <?xml version="1.0" encoding="UTF-8"?>
- <device schemaVersion="1.1" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:noNamespaceSchemaLocation="CMSIS-SVD.xsd">
- <name>GD32VW553x</name>
- <description>GD32VW553x RISC-V Microcontroller based device</description>
- <peripherals>
- <peripheral>
- <name>ADC</name>
- <description>Analog to digital converter</description>
- <baseAddress>0x40012000</baseAddress>
- <registers>
- <register>
- <name>STAT</name>
- <description>status register</description>
- <addressOffset>0x0</addressOffset>
- <size>32</size>
- <resetValue>0x00000000</resetValue>
- <resetMask>0xFFFFFFFF</resetMask>
- <fields>
- <field>
- <name>ROVF</name>
- <description>Regular data register overflow</description>
- <bitOffset>5</bitOffset>
- <bitWidth>1</bitWidth>
- </field>
- <field>
- <name>STRC</name>
- <description>Start flag of regular channel group</description>
- <bitOffset>4</bitOffset>
- <bitWidth>1</bitWidth>
- </field>
- <field>
- <name>STIC</name>
- <description>Start flag of inserted channel group</description>
- <bitOffset>3</bitOffset>
- <bitWidth>1</bitWidth>
- </field>
- <field>
- <name>EOIC</name>
- <description>End of inserted group conversion flag</description>
- <bitOffset>2</bitOffset>
- <bitWidth>1</bitWidth>
- </field>
- <field>
- <name>EOC</name>
- <description>End of group conversion flag</description>
- <bitOffset>1</bitOffset>
- <bitWidth>1</bitWidth>
- </field>
- <field>
- <name>WDE</name>
- <description>Analog watchdog event flag</description>
- <bitOffset>0</bitOffset>
- <bitWidth>1</bitWidth>
- </field>
- </fields>
- </register>
- <!-- 省略其它 register -->
- </registers>
- </peripheral>
- <!-- 省略其它 peripheral -->
- </peripherals>
- </device>
第二步:使用 svd2rust 生成外设库获得 SVD 文件后,下一步是使用 `svd2rust` 工具生成 Rust 外设访问库,`svd2rust` 是一个强大的工具,它能够根据 SVD 文件自动生成类型安全的 Rust API 来访问硬件寄存器.
svd2rust -i gd32vw553.svd --target riscvform -i lib.rs -o src生成的主要结构包括
这个过程会生成一个完整的 Rust crate,包含所有外设和寄存器的类型定义和访问方法,生成的主要结构包括
- Peripherals 结构体:提供对所有外设的单例访问
- 每个外设的模块和对应的寄存器定义
- 类型安全的位字段操作 API
第三步:编写硬件驱动代码有了外设库后,我们就可以编写具体的硬件驱动代码了,以下是如何实现 LED 控制和串口通信的示例.
项目配置首先,需要在 `Cargo.toml` 中添加依赖:
- [dependencies]
- gd32vw55x = { path = "../gd32vw55x" }
- riscv-rt = "0.10"
内存布局描述我们可以参考 GigaDevice 官方手册中的内存布局,最重要的是 `FLASH` 与 `RAM` 这两个区域.
编写一份 linker script 描述内存布局
- /*
- Reference: GD32VW553xxDatasheet_Rev1.2.pdf
- 2.4 Memory map
- */
- MEMORY
- {
- ROM (rx) : ORIGIN = 0x0bf40000, LENGTH = 256K
- FLASH (rwx) : ORIGIN = 0x08000000, LENGTH = 4M
- RAM (wxa) : ORIGIN = 0x20000000, LENGTH = 320K
- }
- REGION_ALIAS("REGION_TEXT", FLASH);
- REGION_ALIAS("REGION_RODATA", FLASH);
- REGION_ALIAS("REGION_DATA", RAM);
- REGION_ALIAS("REGION_BSS", RAM);
- REGION_ALIAS("REGION_HEAP", RAM);
- REGION_ALIAS("REGION_STACK", RAM);
LED 控制实现
- int main(void)
- {
- /* enable the LED clock */
- rcu_periph_clock_enable(RCU_GPIOA);
- /* configure LED GPIO port */
- gpio_mode_set(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6);
- gpio_bit_reset(GPIOA, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6);
- while(1) {
- /* turn on LED1, turn off LED3 */
- gpio_bit_set(GPIOA, GPIO_PIN_4);
- gpio_bit_reset(GPIOA, GPIO_PIN_6);
- delay_1ms(1000);
- /* turn on LED2, turn off LED1 */
- gpio_bit_set(GPIOA, GPIO_PIN_5);
- gpio_bit_reset(GPIOA, GPIO_PIN_4);
- delay_1ms(1000);
- /* turn on LED3, turn off LED2 */
- gpio_bit_set(GPIOA, GPIO_PIN_6);
- gpio_bit_reset(GPIOA, GPIO_PIN_5);
- delay_1ms(1000);
- }
- }
我们参考官方例程 `Running_led` 来设置对应的寄存器,需要首先在 `RCU` 的 `AHB` 块中启用外设时钟,然后再在对应的 GPIO 控制块中配置为输出模式(默认是输入模式),然后就可以通过 `BOP` 与 `CR` 这两个寄存器控制 GPIO 状态了.
查询原理图知道板载的 LED 位于 `PB2`
我们首先要启用的应该是 `AHB1` 这个块下的 `PBEN` 寄存器,具体的 GPIO 控制块应该是 `GPIOB`,然后所有与 GPIO 配置相关的字段都应该是寄存器内第 2 个(从 0 开始)同类项,例如 `CTL2` / `BOP2` / `CR2`.

- #![no_main]
- #![no_std]
- use riscv_rt::entry;
- use gd32vw55x::Peripherals;
- use core::fmt::Write;
- #[panic_handler]
- fn panic(_info: &core::panic::PanicInfo) -> ! {
- loop {}
- }
- struct Led;
- impl Led {
- fn enable() {
- let peripherals = unsafe { Peripherals::steal() };
- peripherals.rcu.ahb1en().modify(|_r, w| {
- w.pben().set_bit()
- });
- peripherals.gpiob.ctl().modify(|_r, w| unsafe {
- // output
- w.ctl2().bits(0b01)
- });
- }
- fn set(value: bool) {
- let peripherals = unsafe { Peripherals::steal() };
- if value {
- peripherals.gpiob.bop().modify(|_r, w| {
- w.bop2().set_bit()
- });
- } else {
- peripherals.gpiob.bop().modify(|_r, w| {
- w.cr2().set_bit()
- });
- }
- }
- }
串口实现我们仍然照葫芦画瓢,参考了官方的 BLE 串口例程中的配置过程.
- void app_uart_config(void)
- {
- rcu_periph_clock_disable(RCU_GPIOA);
- rcu_periph_clock_disable(RCU_GPIOB);
- rcu_periph_clock_disable(RCU_UART2);
- rcu_periph_clock_enable(RCU_GPIOA);
- rcu_periph_clock_enable(RCU_GPIOB);
- rcu_periph_clock_enable(RCU_UART2);
- gpio_af_set(UART2_TX_GPIO, UART2_TX_AF_NUM, UART2_TX_PIN);
- gpio_af_set(UART2_RX_GPIO, UART2_RX_AF_NUM, UART2_RX_PIN);
- gpio_mode_set(UART2_TX_GPIO, GPIO_MODE_AF, GPIO_PUPD_NONE, UART2_TX_PIN);
- gpio_output_options_set(UART2_TX_GPIO, GPIO_OTYPE_PP, GPIO_OSPEED_25MHZ, UART2_TX_PIN);
- gpio_mode_set(UART2_RX_GPIO, GPIO_MODE_AF, GPIO_PUPD_NONE, UART2_RX_PIN);
- gpio_output_options_set(UART2_RX_GPIO, GPIO_OTYPE_PP, GPIO_OSPEED_25MHZ, UART2_RX_PIN);
- /* close printf buffer */
- setvbuf(stdout, NULL, _IONBF, 0);
- usart_deinit(UART2);
- usart_word_length_set(UART2, USART_WL_8BIT);
- usart_stop_bit_set(UART2, USART_STB_1BIT);
- usart_parity_config(UART2, USART_PM_NONE);
- usart_baudrate_set(UART2, 115200U);
- usart_receive_config(UART2, USART_RECEIVE_ENABLE);
- usart_transmit_config(UART2, USART_TRANSMIT_ENABLE);
- usart_interrupt_enable(UART2, USART_INT_RBNE);
- usart_receive_fifo_enable(UART2);
- usart_enable(UART2);
- /*wait IDLEF set and clear it*/
- while(RESET == usart_flag_get(UART2, USART_FLAG_IDLE)) {
- }
- usart_flag_clear(UART2, USART_FLAG_IDLE);
- usart_interrupt_enable(UART2, USART_INT_IDLE);
- }
类似地复位/启用相应的 `AHB` 和 `APB` 块,配置相应 GPIO 的 alternate function
这里我们暂时不考虑 RX,只考虑设备向计算机 TX 数据,因此不配置 `PA8`;与此同时,我们还需要配置 U(S)ART 相关的寄存器,其中的波特率生成部分值得说一说. U(S)ART 外设默认的过采样率是 16,系统默认的时钟是 `16 MHz`,根据手册的计算公式可以计算生成 `115200 bps` 波特率的因子应该是 `16000000 / (16 * 115200) ≈ 8.6806`. 设备的波特率发生器的配置寄存器采用 UQ12.4 的定点数的配置(12 bit 整数 + 4 bit 定点数表示分数部分),因此整数部分配置为 `8`,而定点数部分量化为 `0.6806 * (2**4) ≈ 11`.

- struct Usart;
- impl Usart {
- fn new() -> Self {
- let peripherals = unsafe { Peripherals::steal() };
- peripherals.rcu.ahb1en().modify(|_r, w| w.paen().set_bit());
- peripherals.rcu.ahb1en().modify(|_r, w| w.pben().set_bit());
- peripherals.rcu.apb1en().modify(|_r, w| {
- w.uart1en().clear_bit();
- w.uart1en().set_bit()
- });
- peripherals.rcu.apb1rst().modify(|_r, w| {
- w.uart1rst().set_bit();
- w.uart1rst().clear_bit()
- });
- peripherals.gpiob.ctl().modify(|_r, w| unsafe {
- // af mode
- w.ctl15().bits(0b10)
- });
- peripherals.gpiob.afsel1().modify(|_r, w| unsafe {
- // af7: uart1_tx
- w.sel15().bits(0x07)
- });
- peripherals.gpiob.ospd().modify(|_r, w| unsafe {
- // output speed
- w.ospd15().bits(0b11)
- });
- peripherals.uart1.ctl0().modify(|_r, w| {
- w.uen().clear_bit();
- // word length 8
- w.wl().clear_bit();
- // parity none
- w.pcen().clear_bit();
- // enable tx
- w.ten().set_bit()
- });
- peripherals.uart1.ctl1().modify(|_r, w| unsafe {
- w.stb().bits(0b00)
- });
- peripherals.uart1.baud().modify(|_r, w| unsafe {
- // brr = uclk / (oversample * baud rate)
- // 115200 bps
- w.brr_int().bits(0x8);
- w.brr_fra().bits(11)
- });
- peripherals.uart1.ctl0().modify(|_r, w| {
- w.uen().set_bit()
- });
- while peripherals.uart1.stat().read().idlef().bit_is_set() {}
- peripherals.uart1.stat().modify(|_r, w| w.idlef().clear_bit());
- Self
- }
- fn write(&self, data: u8) {
- let peripherals = unsafe { Peripherals::steal() };
- while peripherals.uart1.stat().read().tbe().bit_is_clear() {}
- peripherals.uart1.tdata().write(|w| unsafe { w.tdata().bits(data as u16) });
- while peripherals.uart1.stat().read().tc().bit_is_clear() {}
- }
- }
- impl Write for Usart {
- fn write_str(&mut self, s: &str) -> core::fmt::Result {
- s.as_bytes().iter().for_each(|b| self.write(*b));
- Ok(())
- }
- }
主应用程序因为我们暂时没有实现 `SysTick` 相关的操作,延时使用并不精确的循环实现,格式化使用 `write!` 宏.
- #[entry]
- fn main() -> ! {
- let mut usart = Usart::new();
- let mut counter = 0u8;
- let mut value = false;
- Led::enable();
- loop {
- Led::set(value);
- let _ = write!(usart, "Counter: {:#02x}\r\n", counter);
- for i in 0..100_000 {
- core::hint::black_box(i);
- }
- value = !value;
- counter += 1;
- }
- }
遇到的挑战与解决方案在整个过程中,我们遇到了几个主要的挑战:
SVD 文件格式差异:SEGGER 的 XML 格式与 SVD 格式在描述寄存器时有细微但重要的差异,需要仔细处理位字段和访问权限的映射;
寄存器命名不一致:厂商提供的文档中的寄存器名称有时与XML文件中的不一致,需要手动检查和修正;
时钟配置:新芯片的时钟系统配置往往比较复杂,需要仔细阅读参考手册以确保正确配置;
外设依赖关系:某些外设需要先启用相关时钟才能正常工作,这种依赖关系需要在代码中正确处理.
结论通过 SES2SVD 工具将 SEGGER 寄存器描述转换为 SVD 格式,再使用 svd2rust 生成类型安全的外设访问库,我们成功地为新的 RISC-V 芯片构建了 Rust 开发环境. 这种方法通过Rust的类型系统大大减少了硬件访问错误的风险.
最终,我们实现了 LED 闪烁和串口通信功能,为后续更复杂的应用开发奠定了基础;这种从底层寄存器描述到高级应用开发的完整流程,展示了 Rust 在嵌入式系统开发中的强大能力和灵活性.
这种方法可以推广到其他芯片平台,为 Rust 在嵌入式领域的发展提供了可行的技术路线.