☰
STM32F103驱动麦克纳姆轮小车:运动学解算与PID闭环实现要点
2026/10/15 14:34:06 网站建设 项目流程

简介:面向STM32F103C8T6的麦克纳姆轮小车完整工程,适用于嵌入式竞赛、课程设计及个人DIY,是四轮全向移动平台的常用参考方案。工程集成Altium Designer原理图与PCB源文件、单片机C代码与启动文件,支持蓝牙及ESP8266 WiFi遥控,可驱动四个麦克纳姆轮实现直行、横移、原地旋转等全向运动组合。压缩包共128个文件,仅3.77MB,以33个头文件、32个C源文件为主,覆盖STM32标准外设库与用户逻辑;同时包含AD设计文档、Gerber制造文件、hex固件、Keil工程及批处理脚本,涵盖硬件设计、固件源码与编译输出,便于按模块复用。目前已有3887人学习/下载,尤其适合需要快速搭建全向底盘原型、验证无线遥控逻辑的学习者。资源从原理图到固件一体齐全,AD工程可直接修改并导出Gerber打样,固件基于标准外设库并集成定时器、PWM、串口等底层驱动,方便移植到其他STM32F103系列,也可扩展摄像头、传感器等模块。

1. 麦克纳姆轮小车:为什么用F103一片就能把全向移动跑起来

四个轮子滚动的方向明明都朝前,车身却能横着平移、原地转圈、斜着45度出库,这是麦克纳姆轮小车最让人「眼前一亮」的地方。它的本质工作和普通差速小车完全不同:差速车只能前进后退加转弯,麦克纳姆轮车有3个自由度——前后、左右、自转,只要稍微学过一点向量分解,就能理解四个轮子是如何「配合着跑」出这些动作的。

我见过不少人在这个项目上投入的第一块主控是STM32F103C8T6,原因是它便宜、资料多、一个芯片能把PWM输出、编码器采集、遥控解析、运动学解算全部包下。72MHz主频听起来不高,但做一辆四轮麦克纳姆轮小车其实绰绰有余:真正的瓶颈从来不是算力,而是定时器资源分配和运动学公式的符号方向。这篇笔记按照「原理 → 硬件 → 软件 → 避坑 → 进阶」的顺序展开,适合做毕设、电子设计竞赛、仓库搬运Demo的开发者参考,尤其是那些第一次接触全向底盘、想从「能转」走到「能走直线」的人。

2. 麦克纳姆轮的运动学:先搞懂四轮怎么配合,再谈代码

麦克纳姆轮小车最核心的智力工作,是把车身的期望速度(vx、vy、wz)转换成四个轮子的各自转速。很多人直接抄公式、跑起来发现车不走直线,回头改了半天电机方向,最后发现是坐标系约定没统一。这一章先把轮子的力学原理和布局讲清楚,再给出可以直接用的逆解代码。

2.1 45度辊子如何产生横向力

一个麦克纳姆轮,外圈是一排可以自由转动的辊子,辊子轴线与轮子的旋转平面呈约45度夹角。当电机驱动轮子旋转时,轮子与地面接触的辊子并不会沿着车轮前进方向滑动,而是沿着自己的轴线方向产生摩擦力。这个摩擦力分解到车体坐标系里,就同时产生了纵向和横向两个分量。换一句话说:一个麦克纳姆轮在转的时候,实际上是在「斜着推地面」。

四个轮子的辊子方向如果相同,横向分量会互相抵消,车只能像普通轮子一样走直线。要让横向分量叠加起来,必须让左右两侧的轮子辊子方向对称布置,并使前后轮辊子方向相反。这就是麦克纳姆轮必须成对安装的根本原因。所有后续的运动学公式,本质上都是在做「这四个斜向推力的向量合成」。

安装轮子之前,先看轮子侧面的标签或者辊子倾斜方向:通常厂家会把轮子分A轮和B轮,A轮的辊子从轮子正面看向左上方倾斜,B轮向右上方倾斜。如果轮子没有明确标识,把轮子放在地上用手推,观察它向哪个方向侧滑,也是非常可靠的判断方式。

2.2 四轮布局:A轮B轮的安装规律

麦克纳姆轮底盘的安装规律只有一条,但很多人装错:左前轮和右后轮用同一种旋向,右前轮和左后轮用另一种旋向,也就是说四个轮子在地面上呈X形对称。具体来说,假设你面对小车头部,左前是A轮,那么右前装B轮,左后装B轮,右后装A轮。前后相邻的两个轮子旋向必须相反,左右两侧同轴位置的轮子旋向也必须相反。

