3. 位置式PID实现

位置式PID,说白了就是最经典的PID算法。很多教材一上来就讲这个,但真正把它用好的工程师,其实不多。我刚开始做电机控制那会儿,也踩过不少坑。今天咱们就把这东西彻底聊透。

3.1 公式推导:从连续到离散

先看连续域的PID公式:

u(t) = Kp * e(t) + Ki * ∫e(τ)dτ + Kd * de(t)/dt

这里面三个部分:

  • 比例项:当前误差,直接响应
  • 积分项:过去误差的累积,消除静差
  • 微分项:误差变化趋势,提前预判

但问题是,单片机不认识连续信号。它只认离散的采样点。所以我们要把公式离散化。

假设采样周期为T,第k次采样:

  • 积分项用累加代替:∫e(τ)dτ ≈ T * Σe(i),i从0到k
  • 微分项用差分代替:de(t)/dt ≈ (e(k) - e(k-1)) / T

代入后得到离散位置式PID:

u(k) = Kp * e(k) + Ki * T * Σe(i) + Kd * (e(k) - e(k-1)) / T

为了简化,通常把Ki和Kd重新定义,把T吸收进去:

u(k) = Kp * e(k) + Ki * Σe(i) + Kd * (e(k) - e(k-1))

嗯,这里要注意。这个简化后的Ki和Kd,其实已经包含了采样周期T。如果你换了一个采样周期,参数要重新整定。我在项目中就吃过这个亏,换了主控芯片,采样频率变了,结果系统直接震荡。

核心要点:位置式PID的输出u(k)直接对应执行机构的绝对位置。比如阀门开度、电机角度。它跟增量式PID最大的区别就在这里。

3.2 C语言代码实现

先定义结构体,把PID相关的变量打包。我个人习惯这样做,方便管理多个PID回路。

typedef struct {
    float Kp;           // 比例系数
    float Ki;           // 积分系数
    float Kd;           // 微分系数
    
    float target;       // 目标值
    float feedback;     // 反馈值
    float error;        // 当前误差
    float last_error;   // 上一次误差
    
    float integral;     // 积分累加值
    float output;       // 输出值
    
    // 限幅参数
    float integral_limit;   // 积分限幅
    float output_limit;     // 输出限幅
} PID_TypeDef;

然后是核心的计算函数:

float PID_Calculate(PID_TypeDef *pid, float target, float feedback) {
    // 1. 计算误差
    pid->target = target;
    pid->feedback = feedback;
    pid->error = target - feedback;
    
    // 2. 积分累加(带限幅)
    pid->integral += pid->error;
    if(pid->integral > pid->integral_limit) 
        pid->integral = pid->integral_limit;
    if(pid->integral < -pid->integral_limit) 
        pid->integral = -pid->integral_limit;
    
    // 3. 计算微分
    float derivative = pid->error - pid->last_error;
    
    // 4. 计算输出
    pid->output = pid->Kp * pid->error 
                + pid->Ki * pid->integral 
                + pid->Kd * derivative;
    
    // 5. 输出限幅
    if(pid->output > pid->output_limit) 
        pid->output = pid->output_limit;
    if(pid->output < -pid->output_limit) 
        pid->output = -pid->output_limit;
    
    // 6. 更新上一次误差
    pid->last_error = pid->error;
    
    return pid->output;
}

小技巧:积分限幅一定要加。我曾经在一个温控项目里没加积分限幅,结果积分项一直累加,输出饱和后系统恢复不过来,温度直接冲过头。嗯,那次教训挺深刻的。

3.3 代码结构分析

这段代码看起来简单,但每个细节都有讲究。咱们拆开来看:

结构体设计:把PID参数和状态变量封装在一起。这样做的好处是,一个结构体就是一个独立的PID控制器。你要控制三个电机?定义三个结构体变量就行。

计算流程

  1. 先算误差——这是PID的输入
  2. 再累加积分——注意限幅
  3. 计算微分——用当前误差减上一次误差
  4. 加权求和——三个分量加起来
  5. 输出限幅——保护执行机构
  6. 更新状态——为下一次计算做准备

你想想看,这个顺序能不能换?比如先更新last_error再算微分?那肯定不行。微分项就变成0了。这种低级错误,我见过不少新手犯。

避坑指南:我曾经在移植代码时,忘记初始化last_error为0。结果第一次计算微分时,derivative = error - 0,相当于多了一个很大的微分冲击。系统直接飞车。所以初始化一定要做彻底。

3.4 参数整定方法

参数整定,说白了就是找三个数:Kp、Ki、Kd。没有万能公式,但有套路。

方法一:试凑法

这是最常用的方法。我一般这么干:

  1. 先把Ki和Kd设为0,只调Kp
  2. 从小到大增加Kp,直到系统出现等幅振荡
  3. 记录此时的Kp值,记为Kp_critical
  4. 然后加入Ki,消除静差
  5. 最后加Kd,改善动态响应

方法二:Ziegler-Nichols法

这个方法更系统一些。找到临界增益Kp_critical和振荡周期T_critical后,查表:

控制器类型 Kp Ki Kd
P 0.5 * Kp_critical - -
PI 0.45 * Kp_critical 1.2 * Kp / T_critical -
PID 0.6 * Kp_critical 2 * Kp / T_critical Kp * T_critical / 8

这个方法给的是初始值。实际应用中,还要微调。我记得有一次调一个流量控制系统,用ZN法算出来的参数,系统响应太快,执行机构跟不上。最后我把Kp降了30%,才稳定下来。

经验总结:参数整定没有银弹。不同系统、不同工况,参数都不一样。我的建议是:先让系统动起来,再慢慢优化。别指望一次调好。

3.5 位置式PID的优缺点

优点很明显:

  • 输出是绝对位置,物理意义清晰
  • 适合控制阀门、舵机等需要绝对位置的执行器
  • 积分项能彻底消除静差

缺点也不容忽视:

  • 积分累积容易导致饱和
  • 输出突变时,微分项会产生冲击
  • 需要保存所有历史误差(虽然实际中只累加)

说白了,位置式PID适合那些对精度要求高、执行机构响应慢的系统。比如温度控制、液位控制。对于快速响应的系统,比如电机转速控制,增量式PID可能更合适。

好了,位置式PID就聊这么多。代码给你了,思路也讲了,剩下的就是动手实践。记住,纸上得来终觉浅,绝知此事要躬行。


专注资料整理