自定义 yocto 镜像中 UART ttyS1 首次发送后写入超时,如何调试?
结论
优先排查自定义 Yocto 镜像中的 UART 驱动、设备树和发送完成处理。ttyS1 在 Qt 示例和 shell 写入中都出现异常,ttyS4 却正常,排查重点应放在串口底层。
目前还无法确定问题出在 DMA、中断、时钟还是其他配置上。“发送超过几字节后卡住”提示需要检查 FIFO 和 DMA 路径,但还不足以认定 DMA 故障,也无法据此判断 FIFO 大小。
先确认实际发送了多少字节,再查清进程卡在 write()、等待发送完成,还是 close()。
为什么“发送成功”后程序仍可能卡住
QSerialPort::write() 返回成功,表示 Qt 接受了数据;bytesWritten() 通知也不代表最后一个字节已经从 TX 引脚发出,更不代表对端已经收到数据。
程序打印“Data successfully sent”后仍不退出,可能是因为应用已经处理完写入通知,随后在关闭串口、等待底层发送排空时卡住。需要通过系统调用跟踪确认这一点。
日志中的 Operation timed out 和 errorString() 返回的 Unknown error 也要分开分析。检查所编译的 SerialPortWriter,确认超时是否由示例自身的定时器触发,并记录当时 QSerialPort::error() 的枚举值。仅凭这段错误字符串,无法确定内核报告了哪种故障。
1. 用精确字节测试确认复现条件
先停止测试程序,确认没有其他进程同时访问串口。若系统提供 fuser,可以运行:
fuser -v /dev/ttyS1 /dev/ttyS4
cat /proc/cmdline
ttyS0 上运行 getty 本身无法解释 ttyS1 的故障。应检查 ttyS1 是否被其他程序、getty 或内核控制台占用,无需直接停掉 ttyS0。
对于不使用流控的 TX/RX 测试,先将串口设为 9600、8N1,并关闭软硬件流控:
stty -F /dev/ttyS1 9600 raw -echo cs8 -parenb -cstopb clocal -crtscts -ixon -ixoff
stty -F /dev/ttyS1 -a
然后用 printf 测试不同长度:
printf '%s' 'te' > /dev/ttyS1
printf '%s' 'tes' > /dev/ttyS1
printf '%s' 'test' > /dev/ttyS1
这三条命令分别写入 2、3、4 字节,不附加换行。echo "te" 通常会附加一个 \n;如果串口启用了输出转换,线上字节数还可能增加。因此,原来的现象还不能严格证明“3 字节是故障边界”。
另一个命令需要补上重定向:
echo "test" > /dev/ttyS1
如果写成 echo "test" /dev/ttyS1,只会向标准输出打印文本,不会写入串口。
如果故障发生后串口一直异常,应在重新启动后单独测试各个长度,避免上一次故障影响后续结果。对 ttyS4 也使用相同参数和负载进行对照。
2. 用 strace 找到阻塞位置
在镜像中安装或加入 strace,先跟踪最小复现命令:
strace -tt -T -o /tmp/ttyS1.trace \
sh -c 'printf "%s" test > /dev/ttyS1'
再跟踪 Qt 程序:
strace -f -tt -T -o /tmp/serial-tester.trace \
./serial-tester-ttyS1
找到打开 /dev/ttyS1 后得到的文件描述符,检查相关调用:
| 跟踪结果 | 下一步重点 |
|---|---|
write() 长时间不返回或出现短写 | 写入队列、流控和驱动发送路径 |
write() 已返回,随后长时间等待 poll() 或类似调用 | 写入通知和发送进展 |
某个 ioctl() 长时间不返回 | 结合该请求检查串口配置或发送排空操作 |
close() 耗时很长 | 驱动是否一直认为还有数据未发送完 |
通过跟踪可以确定“约 30 秒后才退出”发生在哪一层。仅凭等待时长,无法认定这是 Qt 的超时。
3. 给 Qt 程序补上定位日志
在 open() 前连接信号,记录错误和写入通知:
QObject::connect(
&serialPort, &QSerialPort::errorOccurred,
&serialPort,
[&](QSerialPort::SerialPortError error) {
if (error != QSerialPort::NoError) {
qWarning() << "serial error:"
<< static_cast<int>(error)
<< serialPort.errorString()
<< "pending:" << serialPort.bytesToWrite();
}
});
QObject::connect(
&serialPort, &QSerialPort::bytesWritten,
&serialPort,
[&](qint64 count) {
qInfo() << "bytesWritten:" << count
<< "pending:" << serialPort.bytesToWrite();
});
显式设置串口参数,并检查返回值:
if (!serialPort.setBaudRate(9600)
|| !serialPort.setDataBits(QSerialPort::Data8)
|| !serialPort.setParity(QSerialPort::NoParity)
|| !serialPort.setStopBits(QSerialPort::OneStop)
|| !serialPort.setFlowControl(QSerialPort::NoFlowControl)) {
qCritical() << "Configuration failed:"
<< serialPort.errorString();
return 1;
}
if (!serialPort.open(QIODevice::ReadWrite)) {
qCritical() << "Open failed:"
<< serialPort.errorString();
return 1;
}
原代码在打开失败后仍会继续执行,调试前应先修正这一点。
将原来的 return coreApplication.exec(); 改为:
const int result = coreApplication.exec();
qInfo() << "Event loop returned:" << result;
qInfo() << "Before close";
serialPort.close();
qInfo() << "After close";
return result;
如果已经打印 Before close,却迟迟没有 After close,就能确认程序阻塞在关闭阶段。如果事件循环没有返回,则继续检查 SerialPortWriter 的退出分支,并结合系统调用跟踪定位。
bytesToWrite() == 0 仅表示 Qt 没有待提交的数据,仍需检查 TX 波形和驱动状态。
4. 确认 ttyS1 实际对应哪个控制器
设备名无法单独确定它对应哪个 UART 实例。驱动、设备树 aliases 和探测顺序都可能影响编号。
先收集系统和设备信息:
uname -a
dmesg | grep -Ei 'ttyS|serial|uart|dma'
cat /proc/tty/drivers
readlink -f /sys/class/tty/ttyS1/device
readlink -f /sys/class/tty/ttyS4/device
如果系统提供 udevadm,也可以查看设备及父设备的属性:
udevadm info --attribute-walk --name=/dev/ttyS1
udevadm info --attribute-walk --name=/dev/ttyS4
沿 sysfs 父设备查找驱动和设备树节点,记录两个端口的控制器地址、驱动名称和 IRQ。目录层级取决于内核与驱动,某个固定路径未必存在。
然后与同一块 BBB 上能够正常工作的 TI 镜像比较:
- 内核版本、串口驱动和 BSP 补丁。
- 实际加载的 DTB,以及启动时应用的 overlay。
- 对应 UART 节点的 pinctrl、DMA、中断和时钟配置。
- UART 引脚是否被其他外设复用。
代码在 Raspbian 上正常运行,可以帮助判断应用逻辑,但 Raspbian 的硬件和驱动环境不能直接用于验证 BBB 的 UART 配置。
5. 根据证据检查 DMA、中断和电源管理
DMA 与 FIFO
如果实际设备树为故障端口配置了 dmas 和 dma-names,检查 DMA 控制器引用、请求号和收发方向是否符合该 SoC 与当前驱动的要求。
确认驱动支持无 DMA 方式工作后,可以制作一个仅禁用该 UART DMA 的测试 DTB。如果属性来自被包含的 DTSI,通常需要在正确节点中删除继承的属性:
/delete-property/ dmas;
/delete-property/ dma-names;
这两行应放入已经确认的故障 UART 节点,不能仅凭 ttyS1 猜测 DTS 标签。
如果禁用 DMA 后故障消失,就继续检查 DMA 配置或驱动路径,并通过进一步对照确定修复方案。发送长度边界也可能与 FIFO 补充、中断或发送完成处理有关。
中断和发送计数
在发送前后查看:
cat /proc/interrupts
cat /proc/tty/driver/serial
/proc/tty/driver/serial 是否存在、显示哪些信息,取决于驱动和内核配置。
观察已确认对应控制器的 IRQ,仅搜索 ttyS1 字符串可能找不到它。中断计数不增长或增长异常快,都有助于缩小排查范围,但在 DMA 模式下,不能要求每发送一个字节就增加一次中断。
时钟和 runtime PM
如果故障与首次启用或空闲后恢复有关,再检查 UART 的时钟和 runtime PM。若系统暴露了对应控制器的 power/control,可以先记录原值,再临时设为 on 做对照,测试完成后恢复。
只有实验表明故障与挂起或恢复有关,才继续排查电源管理,避免一开始就全局关闭电源管理。
6. 核对持续发送的 \0 是否为真实数据
用逻辑分析仪直接测量 BBB 的 TX 引脚,并与板卡共地。解码参数应与实际使用的波特率和帧格式一致。
除了解码结果,还要检查原始波形:
- 每个
0x00是否都有完整的起始位、数据位和停止位? - TX 是否长时间保持低电平,被解码器解释为零字节或 break?
- 故障前后的位宽是否一致?
持续低电平、错误波特率或 pinmux 异常,都可能产生看似连续的 0x00。确认帧结构正常后,才能将其作为驱动确实发送了额外零字节的证据。
如何落实修复
根据已经验证的差异修复:设备树有误,就修正对应节点;DMA 对照实验指向发送路径,就继续核对配置与驱动补丁;确认端口被占用后,再处理相关服务。使用 Yocto 构建时,还要确认修改后的内核和 DTB 已实际部署并加载。
验收应覆盖连续写入、关闭后重新打开、故障长度附近的负载,以及实际的 12 字节二进制数据,同时核对 TX 波形。延长超时、反复调用 flush() 或把数据拆成短块,都不能单独证明底层故障已经修复。
备注:内容仅供参考。
