☰
STM32F103C8T6麦克纳姆轮全向小车速度合成与闭环控制实战
2026/10/20 23:32:56 网站建设 项目流程

简介:这份源代码面向学习STM32嵌入式开发与全向移动机器人控制的学生及爱好者,针对麦克纳姆轮智能小车实现各个方向运动这一典型实验场景,提供可直接编译运行的完整工程。程序基于STM32F103C8T6处理器,配合L293D电机驱动芯片与TT直流减速电机,并引入1602液晶进行状态显示,覆盖前进、后退、横移、斜行及原地旋转等全向运动控制逻辑,作者已在实物小车上亲测可用。压缩包共202个文件,约3MB,以c与h源码、s汇编启动文件、o目标文件、d依赖文件及crf编译记录为主,另含uvproj工程文件、hex烧录文件与map映射文件,工程结构完整,便于在Keil4中直接打开调试。目前已有3040人学习下载,适合作为课程设计、竞赛备赛或嵌入式入门练手参考,读者可据此理解麦克纳姆轮运动学分解与电机PWM调速的配合方式,并在此基础上扩展循迹、避障等功能。

1. 麦克纳姆轮小车为什么在 STM32F103C8T6 上最容易翻车

很多人第一次做全向小车,脑子里想的是「四个轮子独立转,想往哪走就往哪走」,代码写出来却发现小车要么原地打转,要么斜着走的时候抖得像筛糠。问题不在电机,也不在轮子,而在速度合成这一步没做对。麦克纳姆轮的核心是辊子与地面成 45 度,四个轮子的转速必须按特定比例合成,才能让车体在平面内做任意方向平移加自转。STM32F103C8T6 这颗芯片资源刚好够用:72MHz 主频、3 个通用定时器出 PWM、2 个编码器接口、CAN 或串口留给上位机,价格和生态都适合做这类运动控制 Demo。这篇笔记面向已经能点灯、能出 PWM、但还没把全向运动跑顺的嵌入式开发者,把「四个轮子怎么转」这件事从公式到代码到调参讲透,照着改就能让小车走直线、走斜线、原地转。

2. 麦克纳姆轮速度分解:从车体速度到四个轮子的目标转速

2.1 辊子 45 度带来的速度约束

麦克纳姆轮和普通轮最大的区别是轮缘上有一圈可以自由转动的辊子,辊子轴线与轮子平面成 45 度。当轮子绕自身轴转动时,地面给辊子的反作用力可以分解成沿车体前进方向和垂直方向两个分量。四个轮子对称安装,左前和右后是一对,右前和左后是另一对,辊子方向镜像。这样四个轮子的速度矢量叠加后,车体在 X、Y 两个方向上的平移和绕 Z 轴的旋转可以独立控制。

常见做法是把车体坐标系定义成:X 轴指向车头正前方,Y 轴指向车体左侧,Z 轴向上。四个轮子编号按逆时针:左前 LF、右前 RF、右后 RR、左后 LR。每个轮子的线速度记作 v_LF、v_RF、v_RR、v_LR,单位是米每秒,正方向定义为轮子驱动车体前进时对应的转向。

2.2 速度合成公式与矩阵形式

设车体前进速度为 vx,左移速度为 vy,逆时针自转角速度为 ω,车体中心到轮子接地点在 X 和 Y 方向的距离分别为 a 和 b。对于常见的矩形底盘,a 是半轴距,b 是半轮距。四个轮子的目标线速度满足:

v_LF = vx - vy - ω * (a + b) v_RF = vx + vy - ω * (a + b) v_RR = vx - vy + ω * (a + b) v_LR = vx + vy + ω * (a + b)

这个公式的推导思路是:每个轮子的速度等于车体平移速度在该轮子方向上的投影,加上自转带来的切向速度。辊子 45 度让投影系数变成 1 和 -1 的组合,所以公式里只有加减。实际写代码时不用每次推,直接把这个矩阵抄进函数即可。

注意:不同安装方式(辊子方向、轮子编号顺序)会导致符号相反。如果按公式写出来小车走反了,先别改公式,把某个轮子的符号取反试一下,大概率是安装镜像没对上。

