
做小车底盘最绕不开的就是“先让轮子转起来、再知道轮子转多快”这两件事。这篇是《基于STM32底盘控制与ROS上层导航小车制作》的第一节我直接从STM32电机驱动与编码器反馈这块讲起。内容会覆盖电机驱动模块怎么选、PWM调速怎么做、H桥原理怎么理解、编码器怎么读数、最后还要把速度闭环这一个底盘控制里最核心的环节串起来。适合手里有一块STM32开发板、想自己动手把机器人底盘跑起来的朋友也适合正在做毕设、做竞赛小车、或者准备入门ROS机器人开发但卡在底层控制这一步的人。我最早做小车底盘的时候踩过最大的坑就是把所有精力都放在上层导航算法上结果轮子都走不直。后来才明白一个道理ROS导航再牛底层底盘如果响应慢、反馈不准地图建得再好也白搭。ROS的move_base最终下发的是速度指令真正执行这个指令的是STM32这一层的电机驱动和编码器这一层的实时性和准确性直接决定小车能不能走直线、能不能精确转弯。所以第一节我们先不碰ROS先把STM32这边的电机驱动和编码器读取做扎实。这一篇我会把原理、接线、代码、调试方法全部过一遍尽量让看的人少走弯路。1. 先想清楚再动手底盘方案选型与整体架构1.1 为什么是STM32ROS这条路线底盘控制芯片的选择其实很多Arduino、ESP32、STM32都能干这件事。但我最终选STM32原因有三个资源够用、生态成熟、实时性好。底盘控制的核心任务是高频读取编码器、计算速度、输出PWM这些都需要在微秒到毫秒级别完成。Arduino虽然上手快但它的定时器资源、中断优先级管理都比较简单多个编码器同时高速计数的时候容易出问题。ESP32性能不错但它在机器人生态里的定位更偏无线通信和边缘计算而且很多开源的底盘驱动代码都是围绕STM32写的参考资料多到看不完。ROS上层导航对底盘的要求简单说就是“你给我一个干净利落的速度执行接口”。一般通过串口或CAN把线速度和角速度发给STM32STM32内部再把线速度换算成左右轮的目标转速通过PID闭环控制去执行同时把编码器测到的真实速度再反馈回ROS。这个架构里STM32承担的是实时控制层ROS承担的是规划决策层分工明确这也是目前大多数开源小车项目采用的经典方案。1.2 电机选型直流减速电机带霍尔编码器底盘常用的电机有几种普通直流电机、直流减速电机、步进电机、无刷电机。做ROS小车ROS性价比最高的方案是直流减速电机加霍尔编码器。普通直流电机转速太快、扭矩太小直接带轮子根本跑不动。步进电机扭矩大、精度高但高速性能差而且步进电机的控制方式和底盘常用的速度闭环模式不太匹配功耗也偏高。无刷电机性能好但驱动成本高对于入门级底盘来说没有必要。直流减速电机的优势是减速箱把转速降下来、扭矩提上去同时霍尔编码器直接集成在电机尾部可以实时测出电机轴的转角变化。因为编码器测的是电机轴而不是轮子轴实际轮子的转速换算要乘上减速比。这个换算关系后面会详细讲先在这里留下一个印象。选电机的时候重点关注三个参数额定电压、减速比、编码器线数。额定电压常见的有6V、12V、24V这个要和电池组、驱动模块匹配。减速比常见的30:1、50:1越大扭矩越大但同样PWM下轮子转速越慢。编码器线数常见的有13线/圈每圈13个脉冲配合减速比换算成轮子端的脉冲数就会很大。比如一个减速比30:1的电机编码器每圈13个脉冲那轮子转一圈对应的脉冲数就是13×30390个如果再用上定时器4倍频模式就能达到1560个计数/圈。这个精度对于做速度闭环和里程计已经完全够用了。1.3 电机驱动模块的选择TB6612还是L298N驱动模块我试过好几种L298N、TB6612、AT8236、DRV8833都用过。这里直接给结论做小车底盘首选TB6612除非你的电机电流需求特别大才考虑AT8236或者其他大电流驱动。TB6612是东芝的电机驱动芯片单路最大持续电流约1.2A峰值3.2A这个指标覆盖市面上大多数12V以下的直流减速电机。L298N虽然名气大但它的压降太大用的是三极管加达林顿结构供电12V最终到电机上的可能只有9V白白浪费了功率。TB6612用的是MOSFET管压降小发热低而且体积小可以直接插面包板或者自己打个小板。我见过很多新手在L298N上折腾得死去活来换到TB6612之后问题立刻消失。TB6612引脚也简单VM接电池正极、VCC接3.3V或5V逻辑电源、GND共地、AIN1/AIN2控制A电机方向、PWMA控制A电机速度、STBY接高电平使能。每组对应一个电机的方向控制和PWM调速。2. 电机驱动核心PWM调速与H桥控制原理2.1 H桥电路为什么电机可以正反转先搞清楚一个基础问题为什么电机驱动模块可以控制电机正反转和速度电机内部就是线圈给它两端加上电压电流从正端流向负端电机就朝一个方向转。如果反接电流反向电机就反转。要想灵活控制正反转就需要一个能随时切换电流方向的电路这就是H桥电路。四个开关管组成“H”字形左上和右下导通时电流正向流过电机右上和左下导通时电流反向流过电机电机就实现了正反转。两个上桥臂同时导通或者两个下桥臂同时导通就会短路这在驱动芯片内部已经做了逻辑保护我们自己写代码时只要注意不要给矛盾的控制信号就行。速度控制则通过PWM实现。PWM是脉冲宽度调制可以理解为“快速开关电源”通过调节高电平占一个周期的时间比例也就是占空比来改变加在电机上的平均电压。占空比越大平均电压越高电机转得越快。常见的频率是10kHz到30kHzTB6612支持得很顺畅。2.2 TB6612接线与引脚分配我用的是STM32F103C8T6这一颗经典芯片在底盘控制上它绰绰有余。接线方案如下直接可抄TB6612的VM接电池正极这里有一个需要重视的细节VM最好先经过一个100uF以上的电解电容和0.1uF陶瓷电容滤波否则电机启动的瞬间电流冲击可能导致STM32复位。GND必须和STM32、电池负极共地这个不接的话控制信号根本没法工作。VCC接3.3V或者5V看模块型号一般3.3V足够。STM32引脚分配上我用的是左电机方向1PB0左电机方向2PB1左电机PWMPA8TIM1_CH1右电机方向1PB10右电机方向2PB11右电机PWMPA9TIM1_CH2为什么要用定时器输出PWM而不是直接用普通GPIO输出高电平因为PWM需要精确的频率和占空比用普通的delay翻转GPIO很难稳定做到20kHz的PWM输出而且CPU会被占死。定时器的硬件PWM占空比交给定时器硬件去处理CPU可以在同时在忙编码器计数的事情。接线就按上面的顺序一一对应。我之前见过有些人把PWM接到普通的GPIO引脚上然后想靠delay模拟PWM跑起来之后电机噪声大、转速不稳而且一开无线模块整个程序就被打乱。硬件PWM是底盘控制的基础一定要优先安排。2.3 电机驱动代码实现直接上代码基于STM32的标准库或者HAL库都可以这里用HAL库做讲解。首先是PWM初始化void Motor_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; TIM_OC_InitTypeDef sConfigOC {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_TIM1_CLK_ENABLE(); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8 | GPIO_PIN_9, GPIO_PIN_RESET); GPIO_InitStruct.Pin GPIO_PIN_8 | GPIO_PIN_9; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); htim1.Instance TIM1; htim1.Init.Prescaler 72 - 1; // 72MHz / 72 1MHz htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.Period 1000 - 1; // PWM频率 1MHz / 1000 1kHz htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim1); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 0; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); }然后是电机方向控制和速度设置void Motor_SetSpeed(int motor_id, int speed) { // speed范围 -1000~1000正数前进负数后退 if (speed 1000) speed 1000; if (speed -1000) speed -1000; uint16_t pwm_value (uint16_t)(speed 0 ? speed : -speed); if (motor_id MOTOR_LEFT) { if (speed 0) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); } __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, pwm_value); } else if (motor_id MOTOR_RIGHT) { if (speed 0) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_11, GPIO_PIN_SET); } __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, pwm_value); } }PWM频率的选择我最后定为1kHz而不是一些人习惯用的20kHz。这个频率对TB6612来说是支持的1kHz的PWM更有助于直流的顺畅度实测下来噪声也在可接受范围。如果你想用更安静的高频PWM可以把Prescaler和Period重新算一下只要保证总的分频系数让PWM频率落在驱动芯片的允许范围内就可以。3. 编码器读取让MCU看见轮子的转速3.1 正交增量式编码器的工作原理电机能转了下一步是让单片机知道轮子到底转了多少、转多快。这一步就靠编码器。霍尔编码器内部有两个霍尔传感器间隔一定的角度安装电机轴转动的过程中磁环或者磁栅的磁场变化会让两个传感器输出两路方波信号分别叫A相和B相。这两路方波有一个关键特征就是相位差90度。正是这个相位差让单片机不仅能够数脉冲还能通过判断A、B两路波形的先后关系知道电机是正转还是反转。对增量式编码器来说它输出的是相对位置的变化量而不是绝对角度。也就是说它只能告诉单片机“从上次读数到现在转了多少脉冲”不知道当前绝对角度是多少。对底盘来说这就够了因为速度本身就是位置的差分绝对角度后面可以在STM32内部通过积分累积出来。A、B两路的90度相位差还能带来一个免费的“精度提升”。如果在A相上升沿、A相下降沿、B相上升沿、B相下降沿都进行计数一圈的脉冲数就能放大4倍这就是定时器编码器模式的4倍频计数。同样是13线编码器4倍频之后每个机械周期输出52个计数。加上减速比30:1轮子端一个周期就是1560个计数对速度闭环来说绰绰有余。3.2 定时器编码器模式的配置STM32很贴心地提供了专门的编码器接口模式。这个模式的意思是定时器可以自动根据A、B两相的状态变化来增减计数器的值在整个过程中CPU不需要参与。以STM32F103C8T6为例TIM1、TIM2、TIM3、TIM4都支持编码器模式。但TIM1和TIM8是高级定时器有些附加功能编码器模式一般用TIM2、TIM3、TIM4更顺手。我习惯把左轮编码器接TIM3右轮编码器接TIM4。对应引脚TIM3_CH1PA6左编码器A相TIM3_CH2PA7左编码器B相TIM4_CH1PB6右编码器A相TIM4_CH2PB7右编码器B相编码器模式初始化代码void Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; TIM_Encoder_InitTypeDef sEncoderConfig {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_TIM4_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); htim3.Instance TIM3; htim3.Init.Prescaler 0; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 65535; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Encoder_Init(htim3, sEncoderConfig); htim4.Instance TIM4; htim4.Init.Prescaler 0; htim4.Init.CounterMode TIM_COUNTERMODE_UP; htim4.Init.Period 65535; htim4.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Encoder_Init(htim4, sEncoderConfig); sEncoderConfig.EncoderMode TIM_ENCODERMODE_TI12; sEncoderConfig.IC1Polarity TIM_ICPOLARITY_RISING; sEncoderConfig.IC1Selection TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC1Prescaler 0; sEncoderConfig.IC1Filter 0x0F; sEncoderConfig.IC2Polarity TIM_ICPOLARITY_RISING; sEncoderConfig.IC2Selection TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC2Prescaler 0; sEncoderConfig.IC2Filter 0x0F; HAL_TIM_Encoder_ConfigChannel(htim3, sEncoderConfig, TIM_CHANNEL_1); HAL_TIM_Encoder_ConfigChannel(htim3, sEncoderConfig, TIM_CHANNEL_2); HAL_TIM_Encoder_ConfigChannel(htim4, sEncoderConfig, TIM_CHANNEL_1); HAL_TIM_Encoder_ConfigChannel(htim4, sEncoderConfig, TIM_CHANNEL_2); HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_1); HAL_TIM_Encoder_Start(htim3, TIM_CHANNEL_2); HAL_TIM_Encoder_Start(htim4, TIM_CHANNEL_1); HAL_TIM_Encoder_Start(htim4, TIM_CHANNEL_2); }注意的一个细节是IO口上拉。编码器输出是开漏结构还是推挽结构不同的模块不太一样加上拉电阻保险避免信号悬空时产生毛刺。我的编码器模块自带下拉电阻但STM32内部配置成上拉之后仍然正常工作说明这个上拉和模块的下拉通过信号线上的实际电平状态达成了平衡不影响逻辑判断。3.3 测速算法M法、T法、M/T法编码器计数数值出来了怎么换算成速度这是底盘控制里非常关键的一步。常见的测速算法有三种M法、T法和M/T法。M法测速是在固定时间窗口内统计编码器的脉冲数然后用脉冲数乘以每个脉冲代表的角度再除以时间得到角速度。这种方法在高速段精度高低转速时问题就来了如果速度太慢一个时间窗口内可能只有几个脉冲甚至没有脉冲算出来的速度跳跃很大。T法测速是测量两个相邻脉冲之间的时间间隔用脉冲间隔换算速度。这种方法在低速时精度高因为低速时脉冲间隔长、计时分辨率高但高速时脉冲间隔短计时误差占比变大。M/T法就是把两者结合起来低速用T法、高速用M法理论上最准但实现复杂度也最高。底盘控制中RoboMaster和大部分开源小车底盘实际使用最多的还是M法。原因很简单硬件定时器自带向上计数寄存器每隔固定时间读取一次计数器的值并用新值减去旧值这个差值就是时间窗口内的脉冲数实现极其简单而且在额定转速下M法的精度已经足够支撑起一个稳定的速度闭环。10ms的定时器中断里读一次计数器值左右轮分别算速度做PID更新这是底盘控制最常见的控制周期。M法测速的代码如下void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim htim2) { // 10ms控制周期 int16_t left_encoder (int16_t)__HAL_TIM_GET_COUNTER(htim3); int16_t right_encoder (int16_t)__HAL_TIM_GET_COUNTER(htim4); // 清零计数器重新计数 __HAL_TIM_SET_COUNTER(htim3, 0); __HAL_TIM_SET_COUNTER(htim4, 0); // 换算转速单位圈/秒 float left_speed (float)left_encoder / (4.0f * 13.0f * 30.0f) * 100.0f; float right_speed (float)right_encoder / (4.0f * 13.0f * 30.0f) * 100.0f; // 更新PID并输出PWM Motor_SetSpeed(MOTOR_LEFT, (int)PID_Update(pid_left, left_speed)); Motor_SetSpeed(MOTOR_RIGHT, (int)PID_Update(pid_right, right_speed)); } }上面的计算里除以4是因为4倍频除以13是一圈脉冲数除以30是减速比乘以100是10ms时间窗口换算成秒。如果你的编码器线数和减速比不同把常数替换一下就好。4. 速度闭环从“能转”到“转得准”4.1 为什么一定要上PID闭环如果你只是让小车能跑开环直接给PWM就够了。但ROS导航对底盘的要求是你给底盘一个0.2m/s的线速度它就得稳稳地保持0.2m/s不能因为电池电压下降、地面阻力变化、爬坡负载增大而突然变慢。开环控制在这几种场景下都没有任何修正能力电池电压从12.6V降到10V同样的PWM下电机转速可能掉20%以上这在导航里是不可接受的。闭环控制则把编码器测到的真实速度作为反馈和目标速度比较计算出误差再用PID算法计算出合适的PWM去补偿误差。电池电压下降导致转速变慢时编码器立刻发现实际速度低于目标PID会增加PWM输出让电机回到目标速度。这就是底盘闭环控制最朴素也最核心的意义。4.2 位置式PID与增量式PID的选择底盘的电机速度控制用位置式PID和增量式PID都可以。位置式PID直接计算输出的绝对值增量的计算每次只算输出的变化量。底盘速度环最常用的是位置式PID因为输出是PWM占空比绝对值逻辑直观调参的时候也好理解。位置式PID的实现代码typedef struct { float Kp; float Ki; float Kd; float target; float integral; float lastError; } PID_t; float PID_Update(PID_t *pid, float feedback) { float error pid-target - feedback; pid-integral error; float derivative error - pid-lastError; pid-lastError error; float output pid-Kp * error pid-Ki * pid-integral pid-Kd * derivative; return output; }注意这里有一个坑积分饱和。如果电机被卡住转速一直上不去误差一直在积分项会不停累积最终把输出值推到PWM的上限而且当外部阻碍消失之后累积的积分会让电机猛冲一下。加一个积分限幅可以有效缓解这个问题。4.3 参数整定的实操经验PID调参我分享一下我的方法不用MATLAB也不用什么高级工具纯手动先把Ki和Kd直接设为0只保留Kp。把目标速度设置成你想要测试的恒定值比如轮子端1圈/秒从小Kp开始加大。观察电机的响应如果转速始终达不到目标就增加Kp如果出现明显振荡就减小Kp。找到一个临界值此时系统刚好持续小幅振荡再把Kp减到临界值的一半左右。然后加Ki。Ki的作用是消除稳态误差让转速能精确贴合目标值。Kp已经让系统接近目标了剩下的残差由积分项慢慢补上。Ki从小往大调注意转速从欠调到轻微超调再回稳找到一个合适的值。Kd在速度环里一般用得非常少因为编码器的速度反馈本身带有高频噪声Kd会把这些噪声放大反而让PWM抖动。如果系统没有明显的超调Kd保持0就可以。参数整定的过程最好把编码器反馈的数据通过串口发到电脑上用串口绘图工具看曲线。我就是这么干的用串口把目标速度和实际速度发出来画成两条曲线调参的效率立刻翻了倍。你看如果实际速度曲线总是贴着目标速度曲线走说明调参不错如果像一个弹簧一样来回震荡就把Kp往小调。5. 常见问题排查与避坑实录5.1 编码器读数为0或者数值跳变检查这个问题的顺序是硬件接线、信号类型、定时器初始化。先看A相和B相是不是接反了。接反的直接表现是编码器数值不为0但因为方向反了计数器的增减方向和实际运动方向相反。这种问题不会让数值变成0但会让速度闭环把方向搞反小车会一直往反方向猛冲。如果A、B同时接反计数还是正确的但正转变成反转。再看模块的输出电平。有些编码器模块输出的是5V电平而STM32的PA6、PA7是5V容忍引脚直接接问题不大但有的模块输出是推挽还是开漏很关键开漏输出不加上拉就会导致信号幅度不够计数丢失严重。还要确认是不是有两路信号本来就是同一个通道分出来的。市面上有些便宜的编码器模块其实只接了一路信号另一路悬空或者固定电平这时候STM32的编码器模式无法识别方向必须换成支持A、B双相正交输出的编码器模块。5.2 电机不转或者无力电机不转的第一步是检查STBY引脚。TB6612的STBY必须为高电平如果这个引脚悬空或者被拉低模块整个不工作。我用过的一款模块默认没有把STBY拉到VCC需要在代码里初始化的时候先把它拉高否则电机一点反应都没有。电机无力优先考虑VM电压不足。VM引脚的线径要够粗我见过有人用杜邦线给两个电机同时供电线太细压降太大电机启动瞬间直接跌到4V根本跑不起来。用至少22AWG以上的硅胶线从电池单独给驱动模块供电不要和逻辑电路共用一根细线。PWM极性搞反也会导致电机无力。有的驱动模块是低电平有效高电平反而关断这种情况如果按常规思路给占空比电机只会以最大功率反转。5.3 速度闭环跑起来之后小车走不直线这几乎是所有人都会遇到的问题。两边电机即使型号一样实际特性也会有差异PWM占空比相同的情况下左右轮转速就是不一样。解决方法是加一个底盘层面的纠偏要么在速度PID之外再加一个里程计偏差修正要么在轮子控制里加一个固定的PWM补偿值。我的做法是在程序里保存一组校准参数让左右轮在空载、同一个目标速度下达到相同的实际转速这个校准过程只需要做一次。另外还要检查一下左右轮的编码器方向是否一致。一个轮子的编码器方向定义是“向前转时计数增加”另一个如果因为是镜像安装导致“向前转时计数减少”那么速度闭环会让它在启动的一瞬间疯狂加速或者刹车。这种情况需要在代码里对一侧的编码器数值取反。5.4 从底层控制走向ROS接线的过渡本节把电机驱动和编码器读取做完了底盘的“肌肉”和“神经”已经就位。接下来要做的事情有两个方向一是在STM32内部把底盘里程计算出来通过编码器累计值推算小车的位置和航向角二是把STM32和ROS建立起通信链路让move_base能下发速度指令、接收里程计反馈。里程计的计算其实不复杂核心公式就是左右轮线速度和差速模型。左轮速度是v_left右轮是v_right轮距是L那么底盘线速度就是(v_leftv_right)/2角速度就是(v_left-v_right)/L。有了线速度和角速度再通过积分就能得到x、y坐标和航向角。这个计算在STM32的定时器中断里做然后把结果通过串口或者CAN发出去。ROS那一侧的接线也不复杂用一个串口节点把STM32发上来的里程计数据解析出来发布成ROS的odom话题同时接收cmd_vel话题把线速度和角速度换算成左右轮目标转速再通过串口下发到STM32。这一整套流程我在后面的章节里会详细展开。离完整底盘还差一步写到这里电机驱动这条路基本走通了。回想我自己第一次把底盘跑起来的时候最激动的不是它转起来的那一瞬间而是它能在目标速度和实际速度曲线几乎完全重合的状态下跑出一条直线。那种“算法落地成物理运动”的确认感是做机器人底层控制最上头的时刻。有个小建议送给刚入坑的朋友别一次性在底盘上堆太多功能先把单个电机驱动得丝滑、把单个编码器读得准确再去做双电机差速闭环最后才接导航算法。每一个环节都自己亲手调过一遍后面遇到问题时的直觉会准很多。下一节可以讲讲STM32如何做里程计解算以及和ROS建立通信的具体通信协议设计。这一节先把代码烧进去把你手上的电机转起来再看看串口输出的编码器数值能不能和你转动轮子的方向对得上。对得上的那一刻你的底盘控制就已经成功了一大半。