简介:面向STM32入门级开发者和机器人爱好者的麦克纳姆轮小车完整工程,基于STM32F103C8T6主控,涵盖硬件设计与嵌入式程序,支持蓝牙和ESP8266 WiFi双无线控制方式,适合学习四轮独立驱动、全向移动控制以及无线遥控小车改造。资源包共128个文件,约3.77MB,包含AD设计的原理图与PCB工程文件(pcbdoc、schdoc、pcblib等),以及单片机C语言源码(h/c文件)、Keil工程配置、hex烧录文件和辅助脚本,各类型分门别类存放,便于直接查阅。已有3874人学习/下载。通过这份资料可拿到从硬件原理图到软件控制的全链路方案:原理图与PCB便于理解或二次改板,源码覆盖定时器PWM调速、电机驱动、蓝牙/WiFi串口通信等关键模块,hex文件可快速烧录验证,适合作为课程设计或竞赛小车的参考基础。 做麦克纳姆轮小车,我在实验室里前后折腾了快三周。STM32F103C8T6最小系统板加四个麦克纳姆轮加直流减速电机,这套组合基本是学生项目、课设、电赛热身里最常见的配置。麦克纳姆轮最大的价值在于可以让车体实现横向平移、斜向运动、原地旋转这类普通差速底盘做不出来的动作,灵活性直接上一个档次。这篇文章会把硬件选型、运动学解算、CubeMX工程配置、PID闭环调速,还有我实际踩过的坑从头到尾写一遍,适合刚拿到系统板、想尽快让四轮底盘跑起来的朋友。
1. 项目概述与整体方案设计
1.1 为什么选STM32F103C8T6加麦克纳姆轮
STM32F103C8T6这块片子虽然发布年头不短,但放在小车控制场景里完全够用。主频72MHz,Flash 64KB,RAM 20KB,有4个通用定时器、3个USART、2个SPI、2个I2C,还有足够多的GPIO。驱动四路PWM加四路编码器采集,算上OLED显示、蓝牙或Wi-Fi模块的串口通信,资源仍然有富余,不需要为了跑系统去上F4系列。
选麦克纳姆轮而不是普通轮胎,核心诉求是灵活。大学竞赛里自动行驶、物流搬运这类题目,经常要求小车在狭窄区域里调整姿态,麦克纳姆轮可以横向平移,路径规划时不需要留很大的转弯半径。坏处也明显:结构复杂、辊子易磨损、对运动学解算有要求、对轮子安装方向敏感。所以它不是“无脑选”,而是“值得选”。
1.2 系统框架与硬件清单
整个小车的控制链路其实很清晰:上位机或遥控器给出设定速度,STM32根据运动学逆解把底盘速度分配到四个轮子,每个轮子由编码器反馈实际转速,PID调节器修正PWM占空比,最终驱动芯片控制电机。这个闭环结构是所有轮式机器人的通用骨架。
我手头这套配置清单供参考:
- 主控:STM32F103C8T6最小系统板(蓝色板,板载AMS1117稳压)
- 底盘:四路麦克纳姆轮小车底盘(铝合金框架,带四个TT电机安装位)
- 电机:TT直流减速电机带霍尔编码器,减速比1:30,单轴输出
- 驱动:TB6612FNG双路电机驱动模块,共两片
- 供电:7.4V 2S锂电池,经过DC-DC降压给电机和逻辑分别供电
- 调试:ST-Link V2,串口TTL模块,0.96寸OLED(可选)
这里提醒一句,TT电机所谓的“霍尔编码器”通常只有28线,加上减速比1:30,单圈脉冲才840左右。做精确速度闭环、精确里程积累时会觉得分辨率偏低。如果预算允许,换带500线编码器的MG513电机或N20电机是个质的飞跃。我最初用TT电机跑PID,速度低的时候编码器脉冲间隔太长,测速的离散误差很大,后面换成500线后明显稳了。
2. 麦克纳姆轮运动学解算
2.1 四轮速度分配原理
麦克纳姆轮的奥秘在于辊子。每个主轮周围套着一圈可以自由转动的辊子,辊子轴线和主轮轴线呈45度夹角。当主轮被电机驱动旋转时,辊子与地面的接触会产生一个沿辊子轴线方向的摩擦力分量,于是轮子不只是朝驱动方向走,还会带着车体向侧面滑移。
四个轮子按照辊子朝向分成两种类型:左旋轮和右旋轮。安装时必须遵守“前左、后右一种,前右、后左另一种”的交叉布局。如果装错,四个轮子的受力互相抵消,车只会原地发抖不动地方。这个小细节我在第一次装底盘时栽过跟头,后来用胶带在轮毂上标了A/B标记才彻底避免。
底盘运动可以分解成三个自由度:前后方向的线速度vx,左右方向的线速度vy,绕车体中心旋转的角速度omega。麦克纳姆轮的逆运动学,就是已知这三个速度,求出四个轮子各自的线速度,再换算成各电机的目标转速。
2.2 正逆运动学公式推导与代码实现
设底盘前轮距为L,左右轮距为W,车轮半径为r,四个轮子的线速度分别记为v1(前左)、v2(前右)、v3(后左)、v4(后右)。在常见的辊子布置下,逆运动学公式可以写作:
v1 = vx - vy - (L + W) * omega / 2
v2 = vx + vy + (L + W) * omega / 2
v3 = vx + vy - (L + W) * omega / 2
v4 = vx - vy + (L + W) * omega / 2
注意这个公式的符号不是唯一的,取决于你定义的轮子类型和坐标方向。比如有的资料把轮距相关项写成L+W的倍数,有的写成lx+ly,本质一样。我建议不要死记公式,而是实际推一遍,理解每个速度分量在四个轮子上的投影关系,调车时才能快速判断是不是符号搞反了。
代码实现就很简单了。我习惯把底盘控制写成一个独立的kinematic.c文件,统一管理运动学解算:
typedef struct { float vx; float vy; float omega; float v1; float v2; float v3; float v4; } ChassisSpeed; void inverseKinematic(ChassisSpeed *chassis) { float lw = (CHASSIS_L + CHASSIS_W) / 2.0f; chassis->v1 = chassis->vx - chassis->vy - lw * chassis->omega; chassis->v2 = chassis->vx + chassis->vy + lw * chassis->omega; chassis->v3 = chassis->vx + chassis->vy - lw * chassis->omega; chassis->v4 = chassis->vx - chassis->vy + lw * chassis->omega; }这里的v1到v4是轮子的线速度,还要除以车轮半径r才能得到角速度,或者直接在标定阶段把“轮子线速度”和“目标PWM/目标编码器计数”做成线性映射。我实际做的时候是在单位统一层面直接使用“速度百分比”作为全局变量,比如上位机下发vx=30表示30%最大速度,逆解后分配到的轮子速度也是百分比,测试起来非常直观。
3. 硬件接线与关键器件调试
3.1 STM32F103C8T6最小系统板引脚分配
STM32F103C8T6有PA0-PA15、PB0-PB15、PC13-PC15以及PD0/PD1,总共大约37个可用IO。四路PWM、四路编码器信号、两片TB6612的控制IO,加起来16个引脚左右,极限情况下BLE模块、OLED、按键再占几个,也勉强够用。
我当时的分配思路是:
- PWM输出:TIM3的CH1-CH4,对应PA6、PA7、PB0、PB1,四个通道刚好驱动两片TB6612的PWMA/PWMB
- 编码器A相:TIM4的CH1-CH4,对应PB6、PB7、PB8、PB9
- 编码器B相:TIM2的CH1-CH4,对应PA0-PA3
- 方向引脚:TB6612的AIN1/AIN2、BIN1/BIN2,共8个GPIO,随意分配在PC13-PC15和PA4、PA5等空闲引脚
这里有个坑要提醒:F103C8T6的PB3、PB4、PA13、PA14、PA15默认是JTAG调试引脚。如果这些引脚被配置为普通IO输出,必须在CubeMX里把调试接口改成SWD方式或者直接禁用JTAG,否则你会发现某个引脚怎么都拉不高拉不低。我第一次就是把PA15当方向控制脚,结果死活没输出,后来查了参考手册才反应过来。
3.2 电机驱动模块与编码器接线
TB6612FNG相比L298N体积小、压降低、不需要外部散热片,在小车场景里更合适。每个TB6612是一路双电机驱动,有AIN1/AIN2、BIN1/BIN2四个方向控制脚和PWMA/PWMB两个使能PWM脚。逻辑电源VM接电池电压,VCC接3.3V或5V,GND必须和STM32共地,这是很多人忽略的一点,不共地会导致PWM控制信号无参考电平,电机乱转甚至不转。
编码器接线也有讲究。TT电机霍尔编码器一般输出A、B两相正交信号,STM32定时器的编码器模式可以直接通过两相的上升沿和下降沿四倍频计数。需要注意的是,编码器输出的信号通常是开漏,要加10k上拉电阻到3.3V,或确认板子上已经做了上拉。我用的电机模块自带上拉,所以直接接IO就正常。
电机电源部分:STM32的板载3.3V稳压输出给逻辑电路用没问题,但绝对不能给电机供电。电机启动瞬间电流可能超过1A,如果和主控共用稳压源,电压跌落会直接导致STM32复位。我遇到过一次,小车一转就重启,后来把动力电源和逻辑电源彻底分开,再加了一个470uF电解电容在电机母线缓冲,才解决。
4. CubeMX工程配置与软件实现
4.1 定时器PWM与编码器模式配置
用CubeMX配置STM32F103C8T6的工程,基本几步:选择芯片型号、配置时钟树(HSE外部8M晶振,PLL倍频到72MHz)、按下表配置定时器和GPIO,然后生成代码。
编码器模式配置:TIM4的四个通道都设为Encoder Mode,计数器工作在TI1和TI2组合模式,这样四倍频输出。ARR要设成65535,编码器计数是双向的,计数溢出会自动翻转。配置完成后在代码里读CNT寄存器就能拿到带方向的累计脉冲。
void MX_TIM4_Init(void) { TIM_Encoder_InitTypeDef sEncoderConfig = {0}; sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING; sEncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC1Prescaler = 0; sEncoderConfig.IC2Prescaler = 0; sEncoderConfig.IC1Filter = 10; sEncoderConfig.IC2Filter = 10; HAL_TIM_Encoder_Init(&htim4, &sEncoderConfig); HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL); }PWM配置:TIM3的PWM频率我设成20kHz。之前用过1kHz,电机能转,但低速时电机会有明显啸叫,20kHz可以避开人耳敏感频段。如果你用的是某些MOS驱动或TB6612,太高的频率反而会增大开关损耗,建议从10kHz到20kHz之间调试。
4.2 PID闭环调速与主循环逻辑
速度闭环是整个软件的核心。每个轮子用增量式PID,目标是让编码器测出来的实际转速跟上运动学解算给出的目标转速。增量式的公式是:
delta_u = Kp * (e_now - e_last) + Ki * e_now + Kd * (e_now - 2 * e_last + e_last2)
这里的输出是PWM占空比的增量,累计到当前值上。我用10ms周期做一次控制,这个时间对STM32来说很宽裕。PID结构体定义和单次计算可以写成这样:
typedef struct { float target; float current; float err; float err_last; float err_last2; float kp; float ki; float kd; float output; float output_max; } PidObject; void pidCalculate(PidObject *pid, float target, float current, float dt) { pid->target = target; pid->current = current; pid->err = target - current; float delta = pid->kp * (pid->err - pid->err_last) + pid->ki * pid->err * dt + pid->kd * (pid->err - 2.0f * pid->err_last + pid->err_last2) / dt; pid->output += delta; if (pid->output > pid->output_max) pid->output = pid->output_max; if (pid->output < -pid->output_max) pid->output = -pid->output_max; pid->err_last2 = pid->err_last; pid->err_last = pid->err; }主循环里做的事情很机械:定时中断里读四个TIM的编码器CNT值,算出当前速度;把运动学解算的目标速度和当前速度一起丢给PID;把PID输出转成PWM占空比和方向信号,写入TIM3的CCR寄存器,同时设置对应的GPIO方向。整个过程不建议在主循环while里裸跑,用定时器或操作系统的软件定时器把它固定成周期任务,效果会好很多。
项目的实际表现,我调完PID后小车在地砖上跑直线,500线编码器配置下基本能压住偏差在正负2cm以内。横向平移时,因为辊子滚动摩擦损耗不一样,直行时最需要补偿的方向反而是沿底盘对角线,这个问题我最后是靠给四个轮子分别标定不同Kp值解决的。
5. 实测调试、避坑与常见问题速查
5.1 实际跑起来遇到的问题
第一类问题是运动学解算符号错误。表现为:给vx小车不走,给vy小车反而直行,或者四个轮子里有两个在打架。排查方法很简单:把omega设成0、vx设成非零,单独看四个轮子目标速度的分配,再用串口打印实际PID给定值,确认每个轮子是否按预期方向转。只要有一个轮子反向,就要赶紧去检查逆解公式里的加减号,以及电机方向控制IO是高电平转还是低电平转。
第二类问题是编码器测速值跳变。霍尔编码器在低速时脉冲间隔长,单次采样时间内可能只来几个脉冲,速度值看起来就一顿一顿。解决思路有两个:一是提高测速周期,比如从10ms改成20ms,二是用M法测速,也就是在固定时间内统计脉冲数,速度越低越需要统计时间越长。低速要求特别高的场景,还可以上定时器输入捕获去测量单脉冲的周期,但实现复杂度会明显上升。
第三类问题是PID振荡。电机高速时能听到轮子周期性“呜呜”声,或者静止时车体轻微抖动,通常就是P过大或者D过小。我调PID的习惯是先把Ki和Kd置零,只给Kp,从小到大加;等系统发生等幅振荡,再把Kp退回60%左右,然后逐渐加Kd消除超调;最后加适量Ki消除静差。整个过程大概需要半小时,用串口把目标速度和实际速度曲线打出来看,比肉眼看轮子转要靠谱得多。
5.2 常见问题速查表
调试中很多东西踩过一次就再也不犯,我整理成一张速查表,方便大家对照排查。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 车通电后轮子不转 | 共地缺失、PWM频率不对、芯片复位 | 检查GND是否连接,示波器看PWM波形 |
| 有的轮子正转有的反转 | 运动学符号错、方向IO电平接反 | 单独给每个轮子发固定PWM,确认方向 |
| 编码器数值始终为0 | 编码器模式未开启、A/B相接反、上拉缺失 | 读TIM_CNT寄存器,用手拨动轮子观察变化 |
| 小车直线跑偏 | 四轮机械误差、四个电机特性不一致 | 单独标定每个轮子的PID参数 |
| 电机低速吱吱响 | PWM频率设置过低 | 提高到15kHz以上 |
| 电池没电时速度变慢且跑歪 | 电池电压下降导致PWM输出非线性 | 加电压前馈,或改用较高电压电池 |
| 主控一跑电机就复位 | 电源共地不稳、电机母线电压跌落 | 动力与逻辑分开供电,加滤波电容 |
举一个我之前最头疼的例子:四轮编码器速度都有闭环,但小车从静止起步时总有一侧轮子先动,导致起步瞬间车身歪一下。查了好久,最后发现是四个电机的启动摩擦力矩不一致,PID的积分项又太弱,低占空比下电机还没转起来编码器脉冲接近为零,PID以为没到目标就持续加大输出,结果一侧先克服静摩擦冲出去。解决办法是给PID输出加一个基础占空比偏置,让所有电机在启动时都有一个小幅预驱动,起步就变得很干脆。
5.3 关于国产替代与后续扩展的补充
最后补一个很多人问过的点:STM32F103C8T6的国产替代型号,比如APM32F103、MM32F103、GD32F103,在引脚定义上是兼容的,CubeMX选型时可以直接选对应型号,或者先按STM32生成工程再移植HAL头文件。项目里实测过国产MCU在编码器模式、PWM输出这类外设上的寄存器兼容性没有问题,价格也低很多,学生打样或者批量课设完全可用。
如果要做成完整的课程设计或者竞赛作品,可以在现在这套系统上挂一个无线模块,比如ESP8266或NRF24L01,配合上位机发期望速度;再往下就是加惯性测量单元做底盘姿态闭环,或者接激光雷达做自动导航。麦克纳姆轮底盘的优势会在这些场景里更加突出。
我在实际调车过程中还有一个很深的体会,就是别迷信参数仿真的效果。底盘机械间隙、轮子磨损、地面摩擦系数,这些因素在仿真里根本不存在。最好的方式是先把某个轮子的PID调通,再复制到其他轮子,最后通过整车直线和斜向测试做微调。每个轮子的参数可能有10%-20%的差异,这个很正常,属于机械系统本身的误差容限,不必强迫四个轮子参数完全一样。
本文还有配套的精品资源,点击获取