简介:这套源码来自一个Intel嵌入式参赛项目,围绕二轮机器人的路径规划、上下位机通讯与移动控制展开,并额外集成了语音模块;项目以ROS为开发框架,代码划分清晰,适合机器人、计算机或电子信息类专业学生用于课程设计、毕业设计,也可供ROS初学者对照学习完整工程的组织方式。压缩包共收录426个文件,整体约674KB,核心实现以95个C++源文件和85个头文件为主,辅以15个Python脚本、13个launch启动文件、11个yaml与11个cfg配置参数文件,以及msg/srv等自定义通信接口,可较为完整地还原ROS工程从节点编写到参数配置的结构。目前已有98人学习。资源提供可直接运行的完整源码和项目说明,对上下位机通信链路、路径规划算法选型、移动控制逻辑以及语音部分的集成方式均有参考价值。读者可据此快速上手,并在此基础上继续扩展与调试。
1. 一包源码看清二轮机器人:路径规划与上下位机通讯在解决什么问题
这套源码包解决的不是单点算法问题,而是一条完整工程链路:上层ROS里跑二轮机器人的路径规划,规划出的速度指令通过上下位机通讯送到板级控制器,再驱动轮子转起来,同时把编码器算出的里程计数据回传。很多人在二轮机器人上做自主导航,第一反应是去调规划算法,结果卡在“上位机算出cmd_vel,下位机就是不动”这种通讯坑里。以ROS为基座的源码,依赖的是一整套导航栈(地图、定位、全局/局部路径规划)加下位机侧的驱动底子,适合毕设、课程设计、低成本教学验证平台这几类场景。真正的难点不在算法本身,而在联调时的协议、坐标系、时间戳这些看不见的细节,下面把整条链路拆开讲。
2. 从差速模型到move_base:二轮机器人路径规划链路的落地选型
二轮机器人是移动机器人里最便宜、最常见的形态,所有运动都由左右两轮独立驱动完成。要让它按路径规划走,得先把运动学模型、导航栈选型和参数配置串起来。ros小车自主导航仿真的套路也差不多:先在gazebo仿真里把参数调到位,再上真机验证,下面按这个顺序展开。
2.1 二轮差速运动学:为什么左右轮速度是位姿信息的唯一来源
二轮车没有转向舵,位姿变化完全来自左右轮线速度的合成。设左轮线速度为vl,右轮线速度为vr,轮距L为左右轮中心距离,那么:
- 线速度 v = (vl + vr) / 2
- 角速度 ω = (vr - vl) / L
ROS里坐标系遵循REP-103约定:x轴向前、y轴向左、z轴向上,逆时针方向角速度为正。二轮车无法横向平移,所以路径规划必须围绕“原地转向 + 前进/后退”来设计,这也是move_base里DWA这类局部规划器比全向底盘用得多的原因之一。
在我做过的底盘节点里,这个正向运动学计算通常是下面这段代码:
// 由左右轮速度计算底盘线速度和角速度 // 输入单位 m/s,输出遵循 ROS REP-103 约定 double linear = (left_speed + right_speed) / 2.0; // linear.x double angular = (right_speed - left_speed) / wheel_base; // angular.z逻辑说明:左右轮速度的平均值就是车体中心的线速度,两个轮子的速度差除以轮距换算成弧度每秒,也就是角速度。这段代码真正要调试的对象是wheel_base和两个轮子的实际半径。我的习惯是先在空地上做两次裸测:一次直行,一次原地旋转,确认linear和angular的方向符号都对,再做导航,否则后面所有问题都会被归到“参数不对”这个黑匣子里。
参数说明:wheel_base通常按“轮子外侧间距减去两倍轮宽的一半”来估算,但更可靠的办法是推着车滚一整圈,用实际弧长除以2π倒推轮距。二轮小车走不直的共性原因,多半不在电机而在轮距、轮径这些几何参数上。
2.2 导航栈选型:move_base加什么局部规划器最适合二轮车
ROS里做自主导航,绝大多数方案是move_base一个节点收拢全局与局部规划:全局路径规划在全局代价地图上找一条无碰撞路线,局部路径规划在车身边的小范围代价地图里实时避障,规划结果经cmd_vel话题发布。对二轮差速车,选型一般从两个维度看。
全局规划器用global_planner插件,可切Dijkstra或A*,二轮车在室内5cm分辨率地图上两者差别不大。我偏向Dijkstra,因为它在窄通道里的路径更稳定,不会走Z字抖动。局部规划器则建议先用DWA(TrajectoryPlannerROS),它和二轮车运动学天然契合,速度采样空间就是v和ω两个轴,参数少、能快速跑通;TEB把时间和动力学约束一起优化,轨迹更平滑、过窄门表现更好,但参数多,调起来更像玄学。我一般的做法是:先DWA跑通,再换TEB对比一次。
动态避障则完全依赖代价地图的代价来源,二轮小车通常用激光雷达或深度相机数据,通过observation_sources接入costmap。这里要注意:二轮车转向靠原地旋转,报价地图的膨胀半径直接影响它能不能在窄通道里完成“掉头”动作。
2.3 让move_base在二轮车上跑起来:最小参数与launch组织
“最小”的意义在于不接真车也能用rosbag或gazebo先行验证。我在桌面级二轮车上用的典型costmap参数如下。
robot_base_frame: base_link obstacle_range: 3.0 raytrace_range: 3.5 inflation_radius: 0.20 cost_scaling_factor: 3.0 observation_sources: laser_scan laser_scan: sensor_frame: laser topic: /scan data_type: LaserScan marking: true clearing: true逻辑说明:marking=true表示把激光数据标记为障碍物,clearing=true负责清除误检,比如车前进时附近区域被雷达短暂扫到又被移开的情况。两件事同时打开,动态环境下代价地图才能保持新鲜。inflation_radius是障碍物向外扩展的半径,等价于把机器人简化成质点后保留的安全距离。
参数说明:这条是二轮小车第一个调参重点。inflation_radius设0.15时车能钻很窄的过道,但容易擦墙;设0.35时路径很安全,可半个门都过不去。cost_scaling_factor控制代价在膨胀区域里的衰减速度,值越大路径越贴边,越小越保守。
DWA局部规划器参数:
max_vel_x: 0.5 min_vel_x: -0.2 max_vel_theta: 0.8 min_in_place_vel_theta: 0.3 acc_lim_x: 0.6 acc_lim_theta: 1.2 xy_goal_tolerance: 0.15 yaw_goal_tolerance: 0.15 sim_time: 1.5 vx_samples: 20 vtheta_samples: 40逻辑说明:sim_time是DWA向前模拟轨迹的时间窗口,窗口越长采样轨迹越多、计算越重,但能看到更远的避障方向。二轮车在1.5秒内能扫过的区域一般够用。vx_samples和vtheta_samples是速度采样密度,二轮车v维度只有一维,vx取20、vtheta取40通常能让轨迹质量明显上升,CPU开销也不大。
参数说明:max_vel_x写0.5m/s对室内桌面级小车偏快,我实际常用0.3。更关键的是,下位机通常会对速度指令做限幅,如果底盘把vx封顶到0.4,上位机设成0.6也会在底层被砍掉。规划器眼里能走的路径和实际车体能跟上的速度不一致,就会出现“规划正常但车总在追目标”。
最后是move_base的launch组织,源码包里这步通常已写好,建议对照自己的路径修改包名:
<launch> <node pkg="move_base" type="move_base" name="move_base" output="screen"> <param name="base_global_planner" value="global_planner/GlobalPlanner"/> <param name="base_local_planner" value="base_local_planner/TrajectoryPlannerROS"/> <rosparam file="$(find my_robot)/param/costmap_common.yaml" command="load" ns="global_costmap"/> <rosparam file="$(find my_robot)/param/costmap_common.yaml" command="load" ns="local_costmap"/> <rosparam file="$(find my_robot)/param/global_costmap.yaml" command="load"/> <rosparam file="$(find my_robot)/param/local_costmap.yaml" command="load"/> <rosparam file="$(find my_robot)/param/dwa_planner.yaml" command="load"/> </node> </launch>逻辑说明:全局和局部代价地图共用costmap_common.yaml里的传感器配置,但各自的尺寸、分辨率和更新频率由global_costmap.yaml和local_costmap.yaml分别定义。局部代价地图一般比全局地图小很多,跟随机器人中心移动。
参数说明:local_costmap的width和height我常用2.5m×2.5m,分辨率0.05m;全局地图直接沿用建图分辨率。这里有个常见误导:把局部地图宽度设成0或1m,会导致DWA规划的轨迹一超出局部地图就失效,车在障碍物前反复横跳。
move_base输出的cmd_vel最终要交给下位机执行,路径规划链路到这里才算打通。上下位机通讯是它的出口,我单独放下一章讲。
3. 上下位机通讯协议设计与ROS节点实现:帧格式、打包与解析
移动机器人里“上位机”指跑ROS的工控机、树莓派或Jetson,“下位机”指底盘上的STM32这类MCU。move_base输出的cmd_vel要落到下位机执行,下位机的轮速编码器要反馈成odom,中间靠一条可靠的通讯链路。这一章讲清楚串口为什么是首选、协议帧怎么定、代码怎么写。
3.1 为什么上下位机通讯首选用串口而不是CAN
上下位机通讯的物理层可选UART、CAN、SPI、USB。在二轮小车里,串口是压倒性多数。原因有三个:第一,STM32和几乎所有MCU都带USART外设,不用额外买收发器;第二,USB转TTL模块几块钱一个,调试时直接接到PC串口助手看数据,血泪经验是先让协议在PC上聊通,再交给ROS节点,只有串口这么好用;第三,波特率到115200时,一帧20字节的传输时间在1ms量级,对速度指令50Hz这种需求绰绰有余。
什么时候该换CAN?如果底盘有多个驱动单元,比如左右轮各一块驱动板,还要挂IMU、编码器板等多个节点,CAN总线天然的ID仲裁和广播帧机制会更有优势、更抗干扰。但对“一块主控板加一块底盘板”这种最常见的二轮小车架构,串口是成本最低、调试最方便的选择。源码包里的上下位机通讯如果走串口,大概率就是这个原因。
3.2 协议帧怎么定:帧头、长度、命令字、校验与字节序
一帧数据从哪开始、有多长、干什么用、数据对不对,这四个问题必须在协议里回答。我常用的极简帧格式如下:
| 字段 | 字节数 | 说明 |
|---|---|---|
| 帧头 | 2 | 0xAA 0x55,先固定两个不同字节,避免数据段里误同步 |
| 长度 | 1 | 命令字加数据段的字节数 |
| 命令字 | 1 | 0x01速度指令,0x02里程计回传,0x03心跳等 |
| 数据段 | n | 具体数据,按固定字节序排列 |
| 校验 | 1 | 长度、命令字及数据段的累加和取低8位 |
| 帧尾 | 1 | 固定0x0D |
不同项目的帧头、校验方式可能有差异,但分析思路一致。把帧头、长度、命令字、校验看成四道闸门,解析端只在这四关全过时放行一帧。
数据段里要特别注意浮点转定点。老型号STM32没有FPU,跑浮点会拖慢中断;上下位机通讯用整数传更稳。我一般把线速度乘1000转成mm/s,角速度乘1000转成mrad/s,再用int16_t打包。1mm/s和1mrad/s的分辨率对二轮小车足够。
上位机打包函数示例:
#include <cstdint> #include <cstring> // 构建速度指令帧,返回帧长度;buf 尺寸至少 9 字节 // linear 单位 m/s,angular 单位 rad/s int build_speed_frame(uint8_t buf[], float linear, float angular) { int16_t vx = static_cast<int16_t>(linear * 1000.0f); // 转为 mm/s int16_t wz = static_cast<int16_t>(angular * 1000.0f); // 转为 mrad/s uint8_t data[4]; // 大端字节序:高字节在前 data[0] = static_cast<uint8_t>((vx >> 8) & 0xFF); data[1] = static_cast<uint8_t>(vx & 0xFF); data[2] = static_cast<uint8_t>((wz >> 8) & 0xFF); data[3] = static_cast<uint8_t>(wz & 0xFF); uint8_t payload_len = 1 + 4; // 命令字 + 数据段 buf[0] = 0xAA; buf[1] = 0x55; buf[2] = payload_len; buf[3] = 0x01; // 命令字:速度指令 std::memcpy(buf + 4, data, 4); uint8_t sum = 0; for (int i = 2; i < 8; ++i) sum += buf[i]; buf[8] = sum; return 9; }逻辑说明:命令字0x01占1字节,数据段4字节,长度字段填5,即命令字加数据段的总长度。累加和只覆盖长度、命令字、数据段,帧头和帧尾不参与校验,因为它们是同步用的固定值;即使数据段误码成帧头也能被校验拦下来。
参数说明:大端字节序必须和下位机解包一一对应。STM32是典型小端CPU,如果上位机按大端发,下位机按小端拼,数值全乱。我见过的典型翻车是上位机用memcpy直接拷贝float,下位机按int16解析,结果得到一个完全没意义的速度。避免的笨办法就是这里展示的:编码端手动指定字节序,解码端手动拼回int16。
下位机解析用状态机更稳:
#define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define MAX_PAYLOAD 32 uint8_t recv_buf[MAX_PAYLOAD]; uint8_t recv_len = 0; uint8_t recv_state = 0; // 0:等待AA 1:等待55 2:等待长度 3:接收数据 4:等待校验 uint8_t frame_ready = 0; uint8_t frame_payload_len = 0; void parse_byte(uint8_t byte) { switch (recv_state) { case 0: if (byte == FRAME_HEAD1) recv_state = 1; break; case 1: if (byte == FRAME_HEAD2) recv_state = 2; else recv_state = 0; // 头部没对上,重新同步 break; case 2: if (byte > MAX_PAYLOAD) { recv_state = 0; break; } // 长度异常,弃帧 frame_payload_len = byte; recv_len = 0; recv_state = 3; break; case 3: recv_buf[recv_len++] = byte; if (recv_len >= frame_payload_len) recv_state = 4; break; case 4: { // 校验:长度、命令字及数据段的累加和低8位 uint8_t sum = frame_payload_len; for (uint8_t i = 0; i < frame_payload_len; ++i) sum += recv_buf[i]; if (sum == byte) frame_ready = 1; // 校验通过,置位待处理 recv_state = 0; // 重新等待下一帧头 } break; } }逻辑说明:状态机每个状态只依赖当前输入字节,天然能处理丢字节、多字节、断线重连的情况。比如上一帧卡在半路,下一帧头一到,state从任何位置回到0重新同步。这种写法比按固定偏移直接解析稳得多。
参数说明:MAX_PAYLOAD设32是因为速度指令这类短帧一般不超过8字节数据段;预留到32足够容纳扩展,比如增加PID参数下发、下位机上报电池电压。如果数据段要超过30字节,建议拆成多帧传,避免一帧过长占用串口带宽。
ROS底盘节点里的核心读写循环:
// 典型结构:订阅cmd_vel -> 打包下发;串口读取 -> 解析 -> 发布odom // 省略串口初始化和部分清理代码,只保留核心逻辑 void cmd_vel_callback(const geometry_msgs::Twist::ConstPtr& msg) { uint8_t frame[9]; int len = build_speed_frame(frame, msg->linear.x, msg->angular.z); serial_.write(frame, static_cast<size_t>(len)); // 阻塞写,注意加锁 } void serial_read_loop() { uint8_t byte; while (ros::ok()) { if (serial_.read(&byte, 1) == 1) { parse_byte(byte); if (frame_ready) { // 按命令字分发,这里只讨论0x02里程计回传 frame_ready = 0; if (recv_buf[0] == 0x02) { // 数据段:vx(int16)+wz(int16),按大端拼接 int16_t vx = (recv_buf[1] << 8) | recv_buf[2]; int16_t wz = (recv_buf[3] << 8) | recv_buf[4]; nav_msgs::Odometry odom; odom.header.stamp = ros::Time::now(); odom.twist.twist.linear.x = vx / 1000.0; odom.twist.twist.angular.z = wz / 1000.0; odom_pub_.publish(odom); } } } } }逻辑说明:串口读取单独放一个线程,避免ROS回调阻塞后积压下位机发来的数据。写串口时加锁,因为cmd_vel回调与读取线程会同时访问serial对象,不锁会偶发帧错乱。
参数说明:这版odom只发布了twist,没累计位置姿态。位置姿态一般由上层收到twist后做运动学积分,再发布odom到base_link的TF。如果不做这一步,move_base一定会报“No odometry data received”或TF断链,这是二轮小车最常见的问题,下一章重点讲。
4. 二轮小车联调排查:上下位机通讯与导航的5个高频翻车点
两套系统各自能跑,一合起来就出问题,是上下位机联调的常态。下面按“现象—原因—解决”列出5个我在二轮小车项目里实际遇到的高频点。
4.1 话题正常但电机不动:串口设备权限与帧同步双问题
现象:底盘节点在线,rostopic echo /cmd_vel有数据,电机纹丝不动。
原因:第一层是Linux没有权限打开串口,很多发行版要求用户加入dialout组;第二层是下位机卡在帧头同步,上位机一直在发,下位机却在一个错位状态里找不到0xAA。
解决:先执行sudo usermod -a -G dialout $USER并重新登录;再用串口调试助手直接向下位机发一帧0xAA 0x55开头的原始字节,电机有反应就说明帧内容没问题,问题出在ROS节点的串口配置(波特率、数据位、停止位)或字节错位上。然后把调试助手里确认能跑的原始字节与ROS代码组帧结果逐字节对照。
注意:USB转TTL模块供电不稳会直接导致收发异常,别让它从主板USB口顺便给电机驱动板供电,下位机独立供电更稳。
4.2 小车画龙或原地转圈:轮径、轮距参数不一致
现象:给0.2m/s直行指令,车头慢慢偏往一边;原地旋转指令下,转完位置偏了45度。
原因:左右轮电机减速比或编码器线数不一致,或者底盘节点里给的两个轮径参数不同;轮距偏大偏小会让旋转角度误差线性放大。这类问题在话题上隐藏得很深,因为cmd_vel和odom数值都“正常”,但物理位移和话题数值之间差了一个比例因子。
解决:先把车抬空,左右轮分别以固定PWM跑一段,用编码器读数对比实际轮速;再实测轮距。后期做里程计标定修正三个参数:直行偏差偏调wheel_radius,旋转角度偏差偏调wheel_base,左右不一致则分别修正左右轮径。
4.3 odom出现往后退、方向反了
现象:手推车前进,rviz里base_link往后缩。
原因:电机方向、编码器相位和协议符号约定冲突。ROS标准是linear.x前进为正、angular.z逆时针为正,但某些电机驱动板和编码器接线会让左轮“正方向”被定义成逆时针,合成后整体反向。
解决:不要在上位机盲目加负号,会同时反向掉前进和旋转。正确做法是在下位机单独确认左右轮的PWM方向与编码器计数符号对应关系,给左右轮各留一个方向取反配置位,改完后重新做一次直行和旋转测试。
4.4 move_base一直报“Failed to get global plan”
现象:目标点设置完成,rviz里没有任何global plan,move_base日志持续输出上述错误。
原因:全局代价地图与坐标系的连接断了——TF缺失、odom频率过低、或膨胀半径设得太大导致起点落在障碍物膨胀圈内。
解决:先后台跑rosrun tf tf_echo map odom观察map到odom是否持续刷新,再跑rostopic hz /odom检查频率,二轮底盘要求不低于10Hz,建议20Hz。如果TF和odom都正常,把costmap_common.yaml里的inflation_radius临时降一半,排除起点被膨胀覆盖的可能。
4.5 启动导航后小车抽搐乱跳
现象:导航一开启,cmd_vel从0瞬间跳到峰值又急速回0,车体猛冲一下后停住。
原因:cmd_vel存在多个发布者、AMCL初始位姿误差过大、或局部规划器在一个极短sim_time下采样出不连续的轨迹。二轮车没有全向底盘稳,对速度突变更敏感。
解决:先看rostopic info /cmd_vel的发布者列表,确认没有同时存在teleop和move_base两个发布者,有的话用第6章说的仲裁策略;再到rviz里用2D Pose Estimate重新对齐初始位姿;最后把DWA的max_vel_x临时降到0.2、acc_lim_x降到0.3,看车是否慢下来。车越是抽搐,越要减速度上限,而不是优先减角速度上限。
这5条从通讯到下位机再到规划器都覆盖了,实际排查顺序我固定为:rostopic list → rostopic hz /odom → rostopic echo /cmd_vel → rosrun tf tf_echo odom base_link → rviz里看TF和costmap。多数翻车点在这个顺序里都能定位到具体环节。
| 症状 | 优先检查 | 命令 |
|---|---|---|
| 电机不动 | 串口权限/帧同步 | ls -l /dev/ttyUSB* |
| 画龙 | 轮径/轮距参数 | rostopic echo /odom |
| 方向反 | 电机方向/编码器相位 | 手动PWM直驱测试 |
| 无全局路径 | TF/odom频率 | rostopic hz /odom |
| 抽搐乱跳 | cmd_vel多源 | rostopic info /cmd_vel |
5. 让源码跑起来:环境准备、编译顺序与数据流验证
前面把原理和坑讲完了,这一章给出一条能照着操作的上手路径:先装环境、再编源码、最后用数据流验证上中下位机已经联通。
5.1 环境准备:ROS版本、依赖与常见安装路线
拿到这套二轮机器人源码包,解压后常见结构是一个ROS工作空间的src目录加一份项目说明文档。第一步不急着编译,先确认环境。Ubuntu 20.04对应ROS Noetic,是目前大量二轮小车开源工程的实际默认;Ubuntu 22.04对应ROS2 Humble,如果源码面向ROS1,建议先按ROS1的环境跑通,别一上来就做跨版本迁移。
ROS安装路径现在很成熟,新手用鱼香ROS一键安装脚本最省力,也可以按ROS官方wiki手动装ros-base、navigation、slam、rqt这些组件。我的判断是:能先装ros-base加navigation、gmapping、map_server、rviz,比一次装桌面完整版更快暴露缺什么依赖。
# 假设源码包已解压,示例结构:src/robot_bringup、src/robot_base、src/robot_msgs、src/robot_nav mkdir -p ~/catkin_ws/src cp -r <解压目录>/src/* ~/catkin_ws/src/ cd ~/catkin_ws rosdep update rosdep install --from-paths src --ignore-src -r -y catkin_make source devel/setup.bash逻辑说明:rosdep会根据功能包package.xml里声明的依赖自动安装缺失系统包,省去手工逐个apt安装。catkin_make按包间的依赖拓扑自动排编译顺序,但如果自定义消息包没先被find_package发现,会出现找不到头文件的报错,这时单独编译消息包一次即可。
参数说明:如果上下位机通讯依赖serial库,rosdep有时装不到,需要手动apt install ros-noetic-serial。navigation包版本旧了也会有类似问题,手动apt install ros-noetic-navigation覆盖安装再编译。
5.2 编译顺序的自检手段:从消息包到导航栈
一个容易翻车的点是“明明catkin_make成功了,但roslaunch还是找不到某些包”。我一般的检查顺序是:先确认所有功能包出现在catkin_make输出里;再对自定义msg/srv包单独编译,从新source环境后再编上层控制包;最后用rospack逐个确认包被索引。
# 快速自检:所有包是否被索引 for pkg in robot_bringup robot_base robot_msgs robot_nav; do rospack find $pkg || echo "missing: $pkg" done逻辑说明:包被索引不代表功能完整,但至少说明工作空间编译没问题。真正的联调效果要看数据流,这一步只是前置条件。
5.3 数据流验证:用 rostopic 命令逐段核对上下位机通讯
上下位机通讯是否打通,不要一直盯着电机看,直接用话题数据说话。验证按链路顺序做:/cmd_vel → 下位机 → /odom → TF → move_base。
第一段冒烟测试:发布一个固定速度指令,几秒后停掉,观察里程计是否有回传。
# 终端1:观察里程计 rostopic echo /odom # 终端2:低速直行指令 rostopic pub -r 5 /cmd_vel geometry_msgs/Twist \ '{linear: {x: 0.2, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}' # 几秒后Ctrl+C停掉cmd_vel发布逻辑说明:如果/odom的linear.x在1秒内升到0.2附近并保持,说明下位机正确执行指令并把编码器速度回传了;如果/odom没任何变化,回到第4章的4.1和4.3继续查。这个测试把上下位机通讯隔离验证清楚,再谈导航。
第二段验证导航链路。启动底盘、雷达、导航栈,然后用rviz或命令行给导航目标。
# 终端1:启动底盘和雷达,确保 /scan、/odom 都在 roslaunch robot_bringup robot_bringup.launch # 终端2:启动导航栈,具体包含move_base、costmap、AMCL还是map_server, # 看源码包的navigation.launch里配了什么 roslaunch robot_nav navigation.launch # 终端3:命令行给目标点 rostopic pub /move_base_simple/goal geometry_msgs/PoseStamped \ '{header: {frame_id: "map"}, pose: {position: {x: 1.0, y: 0.0}, orientation: {w: 1.0}}}' -1逻辑说明:导航栈会先输出global plan,再发布local plan,最后move_base才持续输出cmd_vel给底盘节点。如果路径没出来,优先检查TF和costmap。
参数说明:目标点的orientation要填四元数,w=1表示朝向和起点一致。xy_goal_tolerance和yaw_goal_tolerance控制到位误差,联调初期临时放宽到0.3和0.3,先看整条链路能不能走通,再收紧。
| 验证点 | 命令 | 正常表现 |
|---|---|---|
| 串口连通 | rostopic pub -r 5 /cmd_vel ... | 电机转动,/odom有响应 |
| 里程计频率 | rostopic hz /odom | 稳定10~20Hz |
| TF完整性 | rosrun tf tf_echo map odom | 输出实时变化的平移与旋转 |
| move_base运行 | 导航目标命令 | rviz显示global plan并发布cmd_vel |
| 导航闭环 | 对比cmd_vel与odom频率 | 两个话题频率相近,无异常跳跃 |
逻辑说明:teleop和导航并发发布cmd_vel会破坏上表“正常表现”,所以验证过程中尽量只让一个发布者说话。
6. 进阶技巧:cmd_vel多源仲裁与里程计标定的血泪经验
跑通之后,实际使用里最影响体验的是两件事:一个是多发布者抢cmd_vel,另一个是里程计标定。很多二轮小车项目长时间在导航和遥控之间切换,如果底盘节点同时订阅teleop和move_base,cmd_vel会互相覆盖,车就会“抢方向盘”。
cmd_vel仲裁的常见做法是加一个选择器,底盘节点只订阅cmd_vel_selected。遥控节点发布到cmd_vel_teleop,导航节点发布到cmd_vel_auto,中间节点按优先级转发。我用过最简单的策略是:收到teleop新数据就切到teleop,超过200ms没有teleop数据再切回auto,这样既不会让遥控和导航抢线,也不会在导航停车时误判成遥控失效。
里程计标定更值得花时间。直线标定轮径:找一段至少5m的墙面,让车以0.2m/s直行,如果实测走了4.6m,偏差2%,就把wheel_radius按比例修正。旋转标定轮距:原地转10圈,看odom累计角度,如果实际只转340度,说明wheel_base偏小,按比例放大。这是我给二轮底盘做标定最常用也最有效的一套流程,不要相信说明书上的轮距,自己测出来才是真的。
DWA调优一直建议从“慢”入手:max_vel_x先设0.3、acc_lim_x设0.4,跑通了再逐步提高。换TEB方向时则记住vx和omega是同时优化的,改一个参数全盘受影响,最好先在仿真里调好再上真车,不要边跑边猜。
我自己第一次给二轮小车接move_base时,odom的header.stamp直接留了默认时间,结果TF持续超时,反过来查了半小时,最后在rostopic hz /odom里看到“hz为0”才醒过来。从那以后我固定了联调顺序:先看hz、再看tf、最后才看rviz里的路径。这条联调节奏踩过不少坑,希望帮你在二轮小车上少绕点弯。
本文还有配套的精品资源,点击获取