1. 前言

学习ROS移动机器人开发,很多开发者都会遇到几个典型痛点:

  1. 网上大部分ROS教程只有仿真环境,缺少真实硬件载体,学完无法落地实物小车;
  2. 外购商用机器人底盘大多闭源,底层单片机固件、通信协议不开放,只能调用现成接口,无法深入底层原理;
  3. 想要自研全套小车,工作量巨大:机械结构设计、PCB绘制、嵌入式电机控制、IMU数据处理、ROS驱动开发、SLAM参数调优,任何一个环节卡住都会停滞;
  4. 很多开源小车项目只开放ROS上层代码,底层固件、机械图纸不全,很难复现整套系统。

MickRobot(MickX4) 是一套全链路开源移动机器人项目,由作者熊猫飞天开发。从机械3D模型、PCB原理图、BOM物料清单、STM32底层控制固件,到ROS驱动代码、机器人URDF模型、SLAM建图、自主导航参数全部开源。

项目同时维护ROS1和ROS2两个版本,支持两轮差速、四轮差速、麦克纳姆轮多种底盘构型,兼顾室内2D激光SLAM导航,以及室外3D激光建图场景。非常适合机器人入门学习、高校课程实验、本科/硕士毕设、算法验证平台。

文章适合:ROS学习者、机器人爱好者、高校毕设/课程项目开发者。

分支重要说明

  • master分支:ROS2版本(Humble等)
  • ros1分支:ROS1版本,适配Noetic/Melodic,使用move_base导航栈 ⚠️ ROS1分支和ROS2分支代码不兼容,不要交叉复制文件编译。

2. MickRobot项目概述与设计理念

设计目标

作者设计这套小车核心思路:软硬结合,分层解耦。 把机器人拆成底层底盘控制和上层ROS智能算法两个独立模块。

  • 底层STM32只负责实时控制:电机闭环、运动学解算、传感器采集,保证运动控制实时性;
  • 上位机(树莓派/工控机)只负责高层任务:SLAM建图、定位、路径规划、避障,不参与电机实时控制;
  • 上下层之间通过串口通信解耦,降低耦合。

好处:上层ROS算法修改、替换SLAM方案,完全不需要改动单片机固件;同样,更换底盘机械结构,只需要修改底层运动学,上层ROS代码改动很小。

项目组成两大仓库

ROS上位机软件:https://github.com/RuPingCen/mick_robot

底盘底层固件、硬件图纸:https://github.com/RuPingCen/mick_robot_chassis

  1. mick_robot:ROS上位机功能包集合,运行在Linux(Ubuntu),负责机器人模型、底盘驱动、SLAM、导航;
  2. mick_robot_chassis:底盘底层仓库,包含嵌入式固件、PCB原理图、3D机械模型、物料清单、硬件调试文档。

3. 整套系统分层架构详解

整套系统一共分为四层,由下至上:

  1. 硬件层:电机、编码器、IMU、电源、遥控器、激光雷达、STM32主控板;
  2. 嵌入式底层控制层(STM32):电机PID闭环、运动学正反解、传感器采集、遥控解析、串口协议处理;
  3. 通信层:自定义二进制串口协议,实现上位机 ↔ 单片机双向数据交互;
  4. ROS上层软件层:ROS驱动节点、URDF机器人模型、TF坐标变换、SLAM、路径规划导航。

完整数据流链路

正在绘制图表…

  1. 场景A:自主导航模式 RVIZ设置导航目标点 → move_base(ROS导航栈)全局+局部路径规划 → 输出/cmd_vel(Twist速度指令) → mick_bringup节点订阅/cmd_vel → 将速度打包成串口二进制帧下发STM32 → STM32运动学逆解,把整车线速度、角速度换算成4个轮子目标转速 → 电机PID闭环控制电机转动; 同时,电机编码器采集轮子转速、IMU采集姿态 → STM32打包里程、IMU数据通过串口上传上位机 → mick_bringup解析数据包,发布/odom、/imu话题 → SLAM模块接收里程、IMU、激光雷达点云,持续更新地图与机器人定位。
  2. 场景B:遥控器手动控制 遥控器摇杆信号 → DBUS/SBUS接收机 → STM32解析摇杆通道 → 直接生成整车速度指令,优先级高于上位机/cmd_vel,紧急情况可以手动接管小车,保障安全。

4. 硬件层详细介绍(mick_robot_chassis)

主控芯片

当前 mick_robot_chassis master(MickX4-V3)README标注控制器为STM32F407。选型与外设配置应以所用底盘版本为准。

不同硬件迭代的MCU型号不同;不要将早期版本的成本说明直接套用于当前V3控制板。

板载外设

  1. 电机编码器:霍尔编码器,采集电机转速,用于里程计解算和电机闭环;
  2. IMU:MPU6050(加速度计+陀螺仪),获取机器人姿态,辅助里程计;
  3. 遥控接口:支持大疆DR16/DT7 DBUS协议、乐迪SBUS,硬件拨码切换;
  4. 电源管理模块:锂电池供电,电压检测,低压保护;
  5. 串口:和上位机通信;预留扩展IO。

可选外部传感器

  • 2D激光雷达:RPLIDAR A1/A2等,用于室内2D SLAM;
  • 16线/32线机械激光雷达:用于室外3D激光SLAM(LeGo-LOAM)。

机械构型支持

  • 两轮差速底盘
  • 四轮差速底盘
  • 四轮麦克纳姆轮底盘

同一套底层固件和ROS驱动,只需要修改参数,切换运动学模型,不需要大规模改代码。

硬件资料

仓库内提供:

  • 完整PCB原理图;
  • 3D机械STEP模型、STL打印件模型;
  • BOM物料清单,方便采购元器件;
  • 硬件焊接、组装调试文档。

5. STM32底层固件原理剖析

固件模块与控制数据流

模块/文件作用可从哪里开始读
STM32_Code/User/main.c外设初始化、IMU初始化、主循环任务处理、状态/数据上传main();USART、CAN、TIM初始化与主循环flag处理
STM32_Code/User/MOTOR_Control/MOTOR_Control.c差速/麦克纳姆/阿克曼/四轮转向模型,速度目标和里程速度计算DiffX4_Wheel_Speed_Model()、Calculate_DiffX4_Odom()、ChasissDiffX4_Control_Routing()、WSWD_Wheel_Speed_Model()
STM32_Code/User/MOTOR_APSL2DB/通过CAN管理行进电机驱动器,发送RPM目标、读取状态MOTOR_APSL2DB_Set_RPM()、初始化和状态读取函数
STM32_Code/Bsp/bsp_can/CAN外设、接收中断和底层发送/接收CAN初始化、CAN RX中断入口
STM32_Code/User/DBUS/DBUS.c遥控帧解码、通道解析、杆量到车体速度目标映射RC_Remote_Init() 及遥控帧处理回调
STM32_Code/Bsp/bsp_usart_dma/、DBUS/USART2 DMA遥控数据收取DMA缓冲区与接收完成flag
STM32_Code/User/IMU/IMU.c、MPU6050/、DMP/MPU6050初始化、DMP FIFO读取和姿态数据处理mpu_dmp_init()、IMU_Routing()、MPU6050_DMP_Routing()
STM32_Code/User/stm32f4xx_it.c定时器、USART、CAN等中断入口TIM6_IRQHandler()、USART1_IRQHandler() 等
STM32_Code/User/MickRobot_V3.h底盘常数与上传开关最高速度、底盘几何参数、Send_*_Message宏

整体闭环关系如下。实际轮速反馈由驱动器经CAN回报;STM32计算车体速度估计并将状态打包给上位机。这里的“闭环”同时包含底盘软件的命令/反馈链路与电机驱动器内部速度控制,不能把PID环的位置说错。

正在绘制图表…

主程序:初始化、定时任务和中断协作

main.c 的初始化顺序可概括为:初始化系统滴答与微秒计时、GPIO、USART1(上位机,115200)、USART6(调试打印,256000)、Flash、MPU6050 DMP、遥控接收、CAN1、电机驱动器,再启动TIM6(10 ms)和TIM7(50 ms)定时器。中断服务程序负责快速接收字节/设置标志;主循环根据标志处理状态上传、IMU轮询、故障与温度检查,避免在中断里执行大量格式化或协议逻辑。

C
// main.c 结构示意,保留了当前源码的关键初始化顺序
SysTick_Init();
Initial_micros();
Init_Mick_GPIO();
My_Config_USART_Init(USART1, 115200, 1);  // 上位机通信
My_Config_USART_Init(USART6, 256000, 1);  // printf调试串口

mpu_dmp_init();        // 初始化MPU6050 DMP
RC_Remote_Init();      // USART2/DMA遥控接收
CAN_Config(CAN1);      // 行进电机总线
MOTOR_APSL2DB_Init();  // 初始化电机驱动器

