简介:这份资源是面向机器人方向本科生与初入行的SLAM工程师的AGV激光SLAM导航系统实现方案,源自一篇本科学位论文项目,基于ROS搭建,用于解决自动导引车在未知或动态环境中的定位、建图与路径规划问题。压缩包共111个文件,约1.23MB,以yaml参数配置、launch启动脚本、cpp与cc源码、h头文件、xml与msg消息定义为主,另含pgm栅格地图、rviz可视化配置及md说明文档,覆盖base_controller、lslidar_n301、set_gmapping、hwtimu等模块,分别承担底盘控制、激光雷达数据处理、SLAM参数设置与IMU读取等功能。已有85人学习。读者可借此理清ROS下激光SLAM导航的软件包组织方式,参考传感器融合与局部EKF的实现思路,并对照launch与yaml快速复现建图与定位流程,适合作为个人学习与二次开发的入门范例。
1. 从一台跑不直的 AGV 说起:激光 SLAM 导航到底难在哪
很多人第一次做 AGV,都会经历同一个场景:小车在空地上直线走得挺稳,一进走廊、一遇到动态行人、一拐弯就画龙,建出来的地图墙是歪的,导航时贴着墙蹭。问题往往不在电机,而在「定位—建图—规划—控制」这条链路上,任何一环参数没对齐,整机就表现得像玄学。基于 ROS 的 AGV 激光 SLAM 导航系统实现方案,本质是把激光雷达、里程计、IMU、底盘控制器通过 ROS 的 topic 和 tf 串成一条闭环,让 AGV 在未知或半已知环境里先建图、再定位、再规划路径、最后把速度指令发给底盘。它适合做仓储搬运、产线接驳、巡检小车的团队,也适合刚学完 ROS 想找真实项目练手的人。这一章先把链路讲清楚,后面几章再落到安装、建图、导航和调参。
2. 先搭骨架:ROS 环境、底盘与传感器怎么接成一条链路
做 AGV 导航,第一步不是写算法,而是让 ROS 能同时看见雷达、底盘和 IMU。常见做法是 Ubuntu 20.04 配 ROS Noetic,这是目前资料最全、驱动兼容性最好的组合;如果团队已经在用 Ubuntu 22.04,也可以走 ROS 2 Humble,但激光 SLAM 的成熟包在 Noetic 下更多。热词里常出现的「鱼香 ros 一键安装」「小鱼一键安装 ros」这类脚本,本质是帮你换源、装依赖、初始化 rosdep,能省时间,但装完仍要自己验证环境变量和网络配置。这一章按「装环境 → 接底盘 → 接雷达 → 统一 tf」的顺序推进,每一步都给可复现的命令和配置。
2.1 用 Noetic 把 ROS 基础环境跑通
先确认系统版本,再决定装哪个 ROS。Ubuntu 20.04 对应 Noetic,Ubuntu 22.04 对应 Humble。下面以 Noetic 为例,给出最小可用的安装与验证流程。
# 1. 确认系统版本,必须是 20.04 lsb_release -a # 2. 添加 ROS 源和密钥(国内网络建议先换源) sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' sudo apt install curl curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - # 3. 更新并安装完整版桌面环境 sudo apt update sudo apt install ros-noetic-desktop-full # 4. 初始化 rosdep,这一步最容易因为网络失败 sudo rosdep init rosdep update # 5. 写入环境变量,每次开终端自动生效 echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc source ~/.bashrc # 6. 验证:能列出 topic 和运行小乌龟即算通过 roscore & rostopic list逻辑说明:ros-noetic-desktop-full包含 rviz、gazebo、导航包依赖,做 SLAM 必须装完整版而不是 base。rosdep init失败通常是网络问题,可以多试几次或手动配置源。参数上,$(lsb_release -sc)会自动取focal,不要手写错版本代号。验证阶段roscore能起来、rostopic list有输出,说明核心通信正常,再往下接硬件。
提示:如果团队用 Ubuntu 22.04,把上面命令里的
noetic换成humble,但后续gmapping、amcl的用法要换成 ROS 2 的 launch 写法,不能直接照搬。
2.2 底盘、雷达、IMU 的 topic 与 tf 对齐
ROS 导航最怕 tf 树断裂。AGV 上通常有这几个坐标系:map(地图)、odom(里程计)、base_link(车体)、laser(雷达)、imu_link(IMU)。雷达和 IMU 通过base_link挂到车上,里程计由底盘轮速积分得到。常见做法是用robot_state_publisher读 URDF,把静态外参发布出去,动态的odom → base_link由底盘驱动节点发布。
<!-- agv.urdf 片段:定义车体、雷达、IMU 的相对位置 --> <robot name="agv"> <link name="base_link"/> <link name="laser"/> <link name="imu_link"/> <!-- 雷达装在车体前方 0.2m、高 0.3m --> <joint name="laser_joint" type="fixed"> <parent link="base_link"/> <child link="laser"/> <origin xyz="0.2 0 0.3" rpy="0 0 0"/> </joint> <!-- IMU 装在车体中心,注意安装方向 --> <joint name="imu_joint" type="fixed"> <parent link="base_link"/> <child link="imu_link"/> <origin xyz="0 0 0.1" rpy="0 0 0"/> </joint> </robot>逻辑说明:origin的xyz是雷达相对车体中心的平移,rpy是旋转。雷达装歪了,建图时墙就是斜的,所以外参必须实测,不能凭感觉填。参数上,xyz单位是米,rpy单位是弧度。发布 tf 时用:
rosrun robot_state_publisher robot_state_publisher agv.urdf然后检查 tf 是否连通:
rosrun tf tf_echo base_link laser如果这条命令一直等待,说明 URDF 没加载或 joint 名字写错。底盘驱动节点要发布odom → base_link和nav_msgs/Odometry,雷达驱动发布sensor_msgs/LaserScan,IMU 发布sensor_msgs/Imu。三者时间戳必须来自同一时钟,否则后面融合会飘。
2.3 用 launch 文件把多节点一次拉起
手动开一堆终端不现实,标准做法是写 launch。下面是一个最小 bringup,把雷达、底盘、状态发布、rviz 一起启动。
<launch> <!-- 加载 URDF 到参数服务器 --> <param name="robot_description" textfile="$(find agv_bringup)/urdf/agv.urdf"/> <!-- 发布静态和动态 tf --> <node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher"/> <node name="joint_state_publisher" pkg="joint_state_publisher" type="joint_state_publisher"/> <!-- 雷达驱动,具体包名按实际雷达型号替换 --> <node name="lidar" pkg="lslidar_driver" type="lslidar_driver_node" output="screen"> <param name="frame_id" value="laser"/> </node> <!-- 底盘驱动,发布 odom 和 tf --> <node name="base_driver" pkg="agv_base" type="base_driver" output="screen"> <param name="port" value="/dev/ttyUSB0"/> <param name="baud" value="115200"/> </node> <node name="rviz" pkg="rviz" type="rviz" args="-d $(find agv_bringup)/rviz/bringup.rviz"/> </launch>逻辑说明:param把 URDF 路径写进参数服务器,robot_state_publisher读它发布 tf。雷达节点的frame_id必须和 URDF 里的laser一致,否则 tf 对不上。底盘节点的port和baud按实际串口改,插上 USB 转串口后用ls /dev/ttyUSB*确认。启动后rostopic hz /scan看雷达频率,一般 10Hz 左右;rostopic hz /odom看里程计,一般 20~50Hz。频率对不上,后面建图会丢帧。
3. 建图:用 gmapping 还是 cartographer,参数怎么定
建图是导航的地基。AGV 常用两种方案:gmapping轻量、依赖少、适合小场景;cartographer支持回环、适合大场景和多楼层。选型看场地:单层仓库 2000 平米以内,gmapping 够用;有长走廊、多房间、需要回环校正,上 cartographer。这一章讲清两者的取舍,并给出 gmapping 的完整建图流程和关键参数,因为它是新手最容易跑通的方案。
3.1 gmapping 与 cartographer 的选型对比
| 维度 | gmapping | cartographer |
|---|---|---|
| 回环检测 | 无 | 有,基于分支定界 |
| 大场景表现 | 易漂移 | 稳定 |
| 计算资源 | 低,树莓派可跑 | 高,建议 x86 |
| 参数复杂度 | 少,易调 | 多,需调 lua 配置 |
| 适用场地 | 单层小仓库 | 多层、长走廊 |
常见做法是先用 gmapping 把流程跑通,确认雷达、里程计、tf 都没问题,再换 cartographer 提升精度。如果一上来就调 cartographer,参数没调好会误以为是硬件问题,浪费大量时间。
3.2 gmapping 建图的最小启动与参数
建图前先确认odom和scan都在,然后启动 gmapping。
<launch> <!-- gmapping 节点 --> <node name="slam_gmapping" pkg="gmapping" type="slam_gmapping" output="screen"> <!-- 雷达话题,按实际改 --> <remap from="scan" to="/scan"/> <!-- 坐标系 --> <param name="base_frame" value="base_link"/> <param name="odom_frame" value="odom"/> <param name="map_frame" value="map"/> <!-- 建图范围,AGV 场地一般 30m 足够 --> <param name="xmin" value="-30.0"/> <param name="ymin" value="-30.0"/> <param name="xmax" value="30.0"/> <param name="ymax" value="30.0"/> <!-- 分辨率 5cm,越小地图越细但越吃内存 --> <param name="delta" value="0.05"/> <!-- 每帧参与匹配的粒子数,越大越准越慢 --> <param name="particles" value="30"/> <!-- 里程计噪声,轮式 AGV 一般给这个量级 --> <param name="srr" value="0.1"/> <param name="srt" value="0.2"/> <param name="str" value="0.1"/> <param name="stt" value="0.2"/> <!-- 更新阈值,激光变化超过 0.5m 才更新地图 --> <param name="linearUpdate" value="0.5"/> <param name="angularUpdate" value="0.5"/> </node> </launch>逻辑说明:delta决定地图分辨率,0.05 表示每个栅格 5cm,仓库场景够用;particles是粒子滤波的粒子数,30 是平衡值,场地大或特征少可以加到 50。srr/srt/str/stt是里程计噪声模型,轮式底盘打滑越严重,这几个值要适当调大,否则粒子会过度自信导致定位跳变。linearUpdate和angularUpdate控制建图更新频率,太小会频繁重绘,太大则地图稀疏。
建图操作流程:
# 1. 启动 bringup 和 gmapping roslaunch agv_bringup bringup.launch roslaunch agv_slam gmapping.launch # 2. 遥控小车走一圈,覆盖所有区域,速度放慢 rosrun teleop_twist_keyboard teleop_twist_keyboard.py # 3. 建图完成后保存地图 rosrun map_server map_saver -f ~/agv_map保存后会得到agv_map.pgm和agv_map.yaml两个文件。yaml里记录分辨率、原点、阈值,导航时直接加载。走图时速度建议不超过 0.3m/s,转弯要慢,否则激光匹配跟不上,地图会出现重影。
注意:建图时如果发现墙是双层或斜的,先查雷达外参和里程计标定,不要急着调 gmapping 参数,多数问题是 tf 或轮径不准。
3.3 建图质量的三个判断标准
建完图不要直接拿去导航,先看三点。第一,直线墙在 rviz 里是不是直的,如果呈锯齿状,说明雷达安装角度或分辨率有问题。第二,回环处有没有重影,走回起点时墙应该重合,如果错开,说明里程计累积误差大,需要标定轮径或换 cartographer。第三,地图边界是否完整,xmin/xmax设小了会把边缘裁掉。这三点过了,再进入导航环节。
4. 导航:amcl 定位、代价地图与路径规划的落地配置
导航栈的核心是move_base,它把全局规划、局部规划、代价地图、恢复行为串在一起。AGV 导航和普通小车不同,它对贴边距离、转弯半径、减速距离更敏感。这一章讲 amcl 定位、代价地图膨胀、全局与局部规划器选型,并给出可抄的配置文件。
4.1 amcl 定位:粒子数与初始位姿怎么设
amcl 是自适应蒙特卡洛定位,用激光匹配地图来估计位姿。启动前要加载地图,并给一个初始位姿。
<launch> <!-- 加载建图保存的地图 --> <node name="map_server" pkg="map_server" type="map_server" args="$(find agv_nav)/maps/agv_map.yaml"/> <!-- amcl 定位 --> <node name="amcl" pkg="amcl" type="amcl" output="screen"> <param name="odom_frame_id" value="odom"/> <param name="base_frame_id" value="base_link"/> <param name="global_frame_id" value="map"/> <!-- 粒子数:初始 5000,运行中自适应到 500~2000 --> <param name="min_particles" value="500"/> <param name="max_particles" value="5000"/> <!-- 激光模型参数,likelihood_field 适合结构化环境 --> <param name="laser_model_type" value="likelihood_field"/> <param name="laser_max_range" value="20.0"/> <param name="laser_likelihood_max_dist" value="2.0"/> <!-- 里程计噪声,和 gmapping 保持一致 --> <param name="odom_alpha1" value="0.1"/> <param name="odom_alpha2" value="0.1"/> <param name="odom_alpha3" value="0.1"/> <param name="odom_alpha4" value="0.1"/> <!-- 更新阈值 --> <param name="update_min_d" value="0.1"/> <param name="update_min_a" value="0.2"/> </node> </launch>逻辑说明:min_particles/max_particles控制计算量和定位稳定性,场地大、特征少就调大上限。laser_model_type选likelihood_field对仓库货架这类环境更鲁棒。odom_alpha系列是里程计噪声,和建图时保持一致,否则定位和地图会打架。update_min_d是移动超过 0.1m 才更新,避免静止时粒子抖动。
启动后在 rviz 里用2D Pose Estimate给初始位姿,粒子云会收敛。如果粒子一直散,检查地图和实际环境是否一致、雷达是否被遮挡。
4.2 代价地图:膨胀半径决定 AGV 敢不敢贴边
代价地图分全局和局部两层。全局用于长距离规划,局部用于实时避障。关键参数是inflation_radius,它决定障碍物周围多大范围被视为高代价。
# costmap_common_params.yaml # AGV 外形,矩形车要写 footprint,圆形车写 radius footprint: [[-0.4, -0.3], [-0.4, 0.3], [0.4, 0.3], [0.4, -0.3]] # 膨胀半径:车体半宽 + 安全余量,0.3 半宽给 0.55 inflation_radius: 0.55 # 代价衰减系数,越大越陡,AGV 一般 3~5 cost_scaling_factor: 3.0 # 障碍物层 obstacle_layer: enabled: true max_obstacle_height: 2.0 observation_sources: scan scan: topic: /scan data_type: LaserScan clearing: true marking: true max_obstacle_height: 2.0 obstacle_range: 5.0 raytrace_range: 5.5逻辑说明:footprint是车体轮廓,矩形 AGV 必须写四角坐标,写错会导致规划出的路径撞车。inflation_radius要大于车体半宽,否则路径会贴着障碍物,实际跑起来一有误差就撞。cost_scaling_factor越大,离障碍物越近代价上升越快,AGV 会更早避让。obstacle_range是雷达多远内的障碍物计入代价地图,raytrace_range是清除范围,后者要略大于前者。
4.3 全局与局部规划器:A* 还是 Dijkstra,DWA 还是 TEB
全局规划器常用global_planner,可选A*或Dijkstra。A* 带启发式,速度快,适合大场地;Dijkstra 更稳但慢。局部规划器常用base_local_planner(DWA)和teb_local_planner。DWA 简单、参数少,适合差速 AGV;TEB 支持阿克曼和全向,路径更平滑,但参数多、易翻车。
# move_base 关键配置 base_global_planner: global_planner/GlobalPlanner base_local_planner: base_local_planner/TrajectoryPlannerROS GlobalPlanner: use_dijkstra: false # false 即用 A* allow_unknown: true # 允许穿越未知区域,建图完整时可关 default_tolerance: 0.2 # 目标点容差 TrajectoryPlannerROS: max_vel_x: 0.5 # 最大前进速度 m/s min_vel_x: 0.0 max_vel_theta: 1.0 # 最大角速度 rad/s min_in_place_vel_theta: 0.4 acc_lim_x: 0.5 # 加速度限制 acc_lim_theta: 1.0 xy_goal_tolerance: 0.1 # 到点精度 yaw_goal_tolerance: 0.1 sim_time: 1.5 # 轨迹仿真时长 path_distance_bias: 32.0 # 贴近全局路径的权重 goal_distance_bias: 24.0 # 朝向目标的权重 occdist_scale: 0.02 # 避障权重逻辑说明:max_vel_x按 AGV 实际能力设,设大了会急停急起。acc_lim_x和acc_lim_theta是加速度限制,和电机响应匹配,设太大轨迹跟踪不上。path_distance_bias和goal_distance_bias是一对矛盾参数,前者大则贴全局路径,后者大则直奔目标,仓库里建议前者略大。occdist_scale是避障权重,太大车会绕远,太小会蹭障碍物。
提示:如果 AGV 在窄通道里反复前进后退,先看
sim_time是否太短,再看inflation_radius是否把通道堵死。窄通道场景可以把膨胀半径调小,但必须同步降低速度。
5. 避坑与排查:AGV 导航最常见的五类翻车
这一章按「现象 → 原因 → 解决」写五条血泪经验,都是实际调试中反复出现的。
现象一:建图时墙是斜的,导航时定位一直跳。原因:雷达外参rpy没标定,或雷达安装面不水平。 解决:用水平仪确认雷达安装,实测雷达中心到车体中心的xyz,rpy用卷尺和角度尺量,不要凭感觉。改完 URDF 重新建图。
现象二:AGV 走直线画龙,里程计和实际距离对不上。原因:轮径参数不准,或左右轮速标定不一致。 解决:让 AGV 直走 5 米,对比odom读数和实际距离,按比例修正轮径。左右轮分别测,差速底盘还要修正轮距。
现象三:导航时 AGV 在目标点附近来回转,停不下来。原因:yaw_goal_tolerance太小,或局部规划器角速度限制不合理。 解决:把yaw_goal_tolerance放到 0.1~0.2,min_in_place_vel_theta调到能克服静摩擦的值,通常 0.3~0.5。
现象四:动态行人经过时 AGV 急停,行人走后不恢复。原因:局部代价地图没有及时清除障碍物,或恢复行为没配置。 解决:确认raytrace_range大于obstacle_range,在move_base里配置recovery_behaviors,加入clear_costmap_recovery。
现象五:多台 AGV 同时跑,地图和 tf 互相干扰。原因:多机用了相同的map和odom坐标系,topic 没加命名空间。 解决:每台车用独立ROS_NAMESPACE,tf 前缀区分,地图服务器各自独立。调度层再统一分配目标点。
6. 进阶:用 cartographer 建图 + 多传感器融合把精度再提一档
当场地超过几千平米、有长走廊和多楼层,gmapping 的漂移会变得不可接受。这时候换 cartographer,并引入 IMU 做融合,是性价比最高的升级路径。cartographer 的回环检测基于分支定界,能在走回起点时把累积误差拉回来,建出来的地图闭合度明显更好。
配置上,cartographer 用 lua 文件描述传感器和轨迹。核心是trajectory_builder_2d.lua里的几个参数:
| 参数 | 含义 | 建议值 |
|---|---|---|
num_range_data | 每积累多少帧做一次回环 | 30~50 |
submaps.num_range_data | 每个子图包含帧数 | 60~90 |
real_time_correlative_scan_matcher | 实时相关性匹配 | true |
ceres_scan_matcher | 后端优化 | 默认开启 |
pose_graph.optimize_every_n_nodes | 每多少节点优化一次 | 30 |
IMU 接入时,要在 lua 里声明imu并给imu_gravity_time_constant,通常 10 左右。IMU 的作用是提供重力方向和角速度,弥补轮式里程计在打滑时的不足。融合后,AGV 在湿滑地面或急转弯时的定位跳变会明显减少。
验证方法:建完图后,用rosbag record录一段实际运行数据,回放时看map → base_link的 tf 是否连续。如果出现跳变,检查 IMU 时间戳和雷达是否同步,cartographer 对时间同步很敏感,差几十毫秒就会报外推错误。
我自己踩过最深的坑,是早期图省事没标定轮径,建图看着还行,一上导航就各种飘,查了三天才发现是里程计系数差了 8%。从那以后,每台 AGV 落地前我都先做三件事:量轮径、标雷达外参、录一段 bag 回放验证 tf。这套流程看起来慢,但比事后救火快得多。希望帮到你。
本文还有配套的精品资源,点击获取