电机驱动开发学习8. PID 算法

电机驱动开发学习8. PID 算法

  • 一、控制系统简介
    • [1.1 什么是控制系统](#1.1 什么是控制系统)
    • [1.2 开环与闭环](#1.2 开环与闭环)
    • [1.3 为什么需要 PID](#1.3 为什么需要 PID)
  • [二、PID 算法是什么](#二、PID 算法是什么)
    • [2.1 基本公式](#2.1 基本公式)
    • [2.2 三个环节分别做什么](#2.2 三个环节分别做什么)
      • [P --- 比例](#P — 比例)
      • [I --- 积分](#I — 积分)
      • [D --- 微分](#D — 微分)
    • [2.3 位置式与增量式](#2.3 位置式与增量式)
    • [2.4 调参直觉](#2.4 调参直觉)
  • [三、Python 搭建 PID 仿真环境](#三、Python 搭建 PID 仿真环境)
    • [3.1 环境准备](#3.1 环境准备)
    • [3.2 被控对象:一阶惯性系统](#3.2 被控对象:一阶惯性系统)
    • [3.3 PID 控制器实现](#3.3 PID 控制器实现)
    • [3.4 交互式动态仿真](#3.4 交互式动态仿真)
  • 四、仿真实验
    • [4.1 运行方式](#4.1 运行方式)
    • [4.2 实验一:只用 P,观察稳态误差](#4.2 实验一:只用 P,观察稳态误差)
    • [4.3 实验二:加入 I 消除稳态误差](#4.3 实验二:加入 I 消除稳态误差)
    • [4.4 实验三:加入 D 抑制超调](#4.4 实验三:加入 D 抑制超调)
    • [4.5 实验四:方波跟踪(推荐观察超调/振荡)](#4.5 实验四:方波跟踪(推荐观察超调/振荡))
    • [4.6 实验五:扰动与正弦跟踪](#4.6 实验五:扰动与正弦跟踪)
    • [4.7 调参顺序(与在线工具一致)](#4.7 调参顺序(与在线工具一致))
  • 五、仿真结果分析
    • [5.1 阶跃响应指标](#5.1 阶跃响应指标)
    • [5.2 常见问题与参数方向](#5.2 常见问题与参数方向)
  • 六、从仿真到嵌入式
  • 附录:仿真代码结构

一、控制系统简介

在开始学习 PID 之前,需要先理解控制系统在做什么。

1.1 什么是控制系统

控制系统由三部分组成:

组成部分 作用 举例
被控对象 接受输入、产生输出的物理或数学过程 水箱水位、房间温度、机械位置
传感器 测量被控对象的实际状态 液位计、温度计、编码器
控制器 根据目标与实际的偏差,计算控制量 PID 控制器

控制的目标通常是:让被控对象的某个量稳定地跟踪设定值(Setpoint)。

1.2 开环与闭环

开环控制:控制器直接给出固定输出,不参考测量值。

复制代码
设定值 ──→ 控制器 ──→ 被控对象 ──→ 输出

特点:结构简单,但无法自动纠正误差。负载变化、环境干扰都会导致输出偏离目标。

闭环控制(反馈控制):把传感器测到的实际值反馈回来,与目标比较后修正输出。

复制代码
        ┌──────────────────────────────────┐
        │                                  │
设定值 ─┤→ 误差 ──→ 控制器 ──→ 被控对象 ──→ 输出
        │    ↑                           │
        │    └──────── 传感器 ←───────────┘
        └──────────────────────────────────┘

其中:

复制代码
误差 e(t) = 设定值 r(t) - 实际值 y(t)

闭环控制能自动补偿干扰,是 PID 发挥作用的前提。

1.3 为什么需要 PID

闭环里常见的问题是:

  • 响应太慢,很久才接近目标
  • 超调太大,冲过目标后再回来
  • 存在稳态误差,永远差一点点到不了目标
  • 负载突变后恢复慢

PID 通过三个环节分别处理这些问题:

环节 主要作用
P(比例) 误差越大,输出越大,加快响应
I(积分) 累积历史误差,消除稳态偏差
D(微分) 预测误差变化趋势,抑制超调

二、PID 算法是什么

2.1 基本公式

连续时间下的标准 PID 公式为:

复制代码
u(t) = Kp·e(t) + Ki·∫e(t)dt + Kd·de(t)/dt

上式含义:控制量 = 比例项 + 积分项 + 微分项。逐项说明如下。

整体符号

符号 含义
u(t) 控制量(输出):控制器计算后送给被控对象的量,如加热功率、阀门开度、PWM 等
t 时间:连续变化的时刻

误差 e(t)

公式中的 e(t) 通常定义为:

复制代码
e(t) = r(t) - y(t)
符号 含义
r(t) 设定值(目标):希望系统达到的值
y(t) 实际值(反馈):传感器测到的当前值
e(t) 误差 :目标与实际之差;e > 0 表示尚未到达目标,e = 0 表示已跟踪上

PID 的核心就是:根据误差 e 的大小和变化,决定控制量 u 该加多少、减多少。

第一项 Kp·e(t) --- 比例(P)

符号 含义
Kp 比例系数:P 环节的"力度"
Kp·e(t) 比例项:误差越大,输出越大

直觉:现在差多少,就按比例去补多少 。误差大则输出大、响应快;误差小则输出小、动作变缓。单独使用 P 时,系统往往存在稳态误差,即系统已经稳定下来之后,输出仍与设定值之间保留一个固定的小偏差,无法完全归零(例如目标 100,实际长期停在 95)。

第二项 Ki·∫e(t)dt --- 积分(I)

符号 含义
Ki 积分系数:I 环节的"力度"
∫e(t)dt 误差对时间的积分:把历史上每一时刻的误差累加起来
Ki·∫e(t)dt 积分项:对长期偏差的累积补偿

直觉:不只看"现在差多少",还看"已经差多久了" 。只要误差持续不为零,积分项就会不断增大,推动输出继续变化,直到误差归零。用来消除稳态误差Ki 过大会导致超调积分饱和,即输出冲过目标后才慢慢回落,以及控制量已达上限时积分仍在累加、造成恢复缓慢或大幅振荡。

第三项 Kd·de(t)/dt --- 微分(D)

符号 含义
Kd 微分系数:D 环节的"力度"
de(t)/dt 误差对时间的导数:误差变化有多快
Kd·de(t)/dt 微分项:根据误差变化趋势做"提前刹车"

直觉:不看误差大小,而看误差变化有多快 。误差快速减小时,D 项产生反向作用,抑制超调;误差突然变大时,D 项会提前加大反应。对测量噪声较敏感,实际系统中常需滤波。

三项合在一起

复制代码
u(t) =  现在差多少就补多少     +  差多久就补多少        +  差得有多急就提前调节
        ─────────────────       ───────────────         ─────────────────────
        Kp·e(t)    (P)          Ki·∫e(t)dt   (I)         Kd·de(t)/dt   (D)

离散实现(嵌入式和仿真最常用)中,设采样周期为 dt,第 k 次采样时:

复制代码
e(k) = r(k) - y(k)

P项:  Kp · e(k)
I项:  Ki · Σ e(i)·dt
D项:  Kd · [e(k) - e(k-1)] / dt

u(k) = P项 + I项 + D项

这就是位置式 PID:每次直接算出控制量的绝对值。

2.2 三个环节分别做什么

P --- 比例

复制代码
u_P = Kp × e
  • Kp 越大:响应越快,但容易振荡
  • Kp 越小:响应慢,但更平稳
  • 单独使用 P 时,系统往往存在稳态误差(例如被控对象有持续负载时,P 项不足以把误差完全消除)

I --- 积分

复制代码
u_I = Ki × Σ(e × dt)
  • 只要误差不为零,积分项就会持续累加,直到把误差"积"到零
  • 能消除稳态误差
  • 但积分过大会导致超调积分饱和(输出已到极限,积分还在累加)

工程上必须加积分限幅和**抗饱和(Anti-windup)**处理。

D --- 微分

复制代码
u_D = Kd × (e(k) - e(k-1)) / dt
  • 相当于看"误差变化有多快"
  • 误差快速减小时,D 项产生反向作用,抑制超调
  • 对噪声敏感,实际系统中常对测量值滤波后再求微分

2.3 位置式与增量式

形式 输出含义 适用场景
位置式 直接输出控制量绝对值 温度、位置、PWM 占空比
增量式 输出相对上次的增量 Δu 步进电机、阀门开度微调

本教程仿真采用位置式 PID,与后续嵌入式实现一致。

2.4 调参直觉

调参没有唯一公式,但有一个实用顺序:

  1. 先设 Ki=0, Kd=0,从小到大调 Kp,直到出现轻微振荡
  2. 加入 Ki,消除稳态误差
  3. 若超调仍大,再加入少量 Kd

经典 Ziegler-Nichols 法可作为参考,但仿真 + 实机微调往往更直观。


三、Python 搭建 PID 仿真环境

在把 PID 写到 STM32 之前,先用 Python 仿真可以:

  • 快速理解 P / I / D 各自对波形的影响
  • 安全地试错调参,不烧硬件
  • 建立"看到阶跃响应曲线"的直觉

3.1 环境准备

需要 Python 3.8+,安装依赖:

bash 复制代码
pip install numpy matplotlib

3.2 被控对象:一阶惯性系统

用一个简单的一阶惯性环节作为被控对象:

复制代码
T · dy/dt + y = K · u

含义:输出 y 不会瞬间跟随输入 u,而是以时间常数 T 逐渐逼近。很多物理过程(温度、液位、RC 电路)在一阶近似下都类似这个模型。

离散化(欧拉法):

python 复制代码
dy = (K * u - y) / T
y  = y + dy * dt

本教程取 K=1.0, T=2.0,即控制量满幅时,输出约 2 秒后接近稳态。

3.3 PID 控制器实现

python 复制代码
class PIDController:
    def __init__(self, kp, ki, kd, dt, out_min=-10, out_max=10, integral_max=50):
        self.kp, self.ki, self.kd = kp, ki, kd
        self.dt = dt
        self.out_min, self.out_max = out_min, out_max
        self.integral_max = integral_max
        self.integral = 0.0
        self.prev_error = 0.0

    def update(self, setpoint, measurement):
        error = setpoint - measurement

        p_term = self.kp * error
        self.integral += self.ki * error * self.dt
        self.integral = max(-self.integral_max, min(self.integral_max, self.integral))
        d_term = self.kd * (error - self.prev_error) / self.dt
        self.prev_error = error

        output = p_term + self.integral + d_term
        output = max(self.out_min, min(self.out_max, output))

        # 积分抗饱和
        if output >= self.out_max and error > 0:
            self.integral -= self.ki * error * self.dt
        elif output <= self.out_min and error < 0:
            self.integral -= self.ki * error * self.dt

        return output

3.4 交互式动态仿真

本教程采用实时交互仿真 ,体验类似在线工具 ElysiaTools PID 可视化Luis Llamas PID Simulator

  • 拖动 Kp / Ki / Kd 滑块,曲线实时变化
  • 响应曲线:目标值、实际输出、误差
  • PID 分量:P / I / D 三条输出随时间滚动
  • 物理动画:红块=目标位置,蓝块=实际位置
  • 实时指标:当前误差、积分项、微分项、超调量、调节时间
  • 操作按钮 :暂停/继续、重置、阶跃、方波、扰动、正弦跟踪

被控对象为一阶惯性系统 + 纯滞后(更贴近真实工业对象):

复制代码
T · dy/dt + y = K · u(t - delay)

仿真步长 dt = 0.05 s(20 Hz),由定时刷新间隔决定,不是从 PID 公式推导出来的。

完整代码见 sim/pid_simulation.py


四、仿真实验

4.1 运行方式

bash 复制代码
cd sim
python pid_simulation.py          # 交互式动态仿真(默认)
python pid_simulation.py --static # 静态 P/PI/PID 对比图

启动后会出现交互窗口,界面布局如下:

区域 内容
上方 响应曲线(目标 / 输出 / 误差)
左下 P / I / D 分量曲线
右下 物理位置动画
底部 Kp/Ki/Kd/噪声 滑块 + 操作按钮 + 实时指标(目标值滑块仅阶跃模式显示

4.2 实验一:只用 P,观察稳态误差

  1. Ki=0, Kd=0Kp 从 0.5 逐步调到 3.0
  2. 点击「阶跃」(此时会出现「目标值」滑块)
  3. 拖动目标值滑块设为 50(或其他值)
  4. 观察:输出能否快速上升?稳定后是否与目标有固定偏差?

预期:Kp 越大响应越快,但单独 P 往往存在稳态误差

4.3 实验二:加入 I 消除稳态误差

  1. 保持 Kp=2.0Kd=0
  2. Ki 从 0 逐步增大到 0.1 → 0.5 → 1.0
  3. 观察稳态误差是否消失,超调是否变大

预期:Ki 合适时可消除稳态误差;过大则超调增加、调节时间变长。

4.4 实验三:加入 D 抑制超调

  1. 在实验二较好的 PI 参数基础上,加入 Kd(如 0.3 → 0.8)
  2. 观察超调量和振荡是否减小

预期:适量 Kd 可抑制超调;过大且开启「噪声」滑块时,输出会抖动。

4.5 实验四:方波跟踪(推荐观察超调/振荡)

方波目标在高低两档之间反复阶跃,每个边沿都是一次阶跃响应,比正弦更容易观察:

  • 上升/下降沿是否有超调
  • 平台段是否有稳态误差(纯 P 时明显)
  • Ki 过大时是否在边沿附近来回振荡

操作:点击「方波 」,目标在 25↔75 之间切换,周期 4 s(每档 2 s)。建议从 Ki=0, Kd=0 增大 Kp,再逐步加 Ki 对比边沿波形。

4.6 实验五:扰动与正弦跟踪

  • 点击「扰动」:对被控对象施加负载突变,观察 PI/PID 能否拉回目标
  • 点击「正弦 」:目标值平滑变化,测试动态跟踪(相位滞后、幅值衰减)

4.7 调参顺序(与在线工具一致)

  1. 先只用 Kp,调到响应可接受
  2. 有稳态误差时加 Ki
  3. 超调或振荡过大时再加 Kd
  4. 每次只改一个参数,小幅调整

也可参考在线工具的观察指南:ElysiaTools 观察指南


五、仿真结果分析

5.1 阶跃响应指标

指标 含义 期望
上升时间 输出从 10% 到 90% 目标值的时间 越短越好(在稳定前提下)
超调量 超过目标值的峰值比例 越小越好,工业上常要求 < 10%
调节时间 进入并保持在误差带内的时间 越短越好
稳态误差 稳定后与目标的差 越接近 0 越好

5.2 常见问题与参数方向

现象 可能原因 调整方向
响应太慢 Kp 太小 增大 Kp
振荡 / 超调大 Kp 或 Ki 太大 减小 Kp/Ki,或加 Kd
有稳态误差 缺少积分或 Ki 太小 增大 Ki
控制量长时间饱和 积分 windup 加抗饱和,减小 Ki
输出抖动 D 项对噪声敏感 减小 Kd,或滤波

六、从仿真到嵌入式

本篇文章聚焦 PID 原理与 Python 仿真。掌握以下内容后,再进入下一篇(电机速度闭环)会轻松很多:

  • 理解误差 e = 设定值 - 测量值
  • 能手写位置式 PID,含积分限幅与抗饱和
  • 通过仿真观察 P / I / D 对阶跃响应的影响
  • 知道"先 P,再 I,最后 D"的调参顺序

下一篇将把同样的 PID 结构移植到 STM32,用编码器反馈实现电机速度闭环控制。


附录:仿真代码结构

复制代码
lesson8. PID算法/
├── README.md              # 本文
└── sim/
    └── pid_simulation.py  # 完整仿真脚本

依赖安装:

bash 复制代码
pip install numpy matplotlib

快速验证:

bash 复制代码
cd sim && python pid_simulation.py