直流有刷电机实战:STM32驱动、PID速度闭环与编码器测速
说实话,直流有刷电机是电机控制里最基础、也最“友好”的入门对象。你给它通上电它就转,换一下极性它就反转,简单粗暴。但真正要把一个电机控得稳、控得准,让它指哪打哪,那就得把驱动、测速、闭环这三件事串起来。
我个人习惯把这一章叫做“电机控制的第一个闭环”。因为从这一章开始,你不再是让电机瞎转,而是让它按照你设定的速度跑。嗯,咱们一步步来。
3.1 硬件连接:STM32 + 驱动板 + 电机 + 编码器
先看硬件怎么搭。我早期踩过一个坑:驱动板的地线和STM32的地线没共地,结果PWM信号乱飘,电机一顿一顿的。你想想看,数字信号再干净,参考地不一样也是白搭。
典型的连接方式如下:
| STM32引脚 | 驱动板接口 | 说明 |
|---|---|---|
| PA0 (TIM2_CH1) | PWM_IN | 输出PWM控制速度 |
| PB0 | DIR | 方向控制,高/低电平 |
| PB1 | EN | 使能,高电平启动 |
| PA6 (TIM3_CH1) | 编码器A相 | 正交编码器输入 |
| PA7 (TIM3_CH2) | 编码器B相 | 正交编码器输入 |
| GND | GND | 共地,必须连接 |
3.2 编码器测速:从脉冲到转速
编码器是电机的“眼睛”。没有它,你根本不知道电机实际转了多少。常用的增量式编码器输出两路正交方波(A相和B相),通过相位差判断方向,通过脉冲数计算位置和速度。
STM32的定时器自带编码器模式,这个功能特别好用。你只需要配置好定时器,硬件会自动处理A/B相的边沿计数,CPU几乎零开销。
配置代码示例(基于HAL库):
// 编码器定时器初始化
void Encoder_Init(void)
{
TIM_Encoder_InitTypeDef sEncoder = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 0;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 0xFFFF; // 16位自动重装
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
sEncoder.EncoderMode = TIM_ENCODERMODE_TI12; // 双沿计数
sEncoder.IC1Polarity = TIM_ICPOLARITY_RISING;
sEncoder.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sEncoder.IC1Prescaler = TIM_ICPSC_DIV1;
sEncoder.IC1Filter = 0;
sEncoder.IC2Polarity = TIM_ICPOLARITY_RISING;
sEncoder.IC2Selection = TIM_ICSELECTION_DIRECTTI;
sEncoder.IC2Prescaler = TIM_ICPSC_DIV1;
sEncoder.IC2Filter = 0;
HAL_TIM_Encoder_Init(&htim3, &sEncoder);
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}
测速公式很简单:
转速 (RPM) = (脉冲数 / 编码器线数) × (60 / 采样周期秒)
举个例子:编码器线数是400线(每转400个脉冲),采样周期100ms,测到脉冲数200。那么转速 = (200 / 400) × (60 / 0.1) = 300 RPM。
3.3 PID速度闭环:让电机听话
PID控制说白了就是三个字:比例、积分、微分。比例看当前误差,积分看历史误差,微分看误差变化趋势。三个分量加起来,就是你要输出的PWM占空比。
我刚开始做PID时,调参全靠蒙。后来发现一个规律:先调P,让系统不震荡;再加I,消除静差;最后加D,抑制超调。这个顺序别搞反了。
位置式PID的实现代码:
typedef struct {
float target; // 目标值
float actual; // 实际值
float err; // 当前误差
float err_last; // 上一次误差
float integral; // 积分累计
float Kp, Ki, Kd; // PID系数
float output; // 输出值
float integral_limit; // 积分限幅
float output_limit; // 输出限幅
} PID_TypeDef;
void PID_Calc(PID_TypeDef *pid)
{
pid->err = pid->target - pid->actual;
// 比例项
float p_out = pid->Kp * pid->err;
// 积分项(带限幅)
pid->integral += pid->Ki * pid->err;
if (pid->integral > pid->integral_limit)
pid->integral = pid->integral_limit;
else if (pid->integral < -pid->integral_limit)
pid->integral = -pid->integral_limit;
// 微分项
float d_out = pid->Kd * (pid->err - pid->err_last);
// 总输出
pid->output = p_out + pid->integral + d_out;
// 输出限幅
if (pid->output > pid->output_limit)
pid->output = pid->output_limit;
else if (pid->output < -pid->output_limit)
pid->output = -pid->output_limit;
pid->err_last = pid->err;
}
3.4 整体控制流程
把上面三块拼起来,就是一个完整的速度闭环。主循环里做的事情很简单:读编码器 → 算转速 → PID计算 → 更新PWM。就这么循环,电机就会自动往目标速度靠。
下面这张图把整个流程串起来了,你看一眼就明白:
这个流程里有个关键点:控制周期要固定。我习惯用定时器中断来触发控制循环,而不是在主循环里用delay。因为delay不准,CPU负载一变周期就飘,PID参数全白调。
3.5 调参实战:从震荡到稳定
调参是门手艺活。我总结了一个快速调参法,适合新手:
- 先给一个目标速度,比如500 RPM。
- Ki和Kd设为0,只留Kp。从很小的值开始(比如0.1),慢慢往上加。
- 观察电机响应:如果电机嗡嗡响、速度来回晃,说明Kp太大了,减半再试。
- 找到临界值:当Kp大到让系统刚好开始震荡时,记下这个值。然后取它的60%作为最终Kp。
- 加Ki:从0.01开始,每次加0.01,直到稳态误差消失。注意积分限幅别太大。
- 最后加Kd:如果超调严重,加一点点Kd(0.001级别),能有效抑制过冲。
3.6 常见问题与排查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 电机不转 | 使能引脚没拉高、PWM占空比0、驱动板供电不足 | 用示波器看PWM波形,测驱动板电源电压 |
| 电机只往一个方向转 | DIR引脚电平固定、编码器方向判断反了 | 检查DIR控制逻辑,交换编码器A/B相试试 |
| 速度波动大 | 编码器采样周期太短、Kp太大、机械振动 | 延长采样周期,降低Kp,检查电机安装是否牢固 |
| 电机发热严重 | PWM频率太低、死区时间不够、电机堵转 | 提高PWM频率,检查驱动板死区设置,避免长时间堵转 |
嗯,这一章的内容就这些。从硬件连线到编码器测速,从PID代码到调参实战,每一步都是我自己摸爬滚打总结出来的。你照着做一遍,应该能跑通第一个速度闭环。记住:电机控制没有玄学,只有信号和时序。出问题了,拿示波器看波形,拿逻辑分析仪抓时序,答案就在那里。