Timer_2to7_Init(TIM6, 10 * 1000);  // 控制节拍约10 ms
Timer_start(TIM6);
Timer_2to7_Init(TIM7, 50 * 1000);  // 慢速任务节拍约50 ms
Timer_start(TIM7);

while (1) {
    // 读取中断置位的flag,执行遥控状态、CAN反馈、IMU、IO和故障处理
}

源码阅读时发现一个需要优先核实的问题:当前 main.c 在遥控/CAN/TIM初始化之前直接调用 Test_Mick_IO();该函数源码内有一个持续运行的 while(1) 测试循环。按当前可见控制流,这个调用不会返回,后续正常底盘初始化代码不可达。若此仓库版本确实如此,必须确认是否应注释测试调用、通过拨码/编译宏条件进入测试模式,或你使用的固件工程有额外修改。它可能解释“固件烧录后没有正常启动”的问题,文章应把它作为源码注意事项写出。

定时器中断通常只增加节拍计数或调用短小的控制路由;TIM6_IRQHandler() 中可以看到周期性调用 ChasissDiffX4_Control_Routing(...),约每10 ms推进一次四个电机目标下发与车体速度估计。main() 主循环另按CAN、遥控和IMU flag处理上传任务。设计上应确保共享结构体和flag在ISR/主循环之间使用 volatile、临界区或缓冲队列进行安全交接;volatile只能防止编译器省略读写,不会自动保证多字段数据的一致快照。

差速底盘逆运动学:车体速度如何变成四个轮速

差速底盘不能横向平移,因此车体横向速度 vy=0vy=0。设车体前向线速度为 vv,角速度为 ωω,左右轮距为 bb。理想左右轮缘速度为:

vR=v+b2ω,vL=v−b2ωv_R = v + \frac{b}{2}\omega,\qquad v_L = v - \frac{b}{2}\omega

若 rr 是轮半径,轮速角速度为ΩR=vR/rΩ_R=v_R/r、ΩL=vL/rΩ_L=v_L/r。换为RPM:

RPM=vwheel2πr⋅60=vwheelπD⋅60RPM = \frac{v_{wheel}}{2\pi r}\cdot60 = \frac{v_{wheel}}{\pi D}\cdot60

当前 MOTOR_Control.c 的 DiffX4_Wheel_Speed_Model(speed_x, speed_w) 正是先限幅、把很小的速度置零,再按底盘几何生成左右两组目标轮速,并乘以约 112.344668 的换算常数(代码注释对应直径约 0.17 m 的轮子)。随后按电机安装方向写入 recived_cmd.tag_rpm[0..3]。代码里存在符号翻转和电机索引映射,实际左右轮的正负号要与电机接线、CAN ID逐个验证。

C
// 摘自 MOTOR_Control.c 的当前差速模型核心逻辑(简化变量声明)
if (speed_x >  Chassis_MAX_Speed) speed_x =  Chassis_MAX_Speed;
if (speed_x < -Chassis_MAX_Speed) speed_x = -Chassis_MAX_Speed;

speed_w = -speed_w;                  // 适配项目坐标/电机正方向
if (fabsf(speed_x) < 0.05f) speed_x = 0;
if (fabsf(speed_w) < 0.05f) speed_w = 0;

v1 = speed_x + speed_w * Chassis_Parameter_L;
v2 = v1;                             // 同侧两个轮子同目标
v4 = -(speed_x - speed_w * Chassis_Parameter_L);
v3 = v4;

recived_cmd.tag_rpm[0] = -(112.344668f * v1);
recived_cmd.tag_rpm[1] = -(112.344668f * v2);
recived_cmd.tag_rpm[2] = -(112.344668f * v3);
recived_cmd.tag_rpm[3] = -(112.344668f * v4);

理论标准式常用半轮距 b/2;代码中的 Chassis_Parameter_L=0.4 是项目参数,具体代表半轮距还是模型几何长度,应结合控制板电机布局和实车标定核实。代码还对 speed_w 取负,并对四路RPM做负号映射,这是坐标系与电机安装方向的补偿,不应直接删掉而不测试。

CAN电机命令与“PID在哪里”

ChasissDiffX4_Control_Routing() 按电机编号分次调用 MOTOR_APSL2DB_Set_RPM(),把已经换算好的目标RPM下发到电机驱动器。CAN回报更新 moto_chassis[] 的实际RPM、温度和驱动器状态;固件用这些反馈计算底盘速度,也会检查驱动器告警。

典型速度PID理论可以写为:

ek=RPMtarget,k−RPMmeasured,ke_k = RPM_{target,k} - RPM_{measured,k} uk=Kpek+Ki∑j=0kejTs+Kdek−ek−1Tsu_k = K_p e_k + K_i\sum_{j=0}^{k}e_jT_s + K_d\frac{e_k-e_{k-1}}{T_s}

其中 u_k 是驱动器最终施加的电流/力矩或PWM控制量。当前项目这条C代码链路把目标RPM交给APS电机驱动器;MOTOR_Control.c 展示的是上层底盘运动学和调度,不是上述PID计算。写文章时可讲清理论闭环:

正在绘制图表…

若要定位具体PID参数,需要查APS电机驱动器SDK/配置工具与驱动器固件文档;不要在文章中声称MCU里的 MOTOR_Control.c 实现了轮级PID,除非你在实际分支找到对应的误差、积分、微分和输出计算代码。

正运动学与里程速度估计

由左右轮缘速度可反算:

v=vR+vL2,ω=vR−vLbv = \frac{v_R+v_L}{2},\qquad \omega = \frac{v_R-v_L}{b}

当前 Calculate_DiffX4_Odom() 将左右侧两个电机RPM分别求平均,再用约 112.2336 的因子换为线速度;odomvxodom_vx 取左右速度平均,odomvy=0odom_vy=0,角速度由左右差除以 0.4 得到。它还会读取IMU yaw变化估计角速度,并在车轮角速度较大时按阈值用IMU估计替代轮式角速度。这是阈值切换/启发式修正,不是EKF或完整传感器融合。

C
// Calculate_DiffX4_Odom() 的关键关系式
vr = (rpm_right_1 + rpm_right_2) / 2000.0f / 112.2336f;
vl = (rpm_left_1  + rpm_left_2)  / 2000.0f / 112.2336f;

odom_vx = (vr + vl) / 2.0f;
omod_vy = 0.0f;
omod_w  = (vr - vl) / 0.4f;

// 当轮式估计认为转向较明显时,使用IMU yaw差分作角速度修正
if (fabsf(odom_w) > 0.25f && fabsf(imu_yaw_rate) > 0.05f)
    odom_w = imu_yaw_rate;

速度与位姿积分要区分:此函数得到车体速度估计;odom 的位置积分可能发生在上位机ROS驱动层。轮速里程会受轮径误差、轮距参数、地面打滑和驱动器RPM标度影响,IMU yaw又受零偏影响,最终仍需要用直线距离和原地旋转实验标定。

MPU6050与DMP:不是只上传未经处理的原始值

当前 main.c 会尝试初始化MPU6050 DMP;成功后约每10 ms调用 IMU_Routing()(约100 Hz)。IMU.c 通过DMP FIFO读取陀螺仪、加速度计与四元数/姿态数据。配置宏 Send_IMU_Message 在当前 MickRobot_V3.h 中默认为 0,因此默认不向上位机上传IMU消息;里程计消息 Send_Odom_Message 默认为 1。是否发布IMU不是只看有无采集代码,还要检查宏开关和ROS驱动接收协议。

DMP负责传感器数据处理/姿态估计,不代表底盘固件完成了轮速+IMU的高质量融合定位。建图导航仍依赖上位机对传感器消息、时间戳、frame与协方差的正确配置。

DBUS/SBUS遥控器与安全接管

DBUS.c 把接收通道解析到共享遥控状态,并把摇杆偏差归一化、乘以比例系数后调用 DiffX4_Wheel_Speed_Model() 设置底盘目标轮速。USART2 DMA接收完成后置位flag,主循环再处理指示灯和可选遥控数据上传。主程序还维护遥控器/上位机通信超时计数。

不过,不要把“遥控器油门非零就无条件屏蔽ROS速度”写成当前所有版本都保证的逻辑。源码可见的遥控回调会生成/更新同一目标命令结构,主控路由也有急停输入时清零目标的逻辑;实际两个输入的优先级取决于当前代码分支中的状态开关、计时器和命令更新顺序。实车调试前应确认拨码模式和失联策略,并验证遥控器失联、上位机断线、急停触发时电机目标确实归零。

上位机协议输入与中断解析

当前 stm32f4xx_it.c 的 USART1_IRQHandler() 是逐字节接收中断:寻找帧头 AE EA,读取长度字段,按帧长度缓存数据,再检查尾部 EF FE;完整后调用 Motor_WriteData_In_Buff(),成功时清零上位机通信超时计数。它与USART2的遥控DMA路径不同。因此,更准确的描述是“USART1接收中断负责上位机命令帧接收,USART2 DMA接收遥控数据”,而不是泛称“串口收发全部走DMA/循环接收”。

