3. UART串口通信调试:从初始化到故障定位

UART串口,做嵌入式的人天天打交道。说它是调试的“生命线”一点不为过。CH32系列芯片的UART模块,功能挺全,但坑也不少。这一章,我把这些年调串口踩过的坑、总结的经验,一次性说清楚。

本章核心知识点:

  • UART初始化与波特率设置的底层逻辑
  • 串口打印调试法的正确姿势
  • 数据丢帧问题的系统化定位方法
  • 中断接收与轮询模式的实战对比
UART串口调试知识体系 UART调试核心 初始化与波特率 串口打印调试法 数据丢帧定位 中断vs轮询 时钟配置 波特率误差 printf重定向 日志分级 FIFO溢出 中断延迟 CPU占用率 每个环节出问题,都会导致通信失败或数据异常

3.1 UART初始化与波特率设置

初始化UART,第一步就是配时钟。CH32的UART挂在不同总线上,有的在APB1,有的在APB2。我见过不少新手,初始化半天没反应,最后发现是时钟没使能。

波特率设置,这里有个关键点——误差。理论上波特率误差要小于2%,实际项目中我建议控制在1%以内。为什么?

你看CH32的波特率计算公式:

BaudRate = FCLK / (16 * USART_BRR)
// USART_BRR = FCLK / (16 * BaudRate)

FCLK是UART的时钟源,BRR是波特率寄存器值。问题来了:BRR是整数,除出来有小数怎么办?

我曾经踩过的坑: 用8MHz时钟配115200波特率,算出来BRR=4.34,取整后实际波特率是125000,误差高达8.5%。结果就是——数据偶尔对,偶尔错,查了整整一天。

正确的做法是:先算理论BRR,再反算实际波特率,确认误差在可接受范围。如果误差太大,要么换晶振,要么用PLL倍频。

下面是我常用的初始化模板:

void UART1_Init(uint32_t baudrate) {
    uint32_t brr;
    float error;
    
    // 1. 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    // 2. GPIO配置:TX推挽输出,RX浮空输入
    GPIO_InitTypeDef gpio = {0};
    gpio.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
    gpio.GPIO_Mode = GPIO_Mode_AF_PP;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio);
    
    // 3. 计算BRR并检查误差
    brr = SystemCoreClock / (16 * baudrate);
    error = (float)SystemCoreClock / (16 * brr) / baudrate - 1;
    if(error > 0.02 || error < -0.02) {
        // 误差超过2%,报警
        printf("Warning: baudrate error %.2f%%\n", error*100);
    }
    
    // 4. 配置UART
    USART_InitTypeDef uart = {0};
    uart.USART_BaudRate = baudrate;
    uart.USART_WordLength = USART_WordLength_8b;
    uart.USART_StopBits = USART_StopBits_1;
    uart.USART_Parity = USART_Parity_No;
    uart.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    uart.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_Init(USART1, &uart);
    
    // 5. 使能
    USART_Cmd(USART1, ENABLE);
}

个人习惯: 我会在初始化函数里加一个波特率误差检查,如果误差超标直接打印警告。这样上电一看日志就知道有没有隐患,不用等出了问题再回头查。

3.2 串口打印调试法

printf是调试利器,但嵌入式里用printf有个前提——你得把输出重定向到UART。CH32的GCC工具链里,重定向很简单:

int _write(int file, char *ptr, int len) {
    for(int i = 0; i < len; i++) {
        while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
        USART_SendData(USART1, ptr[i]);
    }
    return len;
}

这里有个细节:USART_FLAG_TC是发送完成标志,不是发送缓冲区空标志。用TC能保证每个字节都发完再发下一个,不会丢数据。但代价是——慢。如果你在中断里用这个方式打印,CPU会被死死拖住。

我建议的做法是:

  • 调试阶段: 用阻塞式printf,简单可靠
  • 发布版本: 用DMA+环形缓冲区,或者干脆关掉打印

另外,打印信息要分级。我自己的习惯:

级别 宏定义 使用场景
错误 LOG_E(fmt, ...) 系统无法继续运行
警告 LOG_W(fmt, ...) 异常但可恢复
信息 LOG_I(fmt, ...) 关键状态变化
调试 LOG_D(fmt, ...) 详细流程跟踪

你想想看,如果所有打印都用printf,调试信息一多,根本分不清哪个是重点。分级之后,发布时只需要把LOG_D关掉,其他保留,线上问题也能快速定位。

3.3 数据丢帧问题定位

数据丢帧,是UART调试里最让人头疼的问题之一。现象很典型:数据偶尔少几个字节,或者出现乱码。

我总结了一个排查清单,按优先级排列:

  1. 检查波特率误差 —— 用示波器看波形,测量实际波特率
  2. 检查FIFO溢出 —— 看USART_SR的ORE位是否被置1
  3. 检查中断响应时间 —— 中断里不能做耗时操作
  4. 检查缓冲区大小 —— 环形缓冲区是否溢出
  5. 检查硬件连接 —— TX/RX是否接反,地线是否共地

核心原则: 丢帧的本质是“数据来了,但CPU没来得及取走”。要么是CPU太忙,要么是缓冲区太小,要么是波特率不匹配。

我曾经遇到一个案例:设备每隔100ms发一包128字节的数据,波特率115200。理论上传输时间约11ms,CPU完全来得及。但实际就是丢帧。查了半天,发现是中断里有个printf打印调试信息,打印一次要20ms,直接把下一个中断堵死了。

解决方案很简单:中断里只做数据搬运,打印放到主循环。

3.4 中断接收与轮询模式对比

这两种模式,说白了就是“主动问”和“等着通知”的区别。

对比项 轮询模式 中断模式
CPU占用率 高(持续查询) 低(有数据才响应)
实时性 取决于轮询频率 高(硬件触发)
代码复杂度 低 中(需处理临界区)
适用场景 简单系统、低速率 多任务、高速率

轮询模式我一般只在两种情况下用:一是芯片资源实在紧张,二是调试初期验证硬件。正常产品里,几乎都用中断。

中断接收的标准写法:

// 环形缓冲区
#define RX_BUF_SIZE 256
volatile uint8_t rx_buf[RX_BUF_SIZE];
volatile uint16_t rx_head = 0, rx_tail = 0;

void USART1_IRQHandler(void) {
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
        uint8_t data = USART_ReceiveData(USART1);
        uint16_t next = (rx_head + 1) % RX_BUF_SIZE;
        if(next != rx_tail) {  // 缓冲区未满
            rx_buf[rx_head] = data;
            rx_head = next;
        } else {
            // 缓冲区溢出,丢帧
            // 可以在这里置一个标志位
        }
    }
}

注意: 中断里访问的变量,主循环里也要访问,一定要加volatile。我见过有人忘了加,优化后的代码直接跳过变量读取,数据永远不对。

主循环里取数据:

while(rx_tail != rx_head) {
    uint8_t data = rx_buf[rx_tail];
    rx_tail = (rx_tail + 1) % RX_BUF_SIZE;
    // 处理数据
}

这个环形缓冲区的设计,说白了就是用空间换时间。256字节对于大部分物联网应用足够了。如果数据量特别大,可以改成1024或者用DMA。

我建议: 新项目直接上中断+DMA,CH32的UART支持DMA传输,CPU几乎零负担。轮询模式只用来做硬件测试,产品里别用。

嗯,UART调试这块,说到底就是三个字——稳、准、快。初始化要稳,波特率要准,响应要快。把这三点做到位,串口通信基本不会出大问题。