
把一台巡检小车真正“连起来”这件事说起来简单做起来却比想象中多很多小坑。我说的“连起来”不是指把螺丝拧紧、把排线插上而是让底盘的速度指令、雷达的扫描点云、IMU的姿态数据、摄像头的画面、导航的路径规划全都老老实实在ROS2里打通形成一个能跑闭环的系统。我前段时间刚好整理了一套入门级巡检方案把整个过程拆成了6步每一步都对应一个可以单独验证的里程碑。这篇文章就按这个6步边做边记适合手里已经有一套小车硬件、但节点之间还是一盘散沙的人参考。1. 先把这6步放在桌面上整体设计思路1.1 为什么是这6步而不是4步或8步我自己最开始接触ROS2的时候最容易犯的错误就是拿到一台车就想直接跑导航结果底盘没转起来、雷达数据是空的、TF树警告刷屏最后根本分不清是哪里出的问题。后面我把整台车的“连通”拆成6步思路其实和搭积木一样先让最底层的数据通路稳定再让传感器一层一层把数据送到上位机最后才做定位、建图和导航。这6步分别是底盘驱动、传感器接入、TF坐标树搭建、可视化与遥控验证、建图、导航。每一步做完都有明确的“信号”可以检查比如驱动节点跑通了底盘会转、雷达节点跑通了终端会不断打印scan数据。相邻两步之间有依赖关系前一步没通后一步先不碰这样排查问题时范围会小很多。这个拆法和软件开发里的模块化测试一个道理。底盘驱动只管收发串口数据雷达驱动只管把激光数据发布到/scan导航模块只需要知道“subscribe到哪些话题、发布哪些话题”不需要关心数据是从什么硬件来的。所以6步本质上是在做“数据链路”的逐层打通而不是一上来就调整机。1.2 每一步的验收标准连“通”而不是连“上”连“上”指的是线接上了、驱动识别到设备了连“通”指的是数据在ROS2话题里真实流动并且能被其他节点消费。这两者的差距非常大很多新手卡住就是因为在“连上”就停了后面一跑导航就露馅。所以我每一小节都给定一个验收动作对照着做就能知道自己到底到哪一步了。这里把6步验收标准先列成一张表后面实操的时候可以每完成一步回来打个勾步骤核心动作验收信号对应话题/文件1底盘驱动发布/cmd_vel后轮子转动/odom持续输出/cmd_vel、/odom2传感器接入雷达、IMU、摄像头话题均能看到数据/scan、/imu、/image_raw3坐标树搭建TF树无缺失帧Rviz2里模型随底盘运动/tf、/tf_static4可视化与遥控键盘控制车辆移动画面与真实方向一致Rviz2、teleop5建图建立的地图轮廓与实际场地基本一致/map、PGM/YAML6导航巡检点击目标点后车辆自主避开障碍到达/goal_pose、行为树这张表的妙处在于每一步我都能用一张图或一行命令确认“这里OK了可以进行下一步”。对于想要抄作业的读者来说建议把这6个验收信号打印出来贴在工位旁边比在代码里乱翻有用得多。2. 开始前的准备硬件、系统与一张“连接地图”2.1 硬件清单和整机拓扑在动手敲第一条命令之前先把硬件拓扑画清楚。我用的是一台常规的差速底盘车体上装了一块带编码器的电机驱动板通常由STM32或同级别MCU控制上位机用的是一台小型工控机跑Ubuntu和ROS2。传感器这边我配了入门级单线激光雷达、一颗USB摄像头、以及一块六轴IMU。电源部分用一块大容量锂电池同时给底盘驱动板和工控机供电注意电压等级要匹配否则底盘电机和工控机之间容易出现共地干扰。把这些设备画成一张拓扑图心里会非常清楚底层MCU通过串口线连到工控机的USB口激光雷达通过另一路串口或USB口接入IMU用I2C或串口接入摄像头直接USB接入路由器则负责工控机与外部调试电脑之间的通信。巡检场景里一般不需要机器人自己处理所有重计算地图构建和路径规划可以在车上的工控机完成但远程监控需要局域网所以路由器和DDS网络配置也是“连起来”的一部分。硬件选型上有一个容易被忽视的点串口设备的数量。很多人以为工控机一个USB口接一个设备就够了实际上雷达、底盘、IMU可能都要用串口USB口不够时建议用一个带独立芯片的USB HUB避免多个设备同时占用导致数据堵塞。我这里用的是普通USB HUB实测三条串口同时跑10Hz以上的数据没有丢包但一定要买质量好一点的。2.2 系统安装与ROS2环境初始化系统这边我强烈建议直接用Ubuntu 22.04配ROS2 Humble理由是长期支持版本稳定、教程资料多而且与Nav2等核心栈的兼容性最好。安装的时候不要图省事用精简版系统装完桌面环境后记得把常用的编译工具和git装上后面调试会省很多事。ROS2的安装大致分两步先启用官方软件源然后安装ros-humble-desktop这个完整包。装完之后环境变量需要写进~/.bashrcsource /opt/ros/humble/setup.bash这行一定要加。很多人栽在“明明装了ROS2ros2命令却找不到”这种问题十有八九就是没source环境。另外建议把~/.bashrc里额外加上export ROS_DOMAIN_ID0如果有多台机器人一起调试改成不同的值可以避免话题互相串扰。还有一个新手容易忽略的“工作空间”概念。自己写的节点、自定义的launch文件一定要放到~/ros2_ws/src下用colcon build编译安装而不是直接往/opt/ros里塞。第一次建工作空间会有点懵但这是后面所有自定义节点的运行基础。我一般是mkdir -p ~/ros2_ws/src把代码放进去之后cd ~/ros2_ws colcon build --symlink-install这个--symlink-install参数很实用改Python代码不用重新编译就能生效。2.3 一个很容易被忽视的前置工作串口权限和网络配置连接底盘的串口在Linux下经常遇到权限问题现象是ls /dev/ttyUSB0能看到设备但打开时提示Permission denied。这个问题的根源是用户不在dialout用户组里解决方式很简单sudo usermod -aG dialout $USER改完后要重新登录才生效。除此之外我建议为每个串口设备绑定一个固定的“别名”因为USB设备的编号ttyUSB0、ttyUSB1每次插拔顺序都可能变。具体做法是写一个udev规则文件根据设备ID或串口号识别例如让雷达总是映射为/dev/radar底盘映射为/dev/chassis。这样驱动节点里的设备路径不会因为重启而漂移。网络配置是很多人最后才想起来的事情。如果使用外部电脑通过SSH远程操作机器人一定要确保工控机和调试电脑在同一个局域网里ROS2默认基于DDS通信跨网段基本连不上。我自己的习惯是把工控机IP固定成局域网里的静态地址例如192.168.1.100避免DHCP导致IP频繁变化。ROS2的发现协议依赖多播如果路由器开了AP隔离一定要关掉不然节点之间互相找不到终端只会一直“waiting for node”。3. 六步实操全记录从底盘到导航一步一验证3.1 第一步让底盘听懂/cmd_vel并回报/odom底盘驱动是整个系统里最硬核的一步因为它涉及串口协议解析、控制指令转换和里程计解算三件套。多数底盘的通信协议非常简单上位机往串口发一帧包含目标线速度和角速度的数据MCU解析后驱动电机同时周期向串口回传编码器计数和转向数据。我这里用的底盘波特率是115200数据帧格式固定每一帧有帧头、数据区和CRC校验。ROS2侧我写了一个Python节点订阅/cmd_vel话题收到Twist消息后把它换算成左右轮转速再按协议格式打包发送。代码核心大概是import serial import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist class ChassisDriver(Node): def __init__(self): super().__init__(chassis_driver) self.ser serial.Serial(/dev/chassis, 115200, timeout0.05) self.sub self.create_subscription(Twist, cmd_vel, self.cmd_callback, 10) self.timer self.create_timer(0.05, self.read_serial) def cmd_callback(self, msg): linear msg.linear.x angular msg.angular.z # 差速模型v_l v - w*L/2, v_r v w*L/2 v_l linear - angular * 0.15 v_r linear angular * 0.15 # 这里把v_l/v_r换算成电机转速单位封装成帧并通过串口发送 self.ser.write(build_frame(v_l, v_r))电气和通信层面的细节一定要看底盘厂家提供的协议文档。我在这个环节踩过最大的坑是“后轮倒转”也就是速度和实际运动方向相反。解决方式是先让底盘低速前进如果实际后退就把左右轮速度取反或者在驱动节点里加一个方向校准参数。第二步是里程计。MCU回传的是两个轮子的编码器增量值要在ROS2里转成nav_msgs/msg/Odometry消息给出位置、速度和协方差。这里的核心参数是轮子的每米脉冲数ticks per meter通常要自己标定让车跑一段已知距离统计累计脉冲反推比例系数。这个标定值如果偏差超过百分之几导航时车辆的位置估算就会漂移得很快后面建图会非常吃力。我建议第一步就把里程计话题存成ros2 bag跑完直线和旋转各一组后面调参有据可查。3.2 第二步接入雷达、IMU和摄像头数据话题逐个亮灯底盘通了之后开始让传感器“开口说话”。激光雷达我用的是单线扫描雷达拿到手先确认接口类型常见的有串口和网口两种。串口雷达通常要求波特率固定比如230400我对准设备给的参数写driver核心是确认雷达的扫描频率和角度范围。ROS2里雷达驱动的输出话题是sensor_msgs/msg/LaserScan里面有angle_min、angle_max、ranges这些关键字段。验证方式最简单在终端运行雷达节点后ros2 topic echo /scan ranges --once能看到几百个浮点数数组说明雷达数据进来了。IMU接入比雷达简单一些但要注意校准。MPU6050这类六轴传感器零偏和尺度因子直接影响之后的姿态融合质量。我跑IMU节点后做的第一件事是把车放在水平桌面不动观察/imu/data_raw里的角速度数据理论上应该接近0如果有明显偏置需要在驱动节点里做静态校准补偿。摄像头这边直接用usb_cam节点的现成参数就行但要注意分辨率和帧率的平衡巡检场景我通常用640x480或1280x72015到20帧。分辨率太高会把CPU吃满导致其他节点延迟。摄像头的关键话题是/image_raw验证方式可以用rqt_image_view直接看画面确认镜头朝向与车体坐标的对应关系。这一步做完三个传感器的话题应该同时亮灯。我一般会用一个命令总览ros2 topic list ros2 topic hz /scan /imu /image_raw话题频率稳定才是真的“通”。雷达10Hz、IMU 50Hz、摄像头15FPS这是我在这个配置下比较满意的状态如果雷达频率忽高忽低先检查供电这是最常见的原因。3.3 第三步用TF树把所有传感器“摆到”机器人的正确位置上数据话题通了只是第一步ROS2里的所有传感器数据必须配上坐标才能被正确使用。这里的“坐标”就是一个机器人自身的TF树。简单理解TF树描述了雷达、IMU、摄像头、底盘中心这些部件之间相对位置关系比如雷达装在底盘前方10厘米、上方20厘米处就要在TF树里附加一个偏移量。这一块我推荐两步走。第一步是准备一个URDF模型把车体底盘、雷达、IMU、摄像头的物理尺寸和安装位置描述清楚用robot_state_publisher发布/tf和robot_description。URDF不复杂硬壳机器人脆弱的部分在于坐标系方向约定ROS里x轴朝前、y轴朝左、z轴朝上绝大多数传感器也是按这个约定但摄像头如果有俯仰角麻烦就得认真画坐标。第二步是把传感器数据对应的坐标系挂到TF树上。IMU数据的话题里header.frame_id要写成imu_link雷达数据写成laser_link然后这些link和base_link之间用static_transform_publisher发布静态变换。ros2 run tf2_ros static_transform_publisher 0.10 0.0 0.20 0 0 0 base_link laser_link这项命令的意义是声明激光雷达装在base_link往x方向10厘米、z方向20厘米的位置无旋转。调试时如果Rviz2里雷达点云和底盘模型对不上就回头检查这条命令里的数值。TF树的验证用ros2 run tf2_tools view_frames生成PDF直观看到所有坐标系的父子关系缺哪一根一目了然。3.4 第四步Rviz2可视化与遥控验证到这一步技术上已经“活了”接下来要做的是亲眼确认整辆车感知到的世界与真实世界一致。打开Rviz2后把Fixed Frame设置成odom或map还没建图时用odom然后添加显示项RobotModel显示整车模型、LaserScan显示雷达点云、Image显示摄像头画面、TF显示坐标轴。此时用手慢慢推一下小车点云和模型应该同步移动IMU数值应有变化这就是可视化验证。遥控验证建议用teleop_twist_keyboard这个现成节点在一个终端ros2 run teleop_twist_keyboard teleop_twist_keyboard按键盘上的前后左右键车辆应该按照按键方向运动。我实测下来要特别注意转向方向按下左转键车辆应该往左转而不是像坦克那样原地转反方向。这个如果反了往往是左右轮速度符号处理反了回头改驱动节点。这一步还有一个很重要的检查项是“雷达点云与真实障碍物的对应关系”。让车辆面向一面墙Rviz2里墙面的点云应该是一条清晰的直线如果点云出现毛刺、断点基本可以判断雷达供电不足或者扫描频率设置过高。如果IMU的roll/pitch在平地上明显漂移回第二步重新校准。3.5 第五步建图与保存地图前面四步都稳定了就可以真正开始让机器人“认识”环境了。巡检车的核心能力之一就是环境建模我用的方案是slam_toolbox它比早期流行的gmapping在封闭回环和计算资源上更省心适合入门。启动建图之前先让车在待建图区域缓慢走一圈目的是让激光雷达能得到足够的几何约束。启动slam_toolbox的方式一般是自己写一个launch文件配置里最重要的参数是mode: mapping、map_frame_name: map、odom_frame_name: odom、scan_topic: /scan、base_frame: base_footprint。这些frame_id必须和前面TF树里完全一致否则slam_toolbox根本跑不起来。开始建图后用键盘遥控车辆慢慢地走“之”字形路线速度控制在0.2m/s以内转角不要过猛然后给地图足够的时间闭环。建图过程中可以在Rviz2里实时看到地图逐渐成型。如果地图出现重影比如走廊重复画成两条相距很近的线优先怀疑里程计不准按3.1里说的重新标定轮径和轮距。地图满意后保存地图文件。标准做法是ros2 run nav2_map_server map_saver_cli -f ~/maps/office这条命令会生成office.pgm和office.yaml。yaml里的resolution和origin参数不要手动乱改后续Nav2启动时会读这个文件。保存完地图记得把车移到地图外或者重新启动定位节点否则报告一个“Resume mode”也很容易把人绕晕。3.6 第六步Nav2导航与巡检任务串联导航是整个系统的“最后一公里”。Nav2是一整套行为树框架简单理解就是给它一个目标点它自己决定走哪条路、怎么避障、怎么停下来。启动Nav2相对简单很多现成launch文件可以用但必须把地图路径、参数文件配置好。我建议直接使用Nav2自带的参数模板然后按自己车的尺寸修改几个关键参数robot_radius或robot_shape、footprint、update_frequency、linear/angular速度限制。其中机器人半径如果设置得太大缝隙会过不去设置太小又容易刮蹭。我通常直接用卷尺量底盘最突出的部位。导航启动后在Rviz2里点击“2D Goal Pose”按钮在地图上点一个目标点车辆应该自动规划出一条路径并开始行驶。第一次跑经常会遇到车辆“原地打转”或“来回抽风”的情况大部分原因是Local Costmap的膨胀半径设得过大或者雷达数据在局部代价地图里产生了虚假障碍。可以把局部代价地图里的observation_sources参数打开检查/scan话题是否真的是被正确监听。到了巡检场景需要的不只是“点一点走一走”而是多点自动巡检。这一步建议用行为树或者简单的状态机定义一组巡检点比如仓库里的货架编号对应坐标机器人依次导航到达每个点停留若干秒再接着下一个点。Nav2提供NavigateThroughPoses接口输入一条路径上的多个目标点车会按顺序走。我做了一个简单的Python脚本循环发送goal实测在电量充足的情况下能连续跑完20多个点中途如果遇到障碍由Nav2自己replan不需要人工干预。4. 联调过程中的常见问题速查4.1 底盘驱动没输出的排查顺序底盘驱动是整套系统里最让人头疼的部分因为一旦不顺问题出在串口、供电、协议还是线程同步全都有可能。我的排查顺序永远是设备层、物理层、协议层、ROS2层。设备层先看系统是否识别到串口ls /dev/ttyUSB*没有输出就查USB线是否接触不良物理层用dmesg | tail看内核日志有没有报错以及用示波器或逻辑分析仪看串口是否有电平跳变协议层把底盘串口连到电脑上用串口助手直接发测试帧确认MCU的回传数据是不是符合协议文档ROS2层再看节点运行日志和话题收发。这个顺序一次就能定位大多数问题别一上来就改代码。另外特别提醒很多底盘驱动板对输入的电平有限制电脑主板出的串口有时候电平和驱动板不搭这时串口助手能看到数据但底盘不动作多半是电平问题中间加一个转换模块就好。4.2 TF树断链和坐标混乱怎么办TF问题听起来高级实际上大部分是frame_id拼写错误。ROS2里frame_id必须和TF树里的坐标系名字完全一致比如雷达发布的消息header.frame_id写成了laser但URDF里叫laser_link那Rviz2里这一帧数据就会消失或者乱跳。先跑ros2 run tf2_tools view_frames生成TF树结构图看缺失的坐标系在哪。常见情况是只差了最后一个静态变换没有发布那就再补一条static_transform_publisher。如果一切看起来都对但点云仍不对齐请检查静态变换里旋转量的方向余弦表达90度这种旋转很容易指数错一个符号导致雷达点云旋转90度。4.3 建图重影、导航偏移的调参心得遇到建图重影重点是区分“激光问题”还是“里程计问题”。激光问题通常表现为点云毛刺、局部形状失真里程计问题则表现为整体重影、走廊墙体“增肥”。我调试的顺序是先看里程计把车走一个固定的矩形回来看odom轨迹是不是闭环如果溢出很多先重新计算轮径和轮距。导航偏移的话先确认AMCL定位是否收敛。启动后用Rviz2看粒子分布如果粒子非常分散说明初始位姿给得不准手动在地图上摆放一个大致正确的初始位置然后再点目标。另一个容易被忽略的参数是robot_radius碰撞检测半径和inflation_radius膨胀半径这两个值太大会导致路径绕远、频繁重新规划太小会贴近障碍物危险就在一瞬间。5. 一些踩过的坑写在最后5.1 参数标定比换硬件更重要我在这个项目里最大的体会是绝大多数“车跑不稳”的问题不是硬件不行而是没标定或参数没配对。轮径差1毫米建图都能看出来导航更是很明显。所以从一开始就用最小成本把标定做扎实用卷尺量出10米直线距离让车低速跑对比编码器统计值与实际距离算出精确的ticks_per_meter。轮距就用旋转180度实验来标让车原地转一圈看实际角度和设定角度的差值反复改轮距参数直到误差控制在1度以内。这一步值得花一个下午省下的是后面无数个调试的晚上。另外巡检车长期运行会导致机械磨损、胎压变化同样的标定参数久了会偏离建议每隔一段时间重新做一次快速标定不用全套流程至少验证直线走10米误差是否还在可接受范围内。5.2 日志与启动脚本要当成正经工程来做连一遍能跑通不难难的是每次都稳定跑通。很多初学者习惯一个终端一个终端地手动ros2 run重启一次车就要重新敲一串命令最终只会越来越烦躁。我从项目中期开始把所有启动过程收拢到launch文件里一个launch把底盘、雷达、IMU、摄像头、Nav2全部串起来再用systemd服务注册成开机自启上电之后机器人自己进入工作状态而不是每次都要SSH进去手动操作。日志这里我也想多说一句。ROS2提供了ros2 bag record我建每张图、跑每次导航实验前都会后台记录一份出了问题可以回放话题数据比在终端里肉眼看刷屏可靠得多。具体做法是写个脚本启动ros2 bag记录所有需要的topic日志文件按日期自动归档跑一段时间后发现问题再回放能精确定位是哪一步的数据坏了。最后再分享一个小技巧写一个“连通性检查脚本”它依次检查设备文件是否存在、所有驱动进程是否存活、话题发布频率是否达标、TF树是否完整。跑一次出个报告比手动一条条查命令高效太多。这套方法后来被我移植到另一台底盘不同但架构类似的巡检小车上只改了串口别名和标定参数剩下的东西基本原样就能复用省掉了很多重复劳动。