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,我总结了一个固定套路。按这个顺序来,基本不会漏:

  1. 使能时钟 — RCU模块里打开UART和对应GPIO的时钟
  2. 配置GPIO — TX设为推挽复用输出,RX设为浮空输入或上拉输入
  3. 复位UART — 调用USART_DeInit(),清掉上电默认状态
  4. 配置参数 — 数据位、停止位、校验位、波特率
  5. 使能UART — 设置CTL0的UEN位
  6. 使能收发 — 设置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通道:一个发,一个收。配置步骤:

  1. 使能DMA时钟
  2. 配置DMA通道:源地址、目标地址、数据长度、传输方向
  3. 使能DMA通道
  4. 在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从配置到工作的完整流程画出来了。你对照着看,心里就有谱了。

GD32V UART配置与工作流程 GD32V UART配置与工作流程 1. 使能时钟 2. 配置GPIO 3. 复位UART 4. 配置通信参数 波特率 | 数据位(8/9) | 停止位(1/2) | 校验位(无/奇/偶) 5. 选择工作模式 6. 使能UART 7. 使能收发 8. 数据传输 轮询模式 | 中断模式(RBNE/IDLE) | DMA模式(通道4/5) 💡 关键点: 波特率误差<2% | 中断标志需及时清除 | DMA传输期间勿改源数据

这张图把UART配置的8个步骤串起来了。从时钟使能到数据传输,每一步都不能跳。我刚开始学的时候,总想省几步,结果不是发不出数据,就是收不到数据。老老实实按流程走,最省时间。

2.8 常见问题与调试技巧

搞UART,最怕的就是乱码和丢数据。我总结了几条经验:

  • 乱码 — 先查波特率,再查数据位和停止位是否匹配。示波器看TX脚的波形,量一下位宽对不对。
  • 收不到数据 — 查RX引脚配置,是不是浮空输入?有没有虚焊?用逻辑分析仪抓一下RX脚的信号。
  • 丢字节 — 中断优先级太低?DMA缓冲区太小?接收缓冲区溢出?
  • 发送卡死 — 检查TBE标志有没有清掉,DMA传输完成中断有没有响应。

调试利器:我习惯在UART初始化完成后,先发一个固定的测试字符串(比如"UART OK\r\n")。如果这个能正常收到,说明基本配置没问题。然后再测收发。这个习惯帮我省了不少排查时间。

嗯,UART这部分内容就这些。配置不难,但细节多。你动手写代码的时候,对照着手册和今天的笔记,一步一步来。遇到问题别慌,先查硬件连接,再查软件配置,最后查中断和DMA。按这个顺序排查,90%的问题都能解决。