GD32VW55x 使用 Rust 实现串口通信与 LED 闪烁

[复制链接]
975|0
本帖最后由 iCEasy商城-小易 于 2025-9-28 13:48 编辑


本文转载自iCEasy商城口碑评测作者:scgummy
欢迎大家戳链接GD32VW55x 使用 Rust 实现串口通信与 LED 闪烁与原贴作者互动交流哦~
在嵌入式开发中,为新芯片构建开发环境总是一个充满挑战的过程. 本文将详细介绍如何使用 Rust 语言在 GD32VW55x 上实现LED闪烁和串口通信功能. 整个过程涉及从 SEGGER 寄存器描述文件到 SVD 文件的转换,使用 `svd2rust` 生成外设库,以及最终编写硬件驱动代码.
开源口碑分享内容
引言
在嵌入式开发中,为新芯片构建开发环境总是一个充满挑战的过程. 本文将详细介绍如何使用 Rust 语言在 GD32VW55x 上实现LED闪烁和串口通信功能. 整个过程涉及从 SEGGER 寄存器描述文件到 SVD 文件的转换,使用 `svd2rust` 生成外设库,以及最终编写硬件驱动代码.
这次我们使用的板子是全新升级的 V2,自带 CH340N,并且带有按键、拨码、LED,测评更加方便~
第一步:从 SEGGER 寄存器描述到 SVD 文件
大多数芯片厂商提供各种格式的寄存器描述文件,SEGGER Embedded Studio 的寄存器描述 XML 格式(下简称 SEGGER 寄存器描述)是其中一种常见格式. 然而,Rust 嵌入式生态系统通常使用最主流的 ARM 的 SVD 格式来描述微控制器的外设和寄存器. 虽然 SVD 格式是 ARM 的标准,但其内容本身架构中立,可以描述各类不同架构的嵌入式设备.
SEGGER 寄存器描述示例
GigaDevice 官方提供的 SEGGER 工程中,都附带了一个 XML 文件,其内容就是 SEGGER 的寄存器描述文件,主要用于调试目的.
使用 SES2SVD 工具转换
为了解决格式转换问题,我开发了一个名为 SES2SVD 的工具,它能够将 SEGGER 的寄存器描述 XML 转换为标准的SVD文件,转换过程大致如下:
  • 解析 SEGGER XML 文件的结构
  • 映射到 SVD 的数据模型
  • 处理位字段和寄存器访问权限
  • 生成符合 CMSIS-SVD 标准的 XML 文件
