4. UART串口调试:串口驱动框架、波特率与流控配置、控制台重定向与多路复用
UART串口,做嵌入式Linux开发的人天天打交道。说它是调试的“生命线”一点不为过。你想想看,板子刚上电,系统还没跑起来,网口、USB都还没初始化,你靠什么跟内核对话?就是串口。
我在Rockchip平台上调过不下几十种外设,每次拿到新板子,第一件事就是确认串口能不能正常打印。如果串口都没通,后面的工作基本没法开展。这一章,我就把串口调试的底牌给你翻一翻。
4.1 串口驱动框架:从硬件到用户空间
Linux的串口驱动,说白了就是三层结构。底层操作硬件寄存器,中间层处理数据流和线路规程,上层暴露给用户/dev/ttyS*节点。我习惯把它画成下面这张图,你一看就明白。
嗯,这里要注意。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;
}
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。
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*节点。控制台绑定一个,业务程序用另一个,互不打架。
4.6 调试实战:串口不通怎么办?
串口不通,是嵌入式开发最常见的故障之一。我总结了一套排查流程,你照着做,90%的问题能定位到。
- 查硬件连接:用万用表量TX/RX引脚电压。空闲时TX应该是高电平(3.3V或1.8V)。如果电压不对,查原理图、查焊接。
- 查设备树:确认引脚复用配对了没有。用
cat /sys/kernel/debug/gpio看引脚状态。 - 查驱动加载:
dmesg | grep tty看串口驱动有没有注册成功。 - 查设备节点:
ls -l /dev/ttyS*看节点有没有创建。 - 查权限:
ls -l /dev/ttyS0看用户有没有读写权限。没有就chmod 666或加用户组。 - 查波特率:用
stty -F /dev/ttyS0 -a确认波特率、数据位、停止位、校验位是否匹配。 - 查回环测试:把TX和RX短接,发数据看能不能收到。收不到就是硬件或驱动问题。
4.7 本章小结
串口调试,说难不难,说简单也不简单。核心就三点:驱动框架要懂、波特率流控要配对、控制台要会重定向。多路复用是锦上添花的技巧,不是必须的。
你在Rockchip平台上调串口,记住我这句话:先查硬件,再查软件;先看波形,再看日志。按这个顺序来,没有调不通的串口。