正在绘制图表…

当前源码的上报开关与故障排查

MickRobot_V3.h 中的默认宏可直接解释“为什么ROS看不到某类消息”:

C
#define Send_IMU_Message    0  // 默认不发IMU消息
#define Send_RC_Message     0  // 默认不发遥控器消息
#define Send_Motor_Message  0  // 默认不发电机详细消息
#define Send_Odom_Message   1  // 默认上报里程计

排查建议按层推进:

  1. 控制板是否走完初始化:串口调试输出、状态LED、DMP初始化状态;特别检查是否被 Test_Mick_IO() 无限循环截住;
  2. 遥控帧是否到达:DBUS接收flag/LED变化、通道中位和摇杆方向;
  3. 上位机帧是否到达:USART1接收中断是否识别 AE EA,长度和 EF FE 是否匹配,通信超时计数是否复位;
  4. CAN和驱动器是否正常:CAN接收指示、四个电机ID、故障码、温度以及目标RPM;
  5. 速度比例是否正确:架空车轮低速测试,每个轮逐个核对正方向和RPM尺度;
  6. 里程估计是否合理:量实际直行距离、原地旋转角度,对照 112.x 换算尺度与 0.4 m 几何参数;
  7. ROS话题是否有消息:当前配置默认上报odom,不默认上报IMU/遥控/电机详细数据;根据宏和协议解析同步确认。

本节结论:当前STM32固件的主要工作是接收上位机/遥控命令、进行底盘运动学换算、通过CAN控制电机驱动器、采集电机/IMU/电源状态并上报。源码没有展示传统意义上由STM32直接输出PWM的轮速PID闭环;电机RPM闭环应按电机驱动器能力理解。实际发布文章时,建议固定到具体仓库分支/提交,并把 main.c、MOTOR_Control.c、DBUS.c、IMU.c、stm32f4xx_it.c 与对应ROS协议版本一起标注。

6. 上下位机自定义串口通信协议原理

上下位机采用自定义二进制帧协议,不使用JSON、字符串文本协议(文本协议开销大,实时性差)。

协议设计思路

二进制帧结构:帧头 + 数据段 + 校验和 + 帧尾。

  • 帧头:固定标记,用来识别一帧数据起始位置,解决串口字节流粘包问题;
  • 数据段:存放需要传输的数据(速度指令、里程、IMU、遥控通道);
  • 校验和:简单累加校验,防止串口传输误码;
  • 帧尾:结束标记。

上行帧(STM32 → ROS上位机)

底盘向上位机上报数据:

  • 四个轮子编码器数据;
  • IMU加速度、陀螺仪;
  • 遥控器通道值;
  • 电池电压、底盘状态标志位。

下行帧(ROS上位机 → STM32)

上位机下发运动指令:

  • 机器人目标线速度 v;
  • 机器人目标角速度 w。

⚠️ 重点坑点:ROS驱动协议版本必须和STM32固件协议版本严格匹配。如果协议字段长度、顺序、数据类型改动,两边不同步更新,就会出现解析失败、小车不动、里程数据乱跳。 协议文档在作者在线文档表格中,仓库内没有md文档存放协议,需要查阅配套教程。

7. mick_robot ROS功能包源码深度解析

mick_robot仓库一共3个ROS功能包:mick_bringup、mick_description、mick_navigation。

mickrobot_bringup 底盘驱动包【核心包】——按当前仓库源码拆解

先对齐版本与文件名:你给出的目录链接目前使用包名 mickrobot_bringup,同时兼容 ROS1 与 ROS2。当前 master 的 C++ ROS2 入口在 src/cpp_api/mickx4_bringup_v3.1.cpp,协议和通信实现主要在 src/cpp_api/mick_chassis_protocol.cpp/.h,消息结构在 mick_chassis_msg.h。因此,“核心文件 src/mick_base.cpp”是旧版/另一分支的说法,描述当前 master 时应改为上述文件。

另一个关键差别:当前 ROS2 实现不是“各开一个读写线程”的典型结构。ROS 节点订阅回调发送速度,定时器每 10 ms 调用 read_data_buffer() 读取并解析底盘数据;实际通信类使用 Boost.Asio 串口,并提供 UDP 模式。遥控 Joy 发布器虽然创建了,但当前源码中遥控发布函数被注释,不能把“会发布遥控状态”写成当前默认行为。

文件分工:ROS适配与协议适配分开

源码文件负责什么阅读时重点
src/cpp_api/mickx4_bringup_v3.1.cppROS2节点入口、参数、订阅发布、里程计/IMU/TF消息转换MickRobotNode 构造函数、cmd_vel_callback()、read_data_buffer()、publish_odomtery()、publish_imu()
src/cpp_api/mick_chassis_protocol.h底盘通信类接口、底盘类型和通信参数、数据成员MickRobotCHASSIS 的读写接口与参数默认值
src/cpp_api/mick_chassis_protocol.cpp串口/UDP收发、协议帧解析与封装、运动学分支、里程积分send_speed_to_chassis()、send_speed_to_X4chassis()、analy_uart_recive_data()、readData()
src/cpp_api/mick_chassis_msg.h电机、IMU、遥控器、里程计和底盘状态结构体协议解析后的字段、单位和 available 标志
launch/mickrobotx4_ros2.launch.py节点可执行文件、ROS话题名、通信方式、静态TF启动后实际话题名及 communication_mode
launch/mickrobotx4_ROS1.launchROS1启动参数与ROS1节点ROS1话题命名与旧版节点配置
launch/mickrobotx4_ros2_python.launch.py、src/python_api/Python接口版本用于Python/DORA一类上层调用,不是C++节点的同一入口

节点初始化:读取参数并建立ROS接口

MickRobotNode 继承 rclcpp::Node,构造时声明并读取配置。当前 C++ 中可见的参数包括:

  • sub_cmdvel_topic:速度命令订阅话题,类内默认字符串为 cmd_vel;
  • pub_odom_topic、pub_imu_topic:里程计和IMU发布话题;
  • dev、baud:串口设备和波特率;
  • communication_mode:0 为串口,1 为 UDP;
  • udp_ip、udp_port:UDP对端;
  • chassis_type:选择底盘运动学与命令格式分支;
  • is_pub_path:是否发布由里程计拼出的路径;
  • pub_twist_topic、joy_topic:速度估计和遥控器话题名。

随后创建 /cmd_vel 对应订阅者、Imu/odom/Path/TwistStamped 发布者、TF broadcaster,并构造 MickRobotCHASSIS 通信对象。注意具体全名会受 launch 传入的参数和 ROS namespace 影响;应以实际 launch 文件为准,不能只根据类内默认值推断。

当前 ROS2 launch 文件 mickrobotx4_ros2.launch.py 把 communication_mode 设为 1,也就是 UDP;它设置了 mickrobot/cmd_vel、mickrobot/odom、mickrobot/Imu 等话题参数,并额外启动静态TF发布器,连接 base_footprint → base_link、base_link → laser 和 base_link → imu_node。而包内 README 还介绍串口模式,所以使用时要检查你启动的具体 launch 配置,并确保主机和底盘的通信模式、IP/端口一致。

正在绘制图表…

速度命令:Twist 到底盘命令

当前 C++ 回调的核心逻辑非常短:从 geometry_msgs::msg::Twist 取 linear.x、angular.z,把 linear.y 固定为 0,然后调用 send_speed_to_chassis(speed_x, speed_y, speed_w)。这意味着虽然消息类型含有横向速度字段,ROS回调没有把 msg->linear.y 传给底盘;不要据此假设该 ROS2入口已经启用了麦克纳姆横移控制。

C++
// 摘自当前实现的逻辑(省略日志与空指针检查)
void cmd_vel_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
{
    float speed_x = msg->linear.x;
    float speed_y = 0;                 // 当前回调将横向速度置零
    float speed_w = msg->angular.z;
    mick_robot_chassis_->send_speed_to_chassis(
        speed_x, speed_y, speed_w);
}

下一层 send_speed_to_chassis() 根据 chassis_type 分支:0 调 MickX4 差速命令格式;1 按麦克纳姆运动学把车体速度换算为四轮RPM并发送;2 走阿克曼命令;3 走四轮转向四驱命令。因此,话题回调不直接计算PWM或单电机闭环,它只把车体速度交给协议/底盘适配层。

麦克纳姆分支源码的计算形式可以概括为:

C++
// 对应协议实现中的麦克纳姆分支,WHEEL_K 与 WHEEL_D 在头文件定义
v1 =  speed_x - speed_y - WHEEL_K * speed_w;
v2 =  speed_x + speed_y - WHEEL_K * speed_w;
v3 = -(speed_x - speed_y + WHEEL_K * speed_w);
v4 = -(speed_x + speed_y + WHEEL_K * speed_w);

