
1. 项目概述与总体设计1.1 为什么选择履带底盘做ROS小车这两年我前前后后做了三台ROS小车从麦轮车到舵轮车再到这台履带车如果让我给新手朋友推荐一个最容易出成果、跑起来最稳的平台履带车绝对排第一。原因很简单不挑地面室内瓷砖、室外柏油路、草地、甚至有点小坡度的土路它都能走而且底盘结构简单、装起来省事不需要像麦轮那样去精细调轮子角度也不像舵轮那样要考虑复杂的转向机构。这台履带车的硬件成本控制在一千五百块以内核心组件包括铝合金履带底盘一套、带编码器的直流减速电机两个、电机驱动板一块、Jetson Nano开发板如果你预算紧张换成树莓派4B也完全没问题、思岚RPLIDAR A1激光雷达一台再加上一块12V锂电池和若干杜邦线。选型的时候我最看重的是编码器。没有编码器的电机你只能发速度指令车轮实际转没转、转多快你完全不知道这在做避障和后续的建图导航时是致命的短板。带编码器的电机可以让ROS读取到真实的轮速反馈实现闭环控制也能靠轮式里程计估算小车位置。这个钱不能省。1.2 整体架构从传感器到执行器的数据流这套系统的数据流是典型的ROS发布订阅模式激光雷达不停扫描周围环境把距离数据以/scan话题发布出来里程计节点读取编码器数据计算小车的位移和速度发布到/odom话题避障节点同时订阅这两个话题根据激光雷达测到的距离信息判断前方是否有障碍物如果发现障碍物就计算一个绕行方向把新的速度指令发布到/cmd_vel话题底盘驱动节点订阅/cmd_vel把速度指令解析成左右电机的PWM值最终驱动电机转动。这里有一个很多新手容易想歪的点避障不是把障碍物识别出来再去躲避而是一个纯反应式的过程。代码只关心某个方向上有没有东西、距离多远然后根据障碍分布调整行进方向。不涉及目标检测、不涉及语义分割激光雷达返回的只是一串距离值我们拿这串距离值做判断即可。对于自动驾驶这个概念我得说句实在话ROS小车上的自主导航和真正的自动驾驶汽车是两码事但核心思想一脉相承。你在这台小车上实现的避障算法本质上是自动驾驶里局部路径规划这个模块的简化版。把小车换成正儿八经的汽车把激光雷达换成多线雷达把电机换成线控底盘框架依然成立。这也是这个项目最有学习价值的地方。2. 开发环境与基础准备2.1 ROS版本选择别再纠结直接上对应版本ROS安装问题劝退了不知道多少人我最早就是死在安装这一步上。先说结论你用的是Ubuntu 20.04就装ROS Noetic这是ROS1最后一个长期支持版本资料最多、遇到问题最容易搜到答案你用的是Ubuntu 22.04就装ROS2 Humble这是目前主流的中长期支持版本你用的是树莓派官方系统或者某些ARM架构的板子优先考虑Ubuntu Server ROS2。我在Jetson Nano上用的是Ubuntu 20.04 ROS Noetic所以下面的内容都基于这个组合。如果你问ROS1和ROS2到底选哪个我建议学习用ROS1资料多新项目起步直接用ROS2官方主推但本文为了配合避障代码的简洁性采用ROS1 Noetic的写法。2.2 鱼香ROS一键安装新手最大的福音讲真ROS的安装教程在网上一搜一大把但绝大多数都过时了。早几年装ROS最痛苦的是添加软件源、导入GPG密钥、处理各种依赖冲突光这些就能折腾一整天。现在有了鱼香ROS的一键安装脚本这个痛点被彻底解决了。wget http://fishros.com/install -O fishros . fishros运行之后会弹出菜单选择对应的ROS版本安装即可。脚本会帮你把换源、添加密钥、安装依赖、初始化rosdep这些步骤全部搞定。我在三台不同环境Jetson Nano、虚拟机、实体PC上都用过这个脚本没有一次翻车。重要提示安装ROS时建议把虚拟机内存调到4GB以上、硬盘空间预留20GB以上。Noetic完整版桌面安装大概要占6~8GB空间如果空间预留不够中途磁盘满了会留下一堆半装状态的依赖包后面修起来极其痛苦。2.3 创建工作空间三个基础命令ROS的代码组织方式是工作空间workspace我习惯把它叫catkin工作空间。新手一定要动手敲一遍理解这三行命令背后的逻辑因为这决定了你后面所有代码放在哪、怎么编译。mkdir -p ~/tracked_robot_ws/src cd ~/tracked_robot_ws/src catkin_init_workspace cd ~/tracked_robot_ws catkin_make这里说一下为什么用catkin_make而不是catkin build前者是ROS1最经典的编译方式配置简单、不需要额外安装catkin_tools适合新手后者功能更多、支持并行编译加速适合大型项目。对于这个小车项目catkin_make完全够用。还有一个细节编译完成后source ~/tracked_robot_ws/devel/setup.bash这行命令要加到~/.bashrc里不然每次打开新终端系统都不知道你自定义的这些ROS功能包在哪。2.4 硬件连接与串口权限配置底盘驱动的硬件连接相对直接两个直流减速电机的电源线接到驱动板的电机输出端口编码器信号线一般是A相、B相、VCC、GND四根接到驱动板的编码器接口或Jetson Nano的GPIO/UART端口。具体的接线方式因驱动板型号而异我这里用的是常见的一体化电机驱动板已经集成了编码器读取和PWM输出功能板子通过USB转串口模块连接到Jetson Nano。这里最大的坑是串口权限。Linux默认情况下普通用户没有权限直接访问USB串口设备你会碰到权限不足之类的报错。解决方法是把当前用户加到dialout用户组sudo usermod -a -G dialout $USER改完之后需要注销重新登录才能生效。这一步不做后面跑驱动节点的时候会莫名其妙地打不开串口排错排半天找不到原因。确认设备是否被识别ls -l /dev/ttyUSB*如果看到ttyUSB0之类的设备说明驱动板已经被系统识别了。如果你插上板子却看不到任何ttyUSB设备先检查USB线是不是只有供电没有数据这种线市面上太多了我踩过这个坑。3. 激光雷达驱动与数据接入3.1 RPLIDAR A1驱动安装与测试思岚的RPLIDAR A1是入门级激光雷达里性价比极高的一款360度扫描、测距范围0.15米到12米、采样频率约每秒8000次对这个项目来说绰绰有余。驱动安装最省事的方式是直接用官方提供的ROS功能包cd ~/tracked_robot_ws/src git clone https://github.com/Slamtec/rplidar_ros.git cd ~/tracked_robot_ws catkin_make装好之后把雷达的USB线插到Jetson Nano上先给雷达权限再启动驱动节点sudo chmod 777 /dev/ttyUSB0 roslaunch rplidar_ros rplidar.launch正常情况下终端会输出雷达固件版本、串口波特率等信息然后开始持续发布/scan话题。注意RPLIDAR A1在ROS下的串口默认是/dev/ttyUSB0。如果你同时插了其他USB串口设备比如下面要提到的底盘驱动板雷达可能变成ttyUSB1。这时候需要修改rplidar.launch里的serial_port参数把端口改成实际对应的那一个。验证雷达数据是否正常最直观的方式是打开rvizrviz在左下角把Fixed Frame固定坐标系改成laser然后点Add添加一个LaserScan显示Topic选/scan立刻就能看到周围环境的激光扫描轮廓。3.2 用RViz可视化真机调测的第一块试金石看到雷达点云扫出房间轮廓的那一瞬间整个项目就活了一半。我调试时最常用的验证方法人站在雷达旁边看屏幕上对应方向的距离值是否缩短拿一块纸板在雷达前方0.5米处晃动看rviz里对应角度是否出现了密集的点。这些动作能帮你快速确认雷达硬件工作正常、数据话题发布正常、坐标系配置正确。RViz的操作有几个容易卡壳的地方Fixed Frame下拉框里找不到laser多半是雷达节点没有成功启动看看终端有没有报串口错误添加LaserScan之后显示一堆散乱的点但看不到连续的扫描线这是因为雷达数据中某些角度的距离值不合法值无穷大在rviz的LaserScan显示属性里把Size调小、Style改成Points点就会清爽很多雷达扫描出的房间轮廓是歪的这通常是雷达安装角度问题不是软件问题重新固定一下雷达支架就好。3.3 里程计发布让小车知道自己走了多远纯靠激光雷达数据做避障也能走但你会发现小车在转向时对转弯角度的控制很生硬转多转少全靠蒙。加上里程计之后避障程序就能知道我弯过去了多少度、还要继续转多少度控制精度和流畅度会上一个档次。里程计计算的原理不复杂编码器输出脉冲信号电机每转一圈产生固定的脉冲数驱动板把脉冲数换算成轮子转过的距离左右轮子的距离差再推导出小车的角位移。这部分代码属于底盘驱动包的内部逻辑ROS层面只需要接收一个累计了位姿信息的nav_msgs/Odometry消息。如果做避障时你不想引入里程计可以做一个简化版用原地旋转固定时长来模拟固定角度的转弯比如cmd_vel发0.3rad/s的角速度持续1秒大约转17度。这种办法不是不行但误差累积很快转几次弯之后方向就偏得没法看了。所以我建议既然硬件上有编码器就老老实实把里程计跑起来这个模块后面做建图导航也必须要用。4. 激光雷达避障代码实现4.1 避障策略设计先想清楚避的逻辑写代码之前先想清楚策略。一个实用的反应式避障策略是把雷达扫描范围0~360度分成三个区域前方-30度到30度、左方30度到90度、右方-90度到-30度分别计算每个区域内的最小障碍物距离如果三个区域的最小距离都大于安全阈值比如0.35米说明前方畅通小车直行如果前方有障碍但侧方有空间优先向空间更大的那一侧转弯如果三个区域都被障碍堵死原地旋转180度掉头。这个策略对应到真实场景里很好理解小车在走廊里走雷达发现前方3米处有墙左侧距离2.5米、右侧距离1米程序做出决策优先左转因为左侧空间更大。转弯不是把角度锁定死而是边转边用雷达实时确认侧前方是否打开一旦发现前方又通了就先回正方向继续前进。这个方法不完美比如它没法在先进后退再转弯的组合动作中做全局路径规划但做为一个起步级避障算法它能保证小车在大部分室内环境下不会长时间陷入死胡同。4.2 代码拆解Python版避障节点下面给出一个可以直接跑起来的避障节点。把下面的代码保存为avoidance.py放在你的功能包scripts目录下。#!/usr/bin/env python3 import rospy import math from sensor_msgs.msg import LaserScan from geometry_msgs.msg import Twist class ObstacleAvoidance: def __init__(self): rospy.init_node(obstacle_avoidance_node, anonymousTrue) # 发布速度指令到/cmd_vel话题 self.cmd_pub rospy.Publisher(/cmd_vel, Twist, queue_size1) # 订阅激光雷达的扫描数据 self.scan_sub rospy.Subscriber(/scan, LaserScan, self.scan_callback) # 安全距离阈值单位米 self.safe_dist 0.35 # 小车的最大线速度和角速度 self.linear_speed 0.15 self.angular_speed 0.4 # 分别保存前方、左方、右方的最小障碍距离 self.front_min float(inf) self.left_min float(inf) self.right_min float(inf) self.scan_received False def scan_callback(self, msg): # 一次扫描的角度范围单位弧度通常是从0到2*pi angle_min msg.angle_min angle_increment msg.angle_increment ranges msg.ranges self.front_min float(inf) self.left_min float(inf) self.right_min float(inf) for i, r in enumerate(ranges): # 跳过无效数据nan、inf、或者极小值 if not math.isfinite(r) or r 0.01: continue angle angle_min i * angle_increment # 把角度映射到 -pi ~ pi 范围方便判断 while angle math.pi: angle - 2 * math.pi while angle -math.pi: angle 2 * math.pi if -math.radians(30) angle math.radians(30): self.front_min min(self.front_min, r) elif math.radians(30) angle math.radians(90): self.left_min min(self.left_min, r) elif -math.radians(90) angle -math.radians(30): self.right_min min(self.right_min, r) self.scan_received True def decide_and_move(self): if not self.scan_received: rospy.logwarn(等待激光雷达数据...) return cmd Twist() # 策略判断 if self.front_min self.safe_dist: # 前方畅通直行 cmd.linear.x self.linear_speed cmd.angular.z 0.0 else: # 前方有障碍停一下再转弯 cmd.linear.x 0.0 # 优先转向空间更大的一侧 if self.left_min self.right_min: cmd.angular.z self.angular_speed # 左转 else: cmd.angular.z -self.angular_speed # 右转 self.cmd_pub.publish(cmd) def run(self): rate rospy.Rate(10) # 10Hz控制频率 while not rospy.is_shutdown(): self.decide_and_move() rate.sleep() if __name__ __main__: try: node ObstacleAvoidance() node.run() except rospy.ROSInterruptException: pass这段代码的逻辑很清晰scan_callback里做数据预处理和分区最小距离计算decide_and_move里根据三区距离做决策主循环以10Hz的频率持续发布控制指令。为什么控制频率选10Hz这要看雷达的扫描周期。RPLIDAR A1完成一圈360度扫描大约需要100多毫秒8Hz左右如果控制指令发太快比如50Hz会出现雷达数据还没更新程序却拿着旧数据反复做决策的问题如果太慢比如2Hz机器人对突发障碍的反应会严重滞后。10Hz是个兼顾实时性和稳定性的折中值。4.3 功能包配置CMakeLists和package.xml光有Python脚本还不够ROS要求每个功能包都有正确的配置。你要在功能包目录下确认CMakeLists.txt中有这么一段确保Python脚本能被找到并赋予执行权限catkin_install_python(PROGRAMS scripts/avoidance.py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} )同时在package.xml中声明依赖build_dependrospy/build_depend build_dependsensor_msgs/build_depend build_dependgeometry_msgs/build_depend exec_dependrospy/exec_depend exec_dependsensor_msgs/exec_depend exec_dependgeometry_msgs/exec_depend注意rospy是Python版本的ROS客户端库如果你用C对应的依赖是roscpp。改完配置记得编译一下cd ~/tracked_robot_ws catkin_make chmod x src/你的功能包/scripts/avoidance.pychmod x这步经常被新手漏掉漏掉的后果是运行节点时报Permission denied到时候一脸懵。4.4 让小车动起来三终端联调法运行整个系统需要三个终端终端1启动激光雷达驱动roslaunch rplidar_ros rplidar.launch终端2启动底盘驱动把cmd_vel话题的指令转成电机PWMroslaunch tracked_robot_bringup tracked_robot_base.launch终端3启动避障节点rosrun 你的功能包 avoidance.py如果一切正常你会看到避障节点在终端里输出等待激光雷达数据...之后就不再刷屏小车在远端手动推一下或者直接用键盘控制节点teleop_twist_keyboard测试底盘是否响应确认底盘能收到cmd_vel。经验之谈第一次联调时把小车架起来、让履带悬空先不要放地上跑。手动给一个速度指令观察两个履带的转向是否一致。如果左转时右履带正转、左履带反转说明电机方向接反了在底盘驱动代码里把对应电机的方向标志位取反即可不用改接线。5. 整机调试与常见问题排查实录5.1 雷达数据正常但小车不动的排查路径这是我被问得最多的问题。出现激光雷达有数据、避障节点也启动了、但小车死活不动的现象90%的情况是以下三个原因之一cmd_vel话题没有连接避障节点发布的是/cmd_vel但底盘驱动节点订阅的可能是/cmd_vel也可能因为它启动时定义了不同的命名空间导致没有匹配上。用rostopic info /cmd_vel查看发布者和订阅者的数量发布者应该至少有1个订阅者至少有1个如果某一侧是0就说明话题没有对上。底盘驱动节点没有收到指令用rostopic echo /cmd_vel查看当前速度指令的实时数值。如果一直显示零说明避障节点的决策逻辑根本没有产生直行指令检查雷达数据里front_min的值是不是一直接近0或者无穷大。驱动板没有进入使能状态很多电机驱动板有使能引脚默认状态下电机是不通电的需要驱动代码主动拉高使能信号。这个看驱动板的说明文档单独写一个测试节点发送恒定速度指令排除避障代码的干扰直接验证底盘驱动链路是否完整。排查的顺序建议是先绕开避障程序用键盘控制节点或命令行直接发速度指令确认底盘能转再上避障程序一层层往上测。5.2 激光雷达建图飘的问题标题的热搜词里就有激光雷达建图飘这个问题在做建图时几乎人人都会遇到。所谓飘指的是rviz里看到的激光扫描点云和实际环境对不上比如笔直的墙面显示成了波浪形曲线或者扫描点云在移动过程中整体产生漂移。建图飘的根因通常有三个层面里程计不准轮子打滑、编码器数据有噪声、底盘结构松动都会让里程计计算出的位姿和真实位姿产生偏差。履带车在这方面比轮式车更容易飘因为履带转向时和地面的摩擦更复杂。排查办法让小车原地旋转90度看rviz里里程计是否也显示转过了大约90度如果明显偏多或偏少需要校准轮距和编码器分辨率参数。雷达数据的时间戳不同步建图算法把雷达数据和里程计数据按时间戳对齐如果雷达驱动节点和底盘驱动节点的主机时间不一致数据就会错位。尤其在Jetson Nano这类没有内置实时时钟的板子上每次开机时间可能都是错的建议先sudo apt install chrony做一下时间同步或者至少手动date确认系统时间是对的。雷达本身的安装位置或角度有误雷达倾斜、雷达在rviz里的TF变换没有配置对会导致扫描出的地面反射点被当成墙面点云看起来就像飘。处理思路是先排除最伤元气的那个问题先让小车静止看激光数据还会不会飘。如果静止时点云稳定说明雷达硬件没问题问题出在运动中的里程计或坐标变换上如果静止时点云都在跳先查供电雷达的电压不稳定会导致测量异常换一根质量好一点的USB线或者用一个带屏蔽的电源模块。5.3 避障节点消耗CPU过高怎么办Jetson Nano的CPU算力有限跑避障节点的时候如果整机负载过高会直接影响电机控制的实时性。我遇到过一次避障代码和可视化界面全开CPU占用率直逼90%小车反应明显变肉。排查后发现主要耗CPU的是rviz的3D渲染和雷达数据可视化不是避障算法本身。优化手段有几个避障调试期间不要开rviz用rosrun rqt_console或者直接订阅/scan话题看数据摘要就够了降低雷达相关可视化的话题频率比如rviz里把LaserScan的Decay Time调大减少每帧点云的点数在避障节点里做一次降采样不需要处理全360度的所有点比如每隔2度取一个点计算量可以直接砍一半对精度影响不大。5.4 常见问题速查表现象可能原因解决办法串口设备找不到USB线是充电线不支持数据换一根数据线雷达有数据但距离全为0雷达供电不足或USB接触不良检查供电电压重插USB/cmd_vel有数据但电机不动驱动板未使能或PWM通道接反查看驱动板文档检查使能信号和PWM极性左转和右转方向颠倒左右电机的方向标志位配置反了在驱动代码里对调两个电机的方向参数避障时小车频繁原地打转安全距离阈值设置过大把safe_dist从0.5调低到0.35再试雷达点云在rviz里只有一半雷达在某个角度被遮挡或串口丢包检查雷达周围是否有遮挡物换短线材重试建图时地图越来越歪里程计不准或时间不同步先校准里程计再检查系统时间同步履带跑偏严重两侧电机转速不一致测量左右履带实际转速做PWM补偿电池掉电太快驱动板待机态功耗过高加一个机械开关不用时彻底断电6. 后续扩展方向与个人总结6.1 从避障到建图导航下一站怎么走避障跑通之后自然就会想给小车上地图能力。最推荐的下一步是用Gmapping或Cartographer做SLAM建图然后用move_base做自主导航。热搜词里出现ros2cartographer激光雷达建图并保存其实这套在ROS1下也能做。流程大概是# 启动底盘驱动 roslaunch tracked_robot_bringup tracked_robot_base.launch # 启动激光雷达 roslaunch rplidar_ros rplidar.launch # 启动gmapping建图 roslaunch my_navigation gmapping.launch然后你控制小车在房间里慢慢走一圈用teleop_twist_keyboard边走路边建图房间的轮廓就会逐渐出现在rviz的地图视图里。建完保存rosrun map_server map_saver -f ~/maps/room1这会生成room1.pgm和room1.yaml两个文件前者是图像格式的地图后者是地图的元信息分辨率、原点坐标、占用阈值等。有了地图之后再用AMCL做定位用move_base发布目标点小车就能自己规划路径并避开动态障碍物了。这个过程全部走通你对ROS的导航栈就算有完整理解了。6.2 把激光雷达点云用起来给你的车加眼睛避障代码里用到的LaserScan消息本质上是一个一维距离数组。但如果想识别更复杂的环境特征比如识别行人、区分桌椅腿就需要升级到点云数据处理。方法也不难把LaserScan用laser_geometry的projectLaser函数投影成PointCloud2然后用pcl库做聚类、分割、特征提取。我自己在第二版小车上就加过一个简单的腿检测模块把点云中高度在0.2到0.6米之间的点聚类每个聚类的点数达到一定阈值就认为是一个潜在的人腿。这个玩法很适合用来扩展避障策略比如遇到人时不是急停而是减速绕行体验立刻不一样。6.3 个人踩坑总结最后说几个我在整个过程中最有体感的坑每一个都花了我不少时间串口权限的坑无论雷达还是底盘驱动只要通过USB串口通信都要先把dialout用户组加上。我第一台小车因为忘做这一步反复折腾了两天最后排查到问题竟然这么简单。ROS工作空间路径的坑写避障节点的时候如果提示ModuleNotFoundError找不到自定义的ROS消息八成是没有source对devel/setup.bash或者工作空间没有编译成功。排查顺序先catkin_make看有没有报错再echo $ROS_PACKAGE_PATH确认环境变量包含了你的工作空间路径。雷达供电的坑RPLIDAR A1对供电电压比较敏感如果你的USB口供电能力不足比如通过USB HUB转接雷达会间歇性丢数据。最优做法是雷达单独用一个5V/1A的电源模块供电USB线只负责数据通信不承担供电任务。电机编码器线的坑编码器的信号线很长的情况下容易受到电机电流的干扰导致里程计脉冲数乱跳。如果你的里程计数据在电机转起来之后明显异常试着把编码器线绞合双绞线结构或者远离电机电源线布线。这个项目做到最后最大的感受是ROS小车的难点不在任何一个单独环节而在于把硬件、驱动、算法、调试串起来的那套系统工程能力。你装好了ROS、点亮了雷达、跑通了电机这些单独看都不难难的是让它们像一个团队一样协同工作。把这套协同能力练好后面不管是做建图、做导航、还是做机械臂控制底子都能用得上一辈子。我后来把避障代码里的安全距离参数从固定值改成了动态值车速越快安全距离越大。这个改动通过一个简单的线性关系实现safe_dist 0.25 1.5 * current_speed。效果立竿见影——小车在高速直行时刹车距离明显变短不再一头怼上障碍物才慢慢转向。一个小小的参数改动体验提升了一个档次这也是做机器人项目最让人上瘾的地方一个数值的变化就能让机器的性格发生变化。