简介:面向STM32微控制器与直流电机控制开发者的完整PID控制程序包,以意法半导体STM32F103C8T6微控制器、L298N双H桥驱动芯片和MG513P30直流电机为核心,构建速度闭环控制方案。程序包含GPIO、定时器、PWM输出等外设初始化配置,以及PID算法的初始化、采样周期设定、误差计算、限幅处理与控制输出的完整流程,同时梳理了比例、积分、微分系数整定的常见方法,适用于智能车、机器人底盘、小型自动化设备等电机调速项目。压缩包为zip格式,大小约47.69MB,适合嵌入式爱好者、单片机初学者以及需要快速落地电机控制方案的工程师参考。已有5746人浏览学习,说明该程序在实际项目中具有较高参考价值。下载后可重点阅读代码结构、驱动逻辑和PID参数调试要点,有助于缩短从原理理解到实际运行的距离,深入掌握嵌入式实时控制与电机系统稳定性的调试方法。 去年做智能车底盘的时候,整套系统用的就是STM32F103C8T6最小系统板 + L298N电机驱动模块 + MG513P30直流减速电机这套经典组合。调通编码器测速、再叠上PID闭环之后,小车跑直线的效果比开环控制强了不是一点半点。这篇就完整记录一下这套PID控制程序从硬件接线到代码实现、再到参数整定的全过程,给正在做智能车、机器人小车底盘或者相关毕设的同学一个可以直接抄作业的参考。
1. 项目整体设计与硬件选型思路
1.1 为什么是这三件套
先说结论:这套配置几乎是入门直流电机PID控制最稳妥、资料最全、成本最低的组合之一。STM32F103C8T6是Cortex-M3内核,72MHz主频,片上定时器资源足够同时输出PWM和读取编码器,性能完全够用;L298N是经典的H桥驱动芯片,驱动电流最大2A,带逻辑电源输出,虽然压降大了点,但对于学习阶段的小车底盘完全能接受;MG513P30是带霍尔编码器的直流减速电机,30:1减速比,自带AB相编码器输出,可以直接接在单片机的定时器编码器模式上。
这套组合最大的优势在于:编码器反馈 + 定时器硬件资源刚好匹配。MG513P30自带霍尔编码器,省去了外接测速模块的麻烦,而STM32F103C8T6的定时器支持编码器接口模式,直接把AB相输出接上去,硬件自动完成4倍频计数,CPU零开销。如果换成普通的直流电机自己想办法测速,或者用IO口模拟编码器计数,那PID闭环就完全不是这个难度了。
1.2 控制方案选型:为什么必须上PID
开环控制下,给电机一个固定PWM占空比,转速会随着电池电压下降、负载变化而明显波动。最典型的问题就是:刚充满电的电池和快没电的电池,同一个占空比下转速能差20%以上,小车跑不直,速度也稳不住。
PID闭环控制的核心思路就一个:不断测量实际转速,和目标转速做比较,根据误差动态调整PWM输出。误差大了就加大输出,误差接近零就减小输出,通过比例、积分、微分三项的组合,让实际转速快速且平稳地逼近目标值。这套方案不要求精确的电机模型,不依赖电池电压的补偿,属于经典的“无模型控制”,在小车底盘、云台电机、传送带调速这类场景下非常实用。这次项目目标就是让STM32通过L298N驱动MG513P30,实现“目标转速输入→实际转速快速收敛”的完整闭环。
2. 硬件连接与STM32CubeMX初始化配置
2.1 接线方案与电源分配
接线之前先明确一点:L298N的电机电源和单片机的逻辑电源必须共地。也就是L298N的GND要接到STM32的GND上,否则控制信号没有参考地,电机要么不转,要么乱转,这是新手最常见的问题。
我的实际接线如下:
| STM32F103C8T6引脚 | L298N模块 | 功能说明 |
|---|---|---|
| PA0 | 编码器A相(左电机) | TIM2_CH1,编码器输入 |
| PA1 | 编码器B相(左电机) | TIM2_CH2,编码器输入 |
| PB6 | 编码器A相(右电机) | TIM4_CH1,编码器输入 |
| PB7 | 编码器B相(右电机) | TIM4_CH2,编码器输入 |
| PA6 | ENA(左电机使能) | TIM3_CH1,PWM输出 |
| PA7 | ENB(右电机使能) | TIM3_CH2,PWM输出 |
| PB0 | IN1(左电机方向) | GPIO输出 |
| PB1 | IN2(左电机方向) | GPIO输出 |
| PB10 | IN3(右电机方向) | GPIO输出 |
| PB11 | IN4(右电机方向) | GPIO输出 |
霍尔编码器需要5V供电,注意MG513P30的编码器供电和信号线是独立的,一般红色VCC接5V,黑色GND接GND,白色和黄色分别为A相、B相。这里务必和电机的主电源分开,避免电机启动时的压降把编码器电压拉垮。
电源方面要注意:L298N的电机供电建议用7.4V以上的2S锂电池或者12V电源适配器。L298N自身的饱和压降在2V左右,如果用5V供电,实际到电机两端只剩3V左右,转速和扭矩都难看。逻辑侧的5V可以直接用L298N板载的5V输出,只要不额外带大电流负载就行。
2.2 CubeMX配置要点
在STM32CubeMX里,编码器接口模式配置如下:对于TIM2和TIM4,将Channel1和Channel2均设置为Encoder Mode,Combined Channels选择Encoder Mode TI1 and TI2,这样计数方向由AB相相位差自动决定,不需要额外处理方向判断。分频器和自动重载寄存器设置为0xFFFF,即65535,让计数范围最大化,方便后续软件处理。
PWM输出端TIM3配置为PWM Generation CH1和PWM Generation CH2,预分频71,自动重载值设成1000或者2000。这里选值需要想一下:预分频71配合72MHz主频,得到1MHz的计数频率,自动重载1000时PWM频率为1kHz,自动重载2000时PWM频率为500Hz。对于直流电机,PWM频率太低会有明显的“滋滋”声和转速波动,一般建议至少10kHz以上。实测下来我把预分频改成了7,自动重载1000,即PWM频率10kHz,这样电机运行更安静、更平稳。
提示:PWM频率并非越高越好,太高的话L298N的开关损耗会增大,电机电感滤波效果反而下降。对于L298N这种分立器件驱动,10kHz到20kHz是比较合理的区间。
2.3 编码器模式的原理解读
定时器编码器模式本质上是一种硬件级的正交解码功能。MG513P30的霍尔编码器输出的AB相信号相位相差90度,电机正转时A相超前B相,反转时B相超前A相。定时器通过检测AB相的电平变化和相位关系,自动判断旋转方向并控制递增或递减计数。
这里有个关键的硬件细节:编码器输出的AB相要接在定时器的T1和T2通道上,不能接普通GPIO。软件轮询读AB相也能算转速,但高速转动时会因为读取不及时而丢脉冲,所以硬件解码是必须的。MG513P30减速比30:1,编码器线数实测为13线,经过4倍频后电机输出轴每转一圈产生13×30×4=1560个计数脉冲。这个参数后面计算转速时要用,先记下。
3. PID控制核心原理与程序实现
3.1 位置式PID还是增量式PID
PID控制算法在软件实现上主要有两种形式:位置式和增量式。位置式PID直接输出控制量(比如PWM占空比的绝对值),计算时需要累加历史误差,一旦长时间无法到达目标,积分项会变得很大,出现“积分饱和”问题,系统超调严重。增量式PID输出的则是控制量的增量,即每次在原来PWM基础上增加或减少多少,因为不涉及大范围的误差累加,抗积分饱和能力更强,而且输出变化平稳。
从代码角度说,增量式PID公式如下:
printf("增量式PID: 输出增量 = Kp*(e - e_last) + Ki*e + Kd*(e - 2*e_last + e_last2)\n");这次项目最终采用的是增量式PID。电机转速控制场景下,PWM输出范围有限(0到1000),增量式PID天然适合这种受限输出系统,不容易出现积分饱和导致的剧烈超调。
3.2 核心代码实现
整个控制程序分为三块:PID结构体与计算函数、编码器测速(包含速度换算)、定时器中断服务函数中的控制逻辑。先看PID算法本身:
typedef struct { float Kp; float Ki; float Kd; float target_speed; // 目标转速 (rpm) float current_speed; // 当前转速 (rpm) float error; float last_error; float last_last_error; float output; // PWM输出增量 float pwm_out; // 实际PWM占空比 } PID_TypeDef; void PID_Init(PID_TypeDef *pid, float Kp, float Ki, float Kd) { pid->Kp = Kp; pid->Ki = Ki; pid->Kd = Kd; pid->target_speed = 0; pid->current_speed = 0; pid->error = 0; pid->last_error = 0; pid->last_last_error = 0; pid->output = 0; pid->pwm_out = 0; } float PID_Process(PID_TypeDef *pid, float target, float current) { pid->target_speed = target; pid->current_speed = current; pid->error = target - current; // 增量式PID核心计算 float delta = pid->Kp * (pid->error - pid->last_error) + pid->Ki * pid->error + pid->Kd * (pid->error - 2.0f * pid->last_error + pid->last_last_error); // 更新误差历史 pid->last_last_error = pid->last_error; pid->last_error = pid->error; // 累加PWM输出,并做限幅处理 pid->pwm_out += delta; if (pid->pwm_out > 1000) pid->pwm_out = 1000; if (pid->pwm_out < 0) pid->pwm_out = 0; return pid->pwm_out; }这段程序的要点在于:输出限幅必须放在PWM累加之后,防止转速还没到目标,PWM已经打满的情况。至于限幅上下限,取决于CubeMX里设置的自动重载值,如果PWM周期是1000,那上限就是1000,上限之外就是100%占空比,再加已经没有意义了。
再看测速部分。测速是通过定时器编码器模式读取计数值来实现的,核心是用两次读取的差值除以时间间隔算出转速:
int16_t encoder_count = (int16_t)__HAL_TIM_GET_COUNTER(&htim2); __HAL_TIM_SET_COUNTER(&htim2, 0); // 编码器线数13,减速比30,4倍频,每转一圈1560个脉冲 // 计数值单位是“脉冲数/定时器周期”,需要换算成rpm float encoder_speed_rpm = ((float)encoder_count / 1560.0f) * (60000.0f / CONTROL_PERIOD_MS);其中CONTROL_PERIOD_MS是PID控制周期,单位为毫秒。这段换算的逻辑是:先用计数值除以每转脉冲数得到“转了多少圈”,再除以控制周期时间得到“每秒转多少圈”,最后乘以60换算成rpm。这里有个小坑:__HAL_TIM_GET_COUNTER返回的是32位无符号值,但如果计数超过32767,需要转换为int16_t,否则正反转判断会出错。
最后是控制周期的实现。我用的是TIM7的定时中断,每10ms触发一次PID计算。中断服务函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM7) { // 左电机:读取编码器、计算转速、PID计算、更新PWM int16_t left_count = (int16_t)__HAL_TIM_GET_COUNTER(&htim2); __HAL_TIM_SET_COUNTER(&htim2, 0); left_speed_rpm = ((float)left_count / 1560.0f) * (60000.0f / 10.0f); left_pwm = (uint16_t)PID_Process(&left_pid, left_target_rpm, left_speed_rpm); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, left_pwm); } }方向控制则在主循环里根据误差方向去切换:
if (left_pid.error >= 0) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // IN1=1 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); // IN2=0 } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); }注意方向切换时要加死区延时,也就是先让两个方向引脚都输出低电平,延时几毫秒再切换方向,否则L298N的H桥可能直通,产生很大的冲击电流。编码器测速时还要加一点软件滤波,最简单的方式是取最近三次计数值的平均值,能明显减小转速波动。
4. 参数整定过程与常见问题排查
4.1 PID参数整定的完整步骤
参数整定是整个项目里最考验耐心的一步。我调试时的方法是“先开环、再比例、后积分、最后微分”四步走:
第一步:开环标定。把PID去掉,直接给定一个固定的PWM占空比(比如500),观察并记录电机稳态转速。这样做的目的是确认整个硬件链路正常,同时了解PWM占空比和转速的大致比例关系,方便后面预估PID参数的初始值。
第二步:只调Kp。从Kp=0.1开始,逐步增大。观察实际转速到达目标转速的响应速度。Kp太小,转速爬升慢;Kp太大,转速会围绕目标值震荡,能听到电机周期性的“呜-呜”声。记下刚刚出现震荡的Kp值,然后回退到它的60%到70%作为最终Kp。
第三步:加Ki消除稳态误差。只在Kp作用下,转速通常无法稳定在目标值的精确位置上,会有一个偏差,这对需要精确控速的场景是不能接受的。从Ki=0.01开始,缓慢增大,直到稳态误差消失。注意Ki过大会导致转速出现低频振荡,表现为周期性的加速减速。
第四步:加Kd抑制超调。如果转速超过目标然后又回落到目标之下,说明超调明显,加入Kd后一般能改善。
实际项目中我调出来的参数是Kp=0.35, Ki=0.08, Kd=0.02,10ms控制周期,PWM频率10kHz,MG513P30电机空载目标转速100rpm时,能在0.3秒左右稳定进入±2rpm误差范围,效果符合预期。
4.2 常见问题排查速查表
整个调试过程中踩过的坑,整理成了速查表,方便对应排查:
| 故障现象 | 可能原因 | 排查方法 |
|---|---|---|
| 电机完全不转 | 未共地、使能跳线帽没插、PWM通道配置错误 | 检查GND是否连通,ENA/ENB跳线帽是否接好,用示波器看PWM引脚输出 |
| 电机只能朝一个方向转 | IN1/IN2逻辑反了,或代码方向判断有误 | 检查GPIO初始化,或调换IN1/IN2接线 |
| 编码器数值跳动剧烈 | 编码器电源不稳、AB相接反、信号受电机干扰 | 编码器独立5V供电,AB相对调测试;电机线用双绞线,远离信号线 |
| 电机“嗡嗡”响不转 | PWM频率太低,或占空比太小无法克服静摩擦力 | 调大PWM频率到10kHz以上;尝试加大给定占空比 |
| 转速围绕目标值高频波动 | Kp过大 | 减小Kp,增大Kd,检查编码器数据是否平滑 |
| L298N发热严重 | 电机电流过大、散热不足、PWM频率过高 | 降低电机负载或电源电压,加散热片,降低PWM开关频率 |
有一个我特别想强调的细节:编码器计数方向要和控制方向逻辑匹配。也就是说,当IN1控制电机正转时,定时器计数值要递增,如果发现编码器计数方向反了,要么调换编码器AB两相接线,要么在代码里取反,千万别只在PID算法里做正负号处理,那样会让部分区间出现正反馈,导致电机猛加速刹不住。
4.3 从转速环到位置环的扩展思路
这次做的实际上是最基本的转速单环PID。如果把控制周期拉长,不控制转速,而是去累加编码器脉冲数来当作电机输出轴转过的角度,那就能很自然地扩展出位置环PID。智能车底盘做直线行驶时,通常会用内环转速、外环位置的双环PID结构。这个项目做完转速环后,我直接把目标转速换成了位置误差乘以一个比例系数,实现了小车开固定距离停车,效果已经比开环好了非常多。
双环PID的调试顺序是:先把内环(转速环)调稳,再调外环(位置环),外环输出是内环的目标值,内环的响应速度要比外环快3到5倍,否则整个系统容易耦合振荡。这些都是在转速环的基础上叠加出来的,所以这次把转速环PID吃透,后续做更复杂的控制都会顺畅很多。
4.4 调试辅助技巧
PID调试过程中,串口打印目标转速和实际转速是必不可少的。我习惯用串口以目标值,实际值的CSV格式输出,然后在电脑上用串口示波器或者直接将数据丢进Excel里画曲线。观察曲线形状比看一串数字直觉得多:Kp太大曲线会上下震荡,Ki太大会出现低频喘振,Kd太大会引入高频噪声。
另外,编码器数据建议在PID进入闭环之前,先单独开环测量并打印足够长时间的转速数据,确认编码器读数在固定占空比下是稳定且平滑的。这一步能帮你提前分清楚“编码器本身波动”和“PID参数不当造成的波动”。如果编码器原始数据就有大幅抖动,先解决滤波和干扰问题,再谈PID整定,否则后续所有调试都会被带偏。
最后再分享一个我踩过多次的坑:当系统刚上电时,PID输出初始值永远是0,此时如果目标转速不为0,误差会非常大,PID输出增量会迅速拉满,产生过冲。我的做法是启动时让目标转速从0起步,以斜坡函数形式逐步增加到目标值,这样既能减小启动冲击,又能让曲线更平缓。处理好这一步,整个系统的稳定性和使用寿命都会明显提升。
本文还有配套的精品资源,点击获取