PID 进阶教程:从能用到好用的嵌入式闭环控制实践
PID 进阶教程:从能用到好用的嵌入式闭环控制实践
摘要
PID 控制算法看起来公式很简单:
输出 = Kp × 误差 + Ki × 误差积分 + Kd × 误差变化率
但是在真实 MCU、电机控制、温度控制、机器人底盘、FOC 控制、平衡小车等项目中,PID 并不是把公式写出来就能稳定工作。真正的工程难点在于:
采样周期是否稳定
积分是否饱和
输出是否限幅
微分是否放大噪声
目标值是否突变
反馈值是否可靠
多环 PID 是否互相打架
本文属于 PID 进阶教程,默认读者已经知道 P、I、D 的基本含义,重点讲解 PID 在嵌入式项目中的工程优化方法,包括离散化、位置式 PID、增量式 PID、积分抗饱和、微分滤波、不完全微分、串级 PID、前馈控制、FOC 三环 PID 选择以及实际调参流程。
一、PID 从基础到进阶的核心区别
很多初学者写 PID 时,会直接写成下面这样:
error = target - actual;
integral += error;
derivative = error - last_error;
output = kp * error + ki * integral + kd * derivative;
last_error = error;
这段代码可以跑,但在实际项目中很容易出现问题:
1. PID 调用周期不固定
2. 积分项无限累加
3. 输出超过 PWM 或电压范围
4. 微分项对噪声过于敏感
5. 目标值突变导致输出突变
6. 传感器反馈抖动导致控制抖动
7. 多环控制中内外环频率不匹配
所以 PID 进阶的重点不是重新背公式,而是让 PID 在真实系统中更加稳定、可控、抗干扰。
二、PID 离散化:MCU 中的 PID 不是连续运行的
理论上的 PID 是连续时间控制:
u(t) = Kp × e(t) + Ki × ∫e(t)dt + Kd × de(t)/dt
但是 MCU 程序是周期性执行的,例如每 1ms、5ms、10ms 执行一次 PID。
所以在程序里要写成离散形式:
error = target - actual
integral += error × Ts
derivative = (error - last_error) / Ts
output = Kp × error + Ki × integral + Kd × derivative
其中:
Ts:PID 调用周期,单位是秒
例如 PID 每 10ms 运行一次:
Ts = 0.01s
对应代码:
pid->integral += pid->error * pid->dt;
derivative = (pid->error - pid->last_error) / pid->dt;
为什么要加入 dt?
如果不考虑 dt,那么 PID 参数会强依赖调用频率。
比如原来 PID 是 10ms 调用一次,后来变成 5ms 调用一次:
积分累加速度变快
微分项变化幅度变大
原本稳定的参数可能突然不稳定
所以进阶 PID 建议在结构体中加入 dt:
typedef struct
{
float kp;
float ki;
float kd;
float dt;
float target;
float actual;
float error;
float last_error;
float integral;
float derivative;
float output;
} PID_TypeDef;
三、位置式 PID 和增量式 PID 的本质区别
PID 常见实现方式有两种:
1. 位置式 PID
2. 增量式 PID
四、位置式 PID
位置式 PID 是直接计算当前的控制输出。
公式:
output = Kp × error + Ki × integral + Kd × derivative
代码形式:
float PID_Position_Calc(PID_TypeDef *pid, float target, float actual)
{
pid->target = target;
pid->actual = actual;
pid->error = pid->target - pid->actual;
pid->integral += pid->error * pid->dt;
pid->derivative = (pid->error - pid->last_error) / pid->dt;
pid->output = pid->kp * pid->error
+ pid->ki * pid->integral
+ pid->kd * pid->derivative;
pid->last_error = pid->error;
return pid->output;
}
位置式 PID 的特点
优点:
公式直观
输出含义明确
适合位置控制、温度控制、电流控制
方便加入限幅、前馈、死区补偿
缺点:
积分项容易累积过大
输出容易饱和
执行器饱和时可能出现积分饱和
适合场景:
温度控制
位置控制
FOC 电流环
舵机角度控制
慢速系统控制
五、增量式 PID
增量式 PID 不是直接计算最终输出,而是计算本次输出相对于上一次输出应该增加或减少多少。
公式:
delta_output = Kp × (error - last_error)
+ Ki × error × Ts
+ Kd × (error - 2 × last_error + last_last_error) / Ts
output += delta_output
代码形式:
float PID_Increment_Calc(PID_TypeDef *pid, float target, float actual)
{
float delta_output;
pid->target = target;
pid->actual = actual;
pid->error = pid->target - pid->actual;
delta_output = pid->kp * (pid->error - pid->last_error)
+ pid->ki * pid->error * pid->dt
+ pid->kd * (pid->error
- 2.0f * pid->last_error
+ pid->last_last_error) / pid->dt;
pid->output += delta_output;
pid->last_last_error = pid->last_error;
pid->last_error = pid->error;
return pid->output;
}
增量式 PID 的特点
优点:
输出变化比较平滑
适合电机速度控制
适合控制 PWM、Iq 目标、电压等连续执行量
对执行器比较友好
缺点:
理解比位置式复杂
需要保存前两次误差
输出仍然需要限幅
调试时不如位置式直观
适合场景:
直流电机速度控制
FOC 速度环
执行器连续调节
需要平滑输出变化的场景
六、位置式 PID 和增量式 PID 怎么选?
| 控制对象 | 推荐形式 | 原因 |
|---|---|---|
| 温度控制 | 位置式 PID / PI | 系统慢,输出直接对应加热功率 |
| 电机速度控制 | 增量式 PI 或位置式 PI | 增量式输出更平滑 |
| 电机位置控制 | 位置式 P / PID | 根据距离目标多远直接计算速度或力矩 |
| FOC 电流环 | 位置式 PI | 直接计算 Vd、Vq |
| FOC 速度环 | 增量式 PI 或位置式 PI | 输出 Iq 目标,适合平滑调节 |
| FOC 位置环 | 位置式 P | 根据位置误差输出速度目标 |
| 平衡小车角度环 | 位置式 PD / PID | 需要快速根据角度误差输出控制量 |
简单记忆:
需要直接算当前控制量:位置式
需要平滑改变输出量:增量式
七、积分饱和问题
积分项的作用是消除稳态误差,但是积分项也最容易带来问题。
假设电机目标速度为 3000 RPM,但是由于电压不足,电机最高只能到 2500 RPM。
此时误差长期存在:
error = 3000 - 2500 = 500
积分项会一直累加:
integral += error
时间一长,积分项会变得非常大。即使后来目标速度降低,积分项仍然很大,导致系统恢复很慢,甚至严重超调。
这种现象叫做:
积分饱和
积分饱和常见表现:
输出长时间顶满
目标变化后系统恢复慢
超调严重
电机冲过头
温度超过目标后还继续加热
八、积分限幅:最常用的抗饱和方法
最简单的方法是限制积分项范围:
pid->integral += pid->error * pid->dt;
if (pid->integral > pid->integral_max)
{
pid->integral = pid->integral_max;
}
else if (pid->integral < pid->integral_min)
{
pid->integral = pid->integral_min;
}
或者写成通用限幅函数:
static float Limit(float value, float min, float max)
{
if (value > max)
{
return max;
}
if (value < min)
{
return min;
}
return value;
}
使用:
pid->integral = Limit(pid->integral,
pid->integral_min,
pid->integral_max);
这是最常见、最简单、最适合初学者的抗积分饱和方法。
九、条件积分:输出饱和时停止积分
积分限幅虽然简单,但有时候还不够。更进一步的方法是:当输出已经饱和,并且误差还在推动输出继续饱和时,停止积分。
例如:
输出已经达到最大值
误差仍然为正
继续积分只会让输出更饱和
这时候就不应该继续累加积分。
示例代码:
if (!((pid->output >= pid->output_max && pid->error > 0.0f) ||
(pid->output <= pid->output_min && pid->error < 0.0f)))
{
pid->integral += pid->error * pid->dt;
}
完整逻辑:
float PID_Position_Calc_AntiWindup(PID_TypeDef *pid,
float target,
float actual)
{
float derivative;
float output_unsat;
pid->target = target;
pid->actual = actual;
pid->error = pid->target - pid->actual;
derivative = (pid->error - pid->last_error) / pid->dt;
output_unsat = pid->kp * pid->error
+ pid->ki * pid->integral
+ pid->kd * derivative;
if (!((output_unsat >= pid->output_max && pid->error > 0.0f) ||
(output_unsat <= pid->output_min && pid->error < 0.0f)))
{
pid->integral += pid->error * pid->dt;
pid->integral = Limit(pid->integral,
pid->integral_min,
pid->integral_max);
}
pid->output = pid->kp * pid->error
+ pid->ki * pid->integral
+ pid->kd * derivative;
pid->output = Limit(pid->output,
pid->output_min,
pid->output_max);
pid->last_error = pid->error;
return pid->output;
}
条件积分比单纯积分限幅更加智能。
十、积分分离:误差太大时不积分
积分分离的思想是:
误差很大时,主要靠 P 项快速靠近目标;
误差变小后,再启用 I 项消除静态误差。
为什么误差大时不积分?
因为误差大时如果积分项一直累加,系统很容易超调。
示例:
if (fabsf(pid->error) < pid->integral_separation_threshold)
{
pid->integral += pid->error * pid->dt;
}
完整示例:
if (fabsf(pid->error) <= pid->integral_start_error)
{
pid->integral += pid->error * pid->dt;
}
else
{
pid->integral = 0.0f;
}
适合场景:
位置控制
温度控制
目标变化较大的系统
容易超调的系统
十一、积分衰减:让旧积分慢慢消失
有些系统中,积分项不应该永久累积,可以让积分项随时间慢慢衰减:
pid->integral *= 0.99f;
pid->integral += pid->error * pid->dt;
这样历史误差影响会逐渐减小。
适合场景:
目标经常变化
负载经常变化
希望系统更灵活
不希望积分项残留太久
衰减系数一般接近 1:
0.95 ~ 0.999
越接近 1,积分保留越久。
十二、微分项为什么容易抖?
微分项本质上看的是误差变化率:
derivative = (error - last_error) / dt
如果传感器反馈值有噪声,那么误差也会抖动,微分项会把这种抖动放大。
例如:
实际速度:1000, 1005, 998, 1003, 997
速度本身只是轻微波动,但是微分项看到的是误差变化,很容易导致输出抖动。
所以很多电机速度控制中,实际常用的是:
PI 控制
而不是完整 PID。
十三、微分先行:对反馈值微分,而不是对误差微分
普通微分:
D = Kd × (error - last_error) / dt
但是当目标值突然变化时,error 会突变,D 项也会突然变得很大,这叫做:
微分冲击
解决方法是对反馈值微分,而不是对误差微分。
因为:
error = target - actual
如果 target 突变,error 会突变。
而实际反馈 actual 不会瞬间突变,所以对 actual 微分可以减少微分冲击。
代码:
pid->derivative = -(actual - pid->last_actual) / pid->dt;
输出:
pid->output = pid->kp * pid->error
+ pid->ki * pid->integral
+ pid->kd * pid->derivative;
注意这里有负号,因为反馈值上升时,误差会下降。
结构体中需要保存:
float last_actual;
十四、不完全微分:给 D 项加低通滤波
为了减少微分噪声,可以给微分项加一阶低通滤波。
普通微分:
derivative = (error - last_error) / dt;
滤波后:
derivative_raw = (error - last_error) / dt;
derivative_filtered = alpha * derivative_filtered
+ (1.0f - alpha) * derivative_raw;
其中:
alpha 越大,滤波越强,响应越慢
alpha 越小,滤波越弱,响应越快
常见取值:
alpha = 0.7 ~ 0.95
示例代码:
float PID_Derivative_Filter(PID_TypeDef *pid)
{
float derivative_raw;
derivative_raw = (pid->error - pid->last_error) / pid->dt;
pid->derivative = pid->d_filter_alpha * pid->derivative
+ (1.0f - pid->d_filter_alpha) * derivative_raw;
return pid->derivative;
}
十五、目标值平滑:避免目标突变导致输出突变
很多系统不适合目标值突然变化。
例如电机速度从 0 突然设置为 3000 RPM:
误差瞬间很大
P 项瞬间很大
输出瞬间顶满
系统容易冲击、打滑、震荡
所以可以对目标值做斜坡限制:
float Target_Ramp(float current_target,
float final_target,
float step)
{
if (current_target < final_target)
{
current_target += step;
if (current_target > final_target)
{
current_target = final_target;
}
}
else if (current_target > final_target)
{
current_target -= step;
if (current_target < final_target)
{
current_target = final_target;
}
}
return current_target;
}
使用:
speed_target_smooth = Target_Ramp(speed_target_smooth,
speed_target_final,
10.0f);
这样目标速度会逐渐变化,而不是一步到位。
十六、输出变化率限制:限制控制量变化速度
除了目标值平滑,还可以限制 PID 输出变化率。
例如本次输出不能比上次输出变化太大:
float Output_Rate_Limit(float output,
float last_output,
float max_delta)
{
if (output - last_output > max_delta)
{
output = last_output + max_delta;
}
else if (last_output - output > max_delta)
{
output = last_output - max_delta;
}
return output;
}
适合场景:
电机速度控制
机械臂关节控制
执行器不能突然变化的系统
降低机械冲击
减少电流尖峰
十七、死区补偿
很多执行器存在死区。
例如直流电机:
PWM 小于 80 时电机不转
PWM 大于 80 后才开始转
如果 PID 输出是 30、50、70,电机都不动,积分项可能一直累加,最后突然冲出去。
可以加入死区补偿:
int Motor_DeadZone_Compensate(int pwm)
{
int dead_zone = 80;
if (pwm > 0 && pwm < dead_zone)
{
pwm = dead_zone;
}
else if (pwm < 0 && pwm > -dead_zone)
{
pwm = -dead_zone;
}
return pwm;
}
注意:
死区补偿不等于输出限幅
死区补偿是为了克服执行器启动门槛
十八、前馈控制:不要什么都交给 PID
PID 是反馈控制,必须看到误差后才会动作。
但是有些控制量是可以提前估计的,这时候可以加入前馈控制。
最终输出:
output = feedforward + PID_output
例如速度控制中,目标速度越高,需要的基础 PWM 越大:
feedforward = kf * target_speed;
output = feedforward + pid_output;
完整示例:
float Speed_Control_With_Feedforward(float target_speed,
float actual_speed)
{
float ff;
float pid_out;
float output;
ff = speed_kf * target_speed;
pid_out = PID_Calculate(&speed_pid,
target_speed,
actual_speed);
output = ff + pid_out;
output = Limit(output, -1000.0f, 1000.0f);
return output;
}
前馈的作用:
减少 PID 压力
提高响应速度
减少稳态误差
减少积分项依赖
常见前馈:
速度前馈
加速度前馈
重力补偿
摩擦补偿
电压前馈
十九、串级 PID:多环控制的核心思想
很多实际系统不是一个 PID 能解决的,而是多个 PID 串起来。
典型串级 PID:
位置环 -> 速度环 -> 电流环
例如 FOC 三环:
位置误差
↓
位置环输出速度目标
↓
速度误差
↓
速度环输出 Iq 目标
↓
电流误差
↓
电流环输出 Vq 电压
也就是:
位置环:Position_ref - Position_actual -> Speed_ref
速度环:Speed_ref - Speed_actual -> Iq_ref
电流环:Iq_ref - Iq_actual -> Vq
串级 PID 的原则:
内环要快
外环要慢
先调内环
再调外环
外环输出必须限幅
二十、FOC 三环中 PID 的选择
FOC 常见三环:
电流环
速度环
位置环
更准确地说:
Id 电流环
Iq 电流环
Speed 速度环
Position 位置环
推荐组合:
| 环 | 输出 | 推荐 PID 类型 | 推荐形式 |
|---|---|---|---|
| Id 电流环 | Vd | PI | 位置式 |
| Iq 电流环 | Vq | PI | 位置式 |
| 速度环 | Iq_ref | PI | 增量式或位置式 |
| 位置环 | Speed_ref | P | 位置式 |
最常用的新手组合:
位置环:位置式 P
速度环:增量式 PI
电流环:位置式 PI
如果为了简单好调,也可以全部先用位置式:
位置环:位置式 P
速度环:位置式 PI
电流环:位置式 PI
二十一、为什么 FOC 电流环一般不用 D?
FOC 电流环通常不用 D 项,原因是:
1. 电流采样噪声比较明显
2. 电流环频率很高
3. D 项会放大电流采样噪声
4. PI 已经能满足电流跟踪要求
所以电流环一般是:
Id_ref - Id_actual -> PI -> Vd
Iq_ref - Iq_actual -> PI -> Vq
二十二、为什么位置环一般先用 P?
位置环输出的是速度目标:
speed_ref = Kp_pos × position_error
位置误差大,速度目标大;位置误差小,速度目标小。
这样电机会自然靠近目标并减速。
如果位置环一开始加 I,容易出现:
位置超调
到目标后还继续冲
来回摆动
积分饱和
所以位置环建议:
先用 P
如果负载偏置明显,再考虑很小的 I
如果超调明显,再考虑 D 或速度前馈/阻尼
二十三、PID 调参的正确顺序
对于单环 PID:
先 P
再 I
最后 D
对于串级 PID:
先内环
再外环
例如 FOC:
先调电流环
再调速度环
最后调位置环
不要一开始三个环全开。
二十四、单环 PID 调参流程
1. 只开 P
设置:
Ki = 0
Kd = 0
逐渐增大 Kp。
现象判断:
Kp 太小:响应慢
Kp 合适:响应快,震荡小
Kp 太大:震荡明显,甚至发散
调到系统刚好有较快响应,但不持续震荡。
2. 加 I
逐渐增大 Ki。
现象判断:
Ki 太小:有稳态误差
Ki 合适:最终能稳定到目标
Ki 太大:超调明显,恢复慢,震荡
3. 加 D
如果系统超调或震荡明显,可以加少量 Kd。
现象判断:
Kd 太小:抑制不明显
Kd 合适:超调减小,响应更稳
Kd 太大:输出抖动,噪声放大
二十五、PID 参数现象对照表
| 现象 | 可能原因 | 调整方向 |
|---|---|---|
| 响应太慢 | Kp 太小 | 增大 Kp |
| 一直达不到目标 | Ki 太小或没有积分 | 增大 Ki |
| 超调严重 | Kp 或 Ki 太大 | 降低 Kp/Ki |
| 来回震荡 | Kp 太大、Ki 太大、D 太小 | 降 Kp/Ki,适当加 D |
| 输出抖动 | D 太大或反馈噪声大 | 降低 D,加滤波 |
| 输出长时间顶满 | 积分饱和 | 加积分限幅/条件积分 |
| 电机反向发散 | 误差方向反了 | 检查 target - actual |
| 到目标附近抖动 | 死区、噪声、Kp 太大 | 加死区、滤波、降 Kp |
| 目标变化冲击大 | 目标突变 | 加目标斜坡 |
二十六、完整进阶 PID 结构体
下面给出一个比较完整的 PID 结构体,包含:
dt
输出限幅
积分限幅
微分滤波
积分分离
输出变化率限制
typedef struct
{
float kp;
float ki;
float kd;
float dt;
float target;
float actual;
float last_actual;
float error;
float last_error;
float last_last_error;
float integral;
float derivative;
float output;
float last_output;
float output_min;
float output_max;
float integral_min;
float integral_max;
float integral_start_error;
float d_filter_alpha;
float output_max_delta;
} PID_Advanced_TypeDef;
二十七、完整进阶 PID 初始化函数
void PID_Advanced_Init(PID_Advanced_TypeDef *pid,
float kp,
float ki,
float kd,
float dt,
float output_min,
float output_max,
float integral_min,
float integral_max)
{
pid->kp = kp;
pid->ki = ki;
pid->kd = kd;
pid->dt = dt;
pid->target = 0.0f;
pid->actual = 0.0f;
pid->last_actual = 0.0f;
pid->error = 0.0f;
pid->last_error = 0.0f;
pid->last_last_error = 0.0f;
pid->integral = 0.0f;
pid->derivative = 0.0f;
pid->output = 0.0f;
pid->last_output = 0.0f;
pid->output_min = output_min;
pid->output_max = output_max;
pid->integral_min = integral_min;
pid->integral_max = integral_max;
pid->integral_start_error = 999999.0f;
pid->d_filter_alpha = 0.8f;
pid->output_max_delta = 999999.0f;
}
二十八、完整进阶位置式 PID 代码
#include <math.h>
static float Limit(float value, float min, float max)
{
if (value > max)
{
return max;
}
if (value < min)
{
return min;
}
return value;
}
static float Rate_Limit(float value,
float last_value,
float max_delta)
{
if (value - last_value > max_delta)
{
value = last_value + max_delta;
}
else if (last_value - value > max_delta)
{
value = last_value - max_delta;
}
return value;
}
float PID_Advanced_Position_Calc(PID_Advanced_TypeDef *pid,
float target,
float actual)
{
float derivative_raw;
float output_unsat;
pid->target = target;
pid->actual = actual;
pid->error = pid->target - pid->actual;
if (fabsf(pid->error) <= pid->integral_start_error)
{
output_unsat = pid->kp * pid->error
+ pid->ki * pid->integral
+ pid->kd * pid->derivative;
if (!((output_unsat >= pid->output_max && pid->error > 0.0f) ||
(output_unsat <= pid->output_min && pid->error < 0.0f)))
{
pid->integral += pid->error * pid->dt;
pid->integral = Limit(pid->integral,
pid->integral_min,
pid->integral_max);
}
}
derivative_raw = (pid->error - pid->last_error) / pid->dt;
pid->derivative = pid->d_filter_alpha * pid->derivative
+ (1.0f - pid->d_filter_alpha) * derivative_raw;
pid->output = pid->kp * pid->error
+ pid->ki * pid->integral
+ pid->kd * pid->derivative;
pid->output = Limit(pid->output,
pid->output_min,
pid->output_max);
pid->output = Rate_Limit(pid->output,
pid->last_output,
pid->output_max_delta);
pid->last_error = pid->error;
pid->last_actual = pid->actual;
pid->last_output = pid->output;
return pid->output;
}
二十九、微分先行版本 PID
如果目标值经常突变,可以使用微分先行:
float PID_Advanced_Position_Calc_D_On_Measurement(PID_Advanced_TypeDef *pid,
float target,
float actual)
{
float derivative_raw;
pid->target = target;
pid->actual = actual;
pid->error = pid->target - pid->actual;
pid->integral += pid->error * pid->dt;
pid->integral = Limit(pid->integral,
pid->integral_min,
pid->integral_max);
derivative_raw = -(pid->actual - pid->last_actual) / pid->dt;
pid->derivative = pid->d_filter_alpha * pid->derivative
+ (1.0f - pid->d_filter_alpha) * derivative_raw;
pid->output = pid->kp * pid->error
+ pid->ki * pid->integral
+ pid->kd * pid->derivative;
pid->output = Limit(pid->output,
pid->output_min,
pid->output_max);
pid->last_error = pid->error;
pid->last_actual = pid->actual;
pid->last_output = pid->output;
return pid->output;
}
适合:
目标值阶跃变化明显
不希望 D 项因为目标突变而冲击输出
三十、完整进阶增量式 PID 代码
float PID_Advanced_Increment_Calc(PID_Advanced_TypeDef *pid,
float target,
float actual)
{
float delta_output;
pid->target = target;
pid->actual = actual;
pid->error = pid->target - pid->actual;
delta_output = pid->kp * (pid->error - pid->last_error)
+ pid->ki * pid->error * pid->dt
+ pid->kd * (pid->error
- 2.0f * pid->last_error
+ pid->last_last_error) / pid->dt;
pid->output += delta_output;
pid->output = Limit(pid->output,
pid->output_min,
pid->output_max);
pid->output = Rate_Limit(pid->output,
pid->last_output,
pid->output_max_delta);
pid->last_last_error = pid->last_error;
pid->last_error = pid->error;
pid->last_actual = pid->actual;
pid->last_output = pid->output;
return pid->output;
}
三十一、PID 在 MCU 工程中的文件组织
建议将 PID 独立成模块:
Core
├── Inc
│ ├── pid.h
│ ├── motor.h
│ ├── encoder.h
│ └── control.h
│
└── Src
├── pid.c
├── motor.c
├── encoder.c
└── control.c
PID 模块只负责算法,不要直接操作 PWM、ADC、编码器。
推荐分层:
encoder.c:获取实际速度/位置
motor.c:设置 PWM 或电压
pid.c:计算控制输出
control.c:组织控制逻辑
这样代码更加清晰,也方便移植到不同 MCU。
三十二、PID 进阶实战:电机速度控制
控制目标:
让电机实际速度跟随目标速度
控制链路:
目标速度
↓
速度 PID
↓
PWM 输出
↓
电机驱动
↓
电机转动
↓
编码器测速
↓
反馈给 PID
代码示例:
PID_Advanced_TypeDef speed_pid;
void Speed_Control_Init(void)
{
PID_Advanced_Init(&speed_pid,
0.8f,
2.0f,
0.0f,
0.01f,
-1000.0f,
1000.0f,
-300.0f,
300.0f);
speed_pid.output_max_delta = 50.0f;
speed_pid.integral_start_error = 500.0f;
}
10ms 调用一次:
void Speed_Control_10ms(void)
{
float target_speed;
float actual_speed;
float pwm;
target_speed = Get_Target_Speed();
actual_speed = Encoder_GetSpeed();
pwm = PID_Advanced_Increment_Calc(&speed_pid,
target_speed,
actual_speed);
Motor_SetPWM((int)pwm);
}
三十三、PID 进阶实战:位置控制
位置控制常用串级结构:
位置环输出速度目标
速度环输出 PWM 或 Iq
位置环:
float Position_Loop(float pos_target, float pos_actual)
{
float pos_error;
float speed_target;
pos_error = pos_target - pos_actual;
speed_target = kp_pos * pos_error;
speed_target = Limit(speed_target,
-speed_max,
speed_max);
return speed_target;
}
速度环:
void Position_Speed_Control(void)
{
float speed_target;
float pwm;
speed_target = Position_Loop(position_target,
position_actual);
pwm = PID_Advanced_Increment_Calc(&speed_pid,
speed_target,
speed_actual);
Motor_SetPWM((int)pwm);
}
这种结构比单独一个位置 PID 更稳定。
三十四、PID 进阶实战:FOC 三环控制
FOC 三环一般是:
位置环:输出 speed_ref
速度环:输出 iq_ref
电流环:输出 vd/vq
伪代码:
void FOC_Control_Loop(void)
{
current_loop_count++;
if (position_enable)
{
if (position_loop_count >= POSITION_DIV)
{
position_loop_count = 0;
speed_ref = Position_P_Calc(position_ref,
position_actual);
}
}
if (speed_loop_count >= SPEED_DIV)
{
speed_loop_count = 0;
iq_ref = PID_Advanced_Increment_Calc(&speed_pid,
speed_ref,
speed_actual);
iq_ref = Limit(iq_ref, -iq_max, iq_max);
}
vd = PID_Advanced_Position_Calc(&id_pid,
0.0f,
id_actual);
vq = PID_Advanced_Position_Calc(&iq_pid,
iq_ref,
iq_actual);
FOC_SetVoltage(vd, vq);
}
注意:
电流环频率最高
速度环频率低于电流环
位置环频率低于速度环
例如:
电流环:20kHz
速度环:1kHz
位置环:200Hz
三十五、PID 工程调试 checklist
调 PID 之前,先检查下面这些内容。
1. 反馈方向是否正确?
目标增大后,实际值是否朝目标方向变化?
如果越调越远,大概率方向反了。
2. 反馈值是否可靠?
编码器速度是否正常?
ADC 是否抖动严重?
角度是否跳变?
电流采样是否有偏置?
3. PID 周期是否固定?
不要在不固定周期的 while(1) 中直接跑 PID。
优先使用定时器中断或 RTOS 周期任务。
4. 输出是否限幅?
PWM 范围是多少?
电压输出范围是多少?
Iq 最大允许值是多少?
速度目标最大值是多少?
5. 积分是否限幅?
积分项有没有限制?
输出饱和后是否还在继续积分?
目标变化后积分是否及时清零?
6. 内外环频率是否合理?
内环一定要比外环快。
不要让位置环、电流环同频乱跑。
三十六、PID 常见错误写法
1. 没有限幅
output = kp * error + ki * integral + kd * derivative;
错误原因:
实际 PWM、电压、电流都有范围
不限制输出会导致执行器异常
2. 积分无限累加
integral += error;
错误原因:
长期误差会导致积分饱和
系统恢复慢,超调严重
3. PID 调用周期不固定
while (1)
{
PID_Calculate();
}
错误原因:
循环执行时间会被通信、显示、其他任务影响
PID 参数会变得不可控
4. D 项直接用在噪声很大的反馈上
derivative = error - last_error;
错误原因:
传感器噪声会被 D 项放大
输出抖动
电机啸叫
三十七、总结
PID 从入门到进阶,核心并不是公式越来越复杂,而是工程细节越来越完善。
基础 PID 只关注:
P:比例
I:积分
D:微分
进阶 PID 更关注:
采样周期
积分限幅
条件积分
积分分离
微分滤波
目标平滑
输出限幅
输出变化率限制
死区补偿
前馈控制
串级控制
在 MCU 项目中,想让 PID 真正好用,建议记住以下几点:
1. PID 必须固定周期运行
2. 输出必须限幅
3. 积分必须做抗饱和处理
4. D 项必须谨慎使用
5. 反馈值要滤波但不能过度延迟
6. 多环控制要先内环后外环
7. 不要让 PID 解决所有问题,能前馈就前馈
最后用一句话总结:
真正的 PID 调试不是单纯调 Kp、Ki、Kd,而是在目标、反馈、限幅、滤波、周期、执行器特性之间找到一个稳定又响应快的平衡点。
更多推荐


所有评论(0)