// 轮缘线速度换算成RPM
rpm = wheel_linear_speed / (WHEEL_D * PI) * 60;
send_rpm_to_chassis(rpm1, rpm2, rpm3, rpm4);

源码头文件给出的当前示例常量为 WHEEL_K = 0.355、WHEEL_D = 0.17,但实际轮型、轮径和固件映射必须对应你的车。轮子排列和电机正方向决定符号;若车体前进时某个轮子反转,应查电机方向映射与配置,不能只改速度单位。

命令帧封装:协议层做的事

send_speed_to_X4chassis() 为 MickX4 构造下行帧;当前源码体现的字段包括帧头 AE EA、长度字段、命令字 F3、经过偏移与缩放编码的 x/y/w、占位字段、校验占位和帧尾 EF FE,之后交给 send_bytes_to_udp_or_serial() 发出。麦克纳姆类型则发 F1 命令和四个轮速值。

TEXT
下行帧大致布局(字段长度/缩放按当前协议实现核对)
AE EA | LEN | CMD | 数据载荷 ... | CHECK | EF FE

值得在文章里讲清楚:协议实现里 check 累加值有计算,但当前发送代码写入的校验位是固定 0xFF;接收侧校验也把 0xFF 视为可接受值。因此不要把它描述成“发送端严格计算并验证累加和”。这是仓库当前实现的细节,可靠性要求高时需要结合底盘协议定义确认是否应改进。

通信收发:定时器驱动读取,不是读写双线程

ROS2构造函数创建 10ms wall timer,定期运行 read_data_buffer()。它调用通信对象的 readData();后者从串口或UDP读取一个缓冲块,交给 analy_uart_recive_data() 按帧头、长度、帧尾和数据类型解析。每解析到一类有效数据,协议类更新 chassis_measurements 中相应结构体的 available 标志。ROS节点看到标志后清零并发布对应ROS消息。

C++
// 当前ROS2节点的主循环结构
 timer_ = create_wall_timer(10ms,
   std::bind(&MickRobotNode::read_data_buffer, this));

void read_data_buffer()
{
  if (mick_robot_chassis_->readData()) {
    if (imu_measurements.available) {
      imu_measurements.available = 0;
      publish_imu(imu_measurements);
    }
    if (odom_measurements.available) {
      odom_measurements.available = 0;
      publish_odomtery(odom_measurements);
    }
  }
}

这是一种单线程 ROS executor 中的定时轮询式组织。串口读取本身可能等待数据;理解其阻塞特性时应结合通信模式和底层读取配置。不能写成“读线程与写线程隔离,保证串口不阻塞回调”,因为当前 ROS2 源码的速度回调和定时器都由 rclcpp::spin() 的 executor 调度,协议层也没有在此入口创建独立读写线程。

上行数据解析与发布内容

接收解析函数扫描字节流中的 AE EA 帧头,再依据长度和 EF FE 帧尾判断完整帧,按命令字分发。当前源码分支能看到的类型包括:

  • 0x07:四个电机的转速、圈数、角度等数据;
  • 0x08:电机状态数据分支(当前代码中的处理较少);
  • 0xA7:底盘上报的 vx/vy/wz,以千分尺度还原为浮点速度;
  • 0xA0:IMU加速度、角速度、磁力计及姿态相关字段;
  • 遥控器数据结构已定义,但 ROS2 发布 Joy 的处理函数在当前源文件里被注释。

协议层的 readData() 在收到数据并解析成功后,对差速和麦克纳姆底盘调用里程积分逻辑。calculate_chassisDiffX4_position_for_odometry() 用收到的 vx 和 wz,按 dt 积分得到平面位置与航向;对低于小阈值的速度置零,并把 dt > 1s 的异常间隔清为零。这不是SLAM,也不是IMU融合,属于底盘速度积分里程计。

/odom、TF、/imu 如何构造

publish_odomtery() 将平面航向角 yaw 转成四元数,发布父坐标系 odom、子坐标系 base_link 的 nav_msgs/Odometry,并用同一位姿广播 odom → base_link TF。线速度填入 twist.linear.x/y,角速度填入 twist.angular.z。若 is_pub_path 为真,它还把位姿追加进 nav_msgs/Path。

publish_imu() 把协议结构里的欧拉角转换成四元数,并复制角速度和线加速度到 sensor_msgs/Imu,frame id 写为 imu。阅读时要特别留意单位:源码把姿态角乘以 π/180 转弧度;加速度和角速度原始量是否已按 ROS SI 单位缩放,则要继续核对协议定义和固件端换算。若缩放不一致,消息虽然能在 RViz 显示,融合估计仍会失真。

正在绘制图表…

launch参数、话题和TF要逐项核对

包 README 列出 ROS1 典型话题为 /mickrobot/chassis/Imu、/mickrobot/chassis/odom、/mickrobot/chassis/cmd_vel;ROS2 launch 文件则通过参数传入自己的命名,例如 mickrobot/cmd_vel、mickrobot/odom、mickrobot/Imu。ROS1与ROS2名称不应混为一组,也不要在文章里固定写成 /cmd_vel、/odom、/imu,除非你明确说明做了 remap。

启动调试时依次核对:

  1. communication_mode、设备路径/UDP IP端口、波特率与对端配置是否一致;
  2. chassis_type 是否和实际底盘及速度编码分支匹配;
  3. 速度订阅话题是否是导航实际发布的话题;
  4. odom、IMU、雷达的 frame id 和静态TF是否连通;
  5. is_pub_path 默认关闭,打开后路径消息会随里程计不断累积,长时间运行应考虑路径长度管理。

源码阅读顺序

建议按数据流而非文件顺序阅读:

正在绘制图表…

一句话概括:这个包由 ROS 节点把标准消息转成底盘协议命令,再把协议测量数据转换成 ROS 消息;协议类负责通信帧、底盘类型分支和里程积分,导航算法并不在这个包里实现。

mickrobot_description 机器人模型包——按当前仓库源码拆解

先核对当前包名和文件:当前 master 的ROS2包名是 mickrobot_description,目录叫 mickrobot_description/,与旧文稿的 mick_description 不同。URDF主文件也不止一个:仓库提供 mickrobot_stl.urdf、mickrobot_3d.urdf 及对应的 RViz 版本 mickrobot_stl_rviz.urdf、mickrobot_3d_rviz.urdf。请按实际用途选择,不能假定存在 mickX4.urdf。

目录结构和各文件用途

当前目录主要包含:

目录/文件用途
urdf/mickrobot_stl.urdfMickX4 STL 外观模型、底盘、车轮与传感器连杆定义;含实机碰撞几何和传感器位置
urdf/mickrobot_3d.urdf另一套3D模型/仿真描述,结构与传感器配置和STL版本不同
urdf/mickrobot_stl_rviz.urdf、urdf/mickrobot_3d_rviz.urdfRViz展示用版本;网格路径采用 package://mickrobot_description/...,车轮关节被设为固定关节,适合静态展示
urdf/mesh/STL网格资产,如车体、车轮、传感器和雷达外观模型
launch/display.launch.py启动 joint_state_publisher、robot_state_publisher 与 RViz2,加载URDF并展示TF
rviz/urdf.rvizRViz显示配置
launch/gazebo*.launch.py、urdf/robot.gazebo、worlds/Gazebo仿真所需的launch、插件配置和世界文件

URDF到底描述什么

URDF由两种核心对象组成:

  • link(连杆):机器人上的刚体坐标系,可包含 visual(可视网格)、collision(碰撞几何)和 inertial(质量、惯量);
  • joint(关节):描述两个link之间的连接类型、父子关系、原点位姿与运动轴。fixed表示固定安装,continuous表示可连续旋转的关节。

在当前 mickrobot_stl.urdf 中,可以看到 base_footprint 与 base_link 通过固定关节连接,四个车轮通过连续关节连接到底盘,sensor_link、imu_link、gnss_link、laser_link、camera_link 等传感器也通过固定关节挂在 base_link 下。坐标原点、origin xyz/rpy 和轮关节 axis 决定每个部件相对底盘的位置与方向。

正在绘制图表…

这张图是从URDF结构概括出的机器人连杆树。运行时还会由底盘驱动发布动态 odom → base_link TF;mickrobot_bringup 的 launch 也可能发布静态传感器TF。必须确认每条父子变换只由一个节点发布,且名称与URDF中的link名一致,避免TF重复或断链。

Visual、Collision、Joint的差别

示意片段如下,表达的是URDF的标准结构;数值举例对应当前模型中的雷达安装位姿形式:

XML
<link name="laser_link">
  <visual>
    <geometry><cylinder length="0.05" radius="0.04"/></geometry>
  </visual>
  <collision>
    <geometry><cylinder length="0.05" radius="0.04"/></geometry>
  </collision>