生成 SVD 文件结果
转换后的 SVD 文件包含完整的外设、寄存器和位字段描述,下面为节选的片段
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <device schemaVersion="1.1" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:noNamespaceSchemaLocation="CMSIS-SVD.xsd">
  3. <name>GD32VW553x</name>
  4. <description>GD32VW553x RISC-V Microcontroller based device</description>
  5. <peripherals>
  6. <peripheral>
  7. <name>ADC</name>
  8. <description>Analog to digital converter</description>
  9. <baseAddress>0x40012000</baseAddress>
  10. <registers>
  11. <register>
  12. <name>STAT</name>
  13. <description>status register</description>
  14. <addressOffset>0x0</addressOffset>
  15. <size>32</size>
  16. <resetValue>0x00000000</resetValue>
  17. <resetMask>0xFFFFFFFF</resetMask>
  18. <fields>
  19. <field>
  20. <name>ROVF</name>
  21. <description>Regular data register overflow</description>
  22. <bitOffset>5</bitOffset>
  23. <bitWidth>1</bitWidth>
  24. </field>
  25. <field>
  26. <name>STRC</name>
  27. <description>Start flag of regular channel group</description>
  28. <bitOffset>4</bitOffset>
  29. <bitWidth>1</bitWidth>
  30. </field>
  31. <field>
  32. <name>STIC</name>
  33. <description>Start flag of inserted channel group</description>
  34. <bitOffset>3</bitOffset>
  35. <bitWidth>1</bitWidth>
  36. </field>
  37. <field>
  38. <name>EOIC</name>
  39. <description>End of inserted group conversion flag</description>
  40. <bitOffset>2</bitOffset>
  41. <bitWidth>1</bitWidth>
  42. </field>
  43. <field>
  44. <name>EOC</name>
  45. <description>End of group conversion flag</description>
  46. <bitOffset>1</bitOffset>
  47. <bitWidth>1</bitWidth>
  48. </field>
  49. <field>
  50. <name>WDE</name>
  51. <description>Analog watchdog event flag</description>
  52. <bitOffset>0</bitOffset>
  53. <bitWidth>1</bitWidth>
  54. </field>
  55. </fields>
  56. </register>
  57. <!-- 省略其它 register -->
  58. </registers>
  59. </peripheral>
  60. <!-- 省略其它 peripheral -->
  61. </peripherals>
  62. </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` 中添加依赖:
  1. [dependencies]
  2. gd32vw55x = { path = "../gd32vw55x" }
  3. riscv-rt = "0.10"
内存布局描述
我们可以参考 GigaDevice 官方手册中的内存布局,最重要的是 `FLASH` 与 `RAM` 这两个区域.
编写一份 linker script 描述内存布局
  1. /*
  2. Reference: GD32VW553xxDatasheet_Rev1.2.pdf
  3. 2.4 Memory map
  4. */


  5. MEMORY
  6. {
  7. ROM (rx) : ORIGIN = 0x0bf40000, LENGTH = 256K
  8. FLASH (rwx) : ORIGIN = 0x08000000, LENGTH = 4M
  9. RAM (wxa) : ORIGIN = 0x20000000, LENGTH = 320K
  10. }


  11. REGION_ALIAS("REGION_TEXT", FLASH);
  12. REGION_ALIAS("REGION_RODATA", FLASH);
  13. REGION_ALIAS("REGION_DATA", RAM);
  14. REGION_ALIAS("REGION_BSS", RAM);
  15. REGION_ALIAS("REGION_HEAP", RAM);
  16. REGION_ALIAS("REGION_STACK", RAM);
LED 控制实现
  1. int main(void)
  2. {
  3. /* enable the LED clock */
  4. rcu_periph_clock_enable(RCU_GPIOA);
  5. /* configure LED GPIO port */
  6. gpio_mode_set(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6);


  7. gpio_bit_reset(GPIOA, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6);


  8. while(1) {
  9. /* turn on LED1, turn off LED3 */
  10. gpio_bit_set(GPIOA, GPIO_PIN_4);
  11. gpio_bit_reset(GPIOA, GPIO_PIN_6);
  12. delay_1ms(1000);


  13. /* turn on LED2, turn off LED1 */
  14. gpio_bit_set(GPIOA, GPIO_PIN_5);
  15. gpio_bit_reset(GPIOA, GPIO_PIN_4);
  16. delay_1ms(1000);


  17. /* turn on LED3, turn off LED2 */
  18. gpio_bit_set(GPIOA, GPIO_PIN_6);
  19. gpio_bit_reset(GPIOA, GPIO_PIN_5);
  20. delay_1ms(1000);
  21. }
  22. }
我们参考官方例程 `Running_led` 来设置对应的寄存器,需要首先在 `RCU` 的 `AHB` 块中启用外设时钟,然后再在对应的 GPIO 控制块中配置为输出模式(默认是输入模式),然后就可以通过 `BOP` 与 `CR` 这两个寄存器控制 GPIO 状态了.
查询原理图知道板载的 LED 位于 `PB2`
我们首先要启用的应该是 `AHB1` 这个块下的 `PBEN` 寄存器,具体的 GPIO 控制块应该是 `GPIOB`,然后所有与 GPIO 配置相关的字段都应该是寄存器内第 2 个(从 0 开始)同类项,例如 `CTL2` / `BOP2` / `CR2`.
  1. #![no_main]
  2. #![no_std]


  3. use riscv_rt::entry;
  4. use gd32vw55x::Peripherals;
  5. use core::fmt::Write;


  6. #[panic_handler]
  7. fn panic(_info: &core::panic::PanicInfo) -> ! {
  8. loop {}
  9. }


  10. struct Led;


  11. impl Led {
  12. fn enable() {
  13. let peripherals = unsafe { Peripherals::steal() };


  14. peripherals.rcu.ahb1en().modify(|_r, w| {
  15. w.pben().set_bit()
  16. });


  17. peripherals.gpiob.ctl().modify(|_r, w| unsafe {
  18. // output
  19. w.ctl2().bits(0b01)
  20. });
  21. }


  22. fn set(value: bool) {
  23. let peripherals = unsafe { Peripherals::steal() };


  24. if value {
  25. peripherals.gpiob.bop().modify(|_r, w| {
  26. w.bop2().set_bit()
  27. });
  28. } else {
  29. peripherals.gpiob.bop().modify(|_r, w| {
  30. w.cr2().set_bit()
  31. });
  32. }
  33. }
  34. }
串口实现
我们仍然照葫芦画瓢,参考了官方的 BLE 串口例程中的配置过程.
  1. void app_uart_config(void)
  2. {
  3. rcu_periph_clock_disable(RCU_GPIOA);
  4. rcu_periph_clock_disable(RCU_GPIOB);
  5. rcu_periph_clock_disable(RCU_UART2);


  6. rcu_periph_clock_enable(RCU_GPIOA);
  7. rcu_periph_clock_enable(RCU_GPIOB);
  8. rcu_periph_clock_enable(RCU_UART2);


  9. gpio_af_set(UART2_TX_GPIO, UART2_TX_AF_NUM, UART2_TX_PIN);
  10. gpio_af_set(UART2_RX_GPIO, UART2_RX_AF_NUM, UART2_RX_PIN);
  11. gpio_mode_set(UART2_TX_GPIO, GPIO_MODE_AF, GPIO_PUPD_NONE, UART2_TX_PIN);
  12. gpio_output_options_set(UART2_TX_GPIO, GPIO_OTYPE_PP, GPIO_OSPEED_25MHZ, UART2_TX_PIN);
  13. gpio_mode_set(UART2_RX_GPIO, GPIO_MODE_AF, GPIO_PUPD_NONE, UART2_RX_PIN);
  14. gpio_output_options_set(UART2_RX_GPIO, GPIO_OTYPE_PP, GPIO_OSPEED_25MHZ, UART2_RX_PIN);


  15. /* close printf buffer */
  16. setvbuf(stdout, NULL, _IONBF, 0);


  17. usart_deinit(UART2);
  18. usart_word_length_set(UART2, USART_WL_8BIT);
  19. usart_stop_bit_set(UART2, USART_STB_1BIT);
  20. usart_parity_config(UART2, USART_PM_NONE);
  21. usart_baudrate_set(UART2, 115200U);
  22. usart_receive_config(UART2, USART_RECEIVE_ENABLE);
  23. usart_transmit_config(UART2, USART_TRANSMIT_ENABLE);
  24. usart_interrupt_enable(UART2, USART_INT_RBNE);
  25. usart_receive_fifo_enable(UART2);


  26. usart_enable(UART2);


  27. /*wait IDLEF set and clear it*/
  28. while(RESET == usart_flag_get(UART2, USART_FLAG_IDLE)) {


  29. }


  30. usart_flag_clear(UART2, USART_FLAG_IDLE);
  31. usart_interrupt_enable(UART2, USART_INT_IDLE);
  32. }
类似地复位/启用相应的 `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`.
  1. struct Usart;


  2. impl Usart {
  3. fn new() -> Self {
  4. let peripherals = unsafe { Peripherals::steal() };
  5. peripherals.rcu.ahb1en().modify(|_r, w| w.paen().set_bit());
  6. peripherals.rcu.ahb1en().modify(|_r, w| w.pben().set_bit());
  7. peripherals.rcu.apb1en().modify(|_r, w| {
  8. w.uart1en().clear_bit();
  9. w.uart1en().set_bit()
  10. });


  11. peripherals.rcu.apb1rst().modify(|_r, w| {
  12. w.uart1rst().set_bit();
  13. w.uart1rst().clear_bit()
  14. });


  15. peripherals.gpiob.ctl().modify(|_r, w| unsafe {
  16. // af mode
  17. w.ctl15().bits(0b10)
  18. });
  19. peripherals.gpiob.afsel1().modify(|_r, w| unsafe {
  20. // af7: uart1_tx
  21. w.sel15().bits(0x07)
  22. });
  23. peripherals.gpiob.ospd().modify(|_r, w| unsafe {
  24. // output speed
  25. w.ospd15().bits(0b11)
  26. });


  27. peripherals.uart1.ctl0().modify(|_r, w| {
  28. w.uen().clear_bit();


  29. // word length 8
  30. w.wl().clear_bit();


  31. // parity none
  32. w.pcen().clear_bit();


  33. // enable tx
  34. w.ten().set_bit()
  35. });


  36. peripherals.uart1.ctl1().modify(|_r, w| unsafe {
  37. w.stb().bits(0b00)
  38. });


  39. peripherals.uart1.baud().modify(|_r, w| unsafe {
  40. // brr = uclk / (oversample * baud rate)
  41. // 115200 bps
  42. w.brr_int().bits(0x8);
  43. w.brr_fra().bits(11)
  44. });


  45. peripherals.uart1.ctl0().modify(|_r, w| {
  46. w.uen().set_bit()
  47. });


  48. while peripherals.uart1.stat().read().idlef().bit_is_set() {}
  49. peripherals.uart1.stat().modify(|_r, w| w.idlef().clear_bit());


  50. Self
  51. }


  52. fn write(&self, data: u8) {
  53. let peripherals = unsafe { Peripherals::steal() };
  54. while peripherals.uart1.stat().read().tbe().bit_is_clear() {}
  55. peripherals.uart1.tdata().write(|w| unsafe { w.tdata().bits(data as u16) });
  56. while peripherals.uart1.stat().read().tc().bit_is_clear() {}
  57. }
  58. }


  59. impl Write for Usart {
  60. fn write_str(&mut self, s: &str) -> core::fmt::Result {
  61. s.as_bytes().iter().for_each(|b| self.write(*b));
  62. Ok(())
  63. }
  64. }
主应用程序
因为我们暂时没有实现 `SysTick` 相关的操作,延时使用并不精确的循环实现,格式化使用 `write!` 宏.
  1. #[entry]
  2. fn main() -> ! {
  3. let mut usart = Usart::new();


  4. let mut counter = 0u8;
  5. let mut value = false;


  6. Led::enable();


  7. loop {
  8. Led::set(value);
  9. let _ = write!(usart, "Counter: {:#02x}\r\n", counter);
  10. for i in 0..100_000 {
  11. core::hint::black_box(i);
  12. }
  13. value = !value;
  14. counter += 1;
  15. }
  16. }
遇到的挑战与解决方案
在整个过程中,我们遇到了几个主要的挑战:
SVD 文件格式差异:SEGGER 的 XML 格式与 SVD 格式在描述寄存器时有细微但重要的差异,需要仔细处理位字段和访问权限的映射;
寄存器命名不一致:厂商提供的文档中的寄存器名称有时与XML文件中的不一致,需要手动检查和修正;
时钟配置:新芯片的时钟系统配置往往比较复杂,需要仔细阅读参考手册以确保正确配置;
外设依赖关系:某些外设需要先启用相关时钟才能正常工作,这种依赖关系需要在代码中正确处理.
结论
通过 SES2SVD 工具将 SEGGER 寄存器描述转换为 SVD 格式,再使用 svd2rust 生成类型安全的外设访问库,我们成功地为新的 RISC-V 芯片构建了 Rust 开发环境. 这种方法通过Rust的类型系统大大减少了硬件访问错误的风险.
最终,我们实现了 LED 闪烁和串口通信功能,为后续更复杂的应用开发奠定了基础;这种从底层寄存器描述到高级应用开发的完整流程,展示了 Rust 在嵌入式系统开发中的强大能力和灵活性.
这种方法可以推广到其他芯片平台,为 Rust 在嵌入式领域的发展提供了可行的技术路线.





您需要登录后才可以回帖 登录 | 注册

本版积分规则

6

主题

9

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部