2. UART协议实现:GD32V UART寄存器配置、波特率设置、中断与DMA模式
UART,说白了就是串口通信。搞嵌入式的人,没人能绕过它。调试打印、跟传感器聊天、和蓝牙模块对接,哪样都离不开它。GD32V的UART模块,我用了好几年,踩过不少坑,今天把核心干货倒给你。
2.1 GD32V UART模块概览
GD32V的UART,本质上是一个全双工、异步的串行通信接口。它支持8/9位数据格式,奇偶校验可配,停止位1或2位。这些基础参数,你心里要有数。
我个人习惯,拿到一个新MCU,先看它的UART有几个。GD32VF103系列,通常有3个UART(USART0、USART1、USART2)。每个都支持中断和DMA,这点很关键。
核心寄存器一览:
- USART_STAT — 状态寄存器,看收发是否完成、有没有溢出
- USART_DATA — 数据寄存器,读写都走它
- USART_BAUD — 波特率寄存器,算不对就乱码
- USART_CTL0/1/2 — 控制寄存器,使能、中断、DMA都在这里配
你想想看,如果连寄存器地址都记混,后面调试就是灾难。我建议你直接打开GD32V的用户手册,把UART章节的寄存器列表截图存着,随时翻。
2.2 波特率设置:算不对就乱码
波特率,就是每秒传多少位。GD32V的UART时钟源通常是系统时钟(比如108MHz)。计算公式其实不复杂:
// 波特率计算公式
// 当 USART_CTL1.OVSMOD = 0(16倍过采样)
// 波特率 = fck / (16 * USART_BAUD)
//
// 当 USART_CTL1.OVSMOD = 1(8倍过采样)
// 波特率 = fck / (8 * USART_BAUD)
// 示例:系统时钟108MHz,目标波特率115200,16倍过采样
// USART_BAUD = 108000000 / (16 * 115200) = 58.59
// 取整为59,实际波特率 = 108000000 / (16 * 59) = 114406
// 误差约0.69%,在允许范围内
我的经验:波特率误差控制在2%以内,通信基本没问题。超过3%,偶尔会丢字节。我曾经在一个项目里,因为系统时钟配错了,算出来的波特率误差5%,结果收发的数据全是乱码。查了整整半天才找到原因。
实际配置代码,我习惯这样写:
void uart_baud_set(uint32_t periph_clk, uint32_t baudrate) {
uint32_t baud_div;
uint32_t oversample = 16; // 默认16倍过采样
baud_div = periph_clk / (oversample * baudrate);
// 四舍五入处理
if ((periph_clk % (oversample * baudrate)) >= (oversample * baudrate / 2)) {
baud_div++;
}
USART_BAUD(USART0) = baud_div;
}
嗯,这里要注意:GD32V的波特率寄存器是整数除法,没有小数部分。所以算出来一定要四舍五入,不然误差会偏大。
2.3 寄存器配置:一步一步来
配置UART,我总结了一个固定套路。按这个顺序来,基本不会漏:
- 使能时钟 — RCU模块里打开UART和对应GPIO的时钟
- 配置GPIO — TX设为推挽复用输出,RX设为浮空输入或上拉输入
- 复位UART — 调用USART_DeInit(),清掉上电默认状态
- 配置参数 — 数据位、停止位、校验位、波特率
- 使能UART — 设置CTL0的UEN位
- 使能收发 — 设置CTL0的TEN和REN位
完整初始化代码示例:
void uart_init(void) {
// 1. 使能时钟
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_USART0);
// 2. 配置GPIO:PA9-TX, PA10-RX
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
// 3. 复位UART
usart_deinit(USART0);
// 4. 配置参数:115200-8-N-1
usart_baud_set(USART0, 115200);
usart_word_length_set(USART0, USART_WL_8BIT);
usart_stop_bit_set(USART0, USART_STB_1BIT);
usart_parity_config(USART0, USART_PM_NONE);
// 5. 使能UART
usart_enable(USART0);
// 6. 使能收发
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
}
避坑指南:我曾经在配置GPIO时,把TX设成了推挽输出而不是复用推挽输出。结果UART死活发不出数据,示波器一看,TX引脚电平纹丝不动。记住:UART的TX引脚必须配置为复用功能推挽输出,不是普通推挽输出。
2.4 中断模式:高效收发
轮询方式太浪费CPU了。中断模式才是正经做法。GD32V的UART中断源很多,常用的就这几个:
| 中断标志 | 触发条件 | 典型用途 |
|---|---|---|
| TBE | 发送数据寄存器空 | 可以继续发下一个字节 |
| TC | 发送完成 | 一帧数据全部发完 |
| RBNE | 接收数据寄存器非空 | 收到一个字节,赶紧读走 |
| IDLE | 总线空闲 | 一帧数据接收完毕 |
中断配置代码:
void uart_interrupt_config(void) {
// 使能接收中断
usart_interrupt_enable(USART0, USART_INT_RBNE);
// 使能空闲中断(用于帧接收完成判断)
usart_interrupt_enable(USART0, USART_INT_IDLE);
// 配置NVIC
nvic_irq_enable(USART0_IRQn, 0, 0);
}
void USART0_IRQHandler(void) {
if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE) != RESET) {
// 收到一个字节
uint8_t data = usart_data_receive(USART0);
// 存入接收缓冲区
rx_buffer[rx_index++] = data;
usart_interrupt_flag_clear(USART0, USART_INT_FLAG_RBNE);
}
if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE) != RESET) {
// 一帧数据接收完成
frame_complete = 1;
usart_interrupt_flag_clear(USART0, USART_INT_FLAG_IDLE);
// 注意:读USART_STAT再读USART_DATA才能清掉IDLE标志
usart_data_receive(USART0);
}
}
我的习惯:接收用中断,发送用DMA。这样CPU几乎不用管UART的事,专心跑业务逻辑。你想想看,如果每秒要发几千个字节,每个字节都进一次中断,CPU还干不干别的了?
2.5 DMA模式:解放CPU
DMA模式,说白了就是让DMA控制器帮你搬数据。CPU只需要告诉DMA:从哪搬、搬到哪、搬多少。然后DMA自己搞定,搬完了通知你一声。
GD32V的UART支持两个DMA通道:一个发,一个收。配置步骤:
- 使能DMA时钟
- 配置DMA通道:源地址、目标地址、数据长度、传输方向
- 使能DMA通道
- 在UART里使能DMA模式
void uart_dma_tx_config(uint8_t *buffer, uint32_t len) {
// 使能DMA时钟
rcu_periph_clock_enable(RCU_DMA0);
// 配置DMA通道4用于UART0发送
dma_parameter_struct dma_init_struct;
dma_deinit(DMA0, DMA_CH4);
dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL;
dma_init_struct.memory_addr = (uint32_t)buffer;
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_init_struct.peripheral_addr = (uint32_t)&USART_DATA(USART0);
dma_init_struct.peripheral_inc = DMA_PERIPHERAL_INCREASE_DISABLE;
dma_init_struct.number = len;
dma_init_struct.width = DMA_PERIPHERAL_WIDTH_8BIT;
dma_init(DMA0, DMA_CH4, &dma_init_struct);
// 使能DMA传输完成中断
dma_interrupt_enable(DMA0, DMA_CH4, DMA_INT_FTF);
// 使能DMA通道
dma_channel_enable(DMA0, DMA_CH4);
// UART使能DMA发送
usart_dma_transmit_config(USART0, USART_DMA_ENABLE);
}
避坑指南:我曾经在DMA传输还没完成时,就修改了源缓冲区的内容。结果发出去的数据全是错的。记住:DMA传输期间,不要修改源缓冲区。等传输完成中断来了再改。另外,DMA通道的优先级也要注意,别让高优先级的中断把DMA传输打断了。
2.6 三种模式对比
| 模式 | CPU占用 | 实时性 | 适用场景 |
|---|---|---|---|
| 轮询 | 高(一直等) | 差 | 调试打印、低速通信 |
| 中断 | 中(进进出出) | 好 | 常规通信、不定长数据 |
| DMA | 低(几乎不占) | 最好 | 大数据量、高速通信 |
我个人建议:项目初期调试用轮询,简单直接。正式产品用中断+DMA组合。接收用中断(因为数据不定长),发送用DMA(因为数据量大)。
2.7 UART配置与工作流程总览
下面这张图,把UART从配置到工作的完整流程画出来了。你对照着看,心里就有谱了。
这张图把UART配置的8个步骤串起来了。从时钟使能到数据传输,每一步都不能跳。我刚开始学的时候,总想省几步,结果不是发不出数据,就是收不到数据。老老实实按流程走,最省时间。
2.8 常见问题与调试技巧
搞UART,最怕的就是乱码和丢数据。我总结了几条经验:
- 乱码 — 先查波特率,再查数据位和停止位是否匹配。示波器看TX脚的波形,量一下位宽对不对。
- 收不到数据 — 查RX引脚配置,是不是浮空输入?有没有虚焊?用逻辑分析仪抓一下RX脚的信号。
- 丢字节 — 中断优先级太低?DMA缓冲区太小?接收缓冲区溢出?
- 发送卡死 — 检查TBE标志有没有清掉,DMA传输完成中断有没有响应。
调试利器:我习惯在UART初始化完成后,先发一个固定的测试字符串(比如"UART OK\r\n")。如果这个能正常收到,说明基本配置没问题。然后再测收发。这个习惯帮我省了不少排查时间。
嗯,UART这部分内容就这些。配置不难,但细节多。你动手写代码的时候,对照着手册和今天的笔记,一步一步来。遇到问题别慌,先查硬件连接,再查软件配置,最后查中断和DMA。按这个顺序排查,90%的问题都能解决。