UART驱动调试:从轮询到中断,再到数据丢帧实战

UART这个接口,说实话,在车载SoC里看着简单,但坑一点都不少。很多刚入行的兄弟觉得UART不就是发收几个字节吗?其实不然。你想想看,当系统跑着Linux,主频1.5GHz,UART时钟却只有几十兆,中间还有DMA、FIFO、流控一堆东西,任何一个环节出问题,数据就丢了。

我个人习惯,调试UART驱动之前,先把控制器手册翻透。别急着写代码,先搞清楚硬件怎么工作的。

UART控制器工作原理

一个标准的UART控制器,核心就这几块:

  • 波特率发生器:从系统时钟分频得到目标波特率
  • 发送器(TX):把并行数据转成串行,加起始位、校验位、停止位
  • 接收器(RX):采样串行数据,去噪,恢复并行数据
  • FIFO缓冲区:硬件缓存,减少CPU干预次数
  • 控制/状态寄存器:配置模式、查询状态、处理中断

我在项目中遇到过一个问题:某款SoC的UART接收FIFO只有16字节,而应用层一次发256字节的日志。如果不及时读走,数据就覆盖了。这就是典型的FIFO深度与软件处理速度不匹配。

核心要点:UART驱动调试,本质就是管理好“数据从外设到内存”这条路径上的每一个缓冲区和同步点。

下面这张图,是我自己总结的UART驱动核心流程,建议你收藏。

UART驱动核心数据流与模式切换 应用层 (read/write) UART驱动 (tty层/串口核心) 轮询模式 (Polling) 中断模式 (IRQ) DMA模式 UART控制器硬件 (FIFO/移位寄存器/波特率发生器) 调试要点 • 轮询模式:适合低吞吐、调试阶段,CPU占用高 • 中断模式:适合中低速通信,注意中断频率与响应延迟 • DMA模式:适合高速大吞吐,需处理DMA完成中断与FIFO水线

轮询模式驱动实现

轮询模式,说白了就是CPU死等。驱动里不断读状态寄存器,检查数据是否就绪。这种方式实现简单,但效率极低。

一个典型的轮询发送流程:

  1. 读状态寄存器,检查TX FIFO是否满
  2. 如果不满,把数据写入发送寄存器
  3. 如果满了,循环等待

接收流程类似:

  1. 读状态寄存器,检查RX FIFO是否有数据
  2. 如果有,从接收寄存器读数据
  3. 如果没有,循环等待

代码示例(简化版):

/* 轮询发送一个字节 */
void uart_poll_tx(struct uart_port *port, unsigned char data)
{
    /* 等待TX FIFO空 */
    while (readl(port->membase + UART_FR) & UART_FR_TXFF)
        cpu_relax();

    writel(data, port->membase + UART_DR);
}

/* 轮询接收一个字节 */
unsigned char uart_poll_rx(struct uart_port *port)
{
    /* 等待RX FIFO有数据 */
    while (readl(port->membase + UART_FR) & UART_FR_RXFE)
        cpu_relax();

    return readl(port->membase + UART_DR);
}

注意:轮询模式在单核系统里会阻塞整个CPU。我曾经在调试早期Bootloader时用过轮询,结果因为波特率配置错误,程序直接卡死在while循环里,连串口都救不回来。所以,轮询模式只建议在早期调试或系统未初始化完成时使用。

中断模式驱动实现

中断模式才是正经的驱动写法。硬件有数据了,主动通知CPU,CPU去处理。这样CPU不用空等,可以做其他事。

中断模式的关键配置:

寄存器/位 作用
UART_IMSC (中断屏蔽设置) 使能/禁用特定中断源
UART_RIS (原始中断状态) 硬件中断信号原始状态
UART_MIS (屏蔽后中断状态) 实际触发CPU的中断状态
UART_ICR (中断清除) 写1清除对应中断标志

