4、UART驱动调试:串口设备树节点配置、波特率与数据格式设置、DMA与中断模式切换、流控(RTS/CTS)调试、实际案例:控制台输出乱码排查

UART调试,说白了就是让机顶盒的串口能正常说话。我做了这么多年驱动,遇到最多的坑就是串口。你想想看,串口是嵌入式调试的「生命线」,它要是挂了,你连内核启动日志都看不到,那还玩什么?

这一章,咱们就把它彻底搞明白。

4.1 串口设备树节点配置

设备树是硬件描述文件,UART的节点配置是第一步。我个人习惯先把芯片手册翻出来,确认UART挂在哪条总线上,中断号是多少,时钟源是什么。

一个典型的UART设备树节点长这样:

&uart2 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart2_pins_a>;
    clocks = <&clk_uart2>;
    clock-names = "uart";
    interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
    interrupt-names = "uart";
    dmas = <&dma 2 0>, <&dma 2 1>;
    dma-names = "tx", "rx";
    cts-gpios = <&gpio 3 18 GPIO_ACTIVE_LOW>;
    rts-gpios = <&gpio 3 19 GPIO_ACTIVE_LOW>;
};

这里有几个关键点:

  • pinctrl:管脚复用配置,错了串口根本不通。我在项目中遇到过管脚配成GPIO的情况,查了半天才发现。
  • clocks:时钟必须使能,否则UART模块不工作。
  • interrupts:中断号要跟芯片手册一致,不然中断处理函数不会被调用。
  • dmas:DMA通道,后面会细说。
  • cts-gpios / rts-gpios:硬件流控引脚,不是所有板子都接。
注意: 设备树中的reg属性(寄存器基地址)一定要核对。我曾经见过有人把uart2和uart3的基地址写反了,结果串口2发数据,串口3收到了,调试了一整天。

4.2 波特率与数据格式设置

波特率设置是UART调试的基础。说白了,就是收发双方约定好「每秒传多少位」。常见的波特率有115200、9600、57600等。

在Linux内核中,波特率设置通常在驱动初始化时完成。代码里一般这么写:

struct uart_port *port;
port->uartclk = 24000000;  // 时钟频率24MHz
port->baud = 115200;       // 目标波特率
uart_update_timeout(port, CS8, port->baud);

数据格式包括数据位、停止位、校验位。最常见的配置是8N1:8个数据位,无校验,1个停止位。

参数 常见值 说明
数据位 5, 6, 7, 8 大部分场景用8位
停止位 1, 1.5, 2 1位最常用
校验位 无, 奇校验, 偶校验 无校验最普遍

嗯,这里要注意:波特率误差不能超过3%。如果时钟源不准,算出来的波特率会有偏差。我建议用示波器量一下TX引脚的波形,数一下位宽,确认实际波特率。

4.3 DMA与中断模式切换

UART有两种工作模式:中断模式和DMA模式。怎么选?看场景。

  • 中断模式:每收发一个字节就触发一次中断。CPU频繁进出中断,适合低速率、小数据量场景。
  • DMA模式:DMA控制器负责搬运数据,CPU只在开始和结束时参与。适合高速率、大数据量场景。

我在项目中遇到过一个问题:机顶盒播放4K视频时,串口日志打印导致画面卡顿。后来切换到DMA模式,CPU负载从30%降到了5%。

切换方式通常通过设备树或驱动参数控制:

// 设备树中使能DMA
dmas = <&dma 2 0>, <&dma 2 1>;
dma-names = "tx", "rx";

// 驱动中动态切换
if (use_dma) {
    uart_port->flags |= UPF_HARD_FLOW;
    uart_add_one_port(drv, uart_port);
} else {
    uart_port->flags &= ~UPF_HARD_FLOW;
}
小技巧: 调试阶段建议先用中断模式,稳定后再切DMA。因为DMA涉及缓存一致性、描述符管理等复杂问题,出了问题不好定位。

4.4 流控(RTS/CTS)调试

流控分两种:硬件流控和软件流控。机顶盒场景下,硬件流控更常见。

硬件流控的原理很简单:发送前先问对方「你准备好了吗?」(RTS信号),对方回复「准备好了」(CTS信号),然后才开始发数据。

调试流控时,我一般先确认几个点:

  1. 硬件上RTS/CTS引脚是否连接?很多开发板根本没接。
  2. 设备树中是否配置了流控引脚?看前面的cts-gpios和rts-gpios。
  3. 驱动中是否使能了流控?

驱动中使能流控的代码示例:

// 使能硬件流控
termios->c_cflag |= CRTSCTS;

// 关闭硬件流控
termios->c_cflag &= ~CRTSCTS;

我曾经遇到一个案例:机顶盒连接蓝牙模块,串口数据经常丢包。查了半天,发现蓝牙模块要求硬件流控,但板子上RTS/CTS引脚没接。最后飞了两根线才解决。

警告: 如果硬件没接流控引脚,千万别在软件里使能CRTSCTS。否则UART会一直等CTS信号,数据发不出去,系统看起来像死机了一样。

4.5 实际案例:控制台输出乱码排查

这个案例我印象特别深。有一次客户反馈机顶盒启动时控制台输出乱码,像这样:

��[    0.000000] Booting Linux on physical CPU 0x0
��[    0.000000] Linux version 4.9.84
��[    0.000000] CPU: ARMv7 Processor [410fc075] revision 5

乱码的排查思路,我总结了一个流程:

  1. 先看波特率:终端软件设置的波特率跟UART实际输出的是否一致?这是最常见的原因。
  2. 再看时钟:UART时钟源频率对不对?如果时钟频率算错了,波特率误差会很大。
  3. 然后看电平:TTL电平还是RS232电平?电平不匹配也会导致乱码。
  4. 最后看接地:GND有没有共地?不共地信号会飘。

那次排查的结果是什么?你猜猜看。其实是bootloader阶段波特率是115200,但内核启动后驱动重新初始化了UART,波特率变成了9600。终端软件一直用115200收,自然乱码。

解决方案很简单:在设备树中固定波特率,或者让bootloader和内核用同一个配置。

// 在设备树中固定波特率
chosen {
    bootargs = "console=ttyS0,115200n8";
};

核心要点:

  • 设备树节点配置要核对管脚、时钟、中断、DMA
  • 波特率误差不能超过3%,用示波器验证
  • 中断模式适合调试,DMA模式适合高性能场景
  • 硬件流控必须确认物理连接,否则别使能
  • 乱码排查:波特率 → 时钟 → 电平 → 接地

UART调试说难不难,说简单也不简单。关键是要有系统性的排查思路,别一上来就瞎改代码。嗯,这一章就到这里,下一章咱们聊SPI总线调试,那个坑更多,到时候再细说。