简介:这是一份面向ROS初学者与机器人建模学习者的四轮小车仿真模型包,围绕带摄像头与激光雷达的移动平台展开,重点解决机器人URDF建模、传感器声明及模型可视化启动等问题。压缩包共6个文件,约22KB,包含urdf、launch、xml、pdf、gv与txt等类型:其中urdf用于描述车体、车轮、关节及摄像头/激光雷达等传感器结构,launch便于加载并展示小车模型,xml与构建脚本用于组织ROS包配置,pdf和gv则呈现模型结构关系与说明,txt可作补充记录。已有2765人学习,说明其作为轻量级入门案例具有一定参考价值。读者可据此理解link与joint的组织方式、传感器在URDF中的声明方法,以及如何通过launch文件快速查看四轮小车模型,为后续接入图像传输、激光扫描消息、TF坐标变换和差速驱动控制等实践打下基础。
1. 需求切入
实验室里要一台能自己在走廊跑、能画地图、还能认出前方是人还是纸箱的四轮小车,用 ROS 把摄像头和激光雷达串起来,这是很多毕设和课程设计绕不开的组合。真正动手时第一道坎不是算法,而是「先买什么、先搭哪一层、哪一步会翻车」。我见过太多车硬件堆得很满,跑起来 TF 树一断、雷达扫到自己车架、里程计积分偏差累积到无法修正,最后演示只能推着车走。这套方案的目标很明确:用一台四轮差速底盘、一颗 2D 激光雷达、一个 USB 摄像头,在 ROS 里跑通建图、定位、自主导航和一路简单的视觉识别,把每个模块的边界、参数和验收口径都写清楚。适合正在做毕设、课设,或者要给出一份能交付的机器人方案的人。
2. 硬件选型与总体架构:四轮底盘、雷达、相机的指标怎么定
2.1 从任务倒推指标:先定速度和精度,再谈传感器
选型最容易犯的错是拿着预算先买最贵的雷达,回头发现底盘跑 0.8 m/s 时雷达只有 5 Hz,转弯时扫描跟不上,地图上全是拖影。正确的顺序是先把任务指标定下来,再倒推硬件。
这台车的任务设定通常是:在室内平整地面自主导航,最大线速度 0.5 m/s,转弯角速度不超过 1.0 rad/s,建图精度要求走廊宽度误差在 10 cm 以内,视觉识别距离 1~3 m。有了这几个数,雷达就基本被锁死了:扫描频率至少 8 Hz 才能匹配 1 rad/s 的转动,角分辨率要优于 1°才能在 3 m 外分辨出 5 cm 的障碍,测距范围 8~12 m 够覆盖一条走廊。
提示:不要为了「看起来高级」上 3D 多线雷达。2D 雷达在单层平面导航里数据量小、标定简单、costmap 直接可用;多线点云还要做地面分割和降采样,工作量翻两三倍,收益在这类项目里体现不出来。
摄像头侧的要求反而低。普通 USB 免驱摄像头 640×480、30 fps 就够做颜色阈值、AprilTag 位姿估计或者轻量目标检测。如果要做深度估计或者避障,才需要考虑双目或 RGB-D,但那样主控算力要跟着涨。
主控是另一个决策点。跑 gmapping 加 move_base,树莓派级别的 ARM 板勉强能跑但很吃力,尤其建图时 CPU 打满会丢雷达数据;跑视觉检测就基本别想。我一般的做法是:导航和感知全放在一块 x86 小工控机或者算力更强的 ARM 板上,下位机单独用一块单片机负责电机 PID 和编码器读取,两边走串口。这样上层算力不够时只影响建图频率,不会让车失控。
2.2 一份可抄的硬件清单与选型对比
把选型落到具体条目上,下面这张表是我在这个项目规模下常用的配置,指标留了余量,不至于卡在临界点上。
| 部件 | 关键指标 | 选型理由 | 容易踩的坑 |
|---|---|---|---|
| 四轮差速底盘 | 轮距 0.30 m,轮径 0.13 m,带编码器 | 差速模型简单,ROS 生态支持最好 | 麦克纳姆轮虽然能横移,但里程计解算复杂,打滑严重 |
| 2D 激光雷达 | 12 m 量程,8~10 Hz,角分辨率 ≤1° | 覆盖走廊,匹配转弯速度 | 装在太低位置会被地面反光干扰 |
| USB 摄像头 | 640×480,30 fps,可手动关自动曝光 | 视觉识别够用 | 自动曝光会让颜色阈值白天黑夜两套参数 |
| 主控 | 四核 x86 或 6 核 ARM,4 GB 内存以上 | 导航 + 视觉并行 | 只有 2 GB 内存时建图容易 OOM |
| 下位机 | 单片机,带 4 路编码器接口、PWM 输出 | 实时性有保证 | 用上位机直接拉 GPIO,时间抖动极大 |
| IMU | 6 轴,100 Hz 以上 | 抑制里程计角度漂移 | 不标定零偏,静止时航向一直在爬 |
| 电源 | 12 V 锂电,独立 5 V 降压给主控 | 电机启动不拉垮主控 | 共用一个降压模块,一加速主控就重启 |
底盘尺寸决定了后面所有标定参数。轮距 0.30 m、轮径 0.13 m 这两个数会直接写进 URDF 和运动学公式,量的时候用卡尺,不要用卷尺估,误差 5 mm 在转十圈之后就是肉眼可见的航向偏差。
注意:电机启动瞬间的电流冲击是这类小车最隐蔽的故障源。主控和电机驱动必须分开供电,或者在电机侧加足够大的电容,否则表现为「车一动,ROS 节点就掉线」,排查半天找不到原因。
2.3 TF 树与坐标系:map→odom→base_link→各传感器
ROS 里所有传感器数据能不能用,取决于 TF 树对不对。这台车固定的一条链路是:
map → odom → base_link → laser_link → camera_link → imu_linkmap → odom由定位模块发布,AMCL 或者建图节点负责;odom → base_link由里程计发布;base_link → laser_link这些是静态变换,用static_transform_publisher或者 URDF 里的 joint 固定住。任何一段断掉,rviz 里就会看到红色的 TF 报错,导航直接不启动。
URDF 里把底盘和传感器的相对位置写死,是最省事的做法:
<!-- 底盘主体,base_link 固定在底盘几何中心 --> <link name="base_link"> <visual> <geometry><box size="0.28 0.22 0.10"/></geometry> </visual> </link> <!-- 激光雷达:安装在底盘前部上方 12 cm 处 --> <joint name="laser_joint" type="fixed"> <parent link="base_link"/> <child link="laser_link"/> <origin xyz="0.08 0 0.12" rpy="0 0 0"/> </joint> <!-- 摄像头:正前方,略高于雷达,带 5 度下俯角 --> <joint name="camera_joint" type="fixed"> <parent link="base_link"/> <child link="camera_link"/> <origin xyz="0.13 0 0.18" rpy="0 0.087 0"/> </joint>xyz三个数必须是实测值,单位米;rpy是弧度,5 度约等于 0.087。摄像头带一点下俯角能拍到车前 0.5 m 的地面,做近距识别很有用。改完 URDF 一定要在 rviz 里加 TF 显示,把 laser_link 和 camera_link 的坐标轴打开看方向,激光雷达的 0 度朝向必须和 base_link 的 x 轴正向一致,反了的话建出来的地图是镜像的,这个坑很隐蔽。
2.4 节点、话题与启动顺序:整车的软件骨架
整车的软件结构按数据流可以分成四层:驱动层负责把雷达、相机、编码器的原始数据转成 ROS 消息;感知层做建图、定位和视觉识别;决策层是 move_base 这套导航栈;执行层是把/cmd_vel变成四个轮子的 PWM。
核心话题的约定如下:
| 话题 | 类型 | 发布者 | 订阅者 |
|---|---|---|---|
/scan | LaserScan | 雷达驱动 | gmapping、costmap |
/camera/image_raw | Image | 相机驱动 | 视觉识别节点 |
/odom | Odometry | 下位机桥接节点 | move_base、AMCL |
/tf | TFMessage | 里程计、静态变换 | 全局 |
/cmd_vel | Twist | move_base | 下位机桥接节点 |
/map | OccupancyGrid | gmapping / map_server | move_base |
启动顺序有讲究:先起雷达和相机驱动,确认/scan有数据、/camera/image_raw有画面;再起静态 TF 和里程计;最后起导航栈。反过来先起 move_base,它会因为拿不到 costmap 数据疯狂报错刷屏,日志淹没有用信息。
launch 文件里用include把各模块拆开,加launch-prefix控制输出,调试时能单独启停某一层:
<launch> <!-- 驱动层:雷达与相机,必须先起 --> <include file="$(find lidar_driver)/launch/lidar.launch"/> <include file="$(find usb_cam)/launch/usb_cam.launch"/> <!-- 车体模型与静态 TF --> <include file="$(find car_description)/launch/description.launch"/> <!-- 里程计桥接:读串口,发布 /odom 与 odom->base_link --> <node pkg="car_base" type="base_node" name="base_node" output="screen"/> <!-- 导航栈,最后起 --> <include file="$(find car_navigation)/launch/navigation.launch"/> </launch>output="screen"让节点日志直接打到终端,省得去翻 rosout。调试阶段我习惯把导航栈这段先注释掉,等雷达和里程计都验证过了再打开,能省掉大量「到底是谁在报错」的排查时间。
3. 感知层落地:雷达建图定位与摄像头识别两条线怎么接
3.1 雷达驱动接入与 /scan 数据自检
雷达驱动跑起来不等于数据能用。判断标准有三条:rostopic hz /scan的频率和标称值一致;rostopic echo /scan里angle_min、angle_max、angle_increment、range_max四个字段和手册对得上;rviz 里把 LaserScan 的 topic 设成/scan、Fixed Frame 设成base_link,看到的点云形状和你眼睛看到的房间一致。
第二条最容易被忽略。有些驱动默认输出的角度范围是 -180°到 180°,有些是 0°到 360°,angle_min不对,建图时整张地图会旋转一个固定角度,怎么调参数都救不回来。检查时重点看angle_min是不是 -3.14 附近,angle_increment是不是约等于 2π 除以点数。
雷达安装位置还要处理「自检点」。车体本身会挡住雷达后方一段扇形区域,这段数据如果直接喂给 costmap,会出现车周围一圈假障碍,导航时车一直以为自己被围住。处理方式有两种:在驱动里设angle_min/angle_max裁掉被遮挡的角度,或者在 URDF 里给底盘加一个box碰撞体,让 costmap 的footprint覆盖掉这块。我一般两个都做,双保险。
3.2 建图方案选择:gmapping 与 cartographer 的取舍
建图这步在这个项目里有两个常选项。gmapping 基于粒子滤波,参数少,对 2D 雷达支持成熟,跑起来 CPU 占用可控,缺点是回环能力弱,走廊绕一圈回到起点会有轻微错位。cartographer 带回环检测和子图优化,大场景下地图闭合好,但配置项多,对算力和内存要求高,小车上跑容易卡。
在这个项目规模下,我一般选 gmapping,理由是场景不大、对回环要求不高,参数十分钟能调完。核心参数就三个:
<launch> <node pkg="gmapping" type="slam_gmapping" name="slam_gmapping" output="screen"> <!-- 每帧激光用于建图的数量,360 线雷达取全量即可 --> <param name="throttle_scans" value="1"/> <!-- 地图分辨率,5 cm 是室内导航的常用值 --> <param name="delta" value="0.05"/> <!-- 粒子数:30 够用,调到 80 以上 CPU 会明显吃紧 --> <param name="particles" value="30"/> <!-- 里程计与激光的融合权重,里程计漂移大时降低这个值 --> <param name="srr" value="0.1"/> <param name="srt" value="0.2"/> <!-- 最大有效测距,超出的点直接丢弃 --> <param name="maxRange" value="12.0"/> <param name="base_frame" value="base_link"/> <param name="odom_frame" value="odom"/> </node> </launch>delta决定地图精细度,0.05 m 意味着每个格子 5 cm,走廊宽度大概占 20 个格子,够用且文件不大。particles调高能提升定位精度,但每颗粒子都要做一次扫描匹配,30 和 80 在建图时的 CPU 占用差一倍以上。
建图的动作要领是「慢」。推着车或者遥控走,速度控制在 0.2 m/s 以内,转弯时更慢,让雷达每帧都有足够的重叠。走快了地图边缘会出现扇形拖影,回环处对不上,这时候别急着改参数,先降速重走一遍,八成能解决。
3.3 摄像头标定与视觉识别节点的搭法
摄像头这条线的第一件事是内参标定,不做标定后面的距离估计全是错的。用棋盘格标定板,跑camera_calibration,采集 20~30 组不同角度的图像,输出一个ost.yaml,里面camera_matrix的 fx、fy 和 cx、cy 就是内参。这个文件通过camera_info_url加载,之后所有用到/camera/camera_info的节点都能拿到正确参数。
标定完再做识别节点。这个项目里视觉任务通常不重,主要是两类:一是识别特定颜色的目标并给出图像坐标,二是识别 Apriltag 之类的标记直接给出位姿。前者的骨架大致是这样:
import rospy import cv2 from sensor_msgs.msg import Image from geometry_msgs.msg import Point from cv_bridge import CvBridge class ColorDetector: def __init__(self): self.bridge = CvBridge() # 目标在图像中的归一化横坐标,-1 到 1,0 表示正前方 self.pub = rospy.Publisher('/target_offset', Point, queue_size=1) self.sub = rospy.Subscriber('/camera/image_raw', Image, self.cb) def cb(self, msg): frame = self.bridge.imgmsg_to_cv2(msg, 'bgr8') hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 阈值需按现场光照重新标定,写死会在换房间后失效 mask = cv2.inRange(hsv, (0, 120, 70), (10, 255, 255)) cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not cnts: return c = max(cnts, key=cv2.contourArea) if cv2.contourArea(c) < 800: # 面积过滤,去噪点 return x, y, w, h = cv2.boundingRect(c) width = frame.shape[1] offset_x = (x + w / 2.0 - width / 2.0) / (width / 2.0) p = Point() p.x = offset_x # 横向偏移,供转向控制使用 p.z = 1.0 / max(w, 1) # 宽度倒数,粗略表征距离 self.pub.publish(p) rospy.init_node('color_detector') ColorDetector() rospy.spin()offset_x是归一化后的横向偏差,直接可以喂给一个 P 控制器做转向;p.z用目标宽度倒数粗略表示距离,不追求精度,只做「远了靠近、近了停下」的判断。HSV 阈值一定要现场标定,写死一组数换个房间就不灵,这是视觉项目最常见的返工点。
3.4 时间同步、坐标对齐与数据融合的边界
雷达和相机是两条独立的数据流,融合之前要解决两件事:时间戳和坐标系。时间戳上,仿真时间use_sim_time在真车上必须关掉,否则所有消息的时间戳对不上系统时间,tf会报「extrapolation into the future」。检查方法很简单,rostopic echo一条消息,看 header.stamp 和date +%s差多少,超过 0.1 s 就说明链路里有节点时间没对齐。
坐标系对齐上,摄像头要给出目标在base_link下的坐标,得先通过内参把像素坐标反投影成相机坐标系下的射线,再查 TF 从camera_link变换到base_link。这里最容易翻车的是相机光轴朝向:相机坐标系是 z 轴朝前、x 轴朝右、y 轴朝下,和 base_link 的 x 轴朝前完全不同,忘记转换会让目标位置整体偏 90 度。
融合的边界也要说清楚。这个项目的合理定位是:激光雷达负责几何层——建图、定位、避障;摄像头负责语义层——识别目标类别和大致方位。两者不做像素级融合,避免把标定误差引入到导航里。视觉给出「前方 1.5 m 有目标」,导航栈把它当成一个额外的减速条件,而不是直接改 costmap,这样即使视觉误检,车也不会突然急刹或乱转。
4. 运动控制与导航:从 /cmd_vel 到四个轮子转多少圈
4.1 四轮差速运动学与里程计推算
四轮差速本质是两个轮子的差速模型,左右各两轮并联驱动,同侧速度相同。给定线速度 v 和角速度 ω,左右轮速度是:
v_left = v - ω × L / 2 v_right = v + ω × L / 2L 是轮距 0.30 m,这个值必须实测。反过来,从编码器读数推算里程计:
import math class DiffOdom: def __init__(self, wheel_base=0.30, wheel_radius=0.065, ticks_per_rev=1024): self.L = wheel_base self.r = wheel_radius self.tpr = ticks_per_rev self.x = self.y = self.th = 0.0 self.last_l = self.last_r = 0 def update(self, ticks_l, ticks_r, dt): # 编码器计数差 -> 轮子转过的弧长 dl = (ticks_l - self.last_l) * 2 * math.pi * self.r / self.tpr dr = (ticks_r - self.last_r) * 2 * math.pi * self.r / self.tpr self.last_l, self.last_r = ticks_l, ticks_r # 航迹推算:以两轮平均位移作为车体位移 d = (dl + dr) / 2.0 dth = (dr - dl) / self.L self.x += d * math.cos(self.th + dth / 2.0) self.y += d * math.sin(self.th + dth / 2.0) self.th += dth # 角度归一化到 [-pi, pi],防止累计超出范围 self.th = math.atan2(math.sin(self.th), math.cos(self.th)) return self.x, self.y, self.th中值积分th + dth/2比直接用th精度高一个量级,转弯时差别明显。ticks_per_rev是编码器每圈计数,四倍频之后是 1024 还是 4096 要对清楚,填错四倍,走一米显示四米。
里程计漂移是物理事实,不是 bug。轮子打滑、地面不平、轮径磨损都会累积误差,跑十米偏几十厘米很正常。所以定位不能只靠里程计,AMCL 用雷达扫描和地图匹配来纠正,这也是为什么map → odom和odom → base_link要分成两段 TF。
4.2 下位机串口协议与 PID 调速回路
上位机算出的/cmd_vel要变成 PWM,中间隔着一条串口。协议设计上不用复杂,但必须带帧头和校验,否则丢一个字节整帧错位,车会突然全速冲出去。一个够用的帧格式是:帧头 0xAA 0x55 + 长度 + 命令字 + 数据 + 校验和。
import struct def build_frame(v, w): """打包速度指令:v 单位 m/s,w 单位 rad/s,放大 1000 倍转整数""" cmd = 0x01 data = struct.pack('<hh', int(v * 1000), int(w * 1000)) body = bytes([len(data)]) + bytes([cmd]) + data checksum = sum(body) & 0xFF return bytes([0xAA, 0x55]) + body + bytes([checksum]) def parse_frame(buf): """解析下位机上报:编码器左右计数 + IMU 航向角""" if len(buf) < 10 or buf[0] != 0xAA or buf[1] != 0x55: return None length = buf[2] if sum(buf[2:2 + length + 2]) & 0xFF != buf[3 + length]: return None # 校验失败直接丢弃,不要凑合使用 ticks_l, ticks_r, yaw = struct.unpack('<hhh', buf[4:10]) return ticks_l, ticks_r, yaw / 100.0下位机的 PID 回路跑在 50~100 Hz,比上位机快一个量级。调参顺序是先 P 后 I 再 D:P 加到轮子能跟上目标速度但略有超调,I 消掉稳态误差(重载时速度掉下来的那部分),D 抑制过冲。Kp 太大表现为轮子一顿一顿,Ki 太大表现为低速时车「窜一下停一下」,这两个现象分开调,不要一起动。
提示:PID 的输出一定要做限幅,并且加一个「指令超时保护」——超过 500 ms 没收到新的
/cmd_vel就自动把速度归零。通讯断线时车继续按最后一条指令跑,是这类项目里最危险的情况。
4.3 导航栈配置:costmap 与规划器参数怎么调
move_base 里真正决定车能不能跑的是两张 costmap:全局的用静态地图,局部的用实时雷达数据。参数不对的典型表现是车贴着墙走、在门口卡住、或者原地画圈。核心参数如下:
local_costmap: global_frame: odom robot_base_frame: base_link update_frequency: 5.0 rolling_window: true # 局部地图跟着车滚动,必须有 width: 3.0 height: 3.0 resolution: 0.05 obstacle_layer: scan: topic: /scan max_obstacle_height: 0.5 raytrace_range: 3.5 # 清除障碍的最远距离 obstacle_range: 2.5 # 标记障碍的最远距离,大于此值的数据不用 inflation_layer: inflation_radius: 0.35 # 膨胀半径要大于车体半宽 + 安全余量 cost_scaling_factor: 3.0 # 越大越贴近障碍,越小越保守 NavfnROS: allow_unknown: false # 未知区域不规划,防止冲进未建图区域 default_tolerance: 0.2 DWAPlannerROS: max_vel_x: 0.45 min_vel_x: 0.0 max_vel_theta: 1.0 acc_lim_x: 0.5 acc_lim_theta: 1.5 xy_goal_tolerance: 0.15 yaw_goal_tolerance: 0.15 sim_time: 1.5 # 前向仿真时长,太短反应不过来,太长动作犹豫inflation_radius是最关键的一个数。车体半宽 0.11 m,加 0.15 m 安全余量,0.35 是比较合理的选择;设成 0.2 车会贴着墙擦过去,设成 0.6 走廊窄一点就直接无解。obstacle_range设为 2.5 是因为超过这个距离的雷达点噪声大,用来标记障碍会让局部地图充满虚假障碍。
rolling_window: true忘了开是新手常见错误,局部地图不跟着车走,车一开出初始范围就失去所有障碍信息,表现为「跑出去几米就开始撞墙」。
4.4 限速、急停与安全策略
导航栈跑通之后要做的是「限制它的能力」。DWA 在狭窄空间里会解出很激进的轨迹,参数上必须设死上限:max_vel_x不超过 0.45,max_vel_theta不超过 1.0,acc_lim_x给 0.5。速度上限压下来,建图时的拖影和定位跳变都会明显减少。
安全策略分三层。软件层是 costmap 的膨胀半径和未知区域不规划;控制层是下位机的超时归零和电流限幅;物理层是车头一个碰撞开关或者红外急停,触发后直接切断电机使能。第三层看着土,但答辩现场有人突然走到车前时,它是唯一来得及的东西。
还有一个容易忽略的点:/cmd_vel的发布频率。move_base 默认 20 Hz 左右,如果下位机的超时阈值设成 100 ms,网络一卡就频繁归零,车走起来一顿一顿。超时阈值设成 500 ms,既不会误触发,断线时也来得及停。
5. 避坑与排查:这台车最容易翻车的五个地方
5.1 TF 报错:所有传感器数据「看起来有但用不了」
现象是 rviz 里雷达点云不显示,或者显示位置飘在半空,终端刷Lookup would require extrapolation into the future或者Frame id laser does not exist。
原因通常有三种:URDF 里 link 名字和驱动发布的不一致(驱动发laser,URDF 写laser_link);静态 TF 没启动或者启动了两次互相覆盖;系统时间和消息时间戳差太多。
解决顺序是先rosrun tf view_frames生成一张 TF 树图,看哪一段断了。名字不一致就统一命名,我一般约定所有传感器 link 都带_link后缀。静态 TF 重复发布时用rosparam list查有没有两个节点发同一对变换,删掉一个。时间戳问题先确认use_sim_time是 false,再用rostopic echo /scan/header对比系统时间。
5.2 雷达扫到自己:车周围一圈假障碍
现象是 costmap 里车体周围始终有一圈障碍,全局规划器认为车被围死,一启动就报No valid plan。
原因是雷达安装位置太低或者被车架、线缆挡住,扫描到自身结构。也可能是雷达的angle_min和angle_max覆盖了后方,而后方正好是电池盒。
解决分两步:物理上把雷达往前挪、抬高到车体最高点以上,线缆用扎带固定不要垂在扫描面上;软件上在 URDF 给底盘加 collision 标签,让 costmap 的 footprint 把这块盖掉,同时在驱动里裁掉确实被遮挡的角度区间。改完在 rviz 里看/scan的原始数据,正常情况下应该是一个干净的半圆或整圆,没有任何固定不动的点。
5.3 里程计漂移:直走十米,地图上歪了半米
现象是建图时回到起点,地图闭合不上,或者 AMCL 定位频繁跳变,车在导航中「瞬移」。
原因基本是三类:轮径或轮距参数填错;编码器线数不对;轮子打滑。前两个是标定问题,第三个是物理问题。
排查方法是做一次直线标定:在地面画一条 3 m 的直线,让车直行,量实际走了多远,和/odom报的位移比。比值不是 1 就改wheel_radius,按比例缩放。再做原地转十圈,看/odom的航向是不是 3600 度,偏差大就改wheel_base。打滑问题改结构,比如换摩擦更大的轮子、降低加速度、避免在地毯上跑。
5.4 导航原地打转或撞墙:参数之间的连锁反应
现象是给了目标点,车在原地左右摆动不前进,或者直冲障碍物直到碰撞。
原地打转多半是xy_goal_tolerance和yaw_goal_tolerance设得太小(比如 0.01),DWA 一直在做微小修正,永远达不到收敛条件,看起来就是抖。改成 0.15 立刻正常。另一个原因是局部 costmap 的obstacle_range太小,车看不到远处障碍,等看到时已经来不及。
撞墙则要看inflation_radius和cost_scaling_factor的配合。膨胀半径小于车体半宽时,规划出的路径本身就穿过障碍物边缘。还有个隐蔽原因是footprint用的是圆形近似,车是长方形,斜着过门框时圆半径不够,实际车角已经蹭上去了。用多边形 footprint 更准,把四个角点坐标写进去。
5.5 rosbag 录了但回放不对:时间与话题的坑
现象是录了包,回放时 rviz 一片空白,或者所有数据瞬间放完。
原因是rosbag record默认只录当时存在的话题,如果录制时导航没启动,/map和/move_base/...全是空的。另一个原因是回放时没加--clock,也没设use_sim_time,所有节点的定时器按真实时间跑,数据却是几秒前的。
正确的做法是录制前先rostopic list确认要的话题都在,用rosbag record -O run1 /scan /odom /tf /tf_static /cmd_vel /camera/image_raw显式列出。回放时rosbag play --clock run1.bag,同时rosparam set use_sim_time true。tf_static 一定要录,很多人漏掉这个,回放时 TF 树直接断。
6. 验收与答辩:十五分钟里怎么证明这台车真的跑通了
验收不是「车能动就行」,而是把能力拆成能打勾的条目。我一般准备这样一张表,每一项都现场演示,演示不了的就说明没做完,比含糊其辞强。
| 验收项 | 操作方法 | 通过标准 |
|---|---|---|
| 传感器数据 | rostopic hz /scan、看相机画面 | 频率达标,画面无卡顿 |
| TF 完整性 | rviz 打开 TF,view_frames | 全链路连通,无红色警告 |
| 里程计标定 | 直行 3 m、原地转 10 圈 | 位移误差 <5%,航向误差 <10° |
| 建图 | 遥控走完整个场地 | 地图闭合,走廊宽度误差 <10 cm |
| 定位 | 在已知点启动 AMCL | 30 s 内收敛,粒子云收缩 |
| 自主导航 | 给 3 个目标点,含一个窄门口 | 全部到达,无碰撞,无人工干预 |
| 视觉识别 | 目标放在车前 1~3 m | 稳定输出偏移量,误检率可接受 |
| 异常处理 | 拔掉串口 / 挡住雷达 | 车在 0.5 s 内停下 |
答辩现场的节奏我建议这样安排:先用三十秒讲清楚系统架构图,重点说清「雷达管几何、相机管语义、下位机管实时」这个分工;接着放一段提前录好的完整建图视频,因为现场建图受场地和光线影响太大,容易翻车;然后现场跑一次导航,目标点选在两个容易的位置,把成功率留出来;最后演示视觉识别和拔线急停。整个过程控制在十二分钟内,留三分钟回答问题。
最容易被追问的是「你的方案和别人有什么不同」。这时候不要吹算法,讲工程细节:TF 怎么保证不断、里程计怎么标定、inflation_radius为什么取 0.35、超时保护阈值怎么定的。这些数都是有出处的,答起来有底气。反过来,如果只讲「用了 ROS 和 SLAM」,追问两层就露馅了。
注意:答辩前一定要做一次「冷启动」测试——整车断电,按正常流程重新上电启动,全部走一遍。热机状态下跑得通、冷启动各种报错,是 launch 文件依赖顺序和参数没落盘的典型表现,现场翻车大多出在这里。
从那以后我每次交付这类小车方案,都会强制走一遍「拔线测试」和「冷启动测试」,两个都过了才敢让人来看演示。这套流程看着笨,但它是唯一能把「我觉得能跑」变成「我知道它能跑」的办法。希望帮到你。
本文还有配套的精品资源,点击获取