PAYATHON 2026

自定义 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() 或把数据拆成短块,都不能单独证明底层故障已经修复。

备注:内容仅供参考。