装反的典型后果是:小车轮子全部转动正常,但一给前进指令,车就在原地打转或者画一个很大的圆弧。这是因为四个轮子的横向分量没有互相抵消,反而合成出了一个净旋转力矩。遇到这种「推着不走、原地转圈」的情况,第一反应不要改代码,先去检查轮子布局。

以下是我常用的快速核对表,装完轮子按这个表自查一遍:

轮位旋向备注
左前A与右后相同
右前B与左后相同
左后B与右前相同
右后A与左前相同

旋向定义以轮子从车头方向看去、辊子朝左上为A。不同厂家的标签命名不一定统一,但布局规律是通用的。

2.3 运动学逆解:从期望速度到四轮转速

设定车体坐标系:x轴向前,y轴向左,z轴垂直向上,逆时针旋转为正。四个轮子按顺序命名为1号轮(左前)、2号轮(右前)、3号轮(左后)、4号轮(右后),每个轮子的线速度记为v1到v4。已知车体的三个期望通道vx(前后速度)、vy(左右速度)、wz(自转角速度),逆解公式如下:

// 输入:vx, vy, wz 为车体期望速度,单位自行统一即可 // lx, ly:轮子中心到车体中心的纵向/横向距离 float v1 = vx - vy - (lx + ly) * wz; // 左前轮 float v2 = vx + vy - (lx + ly) * wz; // 右前轮 float v3 = vx + vy + (lx + ly) * wz; // 左后轮 float v4 = vx - vy + (lx + ly) * wz; // 右后轮

这段代码里的符号对应关系非常严格:vx项的符号四个轮子全为正,vy项左前左后为负、右前右后为正,wz项前两轮为负、后两轮为正。如果你的轮子布局和上面一致,这个公式可以直接用。如果某一个轮子的实际安装旋向与假设相反,对应变量的符号就要取反。

这里算出来的是每个轮子的线速度,单位与vx保持一致。如果要把线速度换算成电机的目标转速,还需要除以轮子半径,再乘以减速比。实际工程中我一般先不换算,直接在代码里用一个统一的比例系数把线速度映射到PWM占空比附近,等编码器闭环做好之后再替换成正规的「m/s 转 r/min」换算。

2.4 运动学正解:用编码器反推车身速度

逆解负责「下发指令」,正解负责「反馈状态」。正解是从四个轮子的实际转速推算车体现在实际在往哪个方向移动、转得多快。虽然F103的资源做四轮编码器有点紧张,但至少两个编码器就能做部分正解验证。正解公式是逆解的逆过程:

// 已知四个轮子的线速度 v1-v4,求车体速度 float vx = (v1 + v2 + v3 + v4) / 4.0f; float vy = (-v1 + v2 + v3 - v4) / 4.0f; float wz = (-v1 - v2 + v3 + v4) / (4.0f * (lx + ly));

实际调试的时候,我通常用小车的遥控手柄给一个纯vx前进指令,然后打印正解出来的vy——如果vy接近0,说明运动学符号正确;如果vy明显不为0,说明某个轮子的符号反了。这个验证手段比肉眼观察走不走直线要客观得多,因为走直线会受到地面不平、轮胎打滑的影响,而正解反映的是运动学本身的正确性。

3. 硬件选型与接线:把F103的定时器资源算清楚再动手

麦克纳姆轮小车不是「原理图越复杂越好」,而是「资源分配越清爽越好」。F103C8T6的定时器数量有限:TIM1是高级定时器,TIM2、TIM3、TIM4是通用定时器,不像F103ZET6那样有TIM5到TIM8可以用。硬件方案的第一步,是先想好哪些定时器做PWM、哪些做编码器接口,避免画完板才发现通道冲突。

3.1 器件清单与选型理由

主控选择STM32F103C8T6,常见蓝板或者自己画的板子都可以,核心是要引出足够的定时器通道引脚。电机驱动推荐TB6612FNG,原因很简单:逻辑电压支持3.3V,可以直接由F103的GPIO驱动,不需要额外电平转换;输出能力单路1.2A,带常见的N20减速电机或365减速电机绰绰有余。比L298N发热小、体积小,是麦克纳姆轮小车最常用的搭档。