</link>

<joint name="laser_joint" type="fixed">
  <parent link="base_link"/>
  <child link="laser_link"/>
  <origin xyz="0.23 0.0 0.38" rpy="0 0 0"/>
</joint>
  • visual只影响RViz/仿真的外观;
  • collision定义碰撞与仿真接触使用的几何,导航的机器人外形通常还由 costmap footprint 参数单独描述,不应认为URDF collision会自动替代全部导航 footprint 配置;
  • joint origin定义子link坐标系在父link下的位置和姿态;雷达外参写错会使点云/扫描和底盘坐标对不上,进而影响建图和定位。

Mesh路径:实机模型与RViz模型需区分

当前仓库中,mickrobot_stl.urdf 的部分 mesh 路径写成开发者机器上的绝对路径,例如 /home/mickrobot/.../urdf/mesh/...;而 mickrobot_stl_rviz.urdf 使用 package://mickrobot_description/urdf/mesh/...。后者可通过ROS包索引定位,通常更适合其他机器复用。若RViz里模型为空或报 mesh 找不到,优先检查URDF选用版本、网格路径和包是否正确安装。

display.launch.py 做了什么,以及当前文件里的路径问题

display.launch.py 声明了URDF路径、RViz配置、是否启动 robot_state_publisher、是否启动RViz、是否使用仿真时钟等launch参数;它启动 joint_state_publisher(或GUI版本)、robot_state_publisher 与 rviz2。robot_state_publisher 读取 robot_description,结合关节状态发布URDF各link之间的TF。

需要留意当前 master 的一个文件名不一致:GitHub目录列出了 mickrobot_stl.urdf、mickrobot_stl_rviz.urdf 等文件,但 display.launch.py 默认值仍指向 urdf/mickrobot_new_rviz.urdf。如果该文件在你检出的提交中确实不存在,直接运行默认launch会找不到模型文件。可显式传入仓库里存在的RViz URDF,例如:

BASH
ros2 launch mickrobot_description display.launch.py \
  urdf_model:=/绝对路径/mick_robot/mickrobot_description/urdf/mickrobot_stl_rviz.urdf

启动前先在当前检出目录确认文件名;launch文件和URDF文件都可能随提交变化。另一个实机注意点是:mickrobot_stl.urdf 使用绝对mesh路径,RViz版URDF使用 package:// 相对包路径,指定模型文件时也要选对版本。

TF调试:分开检查树结构和数值外参

正在绘制图表…

建议按这几步检查:

  1. 在RViz中固定坐标系选 base_link 或已存在的 odom,观察模型是否完整;
  2. 使用 view_frames 生成TF树,确认 odom、base_link 与雷达link之间连通;
  3. 查询 base_link 到雷达link的变换,检查平移和旋转是否与实物安装一致;
  4. 检查雷达消息的 header.frame_id 是否和TF中的雷达帧名称完全一致;
  5. 若雷达驱动/bringup另发静态TF,避免URDF同时定义同一父子关系造成重复发布。

ROS2常用命令:

BASH
# 生成TF树PDF(需安装 tf2_tools)
ros2 run tf2_tools view_frames

# 查询底盘到雷达坐标系的变换,帧名按实际模型替换
ros2 run tf2_ros tf2_echo base_link laser_link

# 查看scan消息使用的坐标系
ros2 topic echo /scan --once

调试顺序建议:先验证模型文件可加载、mesh路径可解析,再检查link/joint树,之后对照实物测量传感器外参,最后接入 /scan 和SLAM。RViz中“看起来位置大致正确”只能证明显示路径可用,不能替代外参标定。

mickrobot_navigation SLAM与导航包——按当前仓库源码拆解

版本和范围先说明:当前 master 中导航相关目录叫 mickrobot_navigation,但其中 Nav2 配置子包在 navigation/,其ROS包名是 navigation_sim;另有 gmapping/slam_gmapping_sim ROS包,包含GMapping节点实现、参数和启动文件。当前目录实际是ROS2结构,不能把它概括成ROS1 move_base 的纯YAML配置包。当前目录也没有原文所列的Cartographer/LeGO-LOAM/Autoware配置;这些能力需到仓库其他模块或相应分支核实。

仓库结构与启动入口

路径ROS包/用途主要内容
mickrobot_navigation/navigation/ROS包名 navigation_simNav2启动文件、参数、地图、RViz配置
navigation/launch/nav2_gazebo.launch.pyGazebo仿真导航入口引用Nav2的 bringup_launch.py,加载地图与参数,并启动RViz2
navigation/launch/bringup_launch.pyNav2 bringup衍生入口根据 slam 参数切换SLAM或静态地图定位,再纳入导航launch
navigation/param/nav_gazebo.yamlNav2配置AMCL、BT Navigator、Controller、Planner、Costmap等参数
navigation/maps/map_out.yaml + map_out.pgm静态地图地图图像及分辨率、原点、占用阈值
navigation/rviz/navigation2.rviz可视化配置Nav2仿真下的RViz面板与显示项
gmapping/slam_gmapping_sim/ROS包名 slam_gmapping_simGMapping节点源码、launch、参数和RViz配置

原文“launch/config/maps”目录名需要按当前仓库修正:Nav2参数目录实际叫 param/,GMapping代码单独放在 gmapping/slam_gmapping_sim,地图则在 navigation/maps/。

SLAM建图与静态地图导航是两条启动路径

GMapping launch 启动 slam_gmapping_sim 节点并打开RViz,输入激光 /scan 与里程计/TF数据,持续生成占用栅格地图。建图时需要驱动小车移动;地图完成后使用 nav2_map_server map_saver_cli 保存成图像和YAML。

静态地图导航则读取 map_out.yaml 指向的PGM,Nav2的定位节点根据地图、激光与里程计估计 map → odom,规划与控制节点根据目标和代价地图输出速度。当前 nav2_gazebo.launch.py 明确是仿真入口:设 use_sim_time=True,加载 navigation_sim/maps/map_out.yaml 和 navigation_sim/param/nav_gazebo.yaml,再启动RViz。

正在绘制图表…

当前Nav2配置中几个关键选择

nav_gazebo.yaml 把Nav2配置拆在各节点命名空间下,主要可以按以下职责读:

  • AMCL:配置地图坐标系 map、里程计坐标系 odom、底盘坐标系 base_footprint,使用 /scan 做粒子滤波定位;包含粒子数量、激光模型、运动噪声和初始位姿等参数。
  • 全局代价地图:使用 map 为全局坐标系,启用静态地图层、激光障碍层和膨胀层,供全局规划使用。
  • 局部代价地图:以 odom 为全局坐标系,采用滚动窗口;配置激光体素层和膨胀层,供近距离动态避障使用。
  • Planner Server:当前配置选择 nav2_smac_planner/SmacPlannerHybrid,运动模型为 DUBIN,并设置最小转弯半径等参数。这是带运动学约束的Hybrid搜索配置,应用到具体底盘前应确认运动模型与底盘能力相符;麦克纳姆全向运动不能只靠设置此插件就自动得到横向路径控制。
  • Controller Server:当前选择 nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController,配置期望线速度、前瞻距离、障碍物碰撞检测和靠近障碍时的减速参数,最终产生速度命令。
  • BT Navigator:负责行为树导航流程,连接路径规划、路径跟随等Action服务器。

Nav2中“全局规划器”和“局部控制器”共同构成闭环:全局规划器根据地图找路线,控制器跟踪路线并根据局部代价地图调整速度。局部控制器输出最终速度话题,底盘驱动再把它转换为底盘协议指令。

地图YAML各字段含义

当前 map_out.yaml 的配置为:

YAML
image: map_out.pgm
mode: trinary
resolution: 0.05
origin: [-34, -45.2, 0]
negate: 0
occupied_thresh: 0.65
free_thresh: 0.25
  • image:地图像素文件名,相对该YAML所在目录解析;
  • resolution:每个像素对应的实际长度,单位米;这里是5厘米/像素;
  • origin:地图左下角在地图坐标系中的 (x, y, yaw);
  • occupied_thresh、free_thresh:像素概率映射为占用/空闲的阈值;
  • mode: trinary:地图像素按占用、空闲、未知三类解释。

地图、AMCL和TF必须使用一致的坐标约定。保存了地图但地图原点或雷达外参不正确,导航仍会表现为定位偏差、目标点偏移或规划路线不合理。

启动命令与实车迁移

仓库README建议的仿真流程包括:

BASH
# GMapping建图(仿真时间)
ros2 launch slam_gmapping_sim slam_gmapping.launch.py

# 保存地图
ros2 run nav2_map_server map_saver_cli -f ~/map_name

# 使用navigation_sim包的Nav2仿真入口
ros2 launch navigation_sim nav2_gazebo.launch.py

