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驱动核心流程,建议你收藏。
轮询模式驱动实现
轮询模式,说白了就是CPU死等。驱动里不断读状态寄存器,检查数据是否就绪。这种方式实现简单,但效率极低。
一个典型的轮询发送流程:
- 读状态寄存器,检查TX FIFO是否满
- 如果不满,把数据写入发送寄存器
- 如果满了,循环等待
接收流程类似:
- 读状态寄存器,检查RX FIFO是否有数据
- 如果有,从接收寄存器读数据
- 如果没有,循环等待
代码示例(简化版):
/* 轮询发送一个字节 */
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清除对应中断标志 |
中断处理流程:
- 进入中断服务函数
- 读MIS寄存器,判断中断类型
- 如果是RX中断:从FIFO读数据,存入环形缓冲区
- 如果是TX中断:从环形缓冲区取数据,写入FIFO
- 处理完成后,写ICR清除中断标志
- 退出中断
代码示例:
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输出。这就是典型的时钟树配置错误导致的波特率偏差。
数据丢帧的定位步骤:
- 用示波器抓波形:看TX/RX引脚的实际波特率,计算误差
- 检查时钟树:确认UART模块的时钟源频率是否正确
- 检查分频寄存器:读硬件分频值,反算实际波特率
- 检查FIFO水线:如果FIFO水线设置太高,中断响应不及时也会丢数据
- 检查流控:硬件流控(RTS/CTS)是否使能,对方是否支持
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 偶尔丢一个字节 | 波特率误差略大,FIFO溢出 | 检查时钟源、降低波特率 |
| 连续丢多个字节 | 中断响应延迟,FIFO溢出 | 检查中断优先级、增大FIFO水线 |
| 数据全是0xFF | RX引脚悬空或电平错误 | 检查硬件连接、GPIO复用 |
| 数据错位 | 波特率误差过大,采样点偏移 | 计算实际波特率误差 |
避坑指南:我曾经调试一个车载T-Box的UART,发现波特率配置完全正确,但就是丢帧。最后发现是SoC的UART模块在进入低功耗模式后,时钟被gate掉了。唤醒后时钟恢复,但波特率发生器没有重新初始化。所以,如果你遇到休眠唤醒后丢帧,先检查时钟恢复流程。
嗯,UART驱动调试,说到底就是三个字:看时钟、查FIFO、管中断。把这三点吃透了,大部分问题都能快速定位。剩下的,就是经验积累了。