电机选择带霍尔编码器的减速电机,常见的是12V N20减速电机或微型行星减速电机。编码器通常输出两路正交信号(A相B相),用来做速度闭环。轮子直接买标准的麦克纳姆轮,直径一般在60mm到80mm之间,注意辊子材质:橡胶辊抓地力好但磨损快,硬塑料辊寿命长但在光滑地板上容易打滑,建议第一辆车用橡胶辊,调试期更省心。

供电方案是新手最容易翻车的点。电机和主控尽量不要共用同一路LDO,电机启动瞬间电流可能到2A以上,会把3.3V拉垮导致单片机复位。常见做法是:电池直接给驱动板VM供电,F103通过单独的降压模块供电,两路电源在驱动板的地端单点共地。

3.2 F103定时器资源分配

以「四路PWM输出 + 两路编码器反馈」的典型方案为例,这是我做这种小车最常用的分配方式:

定时器用途引脚
TIM1四路PWM输出CH1: PA8, CH2: PA9, CH3: PA10, CH4: PA11
TIM2左轮编码器CH1: PA0, CH2: PA1
TIM4右轮编码器CH1: PB6, CH2: PB7

TIM3在这个配置里空出来,可以留着做超声波或者舵机控制,也可以作为备用PWM。如果将来要加蓝牙遥控,串口用PA2/PA3即可,与上述资源不冲突。如果你用USB下载程序,PA11恰好与USB的DM引脚复用,注意调试时不要让USB和TIM1的CH4同时争抢引脚。

四轮独立PWM意味着四个电机各自调速,但是编码器资源不够四路,怎么办?工程上的折中做法是左右各装一路编码器,同侧两个电机共用同一路PWM占空比,并让同侧电机之间通过机械结构保持同步。麦克纳姆轮车的同侧两个轮子在直线和自转工况下受力接近,这种「两路闭环、四路输出」的方案在实际项目中已经足够稳定。

3.3 接线定义与供电拓扑

TB6612驱动模块的接线要逐一核对:VM接电池正极,VCC接3.3V逻辑电源,GND与主控共地。AIN1/AIN2与BIN1/BIN2接F103的GPIO,用来控制两个电机正反转;PWMA/PWMB接PWM信号。如果用了两片TB6612驱动四个电机,就需要4路PWM和8路方向控制GPIO。

方向的GPIO分配随意,但建议养成固定习惯:左前轮和左后轮使用同一片驱动,右前轮右后轮使用另一片,这样调试的时候「左侧不走」这种问题可以直接锁定到单片驱动。四个电机的编码器A相B相分别接TIM2和TIM4的CH1/CH2,注意A相B相不能接反,否则编码器数值会是逆着计数方向变化,速度反馈符号不对。

供电拓扑方面,我给一个经过验证的自检顺序:先把主控单独上电,确认下载正常;再把电机电源接上,先不插电机,确认驱动板VM端电压正常;最后才接电机端子。三步分开做,可以避免「一接电机就复位」时还要拆线排查。

4. 软件实现:从PWM初始化到PID闭环的完整路径

软件部分要做到「电机动起来容易,动得稳需要闭环」。这一章给出可直接抄的代码骨架:PWM输出、编码器读取、PID速度环、以及把遥控指令换算成四轮转速的主循环。每一段代码都按F103标准外设库写,方便移植到HAL库。

4.1 TIM1四路PWM初始化

TIM1是高级定时器,和普通定时器相比多了一个重复计数器和刹车功能。用高级定时器输出PWM时,最容易忘记的是把RCR设置为0,否则PWM输出频率会被分频,电机转速看起来「一顿一顿」的。初始化代码如下:

// TIM1 四路PWM输出初始化,输出引脚 PA8/PA9/PA10/PA11 void TIM1_PWM_Init(uint16_t arr, uint16_t psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装值,决定PWM周期 TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频,72MHz除以(psc+1) TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; // 高级定时器必须设为0 TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比为0,电机不转 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC2Init(TIM1, &TIM_OCInitStructure); TIM_OC3Init(TIM1, &TIM_OCInitStructure); TIM_OC4Init(TIM1, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC4PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); // MOE主输出使能,高级定时器必须开 TIM_Cmd(TIM1, ENABLE); }