具体命令以工作空间实际安装的包名和launch文件为准;README或不同提交的文件名可能变化。实车导航时应把仿真时钟切换为真实时钟,确保雷达、里程计、TF时间戳一致,并校准代价地图中的footprint/robot_radius、障碍距离、速度限制与底盘制动能力。当前 nav_gazebo.yaml 中包含 use_sim_time: True 和仿真地图配置,不能未经检查直接当作实车参数使用。

导航问题如何按数据链定位

正在绘制图表…

排查时观察“目标是否进入BT、规划是否产生路径、控制器是否发出速度、底盘是否执行”这几处边界,可以区分地图/定位问题、Nav2参数问题和底盘通信问题。使用 ros2 topic echo /cmd_vel 确认速度命令是否实际产生;同时在RViz显示global/local costmap、Path、TF和LaserScan。

8. 运动学模型原理(差速轮 / 麦克纳姆轮)

MickX4支持三种底盘运动学模型,底层STM32做运动学逆解。

  1. 两轮差速底盘 左右两个驱动轮,依靠两轮转速差实现转向。无法横向平移;模型简单,适合入门。
  2. 四轮差速底盘 四个轮子,左右两组同步,同样依靠转速差转向。承载更大,依然不能横移。
  3. 四轮麦克纳姆轮底盘 四个麦克纳姆轮,通过四个轮子转速组合,可以实现:前进、后退、原地旋转、左右横移、斜向移动,全向运动能力。

麦克纳姆轮运动学方程是这个项目一大亮点,也是很多学习者重点研究部分。上位机下发v_x, v_y, w_z,通过矩阵运算求解四个轮子转速。

9. 支持的SLAM与导航方案对比

方案适用场景优点缺点
gmapping室内小场景2D建图轻量,参数简单大场景漂移严重,依赖里程计
Cartographer 2D室内中大场景2D建图精度高,回环检测好参数调参复杂
Cartographer 3D室内外3D建图支持三维地图算力消耗大
LeGo‑LOAM室外大场景3D激光SLAM适合室外,激光特征提取,里程依赖低需要机械激光雷达,算力要求高

ROS1版本导航:move_base;ROS2版本使用ROS2 Navigation2。

完整编译部署、调试实操流程

本节以仓库当前 master 的 ROS2 Humble + MickX4-V3 为主线。ROS1是单独的旧版本/分支,不能把ROS1的 roslaunch、catkin_make、move_base 命令和当前ROS2包混在同一个工作空间里使用。底盘固件仓库也不是普通ROS功能包:它应单独用Keil等STM32工具链编译,不需要把整个固件仓库当成ROS包构建。

先确认版本、硬件与通信链路

开始前记录下面信息,后续排错会容易很多:

  • 上位机仓库分支/提交:当前 master 是ROS2结构;
  • 底盘硬件版本:底盘仓库 master README标注MickX4-V3、STM32F407;
  • ROS发行版:此流程选ROS2 Humble;
  • 驱动通信方式:mickrobotx4_ros2.launch.py 当前把 communication_mode 设为 1(UDP);若用串口,需要把它改成 0 并确认 dev、baud;
  • 雷达型号与消息类型:二维雷达通常发布 sensor_msgs/LaserScan 到 /scan,三维雷达通常发布PointCloud2话题,不要把点云说成 /scan;
  • 遥控模式、急停和轮子离地测试条件。

仓库当前可见包/启动名称包括:驱动包 mickrobot_bringup,模型包 mickrobot_description,导航仿真包 navigation_sim(位于 mickrobot_navigation/navigation),GMapping包 slam_gmapping_sim。如果包名与这里不同,先核对本地分支与 package.xml,不要用旧名 mick_bringup、mick_description 直接猜。

安装ROS依赖并获取代码

按ROS2 Humble安装并初始化环境后,创建工作空间。主ROS仓库放入 src;STM32底盘资料可放在工作空间外单独管理:

BASH
mkdir -p ~/mick_ws/src
cd ~/mick_ws/src
git clone https://github.com/RuPingCen/mick_robot.git

# 底盘固件/硬件仓库单独获取,供Keil工程和原理图查阅
cd ~
git clone https://github.com/RuPingCen/mick_robot_chassis.git

安装仓库README和本机实际使用到的ROS依赖。Nav2至少需要对应Humble发行版的导航组件;模型显示需要 robot_state_publisher、joint_state_publisher、xacro 与RViz;雷达驱动依型号安装。建议先对工作空间运行 rosdep,缺失的第三方包再按具体报错补装:

BASH
sudo apt update
sudo apt install python3-rosdep
sudo rosdep init  # 仅在本机尚未初始化rosdep时执行
rosdep update
cd ~/mick_ws
rosdep install --from-paths src --ignore-src -r -y

GMapping在此仓库中有 slam_gmapping_sim 源码包;Nav2依赖版本要与Humble匹配。不要一边 source Noetic,一边编译ROS2工作空间。

单独编译并准备底盘固件

在 mick_robot_chassis/STM32_Code/Project 中打开对应的Keil工程,确认工程对应V3控制板和电机型号,再编译烧录。烧录后先通过调试串口和状态灯确认固件正常初始化;然后检查电机CAN连接、急停输入、遥控接收机和电池状态。

源码提醒:当前 master 的 main.c 在初始化遥控器/CAN/TIM之前调用 Test_Mick_IO();该函数中存在持续运行的测试循环。烧录前要确认所用工程是否已禁用该测试调用,否则代码可能停留在测试循环,后面的底盘初始化不会执行。实际硬件版本或用户本地固件可能已经修正此处。

编译ROS2工作空间

BASH
source /opt/ros/humble/setup.bash
cd ~/mick_ws
colcon build --symlink-install
source install/setup.bash

每开一个新终端都要重新 source ROS和工作空间环境。可先确认包是否成功被发现:

BASH
ros2 pkg list | grep -E 'mickrobot_bringup|mickrobot_description|navigation_sim|slam_gmapping_sim'

若构建失败,先看第一个报错包和缺失依赖;不要同时混用ROS1与ROS2的 build、devel、install 目录。

配置串口或UDP

串口模式:查看实际设备名,再设置用户组权限:

BASH
ls -l /dev/ttyUSB* /dev/ttyACM* 2>/dev/null
sudo usermod -aG dialout "$USER"

重新登录后权限生效。当前launch的串口设备默认 /dev/ttyUSB0、波特率115200,但该launch默认仍使用UDP(communication_mode=1)。切串口时,将launch参数改为 communication_mode: 0,并将设备路径改成实际设备名;也可以复制一份自己的launch配置,避免直接改仓库默认文件。

UDP模式:按底盘和控制板说明配置主机网段、目标IP和端口。驱动README写明ROS端UDP客户端目标为 192.168.1.30:1230;需要确保主机网卡路由可达,并与控制板的串口转网口模块配置一致。UDP连通不代表ROS协议已经正确,后续仍要检查状态话题是否更新。

当前launch的主要参数/话题包括:chassis_type、communication_mode、dev、baud、udp_ip、udp_port、sub_cmdvel_topic、pub_odom_topic、pub_imu_topic。默认速度输入是 /mickrobot/cmd_vel,里程计是 /mickrobot/odom,IMU话题名为 /mickrobot/Imu(大小写敏感)。若固件宏 Send_IMU_Message 仍为默认0,IMU发布话题可能没有有效数据。

先验证URDF、mesh和TF

当前模型包名为 mickrobot_description。display.launch.py 默认引用的 urdf/mickrobot_new_rviz.urdf 与当前目录列出的文件名不一致;先检查已安装文件,再指定存在的RViz URDF,例如:

BASH
ros2 pkg prefix mickrobot_description
ros2 launch mickrobot_description display.launch.py \
  urdf_model:="$(ros2 pkg prefix mickrobot_description)/share/mickrobot_description/urdf/mickrobot_stl_rviz.urdf" \
  use_sim_time:=False

在RViz检查机器人模型、车轮位置、雷达/IMU坐标和TF连通性。若mesh不显示,检查选用的URDF路径:部分实机URDF含开发机绝对路径,RViz版本采用 package://mickrobot_description/... 网格路径,通常更适合安装后加载。

启动底盘驱动并做低速验证

先只启动底盘驱动,不要同时启动导航。当前ROS2 launch名为:

BASH
ros2 launch mickrobot_bringup mickrobotx4_ros2.launch.py

在另一个终端检查实际话题名和频率:

BASH
ros2 topic list | grep -E 'mickrobot/(cmd_vel|odom|Imu|odom/twist)'
ros2 topic hz /mickrobot/odom
ros2 topic echo /mickrobot/odom --once

确认话题命名后,在驱动轮架空、急停可触及、遥控模式正确的条件下,以很小速度做单次短测试。底盘回调当前将 Twist.linear.y 固定为0,因此不能用此入口验证横向平移:

BASH
ros2 topic pub --once /mickrobot/cmd_vel geometry_msgs/msg/Twist \
  "{linear: {x: 0.05, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"

测试通过后再落地低速验证。若命令已发布但轮子不动,依次检查ROS订阅话题、遥控器自动/手动档位、急停输入、通信模式、底盘类型、帧协议与CAN电机状态。遥控接管和失联停车必须在现场验证,不能从“节点启动成功”推断安全逻辑已生效。

启动雷达,核实传感器消息和坐标系

启动与雷达型号匹配的驱动,再确认话题持续更新、时间戳和frame正确:

BASH
ros2 topic list | grep -E 'scan|points|cloud'
ros2 topic echo /scan --once
ros2 topic hz /scan
ros2 run tf2_ros tf2_echo base_link laser

当前底盘launch示例发布的雷达静态frame名为 laser,而description URDF中使用 laser_link;这两个名称必须与雷达消息的 header.frame_id 统一,不能让TF树出现两个互不相连的雷达帧。以 /scan 消息里的frame为准,必要时调整launch静态TF或雷达驱动参数,再查询 base_link 到该帧的变换。/scan 应是 LaserScan;3D雷达通常使用点云话题,Nav2 costmap需按数据类型配置对应观测源。雷达没有数据或TF不连通时先不要启动SLAM。

GMapping建图并保存地图

仓库提供的 slam_gmapping.launch.py 默认把 use_sim_time 设为True,更适合仿真。实车建图必须使用真实时钟;将该launch默认值改为False,或确认当前launch支持 use_sim_time:=False 覆盖。然后启动驱动、雷达和GMapping,遥控小车缓慢覆盖环境,避免快速旋转或长直线冲刺造成匹配失败。

BASH
# 实车场景确保launch中的 use_sim_time 为 False
ros2 launch slam_gmapping_sim slam_gmapping.launch.py

# 如使用仓库提供的键盘节点(确认当前分支安装了keyboard包)
ros2 run keyboard keyboard

# 建图完成后保存地图;输出 map_name.pgm 和 map_name.yaml
ros2 run nav2_map_server map_saver_cli -f ~/map_name

保存后打开YAML确认 image 相对路径有效、resolution 和 origin 合理。地图应覆盖导航区域,机器人本体尺寸不要贴着墙或被动态物体遮挡。

启动定位和Nav2导航

当前 navigation_sim/nav2_gazebo.launch.py 是仿真启动入口:它使用Gazebo地图、RViz配置,并固定 use_sim_time=True。不要直接拿它启动实车。当前包中的 bringup_launch.py 可复用Nav2的定位和导航启动逻辑;实车时传入自己的地图与参数文件,并关闭仿真时钟:

BASH
NAV_SHARE="$(ros2 pkg prefix navigation_sim)/share/navigation_sim"
ros2 launch navigation_sim bringup_launch.py \
  map:="$NAV_SHARE/maps/map_out.yaml" \
  params_file:="$NAV_SHARE/param/nav_gazebo.yaml" \
  use_sim_time:=False slam:=False

实际使用应把 map_out.yaml 换成刚保存/校准过的地图,并复制一份实车参数:核对 base_frame_id、robot_base_frame、odom_frame_id、激光话题、footprint、障碍物范围、最大速度与加速度限制。仓库的 nav_gazebo.yaml 带有Gazebo参数和 use_sim_time: True,只是示例起点,不适合未经检查直接用于实车。

Nav2节点启动并进入Active后,在RViz设置初始位姿(通常通过“2D Pose Estimate”),再设置导航目标(“Nav2 Goal”)。观察地图、AMCL粒子/定位、全局和局部costmap、规划路径、TF以及速度命令:

BASH
ros2 topic echo /mickrobot/cmd_vel
ros2 topic echo /scan --once
ros2 run tf2_tools view_frames

当前驱动launch默认订阅 /mickrobot/cmd_vel,标准Nav2通常输出 /cmd_vel。若无namespace/remap,两者不会自动连通。可以复制驱动launch并将 sub_cmdvel_topic 改为 cmd_vel,或者给Nav2节点添加一致的remap;也要检查键盘节点的实际发布话题。导航目标成功不等于机器人一定执行,必须确认速度话题消息链闭合。

ROS1旧版流程不要与ROS2混用

如果你使用的是旧ROS1分支,应另建ROS1工作空间,并按该分支README确认包名、launch、依赖和话题;ROS1典型工具是 catkin_make、source devel/setup.bash、roslaunch、rostopic,导航通常使用 move_base。这些命令不适用于当前 master 的ROS2包。底盘仓库固件、ROS1驱动和ROS2驱动的协议字段也必须来自同一兼容版本。

正在绘制图表…

11. 系统数据流全链路梳理

再做一次完整数据流总结,方便读者理解:

  1. 上层规划节点(move_base) → Twist消息 /cmd_vel;
  2. mick_bringup订阅Twist → 封装二进制串口帧;
  3. 串口下发STM32;
  4. STM32运动学逆解,计算4轮目标转速,电机PID控制;
  5. 编码器采集轮速,正解得到整车里程;IMU采集姿态;
  6. STM32打包上行二进制帧,串口上传上位机;
  7. mick_bringup解析数据包,发布/odom、/imu;
  8. SLAM节点订阅 /scan激光、/odom、/imu,输出地图与机器人定位;
  9. 定位信息输入导航栈,持续更新代价地图,进行路径规划,形成闭环。

12. 高频问题与疑难故障排查手册

串口通信类问题

  1. 打开串口失败:没有dialout权限、设备号不对、USB转串口芯片损坏;
  2. 小车完全不动:协议版本不匹配;底盘没有上电;遥控器处于使能状态;
  3. 里程数据乱跳:串口波特率不匹配;协议帧解析错误;电磁干扰。

里程计、TF、建图漂移类

  1. 建图严重漂移:URDF雷达TF坐标错误;轮子参数(半径、轮距)设置错误;IMU干扰;编码器打滑;
  2. RVIZ机器人模型消失:TF树断裂,检查URDF。

导航相关故障

  1. 小车原地不动,收到目标点但是不运动:代价地图没有障碍物,但是机器人本体参数错误;局部规划器参数不合适;
  2. 小车频繁撞障碍物:激光雷达标定错误,代价地图膨胀参数不对。

硬件类

  1. IMU数据噪声大:电机PWM电磁干扰,IMU远离电机电源线;
  2. 电机抖动:底层PID参数不合适,需要调STM32里面电机PID。

13. 项目二次开发方向与扩展思路

这套项目因为分层解耦,二次开发空间很大:

  1. 上层ROS开发
  • 增加视觉SLAM(ORB-SLAM3);
  • 增加目标检测,实现跟随、避障;
  • 开发多机协同;
  • 替换导航栈,使用ROS2 Nav2;
  • 增加状态机,实现巡逻、定点任务。
  1. 底层STM32固件开发
  • 增加底盘故障检测;
  • 优化IMU姿态融合(EKF);
  • 增加CAN总线电机驱动支持;
  • 增加底盘振动滤波算法优化里程。
  1. 硬件扩展
  • 加装深度相机;
  • 增加超声波避障传感器;
  • 升级更大功率电机,提升载重。

开发建议:增量开发。原版demo跑通,所有话题、TF、里程正常之后,再新增功能。不要一次性大规模修改代码,否则故障很难定位。

14. 项目优缺点分析

✅ 优点

  1. 真正全栈开源:硬件图纸、底层固件、ROS上层全部开放,市面上同类小车很少做到;
  2. 分层解耦架构,底层和上层分离,方便学习、二次开发;
  3. 支持多种底盘构型,一套代码切换差速/麦克纳姆;
  4. ROS1/ROS2双版本维护,适配新旧学习环境;
  5. 作者配套完整系列教程,降低学习门槛;
  6. 遥控器紧急接管机制,实物调试安全性更好。

❌ 缺点

  1. 自定义串口协议无内置校验异常重试机制,高速运动下偶尔存在丢包;
  2. IMU只上传原始数据,底层没有姿态融合,里程长期会漂移,依赖SLAM激光回环修正;
  3. 主控使用STM32F103算力有限,复杂滤波算法只能放到上位机;
  4. 属于教学科研小车,载重有限,不适合工业场景;
  5. 协议文档在线维护,仓库内没有本地协议文档,查阅需要跳转外部文档。

15. 总结

MickRobot(MickX4)最大价值,是给机器人学习者提供了一套完整、可落地、全开源的移动机器人教学平台。 很多ROS学习只停留在仿真,仿真环境屏蔽了真实硬件带来的串口、噪声、TF标定、电机控制等工程问题。而MickRobot把从机械、嵌入式底层电机控制,到ROS驱动、坐标系、SLAM建图、自主导航整个链路完整呈现出来。