中断处理流程:

  1. 进入中断服务函数
  2. 读MIS寄存器,判断中断类型
  3. 如果是RX中断:从FIFO读数据,存入环形缓冲区
  4. 如果是TX中断:从环形缓冲区取数据,写入FIFO
  5. 处理完成后,写ICR清除中断标志
  6. 退出中断

代码示例:

static irqreturn_t uart_irq_handler(int irq, void *dev_id)
{
    struct uart_port *port = dev_id;
    unsigned int mis = readl(port->membase + UART_MIS);

    /* 处理接收中断 */
    if (mis & UART_MIS_RXMIS) {
        while (!(readl(port->membase + UART_FR) & UART_FR_RXFE)) {
            unsigned char data = readl(port->membase + UART_DR);
            /* 存入环形缓冲区,唤醒等待进程 */
            uart_insert_char(port, data);
        }
    }

    /* 处理发送中断 */
    if (mis & UART_MIS_TXMIS) {
        /* 从环形缓冲区取数据,写入FIFO */
        while (!(readl(port->membase + UART_FR) & UART_FR_TXFF)) {
            unsigned char data = uart_get_char(port);
            if (data == UART_NO_DATA)
                break;
            writel(data, port->membase + UART_DR);
        }
    }

    /* 清除中断 */
    writel(mis, port->membase + UART_ICR);

    return IRQ_HANDLED;
}

个人经验:中断处理函数里不要做复杂操作。我曾经见过有人直接在中断里调用printk,结果系统直接死锁。中断里只做数据搬运,唤醒工作队列或tasklet,让下半部去处理协议解析。

波特率配置与数据丢帧问题定位

波特率配置,看着简单,但出错率极高。为什么?因为波特率不是随便设的,它依赖于系统时钟分频。

波特率计算公式:

目标波特率 = 时钟源频率 / (16 × 分频系数)

举个例子,时钟源24MHz,目标115200:

分频系数 = 24,000,000 / (16 × 115200) = 13.02

取整13,实际波特率 = 24,000,000 / (16 × 13) = 115,384

误差 = (115384 - 115200) / 115200 ≈ 0.16%

这个误差在允许范围内(通常±2%)。但如果时钟源是25MHz呢?

分频系数 = 25,000,000 / (16 × 115200) = 13.57

取整13或14,误差都超过2%。这时候就会出现数据丢帧。

丢帧的根本原因:波特率误差导致接收端采样点偏移,当偏移超过半个位宽时,数据就错了。

我曾经在项目里遇到一个经典问题:板子A通信正常,板子B通信丢帧。查了两天,最后发现板子B的UART时钟源被配置成了不同的PLL输出。这就是典型的时钟树配置错误导致的波特率偏差。

数据丢帧的定位步骤:

  1. 用示波器抓波形:看TX/RX引脚的实际波特率,计算误差
  2. 检查时钟树:确认UART模块的时钟源频率是否正确
  3. 检查分频寄存器:读硬件分频值,反算实际波特率
  4. 检查FIFO水线:如果FIFO水线设置太高,中断响应不及时也会丢数据
  5. 检查流控:硬件流控(RTS/CTS)是否使能,对方是否支持
症状 可能原因 排查方向
偶尔丢一个字节 波特率误差略大,FIFO溢出 检查时钟源、降低波特率
连续丢多个字节 中断响应延迟,FIFO溢出 检查中断优先级、增大FIFO水线
数据全是0xFF RX引脚悬空或电平错误 检查硬件连接、GPIO复用
数据错位 波特率误差过大,采样点偏移 计算实际波特率误差

避坑指南:我曾经调试一个车载T-Box的UART,发现波特率配置完全正确,但就是丢帧。最后发现是SoC的UART模块在进入低功耗模式后,时钟被gate掉了。唤醒后时钟恢复,但波特率发生器没有重新初始化。所以,如果你遇到休眠唤醒后丢帧,先检查时钟恢复流程。

嗯,UART驱动调试,说到底就是三个字:看时钟、查FIFO、管中断。把这三点吃透了,大部分问题都能快速定位。剩下的,就是经验积累了。