这里的arr和psc需要重点说明。如果psc取71,则定时器时钟为72MHz除以72等于1MHz。如果arr取999,则PWM频率为1MHz除以1000等于1kHz。电机驱动的PWM频率在1kHz到20kHz之间都常见,我习惯先用10kHz(psc=71, arr=7200-1),听不到明显啸叫,TB6612在这种频率下的开关损耗也比较低。arr预留大一点,相当于占空比分辨率更高,控制更细腻。

4.2 编码器接口初始化与读取

编码器模式是STM32定时器的一个隐藏功能,不需要外部中断,硬件自动根据A相B相信号增减计数。初始化的关键是把两个输入通道都配置为编码器模式,并开启两个通道的输入滤波。代码如下:

// TIM2 编码器模式初始化,用于左侧电机测速,引脚 PA0/PA1 void TIM2_Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 16位计数器满量程 TIM_TimeBaseStructure.TIM_Prescaler = 0; // 编码器计数不额外分频 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0C; // 输入滤波,滤除毛刺 TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_ClearFlag(TIM2, TIM_FLAG_Update); TIM_SetCounter(TIM2, 0); TIM_Cmd(TIM2, ENABLE); } // 读取最近一个周期内的脉冲增量 int16_t Encoder_Get_Count(TIM_TypeDef* TIMx) { int16_t cnt = (int16_t)TIMx->CNT; // 转换成有符号数,支持正反转 TIMx->CNT = 0; // 读完后清零,准备下一周期累计 return cnt; }

读取编码器的周期决定了速度环的采样频率。我一般在定时器中断里每10ms读一次,也就是100Hz的反馈频率。这个频率对麦克纳姆轮小车足够了:频率太高测速脉冲数太少,速度反馈会剧烈跳动;频率太低PID响应变慢,车会「肉」。10ms周期下,如果每个周期脉冲增量只有个位数,可以调大测速周期到20ms,或者检查编码器接线是否可靠。

4.3 位置式PID与增量式PID的选择

麦克纳姆轮小车的速度控制,直接用增量式PID最省事。它不需要累计误差,输出的是每次的修正量,天然抗积分饱和。控制周期固定为10ms,速度目标值由运动学逆解给出,反馈值由编码器增量换算出来。代码骨架如下:

typedef struct { float kp, ki, kd; int16_t last_err; float integral; } PID_TypeDef; // 增量式PID,输出为本次PWM占空比的增量 int16_t PID_Inc_Calc(PID_TypeDef* pid, int16_t ref, int16_t fb) { int16_t err = ref - fb; float inc = pid->kp * (err - pid->last_err) + pid->ki * err + pid->kd * (err - 2 * pid->last_err + pid->last_err); pid->last_err = err; return (int16_t)inc; }

我这里故意写了一个微变形的增量式PID,其中微分项的写法是把前后两次误差变化再次做差,起到一定的滤波作用。实际使用时,如果发现速度波动大,可以把kd直接改为0,因为编码器测速本身已经带了高频噪声,微分项容易放大噪声。

PID参数整定顺序有讲究。先把ki和kd置零,只给kp,从小到大加。观察小车的阶跃响应:如果kp太小,车缓慢加速、到达目标速度很慢;如果kp过大,电机会发出「嗡嗡」声,速度来回震荡。找到临界震荡的kp值后,取它的60%作为工作kp,再从小到大加ki消除稳态误差。kd对于编码器反馈的系统通常不需要,除非你的机械结构刚性很差。

4.4 主循环:遥控指令到四轮转速的完整链路

主程序的结构很简单:接收遥控指令得到vx、vy、wz,经过运动学逆解得到四个轮子的目标线速度,再乘以一个比例系数转成PWM目标值。每个控制周期里,用编码器实际速度做PID修正,叠加上一时刻的PWM占空比,得到新的占空比输出。核心代码如下:

// 每10ms执行一次的控制任务 void Control_Task(void) { // 1. 获取遥控目标速度,来自串口蓝牙或者板载摇杆 float vx = remote.vx; // 前后速度,前进为正 float vy = remote.vy; // 左右速度,向左为正 float wz = remote.wz; // 自转角速度,逆时针为正 // 2. 运动学逆解,得到四轮目标线速度 float v1 = vx - vy - (lx + ly) * wz; float v2 = vx + vy - (lx + ly) * wz; float v3 = vx + vy + (lx + ly) * wz; float v4 = vx - vy + (lx + ly) * wz; // 3. 线速度映射到PWM目标值,scale为实测标定系数 pid_ref[0] = (int16_t)(v1 * scale); pid_ref[1] = (int16_t)(v2 * scale); pid_ref[2] = (int16_t)(v3 * scale); pid_ref[3] = (int16_t)(v4 * scale); // 4. 左侧电机编码器反馈,右轮同理 int16_t fb_left = Encoder_Get_Count(TIM2); int16_t fb_right = Encoder_Get_Count(TIM4); // 5. 增量式PID,输出占空比增量,再叠加到原占空比 pwm_duty[0] += PID_Inc_Calc(&pid_left, pid_ref[0], fb_left); pwm_duty[1] += PID_Inc_Calc(&pid_right, pid_ref[1], fb_right); pwm_duty[2] = pwm_duty[0]; // 同侧轮共用占空比 pwm_duty[3] = pwm_duty[1]; // 6. 占空比限幅,并写回TIM1四个通道 for (int i = 0; i < 4; i++) { if (pwm_duty[i] > PWM_MAX) pwm_duty[i] = PWM_MAX; if (pwm_duty[i] < -PWM_MAX) pwm_duty[i] = -PWM_MAX; TIM_SetCompare1(TIM1, abs(pwm_duty[i])); // 通过GPIO设置方向,占空比绝对值作为速度 } }

这段代码里的scale是需要实际标定的量。因为编码器脉冲数和轮子线速度之间隔着减速比、轮径、编码器线数等多个因素,最靠谱的做法是给一个固定PWM,让车走一段距离,测量实际位移,反推scale。这一步不追求一次精确,可以先用一个差不多的值让车动起来,再慢慢修正。

方向控制方面需要单独说明:PWM占空比只代表速度大小,方向由驱动板的IN1/IN2引脚电平决定。代码里我用了负占空比表示反转,实际输出时需要把负值映射到方向和正占空比上。如果这一步处理不好,会出现「给了负速度车却继续正转」的奇怪现象,在避坑章节会专门展开。

5. 麦克纳姆轮小车常见问题排查:现象、原因与解决办法

这个项目的问题往往不是出在代码逻辑,而是出在「符号」和「机械装配」。我把这几类问题按出现频率排序写出来,每一条都是实操中真实遇到过的,按「现象 → 原因 → 解决」的结构,方便你对照排查。

5.1 轮子装反导致小车原地画圈

现象:四个电机的方向代码确认无误,但前进指令一下发,小车不走直线,而是在原地打转或者车身斜着往一侧冲出去。轮子的转动方向看起来也正常,换了好几个正负号都没用。

原因:麦克纳姆轮的旋向装错了。左前和右后必须同旋向,右前和左后必须同旋向。如果左前装了A轮、右后装了B轮,横向力分量就会合成出一个旋转力矩。

解决:先把运动学逆解代码里的四轮目标速度打印出来,用手按住小车让轮子悬空,确认每个轮子是否按代码给定的方向转动。然后对比第2.2节的布局表,拆下错位的轮子重新安装。注意:轮子的旋向标签有时会印在轮毂内侧,拆下来才能看到,别只看外侧就下结论。

5.2 电机正负方向与PWM符号不一致

现象:给前进指令的时候,左前轮正转、右前轮反转,车身斜着走。用PID控制以后,某个轮子速度反馈值和目标值始终差一个符号,导致这个轮子越转越快,最终PWM顶到限幅值。

原因:编码器A相B相接反,或者电机两根动力线接反,导致「PWM正占空比 → 电机正转」的对应关系与编码器计数方向不匹配。增量式PID只修正误差,如果误差符号一直是反的,它就会不断往错误方向积分。

解决:逐个轮子单独测试。用一个固定PWM让某个轮子转起来,观察编码器计数是正还是负。如果目标是正转但计数为负,交换这个电机编码器的A相和B相。这个操作比在代码里把PID输出取反更干净,因为运动学公式不需要再改符号,且后续调试更直观。

5.3 速度环震荡,电机高频嗡鸣

现象:速度闭环一使能,电机在高转速下发出一阵高频嗡鸣,车身也跟着抖。把kp调小之后嗡鸣消失,但车起步变得很迟钝,目标速度变化半天才跟上。

原因:kp偏大导致速度环振荡,叠加编码器测速噪声后被进一步放大。另一个常见原因是控制周期不固定:如果在主循环里直接做PID,循环耗时随程序复杂度波动,PID参数在不同频率下表现完全不同。

