
做机器人底盘很多人第一反应是上工控机直接怼驱动板。这个思路不能说错但真放到平时项目里折腾一圈你会发现一个很尴尬的问题工控机上跑导航、感知已经累得够呛还得去处理电机换向、编码器中断、PWM输出这种毫秒级甚至微秒级的硬实时任务随便来个进程调度卡顿电机就跟着抽搐给你看。所以现在主流的做法也是我这套项目里实际验证过的方案就是把整个系统拆成两层上层用ROS2负责决策、感知、导航下层用Arduino或者ESP32这类MCU专门干底层的电机控制和里程计采集两层之间通过串口或者micro-ROS通信。这篇就把这个底盘的完整实现思路、关键代码、调参过程和踩过的坑全部梳理一遍。这篇文章适合三类人一是刚开始学ROS2想把手上的Arduino小车变成一个真正可以被ROS2驱动的机器人底盘的人二是已经在用树莓派/工控机跑ROS2但电机控制一直不稳定的朋友三是纯粹想搞明白“上位机和下位机到底怎么配合”的硬件爱好者。我会从方案选型讲到底盘运动学、再到micro-ROS和自定义串口协议最后到RViz2里看到实时里程计整个链路都是我在实车上跑通过的。1. 方案架构与选型解析为什么把任务拆给两层1.1 实时性与通用性的平衡机器人底盘的核心矛盾是控制要实时算法要灵活。电机换向、PWM波动、编码器计数这些事情要求确定性的响应时间你不能说系统忙起来就晚个几十毫秒才响应一次中断那轮子转速会忽快忽慢里程计数据全是毛刺。而ROS2的节点调度依赖操作系统的进程管理和DDS通信哪怕是做了实时内核优化也很难保证微秒级的中断响应。反过来Arduino这类MCU是裸机或者轻量RTOS环境中断响应是硬件级的几十微秒的延迟完全可接受。把这个最硬实时的部分放到MCU上ROS2这边只管发目标速度、收里程计整个系统的稳定性会好非常多。这个思路和工业界常用的“运动控制器上位机”架构是同一个逻辑只是我们用开源方案把它做便宜了、做轻了。1.2 底盘形态与驱动方式对比做底盘之前先想清楚要什么运动模型。我做的是两轮差速底盘前边一个万向轮做支撑成本低、控制简单、室内平地场景足够用。但这个选型不是唯一的得看你后续要跑什么场景。底盘形态运动能力控制复杂度适用场景两轮差速前后、旋转不能横移低适合入门室内平地、SLAM、巡线四轮差速前后、旋转转弯半径大低但轮胎打滑影响里程计户外粗糙地面、大负载麦克纳姆轮全向平移旋转中解算稍复杂狭小空间、全向搬运舵轮/阿克曼类似汽车转向中高户外移动、竞速类我做的是两轮差速一方面是因为大多数新手第一个小车就是这种结构另一方面是后续要讲到的运动学解算和里程计积分两轮差速是个特别好的教学载体。如果你做四轮差速把一对轮子的速度并成一路就行驱动代码几乎不用改。1.3 主控板选择Uno还是ESP32还是STM32这是很多人卡住的第一关。只说结论如果你打算用micro-ROS直接让MCU作为ROS2节点别用Arduino Uno老老实实上ESP32或者STM32如果你用自定义串口协议做桥接那Uno也完全能跑。原因很简单micro-ROS在MCU上跑需要一个微型的DDS客户端光是通信栈和节点管理就要占掉不少内存。AVR架构的Uno只有2KB SRAM我在实验的时候连最基本的micro-ROS示例都很难稳定跑起来经常出现内存不足导致的不定时重启。而ESP32有320KB SRAM跑micro-ROS加电机控制加编码器读取绰绰有余还自带WiFi和蓝牙可以直接用UDP方式跟ROS2通信省一条USB线。我最终的配置是ESP32做下位机跑电机驱动、编码器采集、速度闭环和协议解析上位机用一台装了ROS2的迷你主机负责跑SLAM和导航。如果你手头只有Uno也不是不能用后面我会给一套精简的串口协议方案Uno只做速度执行器它没有余力去跑复杂运算但做好本分工作没问题。2. 下位机固件开发电机驱动、编码器与运动学解算2.1 电机驱动接线与PWM控制电机选型我用的是带霍尔编码器的直流减速电机减速比1:30左右单轴输出扭矩在平地推个小车足够。电机驱动板用的TB6612比L298N好用太多了体积小、压降小、发热低而且逻辑输入可以直接接3.3VESP32的GPIO不需要电平转换。L298N那个东西动不动就1.4V压降电池稍微弱一点底盘就表现得很肉。TB6612的接线非常简单每路电机两个方向控制引脚AIN1/AIN2一个PWM速度引脚PWMA对应ESP32的GPIO输出。控制逻辑是AIN1HIGH, AIN2LOW 时电机正转AIN1LOW, AIN2HIGH 时电机反转如果两个方向脚相同电机刹停PWM我用的是LEDCESP32的硬件PWM外设频率设成20kHz可以明显降低电机啸叫。默认的500Hz左右PWM在驱动电机时会发出很刺耳的噪声而且电流纹波也大我实测20kHz下同样占空比电机转速更平滑。下面是ESP32的PWM初始化部分#define PWM_FREQ 20000 #define PWM_RES 8 // 8位分辨率占空比0-255 void motorInit() { ledcSetup(0, PWM_FREQ, PWM_RES); // 左轮PWM通道 ledcSetup(1, PWM_FREQ, PWM_RES); // 右轮PWM通道 ledcAttachPin(LEFT_PWM_PIN, 0); ledcAttachPin(RIGHT_PWM_PIN, 1); } void motorSetSpeed(int leftPwm, int rightPwm) { // 正负值控制方向取绝对值输出PWM digitalWrite(LEFT_AIN1, leftPwm 0 ? HIGH : LOW); digitalWrite(LEFT_AIN2, leftPwm 0 ? LOW : HIGH); digitalWrite(RIGHT_AIN1, rightPwm 0 ? HIGH : LOW); digitalWrite(RIGHT_AIN2, rightPwm 0 ? LOW : HIGH); ledcWrite(0, abs(leftPwm)); ledcWrite(1, abs(rightPwm)); }注意电机启动瞬间电流很大PWM占空比从0直接跳到200这种突变很可能把电源电压拉垮导致MCU重启。我后来在电源管理上吃了不少亏这个后面专门讲。2.2 编码器测量从脉冲数到轮速带霍尔编码器的直流减速电机通常电机轴上有个磁环两个霍尔传感器输出A、B两路相位差90°的方波信号。通过A、B相的关系不仅能测转速还能判断方向这就是所谓的正交解码。我的电机减速比是1:30电机轴端编码器每转输出20个脉冲单相。那么轮子转一圈单相计数得到的脉冲数是20×30600。如果使用ESP32的PCNT脉冲计数外设做正交解码还能4倍频也就是一轮2400个计数。4倍频的好处是低速时分辨率更高速度环的数据不那么抖。轮速计算的核心就是一个比例换算。设采样周期为T秒采样周期内计数增量为N那么轮子转速转/分就是rpm N / (脉冲数每圈 × T / 60)如果采样周期是50ms一轮600脉冲那么测到30个脉冲对应的转速就是rpm 30 / (600 × 0.05 / 60) 60 rpm再用rpm换算成线速度v 2π × rpm / 60 × rr是轮子半径。我的轮子直径65mm半径0.0325m那么60rpm对应的线速度大约是0.204m/s一个典型的中速移动速度。实现上最重要的是用硬件外设计数不要在主循环里做电平轮询。ESP32的PCNT外设可以自动计数我每50ms读一次并清零非常省CPU。Uno的话就用中断计数两个轮子各接一个中断引脚每次中断对脉冲数加一注意不要在中断里做耗时操作。2.3 差速底盘的正逆运动学解算差速底盘的魅力在于只需要给左右两轮的线速度就能合成出底盘整体的线速度和角速度。反过来ROS2下发的/cmd_vel消息里包含的是机器人坐标系的线速度vx和角速度wz要通过逆运动学换算成左右轮目标速度。假设轮距为b左右轮中心之间的距离轮半径为r两个轮子的线速度分别为vL、vR底盘整体的线速度为v角速度为ω逆时针为正那么正运动学公式是v (vL vR) / 2 ω (vR - vL) / b逆运动学公式则是vL v - ω × b / 2 vR v ω × b / 2举个例子我的车实测轮距0.16m。如果ROS2发来一个0.2m/s的线速度和0.5rad/s的角速度那么左右轮的目标线速度分别是vL 0.2 - 0.5 × 0.16 / 2 0.2 - 0.04 0.16 m/s vR 0.2 0.5 × 0.16 / 2 0.2 0.04 0.24 m/s明显看出右轮快、左轮慢底盘向右旋转的同时向前跑正好符合角速度为正的定义。这套公式直接写在MCU固件里每次收到目标速度就解算成左右轮目标线速度再通过轮径换算成目标RPM进入速度环。2.4 PID速度闭环从调参到跑起来开环PWM控制只有在空载平地上勉强能用负载一变、电池电压一掉转速就飘了。要做真正的机器人底盘必须给每个轮子加一个速度闭环。我用的是增量式PID采样周期50ms控制量是PWM占空比。// 增量式PID返回PWM增量 float pidUpdate(float target, float current, float integral, float lastError) { float error target - current; integral error; // 积分限幅防止积分饱和 if (integral INTEGRAL_LIMIT) integral INTEGRAL_LIMIT; if (integral -INTEGRAL_LIMIT) integral -INTEGRAL_LIMIT; float output KP * error KI * integral KD * (error - lastError); lastError error; return output; }调参顺序我记得特别清楚先只给P从很小开始加让轮子响应变快但不要持续振荡然后加I消除因为摩擦力、坡道产生的稳态误差最后如果有超调和振荡再给一点D。但D这个东西要小心编码器数据稍微有噪声D一大就把噪声放大成PWM抖动电机嗡嗡响。所以我最后把D设得很小甚至有时候直接为0靠P和I已经能跑得不错。还有一个特别实用的技巧是死区补偿。直流减速电机存在静摩擦PWM占空比低于某个阈值时根本不会转导致目标速度从小变大的时候响应慢半拍。我在PID输出后面叠加了一个固定偏置比如deadzone30实际输出是pid_output sign(pid_output)×30这样能明显改善启动响应。PID调好之后用手捏轮子能感到明显的抵抗放开后轮子能稳定回到目标转速。这个时候底盘才算达到了“可以被上层控制”的基本素质。3. ROS2与Arduino通信方案micro-ROS与自定义串口协议3.1 两种方案怎么选底盘固件写完之后接下来是最关键的一步怎么让ESP32和ROS2通讯。目前主流的做法就两种。一种是micro-ROS直接在MCU上面跑一个微型的ROS2节点ESP32可以订阅/cmd_vel也能发布/odom一切都像在PC上写ROS2节点一样自然。另一种是自定义串口协议桥接MCU只通过串口收发字节流PC上跑一个Python或者C的串口桥接节点负责把串口数据转换成ROS2话题。我个人的建议是如果主控是ESP32优先上micro-ROS因为它生态成熟、调试方便如果主控是Uno或者你想彻底搞懂通信协议是怎么设计的那就老老实实写自定义串口协议。3.2 micro-ROS实战Agent与固件配置micro-ROS的架构是Client-Server模式ESP32上是micro-ROS Client也就是你的固件PC上跑一个micro-ROS Agent负责把DDS数据桥接到串口或者UDP。先启动Agent再让MCU连上来。我用的环境是Ubuntu 22.04 ROS2 Humble启动Agent最方便的方式是Dockerdocker run -it --rm --nethost ros:humble \ ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -b 115200如果连不上优先检查两件事串口号是否正确、当前用户是否在dialout组里。权限问题我用一句话解决sudo usermod -aG dialout $USER然后重新登录终端不然每次都要sudo跑Agent。固件这边在Arduino IDE的库管理器里搜micro_ros_arduino注意要选支持你板子的版本。ESP32的micro-ROS初始化非常简单#include micro_ros_arduino.h #include rcl/rcl.h #include geometry_msgs/msg/twist.h #include nav_msgs/msg/odometry.h rcl_node_t node; rcl_subscription_t sub; rclc_support_t support; rclc_executor_t executor; void setup() { // 通过串口连接Agent set_microros_serial_transports(Serial); delay(2000); rclc_support_init(support, 0, NULL, allocator); rclc_node_init_default(node, chassis_node, , support); rclc_subscription_init_default(sub, node, ROSIDL_GET_MSG_TYPE_SUPPORT(geometry_msgs, msg, Twist), /cmd_vel); rclc_executor_init(executor, support, 1, allocator); rclc_executor_add_subscription(executor, sub, twist_msg, twistCallback, ON_NEW_DATA); } void loop() { rclc_executor_spin_some(executor, RCL_MS_TO_NS(10)); // 每次循环处理运动学解算和PID updateChassis(); }这里一个关键点是micro-ROS的Executor不是实时任务调度器所以你不能让控制逻辑完全依赖它。我的做法是loop循环里同时跑Executor和控制逻辑控制逻辑按照自己的定时器节奏执行Executor只是负责收消息和发消息。实测下来ESP32通过115200波特率串口连Agent/cmd_vel消息的延迟在几个毫秒以内完全够底盘控制用。如果你用WiFi的UDP方式还能彻底摆脱USB线但前提是ESP32和PC在同一网段而且网络得稳不然消息丢失率高电机容易抖。3.3 轻量串口协议设计如果你的主控是Uno那micro-ROS就别想了直接走自定义协议。但即便主控够用我也建议你懂一套协议怎么设计因为这个思路能迁移到任何嵌入式设备上。我的协议帧格式是帧头0xAA 0x55 | 数据长度 | 命令字 | 数据区 | 校验字节(累加和)比如下发线速度0.2m/s、角速度0.5rad/s// 左轮16位有符号速度右轮16位有符号速度单位mm/s // 帧头2字节 长度1字节 命令字1字节 数据4字节 校验1字节 9字节 byte frame[9] {0xAA, 0x55, 0x04, 0x01, highByte(left), lowByte(left), highByte(right), lowByte(right), checksum};长度字段指命令字数据区的长度校验是前面所有字节的累加和低位。MCU端解析用一个有限状态机按字节匹配帧头状态依次切换找帧头AA → 找帧头55 → 读长度 → 读命令字和数据 → 读校验 → 验帧执行。这个状态机的代码也就几十行但是比用Serial.readString这种阻塞式解析稳得多因为串口数据什么时候来、来多少都是不确定的。PC端对应写一个桥接节点订阅/cmd_vel把Twist通过差速逆解算成左右轮速度组帧发串口。反向的里程计、电池电压也是同样的帧格式只是命令字不同。这套协议的好处是每条消息大小固定、解析快、出错能及时发现Uno的串口缓冲区只有64字节但9字节一帧十几条攒下来也完全放得下。4. 上位机集成URDF、TF树与RViz2可视化调试4.1 底盘URDF模型与TF树的搭建ROS2里的机器人描述文件我建议直接用URDF或者Xacro不要用之前ROS1里那套太老的方式。一个两轮差速底盘的URDF至少包含三个linkbase_link、left_wheel、right_wheel以及两个continuous类型的joint。写URDF的时候要特别注意坐标系朝向。base_link的X轴定义为机器人的前进方向Z轴向上这是ROS的REP-103规范。轮子joint的Z轴指向轮子旋转轴也就是垂直于车体侧面。之前我有一次把joint轴的方向写反了结果RViz2里模型转的方向跟实际底盘完全反着的排查了半天。TF树的发布我直接用robot_state_publisher这个现成节点它读取URDF里的joint定义发布base_link到左右轮子的TF。这里有个容易漏的地方如果你用Gazebo可能会有额外的odom到base_link的变换这个变换应该由里程计节点发布不要重复用robot_state_publisher发。用Xacro写的好处是可以定义宏比如轮距、轮径这种参数只写一遍后面调整底盘尺寸时不用满文件找数字。我把轮距0.16m、轮径0.065m定义成了变量改起来非常方便。4.2 RViz2里验证模型和里程计底盘的灵魂是TF而RViz2是看TF最直观的工具。正常启动之后RViz2里应该能看到urdf定义的机器人模型、从odom到base_link再到左右轮子的TF连线以及odom话题里的里程计箭头。RViz2显示里程计很容易添加Odometry显示选好话题然后把颜色调成亮绿色箭头的长度和颜色能直接反映机器人的位移和朝向。我把底盘放在地上用手推着往前走半米屏幕上的箭头也跟着移动半米那一刻是真的有成就感。如果发现模型位置和实际不一致优先检查TF和里程计的坐标系是否对齐。我在实际中遇到的坑是URDF里轮子joint的origin设置和实际装配尺寸差了一点点导致轮子看起来悬浮在地面上。URDF里轮子的origin是相对base_link的必须加上轮子半径。比如base_link底面高度0.065m轮子半径0.0325m那轮子中心的Z应该是0.0325m不是0。4.3 ROS2与Python桥接节点的框架示例这里给出PC端桥接节点的核心框架用的是rclpy订阅/cmd_vel话题然后通过串口下发控制指令。串口通信我用pyserial波特率115200超时设成0.1秒避免阻塞。import serial import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist class ChassisBridge(Node): def __init__(self): super().__init__(chassis_bridge) self.serial_port serial.Serial(/dev/ttyUSB0, 115200, timeout0.1) self.sub self.create_subscription(Twist, /cmd_vel, self.cmd_callback, 10) self.timer self.create_timer(0.1, self.check_serial) def cmd_callback(self, msg): left, right self.inverse_kinematics(msg.linear.x, msg.angular.z) frame self.pack_frame(left, right) self.serial_port.write(frame) self.get_logger().info(fsend: left{left}, right{right}) def inverse_kinematics(self, v, omega): wheel_base 0.16 wheel_radius 0.0325 target_left (v - omega * wheel_base / 2) / wheel_radius target_right (v omega * wheel_base / 2) / wheel_radius return target_left * 60 / (2 * 3.1415926), target_right * 60 / (2 * 3.1415926)这段代码里有两个容易踩的细节。第一串口写操作本身是阻塞的如果串口拥堵或者MCU断电write会卡住节点导致/cmd_vel积压所以最好启动之前先确认串口是真的通的。第二逆解算出的左右轮速度单位是m/s但固件里约定的是RPM所以转了60/(2π)。我在联调时因为漏了这个换算发0.2m/s实际跑出了十几倍的速度差点把小车撞墙上。4.4 用teleop_twist_keyboard遥控验证底盘运动学解算对不对先不用急着跑SLAM最简单的方法是拿一个键盘遥控器节点手动发/cmd_vel。ROS2里直接用teleop_twist_keyboardros2 run teleop_twist_keyboard teleop_twist_keyboard按键控制的时候i键是前进逗号是后退j和l是左右转k是停止。我每次调完PID底盘都是先按一个i看车是否走直线。这里有个关键的判断方法走直线实际上极其考验两轮的一致性如果左右轮速度闭环参数不一致车会往一边偏。我最初的PID参数左右轮一模一样但装配时两边的负载不一样导致实际速度有微小偏差长距离走久了就歪了后来我不得不给两个轮子分别标定。键盘遥控能顺畅跑起来之后再上RVIZ2和里程计同时显示就能直观看到遥控转向时TF的旋转和箭头方向的对应关系。4.5 用Gazebo仿真做预验证如果你还没有实体底盘或者每次调试都要蹲在地上看轮子转太累了建议先用Gazebo把整个逻辑跑通。我用的方式是把同一个URDF文件作为Gazebo模型加载给两个轮子装上驱动插件然后让ROS2直接发/cmd_vel到仿真底盘照常通过teleop控制。Gazebo的好处是可以提前验证URDF模型、底盘运动学解算和TF发布逻辑是否正确坏处是仿真里的PID和现实完全不一样所以仿真通过不等于实车通过。我通常把仿真当成一个“语法检查”加上“逻辑检查”帮我把低级错误挡在实车之前。等实车出问题的时候大概率就是机械、电路和真实PID的事了。5. 实际问题排查与调优经验5.1 串口通信故障排查这大概是每个做ROS2MCU项目的人都会撞上的墙。我遇到过几类典型问题整理成一个速查表现象可能原因排查思路Agent启动报Permission denied当前用户不在dialout组用usermod加组后重启终端串口打开了但收不到数据波特率不匹配Agent和MCU都改成115200或确认两者一致MCU重启导致串口断开电源不足检查供电参考5.4的电源处理Agent连上又断开micro-ROS创建内存不足换ESP32或者精简创建的话题数量排查串口问题我有个固定套路先关掉Agent在PC上直接用minicom或python打开串口手动发一帧数据看看MCU回不回。如果这个环节都不通就不可能是micro-ROS的问题先解决物理链路。如果MCU回了再接Agent。分层排查能快速缩小范围别一上来就在Agent日志里瞎猜。5.2 编码器数据跳变编码器数据跳变的表现是车停着不动里程计却自己在漂。我在实车调试中踩到的主要坑有三个。第一是接线接触不良和干扰。霍尔编码器的信号线很长的话很容易受到电机PWM电流的电磁干扰尤其在我最开始用的L298N那个电路上PWM一拉高编码器数据就乱跳。解决办法是把编码器信号线远离电机电源线能加屏蔽更好另外信号线尽量短粗不要和电机线扎在一起。第二是方向判断错。霍尔编码器A、B相如果接反了方向解算就反了表现出来是轮子正转时里程计是负的。PCNT外设的解码模式可以配置或者干脆交换两根信号线试一下就能确认。第三是采样周期不稳。如果是用delay循环采样期间有别的耗时操作会把采样间隔拉长导致计算出的转速偏低。解决方式是改成固定频率定时器中断到时间就采样一次不能依赖主循环的节奏。还有一个细节编码器计数是有上限的ESP32的PCNT计数器是16位的如果采样周期太长高速时可能溢出。我采50ms在最高速也没有溢出过但如果你把采样周期放到1秒就得注意这个溢出问题。5.3 PID调试的典型症状PID调参是很多人都头大的地方我把症状和调参方向整理一下。症状一轮子持续振荡PWM忽大忽小。一般是P太大了或者编码器数据噪声被D放大。先降P再降D如果还振荡检查编码器数据是不是真的干净。症状二低速时抖高速时稳。往往是死区补偿没做好PWM输出在启动阈值附近反复横跳。把死区补偿加回来并且在目标速度低于某个值时用开环启动等速度上来了再切闭环。症状三跑起来之后稳态误差一直存在。比如目标60rpm实际只有55rpm这是摩擦或坡道造成的光靠P压不下去加大KI同时注意积分限幅别无限积分。我一般把积分限幅设成PWM满量程的20%左右再多就会过冲刹车反应也慢。症状四响应慢推一下轮子半天才回来。P太小或者采样周期太长。我的经验是50ms采样周期对入门小车够用但如果你想让底盘更跟手可以缩到20msPID代码的执行开销在ESP32上完全不是问题。调PID最忌讳的是两三个参数同时动。我后来养成一个习惯每次只调一个参数调完记录下当时的波形和数据跑一段路看效果。别看“差不多”就行机器人的重复性就靠这些细节堆出来的。5.4 电源纹波与MCU重启这是很多底盘的隐形杀手。电机启动瞬间电流能达到正常工作电流的好几倍如果控制板和电机共用一块电池电机一启动电压跌落就会导致MCU复位。我排查过的最诡异的一次是车一加速就重启怀疑是代码死循环查了半天都没有最后用示波器一看电机启动瞬间VCC直接被拉到3V以下ESP32当然复位了。解决方式我实测有效的是三个方案并行电源分区电机用单独的电池或电源控制板用另一个电源两边的地线必须连在一起否则编码器信号会乱。在MCU的5V或3.3V电源入口并联一个大电容我用了470uF电解电容板子上的5V口如果设计上允许可以加在Vin附近。电机供电线上加一个1000uF的电容同时给电机并联一个续流二极管能有效抑制反电动势的冲击。有热词提到“arduino在5v端口接电容”说的就是这个问题。很多时候MCU系统的不稳定根本不是程序的问题是供电的问题。5.5 没有硬件时用Wokwi仿真验证逻辑如果你在等快递、或者只是想先跑通控制逻辑Wokwi这个在线仿真平台值得用一下。它支持Arduino Uno、ESP32等常见板子而且可以添加电机、编码器、舵机这些元件直接在浏览器里跑Arduino代码。我自己的使用心得是Wokwi验证不了真实的PID效果也模拟不了霍尔编码器的电气噪声和机械摩擦但用来验证串口协议解析、状态机逻辑、运动学换算这些纯代码层面的东西特别方便。比如你把自定义串口协议写好了想快速确认帧头检测、校验计算对不对直接在Wokwi里放一个虚拟串口仿真跑一遍逻辑比烧录到板子上再连示波器快得多。而且Wokwi还支持模拟外设的printf输出调试协议解析的时候可以直接打印每一帧解析出来的速度值代码写起来体验非常接近用开发板加串口监视器。如果是刚入门Arduino的新手还没买齐硬件先拿Wokwi把电机控制和串口协议跑通了再上手实体硬件就不会那么手忙脚乱。6. 联调与整机验证流程总结把下位机固件、通信、上位机都准备好之后我的联调流程是固定的每一步都验证到位再往下走不要一次把所有环节接起来。第一步单独验证下位机。不启动ROS2用串口监视器直接给ESP32发命令帧观察左右轮转速是不是符合预期。可以用固定PWM开环测试也可以直接发目标RPM验证PID闭环。第二步验证桥接节点。在PC上运行Python桥接脚本用一个简单的定时器往/cmd_vel里发固定的线速度看ESP32是否收到并执行。这一层通了说明通信链路没问题。第三步接入teleop和RViz2。启动teleop_twist_keyboard手动按键控制底盘同时在RViz2里观察底盘模型、TF和里程计。这一步验证整个TF树和里程计发布是否正常。第四步长距离直线测试。让底盘以固定速度前进5到10米看终点位置和里程计累计的距离是否匹配偏差控制在5%以内我认为是可以接受的。如果偏差明显先检查轮径参数设置是否准确再检查编码器每转脉冲数是否算对了。第五步转弯测试。手动下发一个纯角速度观察底盘是否在原地旋转并且旋转角度和里程计积分是否一致。两轮差速底盘如果轮距参数写错旋转测试会非常明显转90度实际转了80度或者100度就能发现问题。这个流程走下来底盘基本就具备了后续做SLAM、导航的基本条件。至于后续要不要接激光雷达、深度相机、IMU那就是在这个底盘平台上叠加的事了。最后分享一个我自己项目里的小技巧底盘调试的时候一定要在代码里加一个看门狗。如果上位机超过一定时间比如500ms没有收到新的/cmd_vel指令底盘应该自动刹停而不是保持最后一帧的速度一直跑下去。我最初调试的时候遥控器掉线过一次底盘直接朝着墙冲了过去幸亏速度不快。加上这个看门狗机制之后不管上位机是崩溃了还是通信断开了底盘都会停在原地安全性提升一大截。家用机器人、教学机器人第一原则始终是“不知道该怎么动的时候就别动”。这个底盘的方案做完之后我已经在它上面跑了简单的SLAM建图、路径点导航效果虽然谈不上惊艳但整个系统的稳定性和可控性是真的稳。如果你也想做机器人底盘我建议不要一上来就追求复杂的功能先把底盘的运动控制做扎实把通信链路跑通把里程计数据弄准后面加再多的上层算法都有底气。