4. UART串口调试:串口驱动框架、波特率与流控配置、控制台重定向与多路复用

UART串口,做嵌入式Linux开发的人天天打交道。说它是调试的“生命线”一点不为过。你想想看,板子刚上电,系统还没跑起来,网口、USB都还没初始化,你靠什么跟内核对话?就是串口。

我在Rockchip平台上调过不下几十种外设,每次拿到新板子,第一件事就是确认串口能不能正常打印。如果串口都没通,后面的工作基本没法开展。这一章,我就把串口调试的底牌给你翻一翻。

4.1 串口驱动框架:从硬件到用户空间

Linux的串口驱动,说白了就是三层结构。底层操作硬件寄存器,中间层处理数据流和线路规程,上层暴露给用户/dev/ttyS*节点。我习惯把它画成下面这张图,你一看就明白。

Linux UART 串口驱动三层架构 用户空间 /dev/ttyS0 /dev/ttyS1 /dev/ttyS2 TTY 核心层 (tty_io.c / n_tty.c) 线路规程 (Line Discipline) · 数据缓冲 · 流量控制 串口核心层 (serial_core.c) uart_ops · uart_port · 波特率/帧格式管理 硬件驱动层 (8250_dw.c / rockchip_uart.c) 寄存器读写 · 中断处理 · DMA传输 · 时钟/复位控制 硬件层 (UART IP核 · 物理引脚) TX/RX · RTS/CTS · 时钟源 Rockchip 常用驱动文件 drivers/tty/serial/ 8250_dw.c (DesignWare) rockchip_uart.c (新平台) serial_core.c (核心层)

嗯,这里要注意。Rockchip早期平台(比如RK3288、RK3399)用的是DesignWare 8250 IP核,驱动走8250_dw.c。到了RK3568、RK3588这些新平台,Rockchip自己写了rockchip_uart.c,但核心逻辑还是基于serial_core.c的框架。你调试的时候,先搞清楚你的平台用哪个驱动文件,别找错地方。

4.2 波特率与帧格式配置

串口通信,收发双方得“说同一种语言”。这个语言就是波特率和帧格式。波特率错了,收到的全是乱码。帧格式错了,连乱码都收不到。

4.2.1 设备树中的串口配置

在Rockchip平台上,串口的波特率和帧格式通常通过设备树配置。我一般这样写:

&uart0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = &uart0_xfer &uart0_cts &uart0_rts;
    clock-frequency = <24000000>;
    current-speed = <115200>;
    dmas = <&dmac 0>, <&dmac 1>;
    dma-names = "tx", "rx";
};

current-speed就是默认波特率。但说实话,这个属性更多是给bootloader看的。内核起来后,实际波特率由用户空间通过stty命令或应用程序的tcsetattr接口设置。

我的习惯:设备树里只配引脚复用和时钟频率,波特率留到用户空间配。这样调试时换波特率不用改设备树重编译,直接stty -F /dev/ttyS0 1500000就搞定。

4.2.2 用户空间配置实战

用stty命令查看和设置串口参数:

# 查看当前串口参数
stty -F /dev/ttyS0 -a

# 设置波特率115200,8数据位,1停止位,无校验
stty -F /dev/ttyS0 115200 cs8 -cstopb -parenb

# 开启硬件流控
stty -F /dev/ttyS0 crtscts

在C代码里,用tcsetattr配置:

#include <termios.h>
#include <fcntl.h>

int setup_uart(int fd, int baud) {
    struct termios tty;
    
    if (tcgetattr(fd, &tty) != 0) {
        perror("tcgetattr");
        return -1;
    }
    
    // 设置波特率
    cfsetospeed(&tty, baud);
    cfsetispeed(&tty, baud);
    
    // 8N1: 8数据位, 无校验, 1停止位
    tty.c_cflag &= ~CSIZE;
    tty.c_cflag |= CS8;
    tty.c_cflag &= ~PARENB;
    tty.c_cflag &= ~CSTOPB;
    
    // 开启硬件流控
    tty.c_cflag |= CRTSCTS;
    
    // 使能接收
    tty.c_cflag |= CREAD | CLOCAL;
    
    // 原始模式,不经过行处理
    tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    tty.c_iflag &= ~(IXON | IXOFF | IXANY | ICRNL);
    tty.c_oflag &= ~OPOST;
    
    // 设置超时
    tty.c_cc[VMIN] = 1;
    tty.c_cc[VTIME] = 5;
    
    tcflush(fd, TCIFLUSH);
    if (tcsetattr(fd, TCSANOW, &tty) != 0) {
        perror("tcsetattr");
        return -1;
    }
    return 0;
}
我曾经踩过的坑:有一次调RK3568的串口,波特率设成1500000,结果收发全是乱码。查了半天,发现是时钟源频率不对。UART模块的时钟源是24MHz,但驱动里默认分频系数算出来不准。最后在设备树里显式指定clock-frequency = <24000000>才解决。记住,波特率不准,八成是时钟源有问题。

