ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ROS2与Arduino机器人底盘开发实战:串口协议设计、PID调速与里程计全解析

ROS2与Arduino机器人底盘开发实战:串口协议设计、PID调速与里程计全解析 从拿到一套 Arduino 电机驱动板和一堆杜邦线到终于让底盘在/cmd_vel话题下听话地前进、转弯这个过程我前前后后折腾了好几周。ROS2 和 Arduino 单看都是成熟得不能再成熟的东西但把它们组合成一个机器人底盘时真正的坑根本不在“装环境”而在通信协议怎么定、下位机怎么闭环、上位机怎么把速度指令变成轮速指令。这篇文章就把我做这个 ROS2 Arduino 机器人底盘的全过程拆开讲清楚包括硬件怎么选、协议怎么设计、PID 怎么调、联调时我都踩过什么坑给正准备从“会跑小乌龟”迈向“真车能动”的朋友一条可以直接照着走的路。1. 为什么用 Arduino 给 ROS2 做底盘先想清楚主控分工很多初学者拿到树莓派或者迷你主机第一反应是“单片机都能干的事为什么不让主控直接干”。这个想法我用实际体验告诉你能跑但非常别扭。1.1 ROS2 主机与 Arduino 下位机算力与实时性的取舍ROS2 节点、导航栈、激光雷达驱动、rviz2 可视化这些东西对 CPU 和内存的需求是实打实的。如果你的主控还在忙着读编码器中断、输出 PWM 波形、处理电机的闭环控制很容易出现高负载下控制周期抖动、编码器计数丢失的情况。Linux 不是硬实时系统哪怕你给节点设置了很高的优先级调度的不确定性依然存在。Arduino 这类单片机恰恰相反。它的主频低、内存小但可以做到中断级别的响应。电机编码器的正交解码、PWM 输出、定时器中断这些对实时性要求高的活儿交给 Arduino 反而更可靠。我见过有人直接在树莓派上用 GPIO 读编码器低速时候看不出来一旦跑导航算法、CPU 占用上去里程计数据就开始跳小车直线跑成蛇形。后来把底层逻辑全部下沉到 Arduino问题立刻消失。所以我的建议非常明确上层做决策底层做执行。ROS2 主机只负责订阅目标速度、发布里程计和其他传感器数据底盘控制这种“肌肉记忆”级别的工作全部交给 Arduino。1.2 底盘形态选型两驱差速还是四驱麦克纳姆底盘形态决定了整篇文章后续所有的代码结构这一步不能拍脑袋。两轮差速底盘是最经典的入门形态两个主动轮加一个万向轮运动学模型简单转向通过左右轮速差实现。控制逻辑清楚调试成本低而且后续接 Nav2 导航也完全够用。四驱麦克纳姆轮可以横向平移、原地旋转看起来很酷但四个轮子的速度解算和标定工作量大得多任何一轮打滑都会让位姿估算明显偏移底盘结构本身的精度要求也高。阿克曼底盘更接近真车但机械结构复杂转向机构和驱动机构需要联动控制不适合作为第一个 Arduino 底盘项目。我自己做的是两轮差速因为学习阶段的重点是 ROS2 与 Arduino 之间的数据链路和闭环控制而不是在机械结构和多轮标定上花费大量时间。如果你以后确实需要麦克纳姆轮在差速底盘跑通之后再切换运动学部分只是多一个坐标变换底层串口协议和三闭环逻辑是通用的。1.3 我的硬件配置清单附选型理由主控板Arduino Uno 或 Nano。Uno 生态最好资料最多适合首版调试Nano 体积小适合直接焊在底盘上做最终集成。电机带霍尔编码器的 TT 马达直流减速电机。霍尔编码器可以反馈轮速是闭环控制的前提。选型时注意减速比30:1 左右的比较适合室内底盘速度适中编码器读数也够用。电机驱动TB6612FNG。比 L298N 的体积小、压降低、效率高不需要额外散热片。驱动两路直流电机绰绰有余。电池7.4V 2S 锂电池。电机直接吃电池电压Arduino 和编码器通过降压模块供电。通信USB 线直连调试阶段或者用 USB 转 TTL 模块接 Arduino 的串口引脚。上位机我用的是一台旧笔记本装 Ubuntu 22.04跑 ROS2 Humble。树莓派 4B 以上也可以。这些硬件加起来不到三百块但能跑通完整的 ROS2 底盘闭环。很多人纠结要不要上 ESP32后面我会单独讲 micro-ROS 和自定义串口协议的区别这里先按住不表。2. 环境搭建ROS2 版本选择与 Arduino 开发链路环境部分看着简单实际上有大量时间浪费在“版本不匹配”和“工具链选择不当”上。我把我用的组合和理由写清楚。2.1 ROS2 版本选择Humble 是当前最稳的选择ROS2 的版本和 Ubuntu 版本是绑定的。Ubuntu 22.04 对应 ROS2 HumbleUbuntu 24.04 对应 Jazzy。如果你是新装系统我建议 Ubuntu 22.04 ROS2 Humble。原因很简单Humble 是 LTS 版本教程最多绝大部分开源机器人项目包括 Nav2、micro-ROS Agent、各种底盘驱动包都优先在 Humble 上验证。Jazzy 虽然更新但在一些第三方包的兼容性上还有摩擦没必要在环境上给自己加难度。安装的时候直接用 apt 源安装完整版桌面版ros-humble-desktop里面已经包含了 rviz2、gazebo、demo 节点这些常用工具。装完以后用ros2 run demo_nodes_cpp talker验证一下能听到listener的回声就说明基本环境没问题。我遇到过很多人卡在“ROS2 装好了但ros2命令找不到”十有八九是忘了 sourcesource /opt/ros/humble/setup.bash echo source /opt/ros/humble/setup.bash ~/.bashrc2.2 Arduino 开发链路Arduino IDE 还是 PlatformIO做底盘这种多文件项目我强烈推荐用 PlatformIO VS Code而不是 Arduino IDE。Arduino IDE 写一个 Blink 没问题但当你需要同时管理motor.cpp、encoder.cpp、protocol.cpp这几个文件的时候它的工程管理能力会让人抓狂。PlatformIO 可以清晰地组织多文件结构还能更方便地管理第三方库依赖。不过如果你只是想在硬件到手前先熟悉一下 Arduino 语法Wokwi 在线仿真平台热搜里很多人问的那个 wokwi 仿真平台是个好东西。它可以直接在浏览器里拖一个 Arduino Uno 出来连上电机驱动、编码器先把逻辑跑通再烧录到真实硬件上。我调试串口协议的时候就是在 Wokwi 里先模拟 Arduino 端收帧、解帧的逻辑确认无误之后再去刷真板子。2.3 micro-ROS 和自定义串口协议两条路线的选择逻辑现在 Arduino 接 ROS2 有两条主流路线第一是 micro-ROS。官方支持方案Arduino 端跑 micro-ROS 库上位机跑一个 micro-ROS Agent两边直接建立 DDS 通信。听起来很完美但有个现实约束micro-ROS 对单片机资源要求不低。Arduino Uno 的 ATmega328P 只有 2KB RAM跑完整 micro-ROS 栈非常吃力官方更多推荐 ESP32、STM32 这类性能更强的板子。如果你坚持用 Uno/Nano这条路基本走不通。第二是自定义串口协议。Arduino 不接入 ROS2只干一件事通过串口接收左右轮目标速度控制电机达到目标转速再把编码器数据回传。ROS2 这边写一个节点订阅/cmd_vel做运动学解算把结果通过串口发下去同时收集 Arduino 上报的编码器数据进行里程计推算。我的选择是方案二因为它在 Arduino Uno 这类低成本板子上完全可行而且协议是透明的你能清楚地看到每一字节在传输什么。这比把 micro-ROS 封装好的黑盒跑起来更有学习价值。等以后升级到 ESP32再迁移到 micro-ROS 也不迟底层的 PID 和编码器逻辑完全不用动。3. 通信协议设计把/cmd_vel从 ROS2 送到 Arduino 再转成轮速这是整篇文章里我认为最核心的部分。很多人 Arduino 端和 ROS2 端单独跑都没问题一连起来就出各种奇怪毛病根因几乎都在协议设计上。3.1 一次完整的指令链路先看一条速度指令从发出到执行的全过程ros2 topic pub /cmd_vel geometry_msgs/Twist {linear: {x: 0.2}, angular: {z: 0.5}}这个命令发布了一个目标速度。底盘节点订阅到之后根据差速运动学解算出左右轮的目标转速通过串口发送给 Arduino。Arduino 解析出左右轮目标转速与编码器实测转速做 PID 运算输出 PWM 驱动电机同时以固定频率通过串口回报当前轮速。底盘节点收到回报后计算并发布/odom里程计话题供 rviz2 显示或导航使用。这条链路里每一步的延迟都要控制。串口通信频率我设置为 20Hz50ms 一帧PID 控制周期对齐到 20ms。也就是说每个串口指令周期内 Arduino 至少执行两次完整的 PID 运算这样轮速对指令的响应才够平滑。3.2 自定义串口协议帧格式设计串口通信最忌讳的就是“随便发”比如直接Serial.println(L100 R100)。字符串解析看着方便但存在几个致命问题解析开销大、容易出现半包、不同平台换行符不一致。我建议直接使用二进制帧格式。我的协议帧定义如下字段长度说明帧头11字节固定 0xAA帧头21字节固定 0x55数据长度1字节数据区长度指令ID1字节0x01 表示速度控制数据区N字节左右轮目标速度校验和1字节数据区所有字节累加取低8位左右轮速度用int16_t表示单位是 mm/s符号代表方向。因为差速底盘只有两个轮子数据区正好 4 字节。整帧长度 9 字节在 115200 波特率下传输时间不到 1ms非常充裕。ROS2 端发送端用 C 写一个封装函数核心代码如下void packSpeedFrame(int16_t left_speed, int16_t right_speed, uint8_t* buf) { buf[0] 0xAA; buf[1] 0x55; buf[2] 0x05; // 数据长度1字节指令ID 4字节数据 buf[3] 0x01; // 指令ID速度控制 buf[4] (left_speed 8) 0xFF; buf[5] left_speed 0xFF; buf[6] (right_speed 8) 0xFF; buf[7] right_speed 0xFF; uint8_t sum 0; for (int i 2; i 8; i) { sum buf[i]; } buf[8] sum; }Arduino 端用状态机方式接收避免半包问题。这一步非常关键我用简化代码说明接收逻辑bool parseFrame(uint8_t byte, int16_t* left, int16_t* right) { static uint8_t buf[16]; static uint8_t index 0; static bool gotHeader false; if (!gotHeader) { if (byte 0xAA) { buf[0] byte; index 1; gotHeader true; } return false; } buf[index] byte; if (index 2 byte ! 0x55) { gotHeader false; // 帧头校验失败重新等待 index 0; return false; } if (index 3 index (uint8_t)buf[2] 4) { // 收到完整一帧做校验和检查 uint8_t sum 0; for (uint8_t i 2; i index - 1; i) { sum buf[i]; } if (sum ! buf[index - 1]) { gotHeader false; index 0; return false; } *left ((int16_t)buf[4] 8) | buf[5]; *right ((int16_t)buf[6] 8) | buf[7]; gotHeader false; index 0; return true; } return false; }这里有个特别容易踩的坑不要用Serial.available()来判断“有没有收到完整的一帧”。串口数据是流式的一个字节一个字节到达available()只代表缓冲区里有数据不代表这些数据凑成了一条完整指令。正确做法就是上面这种状态机每个字节进来都跑一遍按照帧结构逐步推进收到足够长度再做校验和验证。3.3 速度分解差速底盘的线速度角速度到左右轮速ROS2 的/cmd_vel消息给出的是linear.x前进线速度单位 m/s和angular.z旋转角速度单位 rad/s。底盘节点必须把它们转换成左右轮的目标转速公式如下v_left v - ω * L / 2 v_right v ω * L / 2其中 L 是左右轮间距也就是轮距。算出来的v_left和v_right是左右轮的线速度m/s要发送给 Arduino 还需要除以车轮半径 r得到角速度rad/s再按实际需求换成 RPM 或 mm/s。举个例子轮距 L0.35m车轮半径 r0.0325m目标线速度 v0.2m/s目标角速度 ω0.5rad/s。v_left 0.2 - 0.5 * 0.35 / 2 0.1125 m/s v_right 0.2 0.5 * 0.35 / 2 0.2875 m/s左右轮线速度差距明显换算成角速度ω_left 0.1125 / 0.0325 ≈ 3.46 rad/s ω_right 0.2875 / 0.0325 ≈ 8.85 rad/s这个结果发到 Arduino 端就是当前指令周期内左右轮的“目标角速度”。Arduino 的 PID 会让实际轮速尽快收敛到这个值附近。我在代码里将单位统一为 mm/s 和 rad/s避免 m/s 与 mm/s 混用时出现“小车疯了”的诡异现场。4. Arduino 下位机实现电机驱动、编码器与闭环控制Arduino 端是整个底盘控制的心脏。它不关心什么是 ROS2、什么是 DDS只专心做三件事读编码器、跑 PID、输出 PWM。4.1 电机驱动接线TB6612 与 Arduino 的引脚分配TB6612FNG 是一个双路 H 桥电机驱动芯片每路电机用三个引脚控制PWMAPWM 速度、AIN1和AIN2方向控制。我用 Arduino Uno 的硬件 PWM 引脚做速度输出具体接线如下TB6612 引脚Arduino Uno 引脚说明PWMA5左电机 PWMAIN16左电机方向AIN27左电机方向PWMB9右电机 PWMBIN18右电机方向BIN210右电机方向VM电池正极电机电源2S 锂电池GND电池负极 / Arduino GND共地必须接VCCArduino 5V逻辑电源驱动逻辑很简单AIN1HIGH, AIN2LOW时左电机正转反过来反转两个都 LOW 或都 HIGH 时电机制动。PWM 占空比决定输出力矩PID 算完后通过analogWrite()写入。接线时有个容易忽略的点电机电源和控制逻辑电源必须共地。如果 TB6612 的 GND 和 Arduino 的 GND 不连在一起控制信号就没有参考电平电机要么不动、要么乱转。每次上电前我都习惯先拿万用表量一下 VM 和 VCC 是否正常再检查有没有做好共地这一下能省掉大量排查时间。4.2 编码器读取与轮速计算TT 马达上自带的霍尔编码器输出 A、B 两相信号。A、B 相位差 90 度通过判断 A、B 的电平组合和变化顺序可以同时知道转速和转向。这是正交解码的基本原理。我使用的 TT 马达参数是霍尔编码器每转输出 13 个脉冲13 PPR减速比 30:1。也就是说电机输出轴转一圈编码器会在电机输入端转 30 圈乘以 13 个脉冲再考虑 Arduino 做 4 倍频即 A、B 两相同时参与计数每个轮子每转一圈能产生13 x 30 x 4 1560 个计数脉冲在 Arduino 上用中断方式读取编码器。Uno 的引脚 2 和 3 支持外部中断attachInterrupt分别接左右轮编码器的 A 相。B 相接普通数字引脚在中断服务函数里读取 B 相电平判断正反转方向。编码器数据只是脉冲计数要转换成速度需要引入采样周期。我设置了 20ms 的定时中断每次采样时看计数变化量除以周期就得到当前轮速static volatile long left_count 0; static long left_count_last 0; float left_wheel_speed 0.0f; // 单位 rad/s void readLeftEncoder() { if (digitalRead(3) 0) left_count; else left_count--; } // 20ms 定时中断中执行 void updateSpeed() { long delta left_count - left_count_last; left_count_last left_count; // 弧长 脉冲数 / 每转脉冲数 * 2π left_wheel_speed (float)delta / 1560.0f * 2.0f * PI * 50.0f; // 乘以50 1/0.02s }这个思路要理解清楚单片机测速度本质上就是“数脉冲 除时间”。脉冲数除以每转脉冲数得到圈数圈数乘以 2π得到弧度再除以采样周期得到弧度每秒。4.3 增量式 PID 调速与参数整定开环控制是给多少 PWM 就以为轮子跑多快但电池电压会下降、地形阻力会变化、电机特性会有差异实际轮速和期望轮速总是有偏差。PID 闭环的目的就是实时测量实际轮速不断修正 PWM 输出让实际轮速逼近目标值。我使用的是增量式 PID因为它输出的是 PWM 的增量不会产生积分饱和问题代码实现也更简单。控制周期固定为 20ms计算公式Δu Kp * (e(k) - e(k-1)) Ki * e(k) Kd * (e(k) - 2*e(k-1) e(k-2))核心实现代码如下float pidUpdate(float target, float current, float* integral, float* prev_error) { float error target - current; *integral error; float output Kp * error Ki * (*integral) Kd * (error - *prev_error); *prev_error error; return output; }在实际底盘上我把 PWM 限制在 0~255并做了输出限幅防止 PID 运算结果超出analogWrite的范围导致电机全速失控。参数整定我踩过不少坑最终总结出一套适合 TT 马达的整定顺序先只加 PKp从 0.5 开始每次加 0.5观察轮速响应。如果轮速发散抖动说明 P 太大往回减如果始终差一截静差再引入 I。再加 IKi从 0.1 开始缓慢加直到静止状态下轮速能精确收敛到目标。I 太大会导致振荡响应变慢。Kd 多数情况下不需要TT 马达本身的机械阻尼已经够大加 D 反而会放大编码器噪声出现高频抖动。我调好后的一组典型参数是Kp2.5, Ki0.8, Kd0。不同底盘电机特性不同但整定思路是通用的先 P、再加 I、D 慎用。5. ROS2 上位机实现底盘驱动节点、URDF 与 rviz2 可视化底盘的“大脑”在 Arduino“五官”和“思考”在上位机。这一章讲清楚上位机要做的事订阅/cmd_vel、解算、发串口、收编码器反馈、发布/odom。5.1 把串口封装成一个 ROS2 底盘驱动节点ROS2 端的节点我选择用 C 编写rclcpp风格因为 C 的字节处理和性能更适合这类底层驱动节点。Python 也能写但字节数组处理和多线程串口读写的体验都不如 C 顺手。节点需要做几件事订阅/cmd_vel话题收到geometry_msgs/Twist消息后调运动学解算函数得到左右轮目标速度打包成协议帧并通过串口发送。开启一个串口读线程持续读取 Arduino 回报的编码器数据帧解析出实际左右轮速度。发布nav_msgs/Odometry消息包含从编码器推算的底盘位置和姿态。底盘节点核心代码逻辑较长这里只说几个关键点串口打开用 Linux 的标准方式需要处理波特率设置、数据位、停止位等参数。我直接用serial库或者通过/dev/ttyUSB0文件读写操作实现。不管是哪种都要注意打开串口之后一定要清空缓冲区否则上一次运行遗留的旧数据会被当成有效帧解析导致轮速飘忽。/cmd_vel订阅回调里做速度解算时记得把单位统一。我从geometry_msgs/Twist拿到的线速度单位是 m/s角速度单位是 rad/s算完之后发送给 Arduino 的左右轮速统一换算成 mm/s这样在协议帧里用一个int16_t就能表达-32768到32767mm/s 的范围完全够用。5.2 URDF 模型与 TF 树让 rviz2 里看到一个会动的底盘URDF 是 ROS2 里描述机器人几何和运动学关系的 XML 文件rviz2 靠它把抽象的机器人数据变成可视化模型。我在 URDF 里定义了四个关键坐标系base_footprint底盘在地面的投影点一般放在底盘中心的正下方地面上。base_link底盘刚体中心与base_footprint固定连接。left_wheel左轮中心。right_wheel右轮中心。URDF 里左右轮通过continuous关节连接到base_link车轮可以绕自己的转轴旋转这样 rviz2 里就能看到车轮随着编码器数据转动。把这个 URDF 加载进 rviz2 后底盘节点在启动时同时发布 TF 变换odom - base_footprint的位姿由里程计推算结果决定base_footprint - base_link - 车轮的变换是固定的。rviz2 里只要把 Fixed Frame 设为odom添加 RobotModel 显示组件就能看到底盘模型在地图中移动。5.3 没有实物也能先跑Gazebo 仿真验证算法链路热搜里很多人搜过“ros2 gazebo slam”、“ros2 launch fishbot_description gazebo.launch.py”说明大家都在用 Gazebo 做仿真。仿真和真实 Arduino 底盘是互补关系仿真的好处是环境可控、重复性好不需要担心电池没电和撞墙但仿真无法替代 Arduino 端的串口协议调试和 PID 整定因为仿真里的电机模型是理想化的。我的做法是先在 Gazebo 里用diff_drive_controller把/cmd_vel到/odom的整个算法链路验证一遍确认运动学、TF 树、里程计发布都没有问题再切换到真实底盘把diff_drive_controller的输出接到我的串口驱动节点上。这样每次只引入一个新变量问题定位会非常快。很多人在仿真里跑得好好的一接真车就懵就是因为一次引入了“真车机械 串口协议 PID”三个新变量出问题根本无从排查。5.4 手动发指令测试先动起来再说底盘节点和 Arduino 都烧好程序后用一条命令测试整个链路ros2 topic pub /cmd_vel geometry_msgs/Twist {linear: {x: 0.1}, angular: {z: 0.0}} --once正常情况下Arduino 串口监视器里会持续打印解析到的左右轮目标速度和编码器实测速度两个值应该逐渐接近。rviz2 里可以看到底盘模型向前移动。如果小车不动优先检查三点串口设备权限ls -l /dev/ttyUSB0查看当前用户是否有读写权限。Arduino 串口监视器是否被占用串口是独占设备监视器开着ROS2 节点就打不开。电机接线和 TB6612 供电是否正常先给电机直接通 PWM确认驱动板没问题再谈上层。6. 联调与踩坑串口权限、数据丢帧、PID 超调这些坎儿这一章全部来自我自己的惨痛经历每条都能让人折腾一下午。6.1 串口权限与节点启动顺序第一次启动底盘节点大概率会报Permission denied打开不了串口。这是因为当前用户不在dialout用户组里。解决办法sudo usermod -aG dialout $USER改完需要注销重新登录。更规范的做法是写一条 udev 规则把 Arduino 映射成固定的设备名并授予权限这样以后插拔 USB 不会出现设备节点从ttyUSB0变成ttyUSB1的尴尬。echo KERNELttyUSB*, ATTRS{idVendor}2341, MODE0666 | sudo tee /etc/udev/rules.d/99-arduino.rules sudo udevadm control --reload-rules这里idVendor需要根据你的 Arduino 实际 VID 修改用lsusb命令可以查到。改完后插拔一次 USB 就能生效。节点启动顺序也有讲究。底盘节点在启动时会清空串口缓冲区然后等待数据。建议先启动节点再给 Arduino 上电复位这样能保证 Arduino 发出的第一帧数据被节点稳定接收。如果顺序反过来有时首帧数据会卡在缓冲区里导致里程计初始位置错乱。6.2 数据丢帧状态机和二进制协议是正解我刚把串口协议从字符串改成二进制帧之前丢帧问题非常严重。具体表现是小车加速时一顿一顿数据仿佛“漏气”轮速偶尔跳到零再弹回来。排查过程是逐步排除的先说结论直接导致丢帧的原因是字符串解析的不可靠性——字符串命令长度不固定Serial.readStringUntil(\n)这种方式在数据流中断时会把半截字符串当成完整指令解析而解析失败后没有同步机制后续所有数据全部错位。换二进制状态机解析后情况好了很多但还是偶尔丢帧。进一步排查发现问题出在 Arduino 端串口缓冲区只留了 64 字节ROS2 端以 20Hz 频率发送每帧 9 字节再加上 Arduino 自身定时中断里的串口打印开销偶尔会溢出。解决办法是把调试信息从默认串口挪到软串口SoftwareSerial或者直接注释掉让硬件串口专注通信业务。量产阶段我干脆把调试打印全部关闭只保留解析结果红灯闪烁表示正常。6.3 电池供电的两大坑电压跌落和地线干扰电池供电最典型的故障是小车一启动电机Arduino 就重启。原因是电机启动瞬间电流很大电池电压被拉低到 Arduino 的复位阈值以下。解决办法是让电机和控制电路分开供电电机直接吃 7.4V 电池电压Arduino 通过一个 5V 降压模块单独供电两者只在 TB6612 的 GND 处共地。这样电机的大电流波动不会直接影响 Arduino 的电源轨。地线干扰是另一个隐蔽问题。底盘四根电机线都是大电流通路如果编码器信号线和电机线平行走线编码器脉冲信号会被电机的 PWM 噪声污染导致计数错误。我的处理是编码器信号线全部用双绞线或者杜邦线最短路径走线与电机线拉开距离电机线靠近电池端加一个 104 陶瓷电容滤波实测编码器读数瞬间干净了很多。6.4 小车跑偏编码器数据和 PID 的交互作用底盘跑直线跑偏去查 PID 参数之前先确认编码器数据准不准。我自己踩过一个坑左轮编码器齿轮在金属半轴上的紧固螺丝没拧紧高速转动时打滑导致左轮实测速度比实际低PID 为了让“虚低”的轮速追平目标实际给了左轮更多 PWM小车明显向右偏。换了编码器固定螺丝并重新标定后跑偏问题立刻消失。另一个跑偏原因是左右轮的 PID 参数不同。两个电机虽然型号一样但机械阻尼不可能完全一致可以给左右轮分别设一组参数。我在代码里用kp_left/ki_left、kp_right/ki_right分开存储微调后左右轮的阶跃响应曲线接近一致直线稳定性明显提升。跑偏排查顺序应该是机械编码器/齿轮是否打滑→ 编码器数据用手转轮子看读数是否符合→ PID左右轮分开调→ 运动学参数准确的轮距和轮径。为了让里程计更准我对轮径和轮距做了标定。方法是让底盘在地面上直线走固定距离比如 2 米通过编码器累计值和实际距离反推修正轮径actual_wheel_diameter commanded_wheel_diameter * actual_distance / odom_distance轮距标定则让底盘原地旋转 90 度观察 odom 里的 yaw 变化反复调整L值。这一步看似枯燥但对后续接 Nav2 导航至关重要因为导航的路径规划完全依赖里程计反馈的位姿。7. 从“能动”到“能用”里程计、串口共享与后续扩展方向底盘能跑只是第一步要做成“能干活”的机器人还需要把里程计做准、把仿真准备好、把系统结构理清。7.1 里程计发布编码器累计与航迹推算Arduino 回报左右轮速后底盘节点通过普通航迹推算dead reckoning发布里程计。核心公式Δx (v_r v_l) / 2 * Δt * cos(θ) Δy (v_r v_l) / 2 * Δt * sin(θ) Δθ (v_r - v_l) / L * Δt这里v_l和v_r是左右轮线速度Δt是两次里程计更新之间的时间差L是轮距。虽然这套公式在打滑和长距离漂移时误差累积但在室内平整地面上短时间使用已经足够。我调试时让小车沿着客厅墙边走一圈回到原点时的位置误差在 10cm 以内完全可以支持后续避障和简单导航功能。nav_msgs/Odometry消息发布时要注意同时发布odom - base_footprint的 TF 变换否则 rviz2 里模型不会移动。很多新手只发布了 Odometry 话题但没发布 TFrviz2 里底盘纹丝不动查半天查不到原因。7.2 串口带宽规划速度回报和其他传感器数据怎么共用Arduino 只有一个硬件串口既要用它收指令又要回报传感器数据必须规划好通信带宽。115200 波特率下每秒大约传 11.5KB 数据我的速度指令帧 9 字节 × 20Hz 180 B/s编码器回报帧 11 字节 × 50Hz 550 B/s占用不到 7%非常宽裕。如果你还想回报超声波传感器数据或者 IMU 数据可以新增指令 ID比如0x02表示传感器数据。注意一个原则上行数据和下行数据用同一个串口时方向取决于帧的 ID 和内容不要用“谁先发”来区分否则时序错乱会非常难排查。如果你想扩展舵机云台热搜里有人问“arduino控制舵机”可以新增0x03指令 ID数据区长度为 2 字节表示舵机角度。超声波传感器则可以挂在一个独立的引脚上由 Arduino 自行扫描把结果周期性回报给 ROS2 节点。这些扩展都不需要改动核心协议结构只是在指令 ID 和数据长度上做加法。7.3 从底盘到导航Nav2 对接的基本思路做完底盘下一步自然是接 Nav2 做自主导航。Nav2 需要三个输入里程计已经有了、激光雷达或深度相机、机器人模型和 TF 树。只要这三个条件满足Nav2 基本不需要做太多修改就能跑起来。如果你用的是激光雷达先跑通slam_toolbox建图再加载地图跑 Nav2。需要注意的是 Nav2 的局部代价地图对底盘响应速度敏感底盘 PID 调得越跟手导航效果越顺。我在实际测试中体会很明显PID 没调好的时候导航指令到达后车身晃两下再动代价地图就会误判障碍物位置规划路径歪七扭八。我的扩展路线是先加一个单线激光雷达做建图和避障再考虑加 IMU 修正里程计在旋转时的漂移。如果你想让小车更“聪明”一点也可以用热搜里提到的“rviz2 八叉树地图导航”不过那是另一个话题了先把底盘这一个项目吃透再说。做完这个 Arduino 底盘的整套流程我最大的体会是机器人项目里最难的部分往往不是某个新框架而是把一堆不同层次的技术缝合起来。ROS2 负责逻辑和生态Arduino 负责实时和硬件两者之间的接口足够简洁、足够明确整个系统才会稳定。先别急着追求高级功能把串口协议写稳、把 PID 调顺、把里程计标准这些基础工作做好了后面接什么传感器、跑什么算法都不会太费劲。
返回列表