2.3 从线速度到 PWM 占空比的换算

STM32F103C8T6 的定时器输出 PWM 控制电机驱动器,驱动器再驱动直流减速电机。轮子线速度和 PWM 占空比之间不是线性关系,但在一段范围内可以近似线性。常见做法是标定两个点:占空比 0 时轮子不转,占空比 100% 时轮子达到最大线速度 v_max。然后按比例换算:

// 假设 v_max 是实测最大线速度,单位 m/s // duty 范围 0~1000,对应定时器比较值 int16_t speed_to_duty(float v, float v_max) { float duty = v / v_max * 1000.0f; if (duty > 1000.0f) duty = 1000.0f; if (duty < -1000.0f) duty = -1000.0f; return (int16_t)duty; }

参数说明:v_max 需要实测,方法是在空载情况下给 100% 占空比,用秒表量轮子转一圈的时间和轮子周长,算出线速度。这个值受电池电压影响,电压下降 v_max 会变小,所以要么做电压补偿,要么在代码里留一个可调系数。

3. 在 STM32F103C8T6 上把四路 PWM 和编码器接口配起来

3.1 定时器资源分配与引脚选择

STM32F103C8T6 有 4 个通用定时器,其中 TIM1 是高级定时器,TIM2、TIM3、TIM4 是通用定时器。常见分配方案是:TIM1 的四个通道出四路 PWM 给电机驱动器,TIM2 和 TIM3 做编码器接口读两个轮子的编码器,TIM4 做另一个编码器或者做系统滴答。如果编码器只有两个,可以用 TIM2 和 TIM3 分别读左前和右前,后轮用霍尔或者不装编码器做开环。

PWM 频率建议设在 10kHz 到 20kHz 之间。太低电机会有啸叫,太高驱动器开关损耗大。TIM1 挂在 APB2 总线上,时钟 72MHz,预分频设为 0,自动重装载值设为 3599,这样 PWM 频率是 72MHz / 3600 = 20kHz。

// TIM1 PWM 初始化,四通道 void PWM_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE); // PA8 PA9 PA10 PA11 作为 TIM1 四通道输出 gpio.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); tim.TIM_Prescaler = 0; tim.TIM_Period = 3599; // 20kHz tim.TIM_ClockDivision = 0; tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &tim); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 0; oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM1, &oc); TIM_OC2Init(TIM1, &oc); TIM_OC3Init(TIM1, &oc); TIM_OC4Init(TIM1, &oc); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }

逻辑说明:四路 PWM 共用同一个定时器,频率一致,占空比独立。TIM_CtrlPWMOutputs 是高级定时器特有的,必须调用否则没有输出。参数 TIM_Period 决定频率,改这个值要同步改占空比换算里的 1000,保持比例一致。

3.2 编码器接口模式读轮速

编码器接口模式利用定时器的两个输入通道自动计数,不需要中断,CPU 开销小。以 TIM2 为例,PA0 和 PA1 接编码器 A 相和 B 相。

void Encoder_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_ICInitTypeDef ic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); tim.TIM_Prescaler = 0; tim.TIM_Period = 65535; tim.TIM_ClockDivision = TIM_CKD_DIV1; tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &tim); ic.TIM_Channel = TIM_Channel_1; ic.TIM_ICPolarity = TIM_ICPolarity_Rising; ic.TIM_ICSelection = TIM_ICSelection_DirectTI; ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; ic.TIM_ICFilter = 0x0F; TIM_ICInit(TIM2, &ic); ic.TIM_Channel = TIM_Channel_2; TIM_ICInit(TIM2, &ic); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_Cmd(TIM2, ENABLE); }

逻辑说明:TIM_EncoderMode_TI12 表示两个通道都计数,四倍频。读速度时每隔固定周期读一次计数器,做差分。注意计数器是 16 位,会溢出,差分时要处理回绕。参数 TIM_ICFilter 是输入滤波,电机干扰大时可以调大,但太大会丢脉冲。

3.3 速度闭环:增量式 PI 与积分限幅