4.3 硬件流控:RTS/CTS的正确姿势

硬件流控,说白了就是让收发双方互相告诉对方“我准备好了,你发吧”或者“我忙不过来,你等等”。用RTS和CTS两根线实现。

我见过很多工程师,板子上明明接了RTS/CTS,代码里却不开启流控。结果高速传输时丢数据,还以为是驱动有bug。其实不是,是流控没开。

流控方式 信号线 适用场景 注意事项
无流控 TX/RX only 低速调试、控制台 波特率≤115200时可用
硬件流控 TX/RX + RTS/CTS 高速传输、蓝牙模块 需硬件连线支持
软件流控 TX/RX only 无额外线缆时 XON/XOFF字符,不推荐

在设备树里开启硬件流控,需要把RTS/CTS的引脚复用配好:

&uart2 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = &uart2m0_xfer &uart2m0_ctsn &uart2m0_rtsn;
};

然后在用户空间用stty crtscts开启,或者在代码里设置tty.c_cflag |= CRTSCTS。

调试技巧:用示波器抓RTS/CTS波形,可以直观看到流控是否生效。发送数据时RTS拉低(表示准备好接收),接收端忙时CTS拉高(让发送端暂停)。如果这两个信号一直不动,说明流控没配好。

4.4 控制台重定向:让内核打印从指定串口输出

控制台重定向,就是把内核的printk打印信息从默认串口改到别的串口。这在调试时特别有用——比如默认串口被其他功能占用了,或者你想把调试信息和业务数据分开。

4.4.1 内核启动参数配置

在bootloader的bootargs里加console参数:

console=ttyS0,115200n8

这表示内核的printk输出到/dev/ttyS0,波特率115200,8N1。如果想同时保留多个控制台,可以写多个console参数:

console=ttyS0,115200 console=ttyS2,1500000n8

内核会按顺序尝试,最后一个成功的作为主控制台。

4.4.2 运行时重定向

系统跑起来后,也可以动态改控制台。我常用的方法:

# 查看当前控制台
cat /proc/consoles

# 把控制台输出重定向到ttyS2
echo ttyS2 > /sys/class/tty/console/active
我的经验:调试阶段,我习惯把两个串口都设成控制台。一个接调试线看内核日志,另一个接外设模块。这样外设出问题时,内核日志和业务数据互不干扰。

4.5 多路复用:一个串口当两个用

有时候硬件上串口不够用,或者你不想多接一根线。这时候可以用多路复用技术,让一个串口同时跑控制台和业务数据。

Linux提供了console和tty的分离机制。控制台只输出内核日志,业务数据走/dev/ttyS*节点。但默认情况下,两者是共享同一个串口的,会互相干扰。

解决办法是用kgdboc或sysrq机制,或者干脆用软件方法做数据分流。我推荐一个更实用的方案——用mux驱动:

# 在内核配置中开启
CONFIG_SERIAL_8250_MOXA=y
# 或者用虚拟串口方案
CONFIG_SERIAL_MUX=y

配置好后,一个物理串口会虚拟出多个/dev/ttyMUX*节点。控制台绑定一个,业务程序用另一个,互不打架。

注意:多路复用会引入额外的延迟和CPU开销。如果对实时性要求高,建议还是老老实实加硬件串口。Rockchip平台一般都有8-10个UART,够用了。

4.6 调试实战:串口不通怎么办?

串口不通,是嵌入式开发最常见的故障之一。我总结了一套排查流程,你照着做,90%的问题能定位到。

  1. 查硬件连接:用万用表量TX/RX引脚电压。空闲时TX应该是高电平(3.3V或1.8V)。如果电压不对,查原理图、查焊接。
  2. 查设备树:确认引脚复用配对了没有。用cat /sys/kernel/debug/gpio看引脚状态。
  3. 查驱动加载:dmesg | grep tty看串口驱动有没有注册成功。
  4. 查设备节点:ls -l /dev/ttyS*看节点有没有创建。
  5. 查权限:ls -l /dev/ttyS0看用户有没有读写权限。没有就chmod 666或加用户组。
  6. 查波特率:用stty -F /dev/ttyS0 -a确认波特率、数据位、停止位、校验位是否匹配。
  7. 查回环测试:把TX和RX短接,发数据看能不能收到。收不到就是硬件或驱动问题。
我曾经遇到一个奇葩问题:RK3588的UART9,设备树配好了,驱动也加载了,但就是收不到数据。用示波器抓RX引脚,波形正常。最后发现是DMA通道被其他外设占用了,串口DMA模式起不来。改成PIO模式就好了。所以,遇到串口问题,先关掉DMA试试。

4.7 本章小结

串口调试,说难不难,说简单也不简单。核心就三点:驱动框架要懂、波特率流控要配对、控制台要会重定向。多路复用是锦上添花的技巧,不是必须的。

你在Rockchip平台上调串口,记住我这句话:先查硬件,再查软件;先看波形,再看日志。按这个顺序来,没有调不通的串口。