STM32实战:用PID算法精准控制电机转速(附完整代码)
如果你曾经尝试过用单片机控制直流电机,可能会发现一个看似简单却让人头疼的问题:为什么电机转速总是忽快忽慢?尤其是在负载变化或者电源电压波动时,想让电机稳定在某个特定转速,简直就像在驯服一匹野马。我刚开始接触电机控制项目时,也在这个问题上栽过不少跟头——要么是转速响应太慢,要么是超调严重来回震荡,调试过程让人抓狂。
后来我发现,解决这类问题的核心钥匙,其实是一个诞生近百年的经典算法:PID控制。它没有复杂的数学模型,也不需要高深的数学知识,却能通过三个简单的参数组合,实现对电机转速的精准、稳定控制。今天,我就结合自己在STM32平台上的实际项目经验,带你一步步搭建一个完整的直流电机PID速度控制系统。从硬件连接到代码实现,从参数整定到问题排查,我会把那些踩过的坑、总结的技巧都分享出来,让你少走弯路,快速上手。
1. 系统架构与硬件设计
在开始写代码之前,我们需要先搞清楚整个系统的构成。一个完整的PID速度控制系统,本质上是一个闭环反馈系统——我们设定一个目标转速,系统通过传感器测量实际转速,然后根据两者的差值(误差)来调整控制输出,让实际转速不断逼近目标值。
1.1 核心组件与连接方式
我们的系统主要包含以下几个部分:
- STM32微控制器:作为大脑,负责运行PID算法、处理编码器信号、生成PWM控制信号。我使用的是STM32F103系列,性价比高且资源丰富。
- 直流电机与驱动模块:电机需要配合驱动电路使用,常见的有L298N、TB6612等驱动芯片。这些模块接收STM32的PWM信号,控制电机的转速和方向。
- 旋转编码器:这是获取转速反馈的关键。编码器安装在电机轴上,电机转动时会产生脉冲信号,通过测量脉冲频率就能计算出实时转速。
- 电源系统:为STM32、驱动模块和电机提供稳定的电压。注意电机驱动部分可能需要独立的电源,避免大电流对控制电路造成干扰。
硬件连接上,有几个关键点需要特别注意:
注意:编码器的信号线建议使用屏蔽线,并且尽量远离电机电源线,避免电磁干扰导致脉冲计数错误。如果条件允许,可以在编码器信号线上添加RC滤波电路。
下面是一个典型的连接示意表格:
| 组件 | STM32引脚 | 功能说明 | 注意事项 |
|---|---|---|---|
| 电机驱动PWM输入 | TIMx_CHx (如PA8) | 控制电机转速 | 需要配置为PWM输出模式 |
| 电机驱动方向控制 | GPIO (如PA0, PA1) | 控制电机正反转 | 根据驱动模块要求设置高低电平 |
| 编码器A相 | TIMx_CH1 (如PA0) | 编码器脉冲输入 | 需要配置为编码器接口模式 |
| 编码器B相 | TIMx_CH2 (如PA1) | 编码器脉冲输入 | 与A相配合判断转动方向 |
| 编码器电源 | 3.3V或5V | 为编码器供电 | 注意编码器工作电压范围 |
1.2 编码器选型与转速计算
编码器的选择直接影响转速测量的精度。常见的有增量式光电编码器和霍尔编码器。对于速度控制,我们主要关注两个参数:
- 分辨率(PPR):编码器每转产生的脉冲数。比如500线的编码器,每转产生500个脉冲。
- 输出类型:通常是A、B两相正交信号,有些还带有Z相(零位信号)。
转速的计算公式很简单,但实现时需要考虑一些细节:
// 转速计算示例(单位:RPM,转/分钟)
float calculate_rpm(uint32_t pulse_count, uint32_t time_interval_ms, uint16_t encoder_ppr) {
// pulse_count: 在time_interval_ms时间内计数的脉冲数
// encoder_ppr: 编码器每转脉冲数
// 注意:正交编码器模式下,每个机械周期会产生4*PPR个计数
float revolutions = (float)pulse_count / (4.0f * encoder_ppr);
float minutes = time_interval_ms / 60000.0f;
return revolutions / minutes;
}
在实际项目中,我通常使用STM32的定时器编码器接口模式来计数,这样硬件自动处理方向判断和计数,CPU负担小。配置方法如下:
// STM32 HAL库配置编码器接口
void Encoder_TIM_Init(void) {
TIM_Encoder_InitTypeDef encoder_config = {0};
TIM_MasterConfigTypeDef master_config = {0};
// 使能定时器时钟
__HAL_RCC_TIM3_CLK_ENABLE();
// 配置编码器模式
encoder_config.EncoderMode = TIM_ENCODERMODE_TI12; // 使用TI1和TI2作为编码器输入
encoder_config.IC1Polarity = TIM_ICPOLARITY_RISING; // 上升沿触发
encoder_config.IC1Selection = TIM_ICSELECTION_DIRECTTI;
encoder_config.IC1Prescaler = TIM_ICPSC_DIV1; // 不分频
encoder_config.IC1Filter = 0x0F; // 滤波器设置,抗干扰
encoder_config.IC2Polarity = TIM_ICPOLARITY_RISING;
encoder_config.IC2Selection = TIM_ICSELECTION_DIRECTTI;
encoder_config.IC2Prescaler = TIM_ICPSC_DIV1;
encoder_config.IC2Filter = 0x0F;
HAL_TIM_Encoder_Init(&htim3, &encoder_config);
// 启动编码器接口
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}
2. PID算法原理与实现选择
PID控制器的魅力在于它的简洁和有效。虽然只有三个参数,但通过不同的组合,可以应对各种控制场景。不过在实际嵌入式系统中,我们需要考虑计算资源限制和实时性要求,这就引出了两种主要的实现方式:位置式PID和增量式PID。
2.1 位置式PID:直观但需注意积分饱和
位置式PID的输出公式如下:
u(k) = Kp * e(k) + Ki * Σe(j) + Kd * [e(k) - e(k-1)]
其中:
u(k):第k次控制周期的输出值e(k):第k次的目标值与实际值之差Kp, Ki, Kd:比例、积分、微分系数Σe(j):从开始到当前所有误差的累加和
这种方式的优点是直观,输出直接对应控制量(比如PWM占空比)。但缺点也很明显:
- 积分项容易饱和:如果误差长时间存在,积分项会不断累积,可能导致输出超出合理范围。
- 计算量相对较大:每次都需要计算积分累加和。
- 抗干扰能力较弱:输出突变可能对系统造成冲击。
我在实际项目中处理积分饱和的常用方法是积分限幅:
// 带积分限幅的位置式PID实现片段
typedef struct {
float target; // 目标值
float actual; // 实际值
float err; // 当前误差
float err_last; // 上次误差
float integral; // 积分项
float Kp, Ki, Kd; // PID参数
float integral_max; // 积分限幅
float output_max; // 输出限幅
} Positional_PID;
float positional_pid_calc(Positional_PID *pid, float actual_value) {
pid->actual = actual_value;
pid->err = pid->target - pid->actual;
// 积分项计算,带限幅
pid->integral += pid->err;
if (pid->integral > pid->integral_max) {
pid->integral = pid->integral_max;
} else if (pid->integral < -pid->integral_max) {
pid->integral = -pid->integral_max;
}
// PID计算
float output = pid->Kp * pid->err +
pid->Ki * pid->integral +
pid->Kd * (pid->err - pid->err_last);
// 输出限幅
if (output > pid->output_max) {
output = pid->output_max;
} else if (output < 0) {
output = 0; // 对于PWM占空比,通常不会为负
}
pid->err_last = pid->err;
return output;
}
2.2 增量式PID:更适合嵌入式系统
增量式PID的输出是控制量的变化值,而不是绝对控制量:
Δu(k) = Kp * [e(k) - e(k-1)] + Ki * e(k) + Kd * [e(k) - 2e(k-1) + e(k-2)]
u(k) = u(k-1) + Δu(k)
这种方式有几个显著优点:
- 计算量小:不需要保存历史误差的累加和。
- 抗积分饱和:增量输出自然限制了积分效应。
- 手动/自动切换无冲击:由于输出是增量,切换时不会产生突变。
- 抗干扰能力强:单次错误计算的影响有限。
但增量式PID也有缺点:参数整定相对困难,且对控制周期敏感。下面是我的增量式PID实现:
typedef struct {
float target; // 目标值
float actual; // 实际值
float err[3]; // 当前、上次、上上次误差,err[0]=e(k), err[1]=e(k-1), err[2]=e(k-2)
float Kp, Ki, Kd; // PID参数
float output; // 当前输出值
float output_max; // 输出最大值
float output_min; // 输出最小值
} Incremental_PID;
float incremental_pid_calc(Incremental_PID *pid, float actual_value) {
pid->actual = actual_value;
// 更新误差队列
pid->err[2] = pid->err[1];
pid->err[1] = pid->err[0];
pid->err[0] = pid->target - pid->actual;
// 增量计算
float delta_output = pid->Kp * (pid->err[0] - pid->err[1]) +
pid->Ki * pid->err[0] +
pid->Kd * (pid->err[0] - 2 * pid->err[1] + pid->err[2]);
// 更新输出
pid->output += delta_output;
// 输出限幅
if (pid->output > pid->output_max) {
pid->output = pid->output_max;
} else if (pid->output < pid->output_min) {
pid->output = pid->output_min;
}
return pid->output;
}
2.3 如何选择:位置式 vs 增量式
根据我的经验,选择哪种实现方式主要考虑以下几点:
| 考虑因素 | 位置式PID | 增量式PID | 我的建议 |
|---|---|---|---|
| 计算资源 | 需要存储积分项,计算量中等 | 计算量小,内存占用少 | 资源紧张时选增量式 |
| 控制精度 | 高,能消除稳态误差 | 同样高,但参数整定要求高 | 两者都能达到高精度 |
| 抗干扰性 | 较差,积分饱和可能 | 较好,增量输出自然限幅 | 有强干扰时选增量式 |
| 参数整定 | 相对直观 | 需要经验,对周期敏感 | 新手可从位置式开始 |
| 执行机构 | 适合直接控制型 | 适合步进型或积分型 | 电机控制两者都可用 |
在电机速度控制中,我通常推荐使用增量式PID,原因有三:
- 电机驱动本身有惯性,增量输出更平滑
- 避免积分饱和导致的“wind-up”现象
- 实际调试中发现增量式更容易稳定
3. STM32上的完整代码实现
现在我们把各个部分组合起来,形成一个完整的电机速度控制系统。我会分

3178

被折叠的 条评论
为什么被折叠?