开环跑全向运动,电池电压一降,走出来的斜线就偏。加编码器做闭环是常见做法。每个轮子一个独立的 PI 控制器,输入是目标线速度,反馈是编码器算出来的实际线速度,输出是 PWM 占空比。

typedef struct { float kp; float ki; float integral; float integral_max; float target; float actual; float output; } PI_Controller; float PI_Update(PI_Controller *c, float target, float actual, float dt) { float error = target - actual; c->integral += error * dt; // 积分限幅,防止饱和后恢复慢 if (c->integral > c->integral_max) c->integral = c->integral_max; if (c->integral < -c->integral_max) c->integral = -c->integral_max; c->output = c->kp * error + c->ki * c->integral; return c->output; }

参数说明:kp 决定响应速度,太大会震荡,太小走不到位。ki 消除稳态误差,但太大会积分饱和。积分限幅 integral_max 一般取输出上限的 0.5 到 1 倍。dt 是调用周期,建议 5ms 到 10ms,和编码器采样周期一致。

4. 全向运动模式实现:走斜线、横移、原地转的代码怎么写

4.1 车体速度指令到四轮目标的封装

把第 2 章的公式封装成一个函数,输入 vx、vy、omega,输出四个轮子的目标线速度。这个函数是上层运动模式和底层电机控制之间的接口。

typedef struct { float vx; // 前进速度 m/s float vy; // 左移速度 m/s float omega; // 逆时针角速度 rad/s } Chassis_Speed; typedef struct { float lf; float rf; float rr; float lr; } Wheel_Speed; void Chassis_To_Wheel(Chassis_Speed *chassis, Wheel_Speed *wheel, float a, float b) { float k = a + b; wheel->lf = chassis->vx - chassis->vy - chassis->omega * k; wheel->rf = chassis->vx + chassis->vy - chassis->omega * k; wheel->rr = chassis->vx - chassis->vy + chassis->omega * k; wheel->lr = chassis->vx + chassis->vy + chassis->omega * k; }

逻辑说明:a 和 b 是底盘几何参数,单位米。这个函数每 5ms 调用一次,输入来自遥控器或上位机。输出送给四个 PI 控制器。注意 omega 的单位是 rad/s,如果遥控器给的是角度,要先转弧度。

4.2 八种典型运动模式的参数表

实际调试时不需要每次算,把常用模式做成预设,直接填 vx、vy、omega 三个值。下面这张表是实测能跑通的参数,v 是最大线速度的百分比,omega 是最大角速度的百分比。

运动模式vxvyomega说明
前进1.000四轮同向
后退-1.000四轮反向
左移01.00左前左后反转
右移0-1.00右前右后反转
左前斜行0.7070.7070合成方向 45 度
右前斜行0.707-0.7070合成方向 -45 度
原地左转001.0左右轮反向
边走边转0.500.5平移加自转

提示:斜行时 vx 和 vy 都取 0.707 是为了保持合成速度大小不变。如果直接取 1.0 和 1.0,合成速度会变成 1.414 倍,小车会突然加速。

4.3 主循环调度与实时性保证

STM32F103C8T6 没有 RTOS 也能跑,但要注意调度顺序。建议用 TIM4 做一个 5ms 的定时中断,在中断里做三件事:读编码器算实际速度、调用 PI 更新、写 PWM 比较值。主循环只负责解析遥控器指令和更新目标速度。

void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM4, TIM_IT_Update); // 1. 读编码器,算实际线速度 float actual_lf = Encoder_Get_Speed(TIM2); float actual_rf = Encoder_Get_Speed(TIM3); // 2. PI 更新 float out_lf = PI_Update(&pi_lf, target.lf, actual_lf, 0.005f); float out_rf = PI_Update(&pi_rf, target.rf, actual_rf, 0.005f); // 3. 写 PWM PWM_Set_Duty(TIM1, 1, out_lf); PWM_Set_Duty(TIM1, 2, out_rf); } }

逻辑说明:中断服务函数要短,浮点运算在 F103 上没有 FPU,但 5ms 周期内算四个 PI 绰绰有余。如果后轮没有编码器,后轮直接开环给占空比,前轮闭环,也能跑,只是斜行精度差一些。

5. 调试全向小车时最容易踩的五个坑

5.1 现象:小车走斜线时往一边偏,越走越歪

原因:四个轮子的 v_max 不一致,或者某个轮子的机械阻力大。开环控制下,同样的占空比,四个轮子实际转速不同,合成方向就偏了。

解决:先做单轮标定,每个轮子单独给 50% 占空比,量 10 秒走的距离,算出各自的 v_max,写进代码里做归一化。如果不想加编码器,至少把四个轮子的 v_max 手动调成一致。

5.2 现象:原地转的时候车体抖动,轮子发出咔咔声

原因:PI 参数太激进,或者 PWM 频率太低。原地转时四个轮子方向相反,车体受力复杂,编码器反馈有延迟,kp 太大就会震荡。

解决:先把 kp 降到原来的三分之一,ki 暂时置零,看能不能稳定。稳定后再慢慢加 ki。PWM 频率从 10kHz 提到 20kHz,抖动会明显减轻。

5.3 现象:编码器读数偶尔跳变,速度曲线有毛刺

原因:编码器信号线没有屏蔽,和电机线捆在一起走,电机换向时干扰串进来。或者 TIM_ICFilter 设得太小。

解决:编码器线单独走,远离电机线。TIM_ICFilter 从 0x0F 开始试,不行再加大。软件上对速度做一阶低通滤波,截止频率 20Hz 左右。

5.4 现象:电池电压下降后,同样的指令小车越走越慢

原因:v_max 是在满电时标定的,电压降了电机转速下降,开环占空比不变但实际速度变小。

解决:加电池电压检测,用 ADC 读分压后的电压,在速度换算时乘以电压补偿系数。或者直接用编码器闭环,闭环对电压变化不敏感。

5.5 现象:四个轮子同时给 PWM,定时器输出不正常

原因:TIM1 是高级定时器,需要调用 TIM_CtrlPWMOutputs 使能输出。另外 GPIO 模式要设成复用推挽,设成普通推挽没有输出。

解决:检查 TIM_CtrlPWMOutputs(TIM1, ENABLE) 有没有调用,检查 GPIO_Mode 是不是 GPIO_Mode_AF_PP。用示波器量 PA8 到 PA11,确认四路都有波形。

6. 让全向运动更顺滑的两个进阶技巧

第一个技巧是速度前馈加 PI 反馈。纯 PI 在目标速度突变时响应慢,因为积分项要慢慢累积。如果知道当前电池电压和电机特性,可以先把目标速度换算成一个基础占空比作为前馈,PI 只负责修正误差。这样响应快,超调小。实现上就是在 PI_Update 外面加一行:output = feedforward + PI_Update(...)。前馈系数用实测的 v_max 反推。

第二个技巧是轮速同步补偿。四个轮子独立闭环时,如果某个轮子负载突然变大,它的实际速度会掉,PI 会加大占空比去追。但其他轮子不知道,合成方向就偏了。可以在四个 PI 之间加一个同步项:每个轮子的误差里减去四个轮子平均误差的一部分。这样负载变化时四个轮子一起调整,方向保持得更稳。

// 同步补偿示例,在 PI 更新前调用 float avg_error = (err_lf + err_rf + err_rr + err_lr) / 4.0f; err_lf = err_lf - 0.3f * (err_lf - avg_error); err_rf = err_rf - 0.3f * (err_rf - avg_error); err_rr = err_rr - 0.3f * (err_rr - avg_error); err_lr = err_lr - 0.3f * (err_lr - avg_error);

系数 0.3 是实测比较稳的值,太大反而会引入震荡。这个补偿对斜行和原地转效果最明显。

验证方法很简单:在地面贴一条直线胶带,让小车沿胶带走斜行,看轮子会不会压线。如果偏了,先调同步系数,再调 PI。我自己的习惯是每次改完参数都用手机慢动作拍一段,回放看四个轮子的启动和停止是否同步,比盯着车看准得多。这套东西调顺之后,F103 的资源还剩不少,后面加超声波避障或者串口摄像头都够用。希望帮到你。

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

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

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

立即咨询