对于初学者,可以顺着这套项目理解移动机器人完整工作流;对于毕设和科研,可以直接基于这套底盘,把重心放在SLAM算法、路径规划、机器人上层智能应用开发,节省大量硬件开发时间。

如果你打算做二次开发,建议先吃透整套数据流,理解上下位机边界分工:底层负责实时运动控制,上位机负责智能决策。这个思想,也是移动机器人工程开发通用设计思想。

关键控制代码与流程框架

本节把文章中的“数据如何走、代码如何接、底盘如何响应”落到更具体的代码结构。以下代码均为教学示意,用于解释常见实现模式;MickRobot不同硬件版本、ROS分支和SDK的结构体、话题名与通信字段可能不同,请按实际源码对照修改。特别是串口协议字段,不要凭示意代码直接烧录或下发到实体底盘。

软件节点与数据接口总览

正在绘制图表…

读图要点:驱动节点是硬件和ROS之间的边界。导航算法只产生速度目标;驱动节点负责协议适配;STM32负责确定性的轮速控制。传感器数据经驱动节点变成标准ROS消息,SLAM和导航再消费这些消息。

/cmd_vel 到底盘的ROS订阅示例

下面是ROS2 rclcpp 的最小化结构示意。真实项目中还要结合底盘限速、超时停车、遥控器模式、生命周期管理和SDK协议实现。

C++
// 教学示意:将Twist速度转换为底盘SDK可接受的速度命令。
class BaseDriver : public rclcpp::Node {
public:
  BaseDriver() : Node("base_driver") {
    max_vx_ = declare_parameter("max_vx", 0.6);
    max_wz_ = declare_parameter("max_wz", 1.2);

    cmd_sub_ = create_subscription<geometry_msgs::msg::Twist>(
      "/cmd_vel", rclcpp::QoS(10),
      [this](const geometry_msgs::msg::Twist::SharedPtr msg) {
        const double vx = clamp(msg->linear.x, -max_vx_, max_vx_);
        const double wz = clamp(msg->angular.z, -max_wz_, max_wz_);

        // 实际项目在此处调用SDK/协议封装,并写入串口或CAN接口。
        chassis_sdk_.setVelocity(vx, msg->linear.y, wz);
        last_cmd_time_ = now();
      });

    watchdog_ = create_wall_timer(
      std::chrono::milliseconds(100), [this]() {
        if ((now() - last_cmd_time_).seconds() > 0.5) {
          chassis_sdk_.setVelocity(0.0, 0.0, 0.0);  // 指令超时,停车
        }
      });
  }

private:
  static double clamp(double x, double lo, double hi) {
    return std::max(lo, std::min(x, hi));
  }

  double max_vx_{0.6}, max_wz_{1.2};
  rclcpp::Time last_cmd_time_{0, 0, RCL_ROS_TIME};
  rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;
  rclcpp::TimerBase::SharedPtr watchdog_;
  ChassisSdk chassis_sdk_;  // 用实际仓库SDK替换此占位类型
};

这里值得重点检查三件事:

  1. linear.x 和 angular.z 的单位分别是 m/s 与 rad/s,驱动与固件两侧必须采用一致单位;
  2. ROS消息只表达速度目标,电机PWM与编码器闭环由底盘控制器执行;
  3. 指令超时后发送零速是基础保护逻辑,实体车还应验证急停、遥控接管与通信中断行为。

串口接收不是“一次 read 就是一帧”

串口提供连续字节流。一次读取可能只有半帧,也可能包含多帧,因此驱动应当缓存字节、找帧头、按长度组帧、校验后再解析。以下是语言无关的解析流程:

正在绘制图表…

C++
// 教学伪代码:长度字段和校验算法必须替换为项目协议定义。
void SerialParser::feed(uint8_t byte) {
  buffer_.push_back(byte);

  while (true) {
    auto start = findHeader(buffer_);
    if (start == buffer_.end()) {
      keepPossibleHeaderSuffix(buffer_);
      return;
    }
    buffer_.erase(buffer_.begin(), start);

    if (buffer_.size() < HEADER_SIZE + LENGTH_SIZE) return;
    const size_t frame_size = decodeFrameSize(buffer_);
    if (frame_size > MAX_FRAME_SIZE) {
      buffer_.erase(buffer_.begin());  // 异常长度,向后重新同步
      continue;
    }
    if (buffer_.size() < frame_size) return;

    if (checksumOk(buffer_.data(), frame_size)) {
      dispatchFrame(buffer_.data(), frame_size);
    }
    buffer_.erase(buffer_.begin(), buffer_.begin() + frame_size);
  }
}

协议实现至少要明确:帧头、版本号/命令字、载荷长度、字节序、浮点数或定点数编码、校验范围、最大帧长和超时策略。增加字段时应同步更新上位机与固件,并通过串口日志或回环测试确认两边解释一致。

MCU轮速闭环:目标轮速到PWM

典型控制周期如下。采样周期固定有助于获得可预期的控制效果;实际PID增益和电机方向需要在架空车轮、限流条件下逐步调试。

正在绘制图表…

C
// 教学示意:每个轮子一个速度控制器,dt单位为秒。
typedef struct {
  float kp, ki, kd;
  float integral;
  float prev_error;
  float output_limit;
} PID_t;

float PID_Update(PID_t *pid, float target, float measured, float dt) {
  if (dt <= 0.0f) return 0.0f;

  const float error = target - measured;
  pid->integral += error * dt;
  const float derivative = (error - pid->prev_error) / dt;
  float output = pid->kp * error
               + pid->ki * pid->integral
               + pid->kd * derivative;

  if (output >  pid->output_limit) output =  pid->output_limit;
  if (output < -pid->output_limit) output = -pid->output_limit;
  pid->prev_error = error;
  return output;
}

此处省略了积分抗饱和、低速死区补偿和编码器滤波等工程细节。若输出长期顶到限幅,单纯增加 ki 通常会恶化恢复过程,应先检查负载、电机方向、轮径/减速比参数和测速尺度。

麦克纳姆轮逆运动学示例

对常见X型滚子安装,定义车体速度为 (vx, vy, wz),轮半径为 r,底盘中心到轮子的半长、半宽分别为 a、b,令 k=a+b。按左前、右前、左后、右后顺序,一种常见符号约定为:

[ωFLωFRωRLωRR]=1r[1−1−k11k11−k1−1k][vxvyωz]\begin{bmatrix}\omega_{FL}\\\omega_{FR}\\\omega_{RL}\\\omega_{RR}\end{bmatrix} =\frac{1}{r} \begin{bmatrix} 1&-1&-k\\ 1& 1& k\\ 1& 1&-k\\ 1&-1& k \end{bmatrix} \begin{bmatrix}v_x\\v_y\\\omega_z\end{bmatrix}
PYTHON
# 教学示意:输出 rad/s;轮子排列与正负方向必须和接线/固件定义核对。
def mecanum_inverse(vx, vy, wz, radius, half_length, half_width):
    k = half_length + half_width
    return [
        (vx - vy - k * wz) / radius,  # front-left
        (vx + vy + k * wz) / radius,  # front-right
        (vx + vy - k * wz) / radius,  # rear-left
        (vx - vy + k * wz) / radius,  # rear-right
    ]

不同厂商滚子朝向、轮子命名和电机安装方向会改变符号。标定时先低速逐个验证“正速度是否让该轮向前滚”,再验证纯前进、纯横移和原地旋转,最后检查组合速度。四轮差速底盘不具备独立的 vy 横移自由度,不能套用麦克纳姆轮公式。

从建图到导航的启动与验证顺序

正在绘制图表…

ROS2命令示例(具体launch名称应按当前分支 README 与实际安装包确认):

BASH
# 查看关键话题与消息是否持续更新
ros2 topic list
ros2 topic echo /odom
ros2 topic echo /imu
ros2 topic hz /scan

# 低速短时验证速度接口;实体车测试前架空驱动轮并确保急停可用
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist \
  "{linear: {x: 0.05, y: 0.0}, angular: {z: 0.0}}"

# 保存地图(地图文件前缀按自己的目录修改)
ros2 run nav2_map_server map_saver_cli -f ~/mick_map

调试时建议记录的量

模块建议记录看到异常时先检查
串口/协议帧率、校验失败数、丢帧数、最后有效帧时间波特率、设备名、协议版本、接地与干扰
电机控制目标轮速、实测轮速、PID输出、PWM饱和比例电机方向、编码器方向、采样周期、PID单位
里程计/odom频率、位置/航向连续性、协方差轮径、轮距、编码器倍率、轮胎打滑
TFodom、base_link、雷达坐标是否连通静态变换方向、URDF安装位姿、时间戳
SLAM/导航/scan频率、地图质量、局部代价地图、/cmd_vel雷达外参、里程计漂移、地图分辨率与膨胀半径
PLAINTEXT