算法总结(二)深入浅出 PID 控制算法:原理、优化与 STM32 标准库实现

摘要

PID(比例‑积分‑微分)控制器是嵌入式闭环控制中最经典、应用最广的算法。本文从连续域数学模型出发,推导出适合单片机实现的离散化形式,并重点剖析积分分离、抗积分饱和、微分先行 三种工程优化策略。所有代码均使用结构体封装,仅依赖 math.hstdint.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 手动试凑法

  1. 纯比例:令 (K_i=0, K_d=0),逐步增大 (K_p) 直到系统出现持续等幅振荡,记录临界增益 (K_u) 和振荡周期 (T_u)。
  2. 积分:引入 (K_i) 消除静差,一般取 (K_i = K_p \times T_s / T_i),(T_i) 可先设为 (0.5T_u),观察超调与稳定时间。
  3. 微分:适当加入 (K_d) 抑制超调和振荡,(K_d) 通常为 (K_p) 的几分之一到十分之一。
  4. 微调:根据实际响应曲线精细调整三个系数。

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)

欢迎在评论区交流调参心得与项目经验!

相关推荐
Sinsa_SI16 小时前
2026算法应用主题赛初赛-小学4-6组(c++)试卷(含答案+详细解析)
java·c++·算法
_深海凉_16 小时前
LeetCode热题100-排序链表
算法·leetcode·链表
代码中介商16 小时前
哈夫曼树:高效压缩数据的秘密武器
数据结构·算法
sheeta199816 小时前
LeetCode 每日一题笔记 日期:2026.05.22 题目:33. 搜索旋转排序数组
笔记·算法·leetcode
练习时长一年16 小时前
LeetCode热题100之缺失的第一个正数
数据结构·算法·leetcode
Severus_black16 小时前
【初阶数据结构与算法】八大排序之插入排序(直接插入、希尔),一次性讲清!
数据结构·算法·排序算法
鱼子星_16 小时前
【数据结构与算法】数据结构基础——树(上):树的存储结构,满二叉树,完全二叉树,二叉树的存储结构
c语言·数据结构·算法
高级c16 小时前
MindIE 推理引擎架构解析
深度学习·算法·架构·cann
奶人五毛拉人一块16 小时前
滑动窗口算法及习题讲解
数据结构·算法·滑动窗口·子数组