
11. GD32VW553串口收发实战从轮询到中断详解USART0配置与printf重定向最近在用GD32VW553做一个小项目需要和电脑频繁地收发调试信息串口就成了最常用的工具。很多刚开始接触这块RISC-V芯片的朋友都问过我串口到底怎么配中断接收怎么搞怎么才能像在电脑上一样方便地用printf打印信息今天我就结合自己的踩坑经验把GD32VW553的串口通信从基础配置到中断接收再到printf重定向给大家手把手捋一遍。咱们的目标很明确让开发板通过串口发送数据到电脑的串口助手显示同时也能接收电脑发来的数据并处理。我会先讲最基础的轮询发送然后过渡到更实用的中断接收最后搞定printf这个调试神器。1. 串口配置六步走想让GD32VW553的串口工作起来就像搭积木得按顺序把几个关键的模块摆好。整个过程可以总结为六个步骤咱们一步步来。1.1 第一步开启时钟任何外设要工作首先得给它供电在单片机里就是开启时钟。GD32VW553的串口0USART0可以用多个引脚这里我们选择PB15作为发送引脚TXPA8作为接收引脚RX。所以我们需要开启三个时钟USART0本身的时钟、GPIOB的时钟和GPIOA的时钟。在GD32的库函数里开启时钟用的是rcu_periph_clock_enable函数。为了方便以后修改引脚咱们先用宏定义把要用的时钟资源定义好//时钟定义 #define BSP_USART_RCU RCU_USART0 // 串口0时钟 #define BSP_USART_TX_RCU RCU_GPIOB // 发送引脚GPIOB时钟 #define BSP_USART_RX_RCU RCU_GPIOA // 接收引脚GPIOA时钟定义好后在初始化函数里一行代码就能开启时钟rcu_periph_clock_enable(BSP_USART_RCU); // 开启串口时钟 rcu_periph_clock_enable(BSP_USART_TX_RCU); // 开启GPIOB时钟 rcu_periph_clock_enable(BSP_USART_RX_RCU); // 开启GPIOA时钟1.2 第二步配置GPIO的复用功能单片机的引脚通常身兼数职默认是普通GPIO要用来做串口就得告诉它“你现在是串口引脚了”。这个操作叫“复用功能配置”。查一下GD32VW553的数据手册第24页附近可以找到PA8和PB15作为串口0引脚时对应的复用功能编号AF Alternate FunctionPA8 (USART0_RX) 对应 AF2PB15 (USART0_TX) 对应 AF8同样咱们先用宏定义把这些信息固定下来//串口发送引脚定义 #define BSP_USART_TX_PORT GPIOB #define BSP_USART_TX_PIN GPIO_PIN_15 #define BSP_USART_TX_AF GPIO_AF_8 //串口接收引脚定义 #define BSP_USART_RX_PORT GPIOA #define BSP_USART_RX_PIN GPIO_PIN_8 #define BSP_USART_RX_AF GPIO_AF_2然后调用库函数gpio_af_set来设置复用功能/* 配置PB15复用为USART0_TX */ gpio_af_set(BSP_USART_TX_PORT, BSP_USART_TX_AF, BSP_USART_TX_PIN); /* 配置PA8复用为USART0_RX */ gpio_af_set(BSP_USART_RX_PORT, BSP_USART_RX_AF, BSP_USART_RX_PIN);1.3 第三步配置GPIO模式与输出引脚确定了复用功能还得设置它的工作模式。对于串口的TX和RX引脚都需要设置为复用功能模式GPIO_MODE_AF并且使能内部上拉电阻GPIO_PUPD_PULLUP这样信号更稳定。/* 配置TX引脚为复用模式上拉 */ gpio_mode_set(BSP_USART_TX_PORT, GPIO_MODE_AF, GPIO_PUPD_PULLUP, BSP_USART_TX_PIN); /* 配置RX引脚为复用模式上拉 */ gpio_mode_set(BSP_USART_RX_PORT, GPIO_MODE_AF, GPIO_PUPD_PULLUP, BSP_USART_RX_PIN);接着配置输出选项。虽然RX是输入但这里按惯例也一并配置为推挽输出、50MHz速度。实际通信时TX负责输出RX负责输入但初始化时这样配置没问题。/* 配置TX为推挽输出速度50MHz */ gpio_output_options_set(BSP_USART_TX_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, BSP_USART_TX_PIN); /* 配置RX为推挽输出速度50MHz */ gpio_output_options_set(BSP_USART_RX_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, BSP_USART_RX_PIN);注意原始示例代码中输出速度用的是GPIO_OSPEED_25MHZ这里根据你的实际需求选择即可对于串口通信25MHz或50MHz都行。1.4 第四步配置串口参数引脚准备好了接下来就是配置串口本身的核心参数波特率、数据位、停止位、校验位。这就好比两个人打电话得先说好用哪种语言、多快的语速。GD32库提供了清晰的函数来设置这些参数。一个好习惯是在配置前先复位一下串口避免之前残留的配置干扰。/* 串口参数配置 */ usart_deinit(BSP_USART); // 1. 复位串口 usart_baudrate_set(BSP_USART, dwbaud_rate); // 2. 设置波特率如115200 usart_parity_config(BSP_USART, USART_PM_NONE); // 3. 无校验位 usart_word_length_set(BSP_USART, USART_WL_8BIT); // 4. 8位数据位 usart_stop_bit_set(BSP_USART, USART_STB_1BIT); // 5. 1位停止位这里的BSP_USART我们定义为USART0dwbaud_rate是传入的波特率参数比如115200。1.5 第五步使能串口参数设好了还得把串口的“开关”打开。这里有三层开关总开关、发送开关、接收开关。通常我们需要使能接收和发送功能最后再使能串口模块本身。/* 使能串口接收功能 */ usart_receive_config(BSP_USART, USART_RECEIVE_ENABLE); /* 使能串口发送功能 */ usart_transmit_config(BSP_USART, USART_TRANSMIT_ENABLE); /* 最后使能串口模块 */ usart_enable(BSP_USART);顺序上先配置具体功能再开启总模块是一个比较稳妥的做法。2. 串口发送数据从轮询到printf串口配置好就可以开始通信了。我们先从最简单的发送数据开始。2.1 轮询方式发送一个字节库函数usart_data_transmit用于发送一个字节的数据。但发送不是瞬间完成的如果连续调用它发送多个字节数据会乱掉。我们必须等待上一个字节真正发送完成后再发送下一个。怎么知道发送完了呢检查状态标志位USART_FLAG_TBE发送缓冲区空。当这个标志为1时说明发送缓冲区空了可以发送新数据。基于这个原理我们封装一个发送单字节的函数/** * 发送单个字符 * param ucch 要发送的字节 */ void UsartSendByte(uint8_t ucch) { usart_data_transmit(BSP_USART, (uint8_t)ucch); // 写入数据到发送缓冲区 while(RESET usart_flag_get(BSP_USART, USART_FLAG_TBE)); // 等待发送完成 }这个函数发送一个字符后会“死等”直到发送完成。用起来很简单UsartSendByte(H); UsartSendByte(i);2.2 发送字符串一个一个发字节太麻烦我们封装一个发送字符串的函数/** * 发送字符串 * param ucstr 要发送的字符串指针 */ void UsartSendString(uint8_t *ucstr) { while(ucstr *ucstr) // 判断指针非空且未到字符串结尾 { UsartSendByte(*ucstr); // 发送当前字符指针后移 } }这样一句UsartSendString(Hello\r\n);就能发送整个字符串了。\r\n是回车换行让串口助手能正确换行显示。2.3 重定向printf函数虽然UsartSendString方便了些但调试时我们经常想打印变量值比如int a 10;想打印出来。用上面的函数就得先把数字转换成字符串很麻烦。C语言标准库里的printf函数是解决这个问题的神器。它默认输出到显示器我们需要把它“重定向”到串口。在GD32的NucleiStudio开发环境中官方已经帮我们做了大部分工作。关键点在于printf底层会调用_write函数而GD32的固件库在Firmware/RISCV/stubs/write.c文件中已经实现了通过USART0输出的_put_char函数。所以只要你使用的是USART0并且工程设置正确printf默认就可以用了。需要检查一个工程设置在NucleiStudio中确保项目属性里GNU RISC-V Cross C Linker-Miscellaneous下的Use newlib-nano选项没有勾选。这是因为nano版本对printf支持不全去掉勾选才能使用全功能printf。设置好后你就可以在代码中直接使用printf了#include stdio.h int num 100; float pi 3.14; printf(Number: %d, Pi: %.2f\r\n, num, pi);这比我们自己封装字符串方便太多了。如果你用的不是USART0比如UART1只需要去修改write.c文件里_put_char函数中调用的串口端口号即可。3. 串口接收数据中断接收实战发送搞定了接收更重要。我们总不能一直用while循环去查询有没有数据来吧那样太浪费CPU资源了。正确的方式是使用中断当数据到来时硬件自动打断CPU当前工作去处理数据。3.1 中断接收的原理与配置串口接收中断常用两个标志RBNE中断接收缓冲区非空。每收到一个字节就会触发一次。我们在中断里读取这个字节。IDLE中断总线空闲中断。当一帧数据发送完毕串口总线空闲一段时间后触发。我们用这个来判断一串数据是否接收完成。配置中断的步骤使能RBNE中断。配置中断优先级。注意IDLE中断先不开启等真正收到第一个字节后再开启避免一上电就误触发。/* 使能接收缓冲区非空中断RBNE */ usart_interrupt_enable(BSP_USART, USART_INT_RBNE); /* 配置串口中断优先级抢占优先级2响应优先级2 */ eclic_irq_enable(BSP_USART_IRQ, 2, 2); /* 注意IDLE中断先不在这里开启 */同时我们需要一些全局变量来管理接收到的数据#define USART_RECEIVE_LENGTH 256 // 接收缓冲区大小 uint8_t g_recv_buff[USART_RECEIVE_LENGTH]; // 接收数据数组 uint16_t g_recv_length 0; // 当前接收到的数据长度 uint8_t g_recv_complete_flag 0; // 一帧数据接收完成标志3.2 中断服务函数详解中断服务函数ISR是中断触发后执行的函数。我们需要在里面判断是哪种中断并执行相应的操作。首先在头文件中定义中断服务函数名#define BSP_USART_IRQHandler USART0_IRQHandler然后编写中断服务函数这是整个中断接收的核心void BSP_USART_IRQHandler(void) { // 1. 处理RBNE中断有数据到来 if(usart_interrupt_flag_get(BSP_USART, USART_INT_FLAG_RBNE) ! RESET) { /* 必须清除中断标志 */ usart_interrupt_flag_clear(BSP_USART, USART_INT_FLAG_RBNE); /* 读取接收到的数据存入缓冲区 */ g_recv_buff[g_recv_length] usart_data_receive(BSP_USART); /* 接收长度加1并防止数组溢出 */ g_recv_length (g_recv_length 1) % USART_RECEIVE_LENGTH; // 关键技巧收到第一个字节后才开启IDLE中断 // 判断IDLE中断是否还未开启检查USART_CTL0寄存器的第4位 if( (USART_CTL0(BSP_USART) (14)) 0 ) { usart_interrupt_enable(BSP_USART, USART_INT_IDLE); // 开启IDLE中断 usart_interrupt_flag_clear(BSP_USART, USART_INT_FLAG_IDLE); // 清除可能存在的空闲标志 } } // 2. 处理IDLE中断一帧数据接收完成 if(usart_interrupt_flag_get(BSP_USART, USART_INT_FLAG_IDLE) ! RESET) { /* 清除IDLE中断标志 */ usart_interrupt_flag_clear(BSP_USART, USART_INT_FLAG_IDLE); /* 在数据末尾添加字符串结束符方便用%s打印 */ g_recv_buff[g_recv_length] \0; /* 置位接收完成标志通知主循环处理数据 */ g_recv_complete_flag 1; /* 关闭IDLE中断等待下一次接收第一个字节时再开启 */ usart_interrupt_disable(BSP_USART, USART_INT_IDLE); } }这个函数有两个关键点动态开启IDLE中断只在收到第一个字节后才开启完美避免上电误触发。及时清除中断标志这是中断服务函数的铁律不清除会导致中断不断触发。3.3 主循环中处理接收完成的数据中断服务函数只负责收数据并设置标志实际的数据处理比如回显、解析应该放在主循环中避免在中断里做耗时操作。while(1) { /* 检查是否有一帧数据接收完成 */ if(g_recv_complete_flag 1) { g_recv_complete_flag 0; // 清除标志准备接收下一帧 // 打印接收到的数据长度和内容 printf(Received %d bytes: %s\r\n, g_recv_length, g_recv_buff); // 处理数据... (这里可以添加你的协议解析代码) // 处理完后清空缓冲区准备下次接收 memset(g_recv_buff, 0, g_recv_length); g_recv_length 0; } // ... 其他任务 }4. 完整代码示例与实验现象把上面的所有步骤整合起来完整的bsp_usart.c和bsp_usart.h文件代码在原始资料中已经给出。在main函数中初始化串口和中断后就可以发送测试信息并等待接收了。编译下载程序后打开串口助手波特率115200你会看到开发板主动发送的信息UsartSendString test printf test. float:12.512000 int:10然后你在串口助手的发送框里输入一段文字比如“Hello GD32VW553”点击发送。开发板收到后会通过printf将内容回显出来Received 18 bytes: Hello GD32VW553这样一个完整的、支持中断接收和printf输出的串口通信框架就搭建好了。在实际项目中你可以基于这个框架在数据处理的环节加入你自己的通信协议解析比如Modbus、自定义指令等让串口成为你产品和外界交互的可靠桥梁。