摘要
实际项目里,ROS 2 节点接串口设备时,经常会遇到一种情况:节点日志看起来一切正常,设备却一点反应都没有。
这时候很多人会先去怀疑协议、设备或者业务代码,但真正该先确认的,往往是更底层的一件事:
程序到底有没有把你以为的那帧数据写到串口。
只看日志不够。日志只能说明代码走到了某一段逻辑,不能说明最终写进 /dev/ttyUSB0 的字节流就是对的。
这篇文章只做一件事:用 strace 直接看 ROS 2 节点到底往串口写了什么,然后顺着这条线继续排。
一、问题现象
先看一个很常见的现场:
- ROS 2 节点通过串口控制 Modbus RTU 设备
- 日志显示命令已经执行
- 设备没有响应
这时候真正要排的,通常不是一句“程序有没有跑起来”,而是下面几件事:
- 程序到底有没有发数据
- 发出去的是不是你以为的那一帧
- 串口参数对不对
- 还是硬件、接线、485 收发方向有问题
这里最容易把人带偏的一点,就是把“日志正常”当成“串口发送正常”。这两件事不是一回事。
二、为什么用 strace
Linux 下串口发送最后都会落到系统调用,比如:
write(fd, buffer, len)
strace 看的就是这层,所以它很适合干这种活。
它不依赖你代码怎么封装,也不依赖日志有没有漏。进程最后到底调了什么系统调用,它就给你打出来。对串口排障来说,这一步往往比继续补业务日志更直接。
常用的几个参数:
-f:跟踪子进程-xx:把字符串按十六进制完整打印出来-yy:把 fd 对应的设备路径也一起打出来-e trace=write:只盯write,先把噪声压下去
三、怎么挂 strace
很多人第一反应会这么试:
strace ros2 launch xxx.launch.py
这条命令不是绝对不行,但对串口排障来说通常不太合适。因为 ros2 launch 只是启动器,真正做串口发送的是节点进程。直接 trace 整个 launch 过程,输出会很杂,真正有用的 write() 很容易被淹掉。
更稳的做法是直接 trace 节点。
1. ros2 run 直接挂
如果你平时是用 ros2 run 启动节点,可以直接加 --prefix:
ros2 run --prefix 'strace -f -xx -e trace=write' \
<package_name> <executable_name> --ros-args --params-file config/devices.yaml
这是 ROS 2 官方文档里常见的调试思路:通过 --prefix 在节点前面挂调试器。这里只是把常见的 gdb 换成了 strace。
2. launch 文件里挂
如果你走的是 launch 文件,工程里更顺手的写法是给 Node 加 prefix:
Node(
package="your_pkg",
executable="your_node",
parameters=[config],
prefix=["strace -f -xx -e trace=write"],
output="screen",
)
这样不用手改节点代码,也不用单独去找可执行文件路径,直接复用现有 launch 结构就行。
3. attach 到运行中进程
如果设备已经在现场跑着,不方便重启,那就直接 attach:
ps -ef | grep your_node
sudo strace -p <PID> -xx -e trace=write
这也是现场里很常用的一招。
四、怎么认串口数据
一跑起来,输出里通常会同时混着日志、socket、pipe 和串口写操作。比如:
write(2, "[INFO] ...", 109)
write(24, "\x01\x00\x00...", 32)
write(21, "\x01\x05\x00\x00\xff\x00\x8c\x3a", 8)
1. 先排除日志输出
先记一个最基础的判断:
fd=1一般是stdoutfd=2一般是stderr- 其它 fd 才可能是 socket、pipe 或串口
所以像这种:
write(2, "[INFO] ...", 109)
通常只是日志,不是串口数据。
2. 直接对上 /dev/ttyUSB0
如果你想直接确认某个 fd 对应的是不是 /dev/ttyUSB0,把命令改成:
strace -f -xx -yy -e trace=write ...
输出会变成这样:
write(21</dev/ttyUSB0>, "\x01\x05\x00\x00\xff\x00\x8c\x3a", 8)
这时候就不用再猜了,数据确实是往串口设备写的。
五、怎么看一帧 Modbus 数据
比如你抓到了这一条:
write(21</dev/ttyUSB0>, "\x01\x05\x00\x00\xff\x00\x8c\x3a", 8)
把它按十六进制展开:
01 05 00 00 FF 00 8C 3A
如果这是 Modbus RTU,通常可以这么看:
01:从站地址05:写单线圈00 00:线圈地址FF 00:写入 ON8C 3A:CRC
看到这里,其实已经能先回答一个很关键的问题:程序确实发了“打开某个线圈”的命令,而且不是空写,不是日志,也不是别的 fd。
这一步的意义不在于把协议分析得多细,而是先把“程序根本没发”这种怀疑排掉。
六、有 write 但设备不回怎么继续排
1. 先查常见方向
如果你已经确认:
- 节点确实有
write - 数据帧看起来也对
但设备还是没反应,那问题通常就不在“有没有发”了,而在下面这些地方:
- 波特率、校验位、停止位不匹配
- RS485 收发方向控制有问题
- 从站地址写错
- 寄存器或线圈地址写错
- A/B 线接反、终端电阻、共地这类硬件问题
现场里更常见的情况不是“代码一行都没发出去”,而是发出去了,但设备不认。
2. 把 trace 范围放大
只看 write 不够时,可以把范围放大一点:
strace -f -xx -s 256 -e trace=openat,ioctl,read,write ...
这条命令主要多看三类信息:
openat(...):进程打开的是不是你以为的串口设备ioctl(...):串口参数有没有按预期设置read(...):设备到底有没有返回数据
这里的 -s 256 是把字符串打印长度放大,避免帧太长被截断。
如果输出里能看到:
openat(..., "/dev/ttyUSB0", ...)
再配合 ioctl(TCSETS...) 和后续 read(...) / write(...),基本就能把“打开了哪个口、设了什么参数、有没有发、有没有收”这一整条链路串起来。
结论
ROS 2 串口排障里,很多时候最该先做的,不是继续猜协议,也不是继续补业务日志,而是先把这件事看清楚:
节点到底往串口写了什么。
strace 的价值就在这里。它不关心你的代码写得多漂亮,也不关心日志说了什么,它只看进程最后到底调了什么系统调用。
对串口这种问题来说,这一步通常比继续读代码更快。
评论区