简介本资源是一套面向嵌入式开发者与机器人爱好者设计的麦轮小车底盘全栈控制方案聚焦STM32底层驱动与微信小程序远程交互解决四轮麦轮底盘运动学解算、闭环控制与无线操控等核心难点。压缩包共470个文件约15.09MB涵盖80余个C源文件含TIM/RCC/ADC等外设驱动、81个头文件定义运动学逆解、PID参数及通信协议、78个编译中间文件及小程序端JS/WXML/WXSS代码结构清晰便于分模块学习调试。已有148人下载学习配套博客《如何获得一个丝滑的麦轮底盘》深度解析运动学推导、增量式PID实现、编码器数据离散化处理及小程序指令拆解逻辑所有关键算法均附详细注释可直接移植或用于课程设计、智能小车竞赛开发。 做麦轮小车底盘我最常听到的一句话是“我四个轮子都转起来了为什么车还是原地打转” 这几乎是每个第一次接触麦克纳姆轮的人都会撞上的墙。四个轮子各自转和四个轮子按正确的运动学关系转完全是两回事。 这个项目我当时花了两周时间从STM32的电机控制代码到微信小程序遥控端全部打通中间踩了不少坑也积累了一套可以直接复用的完整方案。这篇博文就把整个开发过程、代码结构和避坑心得都摊开来讲适合手里有麦轮底盘套件、想自己写控制程序的人也适合准备做竞赛小车或机器人课程项目的同学参考。1. 麦轮底盘的“全向移动”是怎么实现的想写好控制代码第一步不是打开Keil而是先搞清楚麦克纳姆轮的运动原理。这块如果没弄明白后面所有代码都是空中楼阁。1.1 麦克纳姆轮的结构与A/B轮安装方向麦克纳姆轮和普通轮子最大的区别是它轮面外侧斜着安装了一圈小辊子。这些辊子与轮子轴线呈45度角可以自由转动。正因为辊子会“卸力”轮子转动时产生的驱动力方向并不是垂直于轮轴的而是沿辊子方向斜向作用。这就带来了一个关键概念麦轮有A轮和B轮之分。A轮的辊子轴线和轮轴线呈“左旋”45度B轮呈“右旋”45度。装车时必须按“左上A、右上B、左下B、右下A”的规则安装。如果方向装反或者四轮混装底盘在收到横移指令时会左右力量相互抵消车只能原地抖动严重时还会损坏电机。提示判断A/B轮最直接的方法是把轮子平放在桌面上用手推一下。往一个方向推时轮子滚动顺畅、几乎没有侧向阻力反方向推则明显有“别住”的感觉。记住这个顺畅方向对应的安装朝向就能避免装反。1.2 运动学解算从速度指令到四轮转速麦轮底盘的三种基本运动模式是前后直行、左右平移、原地旋转。复杂运动比如斜向45度移动就是把平移和旋转叠加。我用的是最常见的“四轮独立驱动”底盘运动学关系如下。假设底盘前后方向为X轴、左右方向为Y轴、车体逆时针旋转角速度为omega输入量是小车期望的三个速度分量vx前后方向速度正值前进vy左右方向速度正值左移omega旋转角速度正值逆时针转动四个轮子的转速与这三个量的关系是左前轮速度 vx vy omega * (LW)右前轮速度 vx - vy - omega * (LW)左后轮速度 vx - vy omega * (LW)右后轮速度 vx vy - omega * (LW)其中L是底盘中心到前/后轮轴的纵向距离W是中心到左/右轮边的横向距离。速度方向为正时用PWM正转为负时反转。我实际调试时发现这里最容易忽略的是符号。比如想让车左移所有轮子中有的要正转、有的要反转。不少教程把符号写反导致代码“看起来对”但车就是不动。所以写完运动学函数后先分别单独测试四个轮子的转向是否正确再做全向运动测试。1.3 主控选型为什么STM32F103C8T6够用小车控制量并不大四路PWM、四个编码器、一个串口用于和小程序通信这些资源STM32F103C8T6完全能应付。它的优势是资料多、HAL库和标准库的代码都能找到参考而且蓝板子价格低适合作为教学和竞赛平台。如果你手里的底盘电机带有霍尔编码器我建议直接用定时器的编码器模式采集速度精度高且不占CPU。如果用的是无编码器版本也可以先开环控制但就没有速度闭环后面想做精确走位会很难受。本项目的代码默认底盘带编码器。2. STM32端控制代码的核心模块控制端的代码整体分成四块PWM输出、编码器测速、速度闭环PID、通信协议处理。下面逐一说明设计思路和关键代码。2.1 电机驱动与PWM输出配置驱动芯片选用TB6612FNG它比传统的L298N发热小、响应快适合小功率小车电机。每个电机需要两个方向引脚IN1、IN2和一路PWM输入控制逻辑很简单IN11, IN20 时正转IN10, IN21 时反转IN1IN2 时刹车或惰行PWM频率我设成10kHz这个频率下TB6612工作稳定电机噪音也可接受。频率过低电机会发出“嗡嗡”声而且低速线性度差频率太高MOS管的开关损耗也会增加。PWM输出使用STM32的定时器通道。比如用TIM2的四个通道分别输出四路PWM代码通过HAL库配置通道比较模式修改比较寄存器实现占空比调整。四个电机的控制函数可以抽象成一个统一接口原始控制代码里最核心的一段逻辑长这样void Motor_SetSpeed(MotorId id, int16_t speed) { // speed范围 -1000 ~ 1000 if (speed 0) { Motor_SetDirection(id, DIR_FORWARD); __HAL_TIM_SET_COMPARE(htim2, Motor_PwmChannel[id], speed); } else if (speed 0) { Motor_SetDirection(id, DIR_BACKWARD); __HAL_TIM_SET_COMPARE(htim2, Motor_PwmChannel[id], -speed); } else { __HAL_TIM_SET_COMPARE(htim2, Motor_PwmChannel[id], 0); } }这样的好处是上层函数只需要传“目标速度”不用关心电机和定时器的具体绑定关系。后续调试四轮转向时也只需要在驱动层改引脚映射。2.2 编码器测速从脉冲计数到mm/s要让小车精确响应指令得知道当前实际轮速。霍尔编码器输出的A、B两路方波接入STM32定时器的CH1、CH2引脚后可以配置为编码器模式硬件自动判断方向和计数。底盘上N20减速电机的常见参数是减速比30、霍尔编码器线数11线加上定时器4倍频轮轴每转一圈产生的脉冲数为11线 x 4倍频 x 30减速比 1320脉冲/圈如果轮子直径60mm那么单轮一圈对应的行驶距离约188.5mm每个脉冲对应的行程就是188.5mm / 1320脉冲 ≈ 0.143mm/脉冲有了这个比例系数定时器周期性地读取计数值并清零就能换算成实时线速度单位mm/s。我建议10ms读一次计数并清零速度计算代码如下uint16_t cnt __HAL_TIM_GET_COUNTER(htim3); __HAL_TIM_SET_COUNTER(htim3, 0); int32_t rpm (int32_t)cnt; speed_mm_s rpm * 0.143f * 100; // 10ms的计数 × 比例系数 × 每秒100个采样周期为什么选10ms因为控制周期越短越能及时发现速度偏差但也越容易受编码器噪声干扰。实测下来10ms对N20电机是较稳妥的折中。2.3 速度环PID代码实现与整定顺序四轮底盘每个轮子独立闭环用增量式PID就够了。增量式PID输出的是一个增量不容易产生积分饱和控制逻辑也直观。typedef struct { float Kp; float Ki; float Kd; float err_prev; float integral; } PidObject; float Pid_Update(PidObject* pid, float target, float current) { float err target - current; pid-integral err; float output pid-Kp * err pid-Ki * pid-integral pid-Kd * (err - pid-err_prev); pid-err_prev err; return output; }调用时把PID输出作为PWM目标值再经过饱和限制比如输出限幅在-1000~1000传给Motor_SetSpeed。整定顺序非常重要我的经验是先只给比例项从小到大慢慢加Kp从0开始每次增加0.1观察轮子启动是否迅速、是否振荡。当轮子出现持续性小幅振荡时说明Kp过大退回80%左右的数值。再加入积分项Ki消除稳定误差让实际转速尽可能接近目标转速。微分项Kd在大多数电机场合可以先用0如果发现启停时有明显超调再适当加入。刚开始调PID不需要追求“完美时刻跟随”只要保证推车时电机有一定阻力松开时轮子能稳定停住就可以进行联调了。3. 指令协议设计STM32和小程序怎么对齐底盘和小程序是两个独立系统之间靠无线链路通信。蓝牙模块或WiFi转串口模块会把收到的数据原封不动地从串口输出给STM32。因此双方必须先约定好数据帧格式否则收到的就是一堆乱码。这部分代码看似不起眼却是整个项目能不能跑通的关键。3.1 协议帧格式与CRC校验我设计的帧格式比较精简适合蓝牙低功耗的短包传输字段长度说明帧头2字节固定0xAA 0x55数据长度1字节后面数据区的字节数命令字1字节如0x01表示速度控制数据区N字节具体指令数据CRC16低字节1字节前面所有字节的校验值低字节CRC16高字节1字节校验值高字节CRC算法用的CRC16-MODBUS多项式0x8005初始值0xFFFF。为什么不用简单的累加和因为无线环境下偶尔会出现单字节翻转累加和很可能校验不出来CRC对这类误码的检出能力好很多。速度控制指令的数据区就放三个int16分别对应vx、vy、omega。为了减少传输字节数换算关系是实际值等于原始整数值除以100。比如前端想发“前进0.50m/s”就发送50。3.2 串口不定长接收空闲中断DMA上层的单片机串口接收代码我用的是HAL库的串口空闲中断结合DMA。这个方案的好处是MCU收完一帧数据后才知道“这次该处理了”没用完的DMA缓冲还能继续接收不用每来一个字节就打断CPU一次。配置好USART的DMA接收后开启空闲中断然后在中断回调里判断收到多少数据void USART_IDLECallback(UART_HandleTypeDef* huart) { if (huart-Instance USART1) { uint16_t len __HAL_DMA_GET_COUNTER(huart-hdmatx); // 实际接收长度 缓冲区总长度 - DMA剩余计数器里的值 uint16_t recv_len RX_BUF_SIZE - len; Protocol_Parse(rx_buf, recv_len); // 重新开启DMA接收 } }如果不想用DMA退一步用串口接收中断状态机也能实现同样的效果只是每字节中断一次波特率较高时CPU占用会明显上升。我实测115200波特率下DMA方案跑起来几乎不占CPU资源后续扩展点按传感器和速度环都比较从容。3.3 状态机一帧数据从接收到底盘响应协议解析核心是一个状态机从找帧头到收长度、收数据、收CRC、校验、执行每一步都有明确状态。这样即使链路中夹了几字节的干扰数据也能通过帧头重新同步不会让整个解析卡死。状态机的实现骨架大致是这样的switch (parse_state) { case STATE_HEADER1: if (byte 0xAA) state STATE_HEADER2; break; case STATE_HEADER2: if (byte 0x55) { state STATE_LENGTH; frame_index 0; } else state STATE_HEADER1; break; case STATE_LENGTH: frame_len byte; state STATE_DATA; break; case STATE_DATA: data_buf[frame_index] byte; if (frame_index frame_len) state STATE_CRC; break; case STATE_CRC: // 校验整个帧通过则执行命令失败则回到找帧头 break; }有了这个状态机不管小程序发的是平滑速度指令还是快速连发的控制包都能稳定解析。实际联调中我还发现一个重要问题蓝牙模块上电瞬间会输出一个随机字节。如果不做帧头校验解析器很容易把这个随机字节当成有效数据。帧头 CRC这套组合能天然避开这种干扰。4. 小程序控制端开发不只是发几个数字小程序控制端的核心是当遥控器。但真正写起来你会发现微信小程序的蓝牙API细节很多网络方案也各有取舍。这一章把我最终采用的BLE蓝牙方案和备用的WiFi方案都讲清楚。4.1 蓝牙选型为什么HC-05接不了小程序市面上很多小车教程用的是HC-05或HC-06这类经典蓝牙模块和手机配对后就是串口透传非常方便。但这里有个坑微信小程序只支持BLE低功耗蓝牙不支持经典蓝牙SPP。也就是说HC-05手机蓝牙调试助手能用但小程序根本搜不到它。所以蓝牙模块要选带BLE的型号比如HM-10、JDY-08或者CC2541方案的模块。这类模块同样支持串口透传但工作在BLE协议下小程序可以直接扫描、连接、读写特征值。提示如果手上只有HC-05又想快速联调可以用串口转WiFi模块或者手机先做调试再换成BLE模块接小程序。我自己的方案是直接上JDY-08因为它默认就是BLE从机透传连接后STM32侧看到的依然是一个串口代码和用HC-05完全一致不用改底层协议。4.2 小程序BLE API的使用要点小程序蓝牙API的调用顺序比较固定写成代码大约是初始化蓝牙适配器wx.openBluetoothAdapter()开始扫描设备wx.startBluetoothDevicesDiscovery()在回调里筛选设备名wx.onBluetoothDeviceFound()按name或advertisData匹配连接设备wx.createBLEConnection(deviceId)获取服务和特征值wx.getBLEDeviceServices()再wx.getBLEDeviceCharacteristics()使能通知wx.notifyBLECharacteristicValueChange()用于接收STM32回传的数据发送数据wx.writeBLECharacteristicValue()把控制帧写入特征值这里面有几个非常容易踩的细节首先BLE默认MTU是23字节扣掉ATT头部单次能写的数据只有大约20字节。所以前面设计的控制帧一定不能超长。我的速度指令帧算上帧头和CRC一共11字节完全没问题。其次不能高频连续写。实测如果不加限速连续调用writeBLECharacteristicValue数据会直接被丢弃。我是在每次写完后加一个300ms的间隔还是用定时器控制发送频率保证速度指令的更新稳定在20Hz左右。最后扫描结果回调可能触发多次要自己去重。否则同一个设备被加入列表几次连接时拿到的deviceId就会混乱。4.3 备选方案ESP8266走WiFi遥控如果后续想做摄像头图传或者蓝牙距离不够用我建议改用ESP8266模块做WiFi遥控通道。思路是ESP8266透传固件下STM32通过串口发AT指令控制ESP8266建立TCP服务器手机连上同一个路由器后小程序用wx.connectSocket创建TCP连接向小车的IP和端口发协议帧。注意小程序连接局域网中的TCP服务器在开发工具里能直接连但真机预览时需要在小程序后台配置socket合法域名或者在开发版开启调试模式。这个限制容易让人卡住提前有个心理准备就行。5. 联调踩坑记录那些不转一下都不知道的事把程序烧进板子、小程序也能连上蓝牙之后真正的折磨才开始。这一节记录我调车时遇到的主要问题每个问题的排查链路都比直接给答案更重要。5.1 PID参数整定从震荡到平稳第一次整定速度环时我把Kp直接给了1.0结果轮子一启动就“嗡嗡”地高速抖动声音像齿轮打架。后来把Kp降到了0.3轮子才平稳转动。这个过程让我意识到不同的电源电压、电机型号、底盘负载PID参数差异非常大照搬别人参数基本都会出问题。调试时我总结出一套顺序先单轮调试把四个轮子分别通电只让一个轮子闭环。用程序给一个固定的目标速度观察轮子响应。如果轮子转速忽快忽慢说明Kp偏大或信号地没接好。轮子能稳住了再四个轮子一起调。最后才做整机联调。另外霍尔编码器线材要用双绞线或屏蔽线。我第一次调试时编码器线悬空小车的速度反馈值一直在跳导致PID输出乱摆车动不动就抽搐。把线整理好、远离电机电源线后问题立刻消失。5.2 转向抖动与四轮配合做完速度闭环我发现一个有意思的现象单独前进很平稳但只要原地旋转车就明显抖动。排查后定位到原因——转向时四个轮子需要同时反向旋转但由于机械误差四个轮子的实际转速很难完全匹配误差一累积车体就开始轻微左右摆。这个问题的解决思路有两层软件上把速度环的死区适当调大允许±10mm/s以内的误差不调整PWM。实测可以有效减小高频抖动。结构上检查四个轮子的胎压如果有充气轮和安装高度。四个轮子如果有一个悬空或压得太紧任何控制代码都救不回来。5.3 串口粘包、看门狗和其他小坑串口粘包当小程序发送帧的速度超过STM32解析速度时DMA缓冲区里会连续堆了多帧数据。我的协议解析器在收到一帧后并没有立即处理而是先把完整帧存入环形队列由主循环统一取帧执行这样即使瞬时来了多帧也不会丢。看门狗调试过程中如果开着独立看门狗一旦停在下断点看门狗就会超时复位芯片直接重启。调试期间最好禁用看门狗等所有功能稳定后再打开。电源问题四个电机同时启动的瞬时电流很大。如果用USB给STM32供电、电机用另一节电池两块电源的地必须共地。否则串口和蓝牙通信会非常不稳定。这些坑单独看都不复杂但放在一起足以让人排查一整天。6. 底盘项目还能怎么延伸麦轮底盘一旦搞定它就不再是一个“遥控车”而是一个可移动的机器人平台。后续扩展空间非常大而且STM32端的代码框架基本不需要大改。循迹与自动引导在底盘前方加灰度传感器或OpenMV摄像头用串口把巡线偏差发给STM32运动学解算模块里直接加入偏差修正量就能实现循迹。SLAM导航如果换个更强的板子跑ROS然后把STM32作为底盘驱动板通过串口接收cmd_vel速度指令并回传当前里程计信息麦轮底盘可以作为服务机器人的移动底座。机械臂联动把控制指令协议扩展一下新增几个命令字就可以在同一个小程序里同时遥控底盘和机械臂做一套完整的智能小车。这个项目的价值在于它把一个看似复杂的“底盘控制无线遥控”系统拆成了清晰的分层运动学、底层驱动、闭环控制、通信协议、UI控制端。每一层都可以单独测试哪一层出了问题查起来边界非常清楚。我后来做其他机器人项目时依然在用这套分层思路只是换了一部分芯片和协议载体。如果你的麦轮小车也卡在某个环节不妨按这个顺序一层层排查大概率能找到问题所在。本文还有配套的精品资源点击获取