解决:把速度环放进固定10ms定时器中断里,保证控制周期稳定。整定参数时先只保留kp,逐步加大到临界振荡,再取60%左右作为工作值。如果震荡依旧,把kd设为0,或者增加编码器输入滤波的TIM_ICFilter值,从0x0C改到0x1F,滤掉更多高频毛刺。

5.4 电压跌落导致上电正常、一跑就复位

现象:小车静止时程序正常,蓝牙遥控也连得上。一旦四个电机同时启动加速,单片机立刻复位,串口打印重新出现启动信息,轮子停转。

原因:电机启动瞬间电流打到2A以上,而主控和电机共用稳压电源,瞬间压降把3.3V拉低到了复位阈值以下。F103的复位电压大约在1.8V左右,但买来的降压模块在瞬态响应上参差不齐,很多便宜的LDO在这种冲击下直接掉电。

解决:把主控供电和电机供电完全分开,电池正极分两路:一路直接进驱动板VM,另一路经过独立的降压模块给F103供电,两者仅在电池负极处共地。如果共地后仍有复位,在降压模块输出端并联一个470uF电解电容,能显著改善瞬态响应。

5.5 自转指令让车身平移而不是旋转

现象:给定纯wz自转指令,小车不应该左右移动,但实际跑起来车身会往侧向漂移,像是「边转边跑」。前进和左右平移两种模式倒是正常的。

原因:运动学逆解里的wz项符号在四个轮子上不匹配。常见的情况是,四个轮子的运动学公式抄自不同的坐标系约定,前两轮和后两轮的wz符号反了。前进跟随不受影响,但自转合成时,vx和vy没有被完全抵消,产生了平移分量。

解决:单独给一个纯wz指令,让车在半空中空转(四轮悬空),用另一套正解公式打印四个轮子的实际线速度。理论上四个轮子速度大小相等、前两轮与后两轮转向相反。把实际值和理论值逐一对比,就能锁定是哪个轮子的符号出了问题。检查时别忘了把lx+ly这个因子也带进去算一遍,常见错误是忘乘这个距离项。

6. 进阶调试技巧:三招验证运动学正确性,避免越调越乱

麦克纳姆轮小车一旦能跑,后面的工作全是「验证」和「打磨」。这里分享三个我每次组装新底盘都会先做的验证动作,每一个都比直接试跑更高效。

第一招是「单通道测试」。让遥控只给vx前进,其他两个通道强制为0,然后用正解公式打印vy和wz的实际推算值。正确的运动学模型下,vy应该近似0,wz接近0。如果vy明显不是0,说明某个轮子的vy符号错了;如果wz不为0,说明wz符号错了。这个测试把三个自由度拆开验证,定位错误的速度比整体试跑快得多。按同样的方法再单独给vy、单独给wz,三个通道各验证一遍,运动学基本就稳了。

第二招是「对称性测试」。缓慢匀速推进小车前进半米,记录四个轮子的编码器累计值。在理想情况下,左前和右前的累计值应该非常接近,左后和右后的累计值也应该接近。如果左右某一对差异超过10%,说明那个轮子的机械负载不对称——可能是轮子辊子磨损、电机减速比不一致,或者某侧的轴承阻力偏大。这一步能提前发现机械问题,避免后期把机械问题误判为PID问题。

第三招是「固定占空比标定」。不启用PID,直接给电机一个固定的PWM占空比,让小车直线走2米,测量实际位移和方向偏移,反复三次取平均。这样得到「PWM→线速度」的实际映射关系,用来反推scale参数。这一步做完再上闭环,PID的初值就不用瞎猜,调参时间至少缩短一半。我习惯把标定结果记在本子上,因为换轮子、换电池电芯都会影响这个值。

麦克纳姆轮小车的调试核心是「先验证机械和运动学,最后才调PID」。很多人一上来就调参数,结果车不走直线,还以为是PID没调好,改了大半天也没效果。把轮子布局、电机方向、运动学符号这三件事验证清楚,PID差一点小车也能跑得像模像样;这三件事有一个错,PID再精确也救不回来。

最后提醒一句:编码器测速周期和PID计算周期必须一致,中断里别放延时函数,看到轮子抖动先怀疑机械或电源,而不是怀疑算法。希望这篇笔记能帮你少走几个弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询