摘要
PID(比例‑积分‑微分)控制器是嵌入式闭环控制中最经典、应用最广的算法。本文从连续域数学模型出发,推导出适合单片机实现的离散化形式,并重点剖析积分分离、抗积分饱和、微分先行 三种工程优化策略。所有代码均使用结构体封装,仅依赖 math.h 和 stdint.h 标准库,可在 STM32 全系列(F1/F4/G0/H7)上直接编译运行。文中还给出直流电机转速控制的完整实例以及参数整定指南,帮助读者快速落地一个稳定、鲁棒的 PID 控制器。

1. 引言
从恒温焊台到无人机姿态,从电机转速到机器人关节,PID 控制器无处不在。它无需精确的系统模型,仅通过三个参数------比例(P)、积分(I)、微分(D)------就能让被控对象稳定地跟踪设定值。
然而,在单片机上实现一个"能用"的 PID 容易,要得到一个"好用且可靠"的 PID 却需要充分考虑积分饱和、微分冲击等实际问题。本文将给出规范、模块化、可直接复用的 STM32 实现,并结合工程经验讲解调参技巧。
2. PID 算法原理
2.1 连续域理想 PID
模拟 PID 控制器的输出为:
u(t) = K_p \\left\[ e(t) + \\frac{1}{T_i} \\int_0\^t e(\\tau) d\\tau + T_d \\frac{de(t)}{dt} \\right
]
其中 ( e(t) = r(t) - y(t) ) 为设定值与实际值的偏差。
- 比例项:即时响应偏差,快速减小误差。
- 积分项:累积历史偏差,消除稳态残差。
- 微分项:预测偏差变化趋势,增加系统阻尼,抑制超调。
2.2 离散化(单片机实现形式)
设控制周期为 ( T_s ),采用矩形积分和后向差分近似,并定义便于编程的离散系数:
K_i = K_p \\frac{T_s}{T_i}, \\qquad K_d = K_p \\frac{T_d}{T_s}
位置式 PID
输出直接为执行器的绝对控制量(如 PWM 占空比):
u(k) = K_p e(k) + K_i \\sum_{n=0}\^{k} e(n) + K_d \\big\[ e(k) - e(k-1) \\big
]
增量式 PID
输出为控制量的变化值 (\Delta u(k)),适用于步进电机或需要无扰动切换的场景:
\\Delta u(k) = K_p\\big\[e(k)-e(k-1)\\big\] + K_i e(k) + K_d\\big\[e(k)-2e(k-1)+e(k-2)\\big
]
此时总控制量 ( u(k) = u(k-1) + \Delta u(k) )。
增量式的天然优势:误动作影响小(仅本次增量错误),无积分无限累积的风险,手动/自动切换平滑。
3. 工程优化要点
3.1 积分分离
当偏差较大时,暂停积分作用,防止积分项过度累积造成严重超调甚至振荡。当偏差进入小范围(如设定值的 5%~10%)时,重新启用积分以消除静差。
3.2 抗积分饱和(遇限削弱法)
如果控制输出已达到物理上限/下限,并且当前偏差方向仍然会驱使输出进一步超限,则停止(或回退)积分。这能有效避免"积分饱和"导致系统在反向调节时响应迟钝。
3.3 微分先行
将微分环节只作用于反馈值 ( y(k) ) 而非偏差 ( e(k) ),即:
D_{\\text{out}} = -K_d \\big\[ y(k) - y(k-1) \\big
]
这样当设定值突然变化时,不会产生巨大的微分冲击(Derivative Kick),系统更平滑。
4. STM32 代码实现
以下代码假设控制周期固定,所有系数 Kp, Ki, Kd 均为已经包含采样周期的离散系数。若需在不同采样时间下使用,用户可自行将系数乘以 ( T_s ) 或除以 ( T_s ) 进行调整。
4.1 位置式 PID(带积分分离、抗饱和)
头文件 pid_position.h
c
#ifndef PID_POSITION_H
#define PID_POSITION_H
#include <stdint.h>
typedef struct {
float kp; // 比例系数
float ki; // 积分系数 (已含Ts)
float kd; // 微分系数 (已含Ts)
float setPoint; // 设定值
float lastError; // 上次偏差 e(k-1)
float integral; // 积分累加项
float outMax; // 输出上限
float outMin; // 输出下限
float integralThreshold;// 积分分离阈值(偏差绝对值大于此值时不积分)
} PID_Position_t;
void PID_Pos_Init(PID_Position_t *pid, float kp, float ki, float kd,
float outMax, float outMin, float integThresh);
float PID_Pos_Update(PID_Position_t *pid, float feedback);
#endif
源文件 pid_position.c
c
#include "pid_position.h"
#include <math.h>
void PID_Pos_Init(PID_Position_t *pid, float kp, float ki, float kd,
float outMax, float outMin, float integThresh) {
pid->kp = kp;
pid->ki = ki;
pid->kd = kd;
pid->outMax = outMax;
pid->outMin = outMin;
pid->integralThreshold = integThresh;
pid->setPoint = 0.0f;
pid->lastError = 0.0f;
pid->integral = 0.0f;
}
float PID_Pos_Update(PID_Position_t *pid, float feedback) {
float error = pid->setPoint - feedback;
// 比例项
float pOut = pid->kp * error;
// 积分分离:偏差较大时清零积分,避免超调
if (fabsf(error) > pid->integralThreshold) {
pid->integral = 0.0f;
} else {
pid->integral += error; // 直接累加偏差,ki 已包含 Ts
}
float iOut = pid->ki * pid->integral;
// 微分项(偏差微分,若需微分先行可改为 -kd*(feedback-lastFeedback))
float dOut = pid->kd * (error - pid->lastError);
pid->lastError = error;
float output = pOut + iOut + dOut;
// 抗积分饱和:输出限幅并回退不利于退饱和的积分
if (output > pid->outMax) {
output = pid->outMax;
if (error > 0) pid->integral -= error; // 撤销本次累加,防止积分再增长
} else if (output < pid->outMin) {
output = pid->outMin;
if (error < 0) pid->integral -= error;
}
return output;
}
4.2 增量式 PID
头文件 pid_incremental.h
c
#ifndef PID_INCREMENTAL_H
#define PID_INCREMENTAL_H
typedef struct {
float kp, ki, kd; // 离散系数
float setPoint; // 设定值
float lastError; // e(k-1)
float prevError; // e(k-2)
float out; // 当前总输出
float outMax, outMin; // 输出限幅
} PID_Incremental_t;
void PID_Inc_Init(PID_Incremental_t *pid, float kp, float ki, float kd,
float outMax, float outMin);
float PID_Inc_Update(PID_Incremental_t *pid, float feedback);
#endif
源文件 pid_incremental.c
c
#include "pid_incremental.h"
void PID_Inc_Init(PID_Incremental_t *pid, float kp, float ki, float kd,
float outMax, float outMin) {
pid->kp = kp;
pid->ki = ki;
pid->kd = kd;
pid->outMax = outMax;
pid->outMin = outMin;
pid->setPoint = 0.0f;
pid->lastError = 0.0f;
pid->prevError = 0.0f;
pid->out = 0.0f;
}
float PID_Inc_Update(PID_Incremental_t *pid, float feedback) {
float error = pid->setPoint - feedback;
// 增量计算
float delta = pid->kp * (error - pid->lastError)
+ pid->ki * error
+ pid->kd * (error - 2.0f * pid->lastError + pid->prevError);
pid->prevError = pid->lastError;
pid->lastError = error;
// 累加增量并限幅
pid->out += delta;
if (pid->out > pid->outMax) {
pid->out = pid->outMax;
} else if (pid->out < pid->outMin) {
pid->out = pid->outMin;
}
return pid->out;
}
5. 应用实例:直流电机转速闭环控制
以 STM32 定时器编码器模式测速、高级定时器 PWM 驱动电机为例,使用位置式 PID 进行转速闭环控制。
硬件接口
- TIM3 编码器模式:读取电机转速(单位:转/分钟)
- TIM1 通道输出 PWM:控制电机驱动板(占空比 0 ~ 100%)
- TIM6 产生 1ms 定时中断作为 PID 控制周期
初始化与主循环片段
c
#include "pid_position.h"
PID_Position_t speedPID;
// 根据实际系统整定得到的参数(示例)
void PID_Config(void) {
// Kp=0.8, Ki=0.15, Kd=0.05 均为离散系数 (Ts=1ms)
PID_Pos_Init(&speedPID, 0.8f, 0.15f, 0.05f, 100.0f, 0.0f, 50.0f);
speedPID.setPoint = 1500.0f; // 目标转速 1500 RPM
}
// 每 1ms 调用一次(放在定时器中断服务函数中)
void Control_Loop(void) {
float currentSpeed = Encoder_ReadRPM(); // 获取当前转速
float duty = PID_Pos_Update(&speedPID, currentSpeed);
PWM_SetDuty(duty); // 更新占空比 (0~100%)
}
// -------------------- 系统初始化(基于 HAL 库)--------------------
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM1_Init(); // PWM 输出
MX_TIM3_Init(); // 编码器接口
MX_TIM6_Init(); // 1ms 定时中断
PID_Config();
HAL_TIM_Base_Start_IT(&htim6); // 启动定时中断
while (1) {
// 主循环可处理通信、显示等其他任务
}
}
// 1ms 定时器中断回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM6) {
Control_Loop();
}
}
提示:
Encoder_ReadRPM()需根据编码器线数和读出的脉冲频率自行实现,PWM_SetDuty()则是将百分比转换成 TIM 比较寄存器的值。
6. PID 参数整定指南
6.1 手动试凑法
- 纯比例:令 (K_i=0, K_d=0),逐步增大 (K_p) 直到系统出现持续等幅振荡,记录临界增益 (K_u) 和振荡周期 (T_u)。
- 积分:引入 (K_i) 消除静差,一般取 (K_i = K_p \times T_s / T_i),(T_i) 可先设为 (0.5T_u),观察超调与稳定时间。
- 微分:适当加入 (K_d) 抑制超调和振荡,(K_d) 通常为 (K_p) 的几分之一到十分之一。
- 微调:根据实际响应曲线精细调整三个系数。
6.2 常见对象参数参考(离散系数,Ts=1ms)
| 被控对象 | Kp | Ki | Kd |
|---|---|---|---|
| 温度(大惯性) | 5~30 | 0.01~0.1 | 5~30 |
| 电机速度 | 0.1~2 | 0.01~0.5 | 0.001~0.05 |
| 平衡车直立 | 250~400 | 0~0.5 | 0~1 |
积分分离阈值可设为设定值的 5%~10%;输出限幅根据执行器能力设置(如 PWM 0~100%)。
7. 与数字滤波的配合
在实际项目中,强烈建议对反馈信号进行滤波后再送入 PID 控制器,尤其是微分项对噪声极其敏感。常用的配合策略:
- 电机转速 :先用滑动平均(窗口 4~8)平滑编码器脉频计数值。
- 温度 :采用一阶低通滤波 或滑动平均去除 ADC 量化噪声。
- 姿态控制 :角速度/角度先经互补滤波 或卡尔曼滤波再输入 PID。
一个典型的联合调用示例:
c
float raw = Encoder_ReadRPM();
float smooth = MovingAvg_Update(&speedFilter, raw);
float duty = PID_Pos_Update(&speedPID, smooth);
8. 总结
本文从连续域 PID 推导到离散化实现,给出了 STM32 上可直接复用的位置式和增量式两套代码,并详细介绍了积分分离、抗积分饱和、微分先行等工程级优化。结合前文的传感器滤波算法,即可构成完整的"传感 → 滤波 → PID 控制 → 执行"闭环。
参数整定需要动手实践,建议在安全范围内大胆尝试,同时用串口绘图工具观察阶跃响应,逐步建立对 P/I/D 作用的直观感觉。
参考资料
- 《PID Controllers: Theory, Design, and Tuning》 -- Åström & Hägglund
- 《嵌入式控制系统开发》
- STM32 Timer Encoder Mode Application Note (AN4013)
欢迎在评论区交流调参心得与项目经验!