PID介绍

PID超通俗讲解 + 工程实用公式(嵌入式直接用)

一、PID 是什么(大白话)

P比例 :现在偏差多大,立刻使劲调
I积分 :慢慢把静态误差消掉,稳住不偏移
D微分:预判变化趋势,提前刹车防超调

三者合起来:快、准、稳

二、标准公式

1. 连续时域公式

u(t)=Kp[e(t)+1Ti∫0te(τ)dτ+Tdde(t)dt]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]u(t)=Kp[e(t)+Ti1∫0te(τ)dτ+Tddtde(t)]

  • e(t)e(t)e(t):偏差 = 目标值 - 实际值
  • KpK_pKp:比例系数
  • TiT_iTi:积分时间
  • TdT_dTd:微分时间

三、工程最常用:离散位置式PID(单片机直接写代码)

离散公式

{e(k)=set−nowu(k)=Kp[e(k)+TTi∑i=0ke(i)+TdT(e(k)−e(k−1))] \begin{cases} e(k) = set - now \\ u(k) = K_p\left[ e(k) + \dfrac{T}{T_i}\sum_{i=0}^k e(i) + \dfrac{T_d}{T}\big(e(k)-e(k-1)\big) \right] \end{cases} ⎩ ⎨ ⎧e(k)=set−nowu(k)=Kp[e(k)+TiT∑i=0ke(i)+TTd(e(k)−e(k−1))]

  • TTT:采样周期(固定)
  • e(k)e(k)e(k) 当前偏差,e(k−1)e(k-1)e(k−1) 上一次偏差

简化参数写法(工程通用)
u(k)=Kp⋅e(k)+Ki⋅∑e+Kd⋅(e(k)−e(k−1))\boldsymbol{u(k) = K_p\cdot e(k) + K_i\cdot \sum e + K_d\cdot (e(k)-e(k-1))}u(k)=Kp⋅e(k)+Ki⋅∑e+Kd⋅(e(k)−e(k−1))
Ki=Kp⋅TTi,Kd=Kp⋅TdTK_i=\dfrac{K_p\cdot T}{T_i},\quad K_d=\dfrac{K_p\cdot T_d}{T}Ki=TiKp⋅T,Kd=TKp⋅Td

四、增量式PID(电机、温控最常用!)

只算输出变化量 ,不易积分饱和,工业首选

\\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)+Δu(k)u(k) = u(k-1) + \Delta u(k)u(k)=u(k−1)+Δu(k)

五、三个参数作用(调参口诀)

  1. P增大:响应变快,容易震荡超调
  2. I增大:消除静差越快,超调变大
  3. D增大:抑制震荡,太大容易抖动、抗干扰差

调参顺序:先P → 再加I消静差 → 最后D抑超调

六、嵌入式C语言最简代码(直接复制用)

c 复制代码
// PID结构体
typedef struct
{
    float Kp, Ki, Kd;
    float set;        // 目标值
    float now;        // 当前值
    float err,err1,err2;
    float out;
}PID_TypeDef;

// 增量式PID计算
float PID_Inc_Calc(PID_TypeDef *pid)
{
    pid->err = pid->set - pid->now;
    float delta = pid->Kp*(pid->err - pid->err1) 
                + pid->Ki*pid->err 
                + pid->Kd*(pid->err - 2*pid->err1 + pid->err2);
    
    pid->out += delta;
    // 输出限幅
    if(pid->out > 1000) pid->out = 1000;
    if(pid->out < 0)    pid->out = 0;
    
    pid->err2 = pid->err1;
    pid->err1 = pid->err;
    return pid->out;
}

七、实际工程应用场景

  1. 电机调速/FOC速度环:增量PID
  2. 温控、恒温箱:位置PID + 积分限幅
  3. 无人机姿态、平衡车:PD为主,弱积分
  4. 压力、液位控制:PI为主,少用D
  5. 流量、风速:纯P/PI即可

需要我给你PID调参步骤 或者温控/电机专用整定参数吗?




嵌入式实际落地PID公式(纯工程版,无学术化简)

核心定义

e=目标值−实测值e = \boldsymbol{目标值 - 实测值}e=目标值−实测值
TTT = 定时采样周期(ms,定时器固定)


1. 位置式PID(温控、稳压、恒压常用)

工程实用公式

Out=Kp⋅e+Ki⋅∑e+Kd⋅(e−elast)Out = K_p\cdot e + K_i\cdot \sum e + K_d\cdot (e - e_{last})Out=Kp⋅e+Ki⋅∑e+Kd⋅(e−elast)

  • ∑e\sum e∑e:历史偏差累加和
  • elaste_{last}elast:上一次偏差
  • 必加:积分限幅 + 输出限幅,防炸机

拆分写法(代码直接对应)

Pout=Kp⋅eIout=Ki⋅SumErrDout=Kd⋅(e−elast)PIDout=Pout+Iout+Dout \begin{align} P_{out} &= K_p \cdot e \\ I_{out} &= K_i \cdot SumErr \\ D_{out} &= K_d \cdot (e - e_{last}) \\ PID_{out} &= P_{out}+I_{out}+D_{out} \end{align} PoutIoutDoutPIDout=Kp⋅e=Ki⋅SumErr=Kd⋅(e−elast)=Pout+Iout+Dout


2. 增量式PID(电机调速、FOC环、舵机、运动控制最常用

只算输出增量,无积分饱和,嵌入式首选

最终工程公式

ΔOut=Kp(e−e1)+Ki⋅e+Kd(e−2e1+e2) \Delta Out = K_p(e-e_1) + K_i \cdot e + K_d(e-2e_1+e_2) ΔOut=Kp(e−e1)+Ki⋅e+Kd(e−2e1+e2)
NowOut=LastOut+ΔOutNowOut = LastOut + \Delta OutNowOut=LastOut+ΔOut

  • eee:当前偏差
  • e1e_1e1:上一拍偏差
  • e2e_2e2:上上一拍偏差

3. 嵌入式常用精简变种(项目高频)

① PI控制(绝大多数恒温、电压、流量)

Out=Kp⋅e+Ki⋅∑eOut = K_p\cdot e + K_i\cdot \sum eOut=Kp⋅e+Ki⋅∑e
去掉微分D,抗干扰强,工业硬件最爱

② PD控制(平衡车、姿态角、倾角控制)

Out=Kp⋅e+Kd⋅(e−elast)Out = K_p\cdot e + K_d\cdot (e-e_{last})Out=Kp⋅e+Kd⋅(e−elast)
去掉积分I,响应快、无滞后、不累积误差


4. 系数换算(把书本参数转嵌入式参数)

书本:Kp、Ti、TdK_p、T_i、T_dKp、Ti、Td

嵌入式实际用:Kp、Ki、KdKp、Ki、KdKp、Ki、Kd
{Ki=Kp⋅TTiKd=Kp⋅TdT \begin{cases} K_i = \dfrac{K_p \cdot T}{T_i}\\[6pt] K_d = \dfrac{K_p \cdot T_d}{T} \end{cases} ⎩ ⎨ ⎧Ki=TiKp⋅TKd=TKp⋅Td
TTT = 采样周期(单位和Ti、TdT_i、T_dTi、Td统一)


5. 嵌入式代码一对一公式映射

c 复制代码
// 增量式 完全对应上面公式
float pid_calc(PID *p)
{
    p->e = p->tar - p->val;

    // 直接套工程公式
    float delta = p->kp*(p->e - p->e1) 
                + p->ki * p->e
                + p->kd*(p->e - 2*p->e1 + p->e2);

    p->out += delta;

    // 实际工程必加限幅
    if(p->out > p->max) p->out = p->max;
    if(p->out < p->min) p->out = p->min;

    // 更新偏差缓存
    p->e2 = p->e1;
    p->e1 = p->e;
    return p->out;
}

6. 工程实战取值规律

  1. 电机速度环:增量PID,D偏大抑制抖动
  2. 电池温控:位置PI,关闭D防温度跳变误调
  3. 位置闭环(云台、滑台):完整PID
  4. 电流环:纯PI

需要我给你STM32/ESP32直接能用的PID完整结构体+定时调用模板吗?




PID 本质上就是一个闭环控制算法

它干的事情很简单:

我想要一个目标值,比如速度 1000rpm;

现在实际只有 800rpm;

PID 根据"差多少、差了多久、变化快不快"来算出一个控制输出,比如 PWM 占空比。


1. PID 里面的几个量

假设:

c 复制代码
target   // 目标值,比如目标速度、目标角度、目标温度
measure  // 实际测量值,比如编码器速度、IMU角度、温度传感器值
output   // 控制输出,比如PWM占空比、电机电压、阀门开度

误差:

c 复制代码
error = target - measure;

也就是:

c 复制代码
误差 = 目标值 - 实际值

例如:

c 复制代码
目标速度 = 1000 rpm
实际速度 = 800 rpm
error = 1000 - 800 = 200

说明速度低了,需要加大 PWM。


2. PID 三个部分分别干啥?

PID = P + I + D


P:比例控制

P 的公式:

c 复制代码
P = Kp * error;

意思是:

当前误差越大,控制力度越大。

例如速度差 200rpm,就多给点 PWM;差 20rpm,就少给点。

P 的特点:

Kp 太小 Kp 太大
反应慢,没力气 容易震荡、抖动、超调

比如平衡车:

c 复制代码
角度偏差越大,电机越用力往回拉

I:积分控制

I 的公式:

c 复制代码
integral += error * dt;
I = Ki * integral;

意思是:

误差如果长期存在,就不断累加,慢慢加大控制输出。

它主要解决静态误差

比如电机速度控制:

目标 1000rpm,实际一直稳定在 950rpm。

P 控制可能觉得误差 50 不大,输出不够继续增加,于是永远差一点。

积分 I 会发现:

c 复制代码
你一直差 50,一直差,一直差......

于是它会慢慢把输出补上去,最后让速度接近 1000rpm。

I 的特点:

Ki 太小 Ki 太大
消除误差慢 容易积分饱和、震荡、超调

D:微分控制

D 的公式:

c 复制代码
D = Kd * (error - last_error) / dt;

意思是:

看误差变化速度,用来提前刹车、抑制震荡。

比如平衡车快要倒下时,角度变化速度很快,D 项可以提前反应。

D 的特点:

Kd 太小 Kd 太大
抑制震荡不明显 对噪声敏感,输出抖动

在工程里,D 项很容易被传感器噪声影响,所以经常要滤波。


3. 连续 PID 数学公式

u(t)=K_p e(t)+K_i\int_0^t e(\tau)d\tau+K_d\frac{de(t)}{dt}

其中:

符号 含义
u(t) 控制输出
e(t) 当前误差
Kp 比例系数
Ki 积分系数
Kd 微分系数

但是单片机里不能直接用这个连续公式,因为 MCU 是周期性运行的,比如 1ms 算一次、5ms 算一次、10ms 算一次。

所以工程里一般用离散 PID


4. 实际工程常用:位置式 PID

这是最常见的写法。

公式

c 复制代码
error = target - measure;

P = Kp * error;

integral += error * dt;
I = Ki * integral;

D = Kd * (error - last_error) / dt;

output = P + I + D;

last_error = error;

完整公式可以理解成:

u_k=K_p e_k+K_i T_s\sum_{i=0}^{k} e_i+K_d\frac{e_k-e_{k-1}}{T_s}

其中:

符号 含义
k 当前第 k 次计算
Ts / dt 控制周期
e_k 当前误差
e_{k-1} 上一次误差

5. 工程代码模板:位置式 PID

c 复制代码
typedef struct
{
    float kp;
    float ki;
    float kd;

    float target;       // 目标值
    float measure;      // 测量值

    float error;        // 当前误差
    float last_error;   // 上一次误差

    float integral;     // 积分累加值

    float output;       // PID输出

    float output_max;   // 输出限幅
    float output_min;

    float integral_max; // 积分限幅
    float integral_min;

    float dt;           // 控制周期,单位:秒
} PID_t;


static float limit_float(float value, float min, float max)
{
    if (value > max)
        return max;
    if (value < min)
        return min;
    return value;
}


float PID_Calc(PID_t *pid, float target, float measure)
{
    float p;
    float i;
    float d;

    pid->target = target;
    pid->measure = measure;

    // 1. 计算误差
    pid->error = pid->target - pid->measure;

    // 2. P项:当前误差越大,输出越大
    p = pid->kp * pid->error;

    // 3. I项:误差累加,用来消除长期静态误差
    pid->integral += pid->error * pid->dt;

    // 4. 积分限幅,防止积分饱和
    pid->integral = limit_float(pid->integral,
                                pid->integral_min,
                                pid->integral_max);

    i = pid->ki * pid->integral;

    // 5. D项:误差变化速度,用来抑制超调和震荡
    d = pid->kd * (pid->error - pid->last_error) / pid->dt;

    // 6. PID总输出
    pid->output = p + i + d;

    // 7. 输出限幅,比如PWM只能是 0~1000
    pid->output = limit_float(pid->output,
                              pid->output_min,
                              pid->output_max);

    // 8. 保存当前误差,供下次计算D项
    pid->last_error = pid->error;

    return pid->output;
}

6. 实际用在 PWM 控制里

假设你控制电机速度。

c 复制代码
float target_speed = 1000.0f;   // 目标速度 1000rpm
float real_speed   = get_motor_speed();

float pwm = PID_Calc(&speed_pid, target_speed, real_speed);

motor_set_pwm((int)pwm);

PID 输出可以直接对应:

控制对象 PID 输出
直流电机速度 PWM 占空比
BLDC FOC 速度环 目标电流 Iq
温度控制 加热功率
舵机位置控制 PWM 或目标角速度
平衡车角度控制 电机力矩 / 电流 / PWM
水泵压力控制 电机转速
灯光亮度控制 PWM 占空比

7. 更工程化的写法:D 项不要对 error 微分

很多实际工程里,D 项不推荐这么写:

c 复制代码
D = Kd * (error - last_error) / dt;

因为目标值 target 突然变化时,error 会突然跳变,D 项也会突然很大,输出会猛冲。

更常用的是对测量值微分:

c 复制代码
D = -Kd * (measure - last_measure) / dt;

为什么前面有负号?

因为:

c 复制代码
error = target - measure

如果 target 不变,那么:

c 复制代码
error变化 = -measure变化

所以工程写法:

c 复制代码
d = -pid->kd * (measure - pid->last_measure) / pid->dt;

这种叫:

c 复制代码
微分先行

或者:

c 复制代码
Derivative on Measurement

它的优点是目标值突变时,不容易让 D 项产生尖峰。


8. 带测量微分的工程 PID

c 复制代码
typedef struct
{
    float kp;
    float ki;
    float kd;

    float error;
    float integral;

    float last_measure;

    float output;

    float output_max;
    float output_min;

    float integral_max;
    float integral_min;

    float dt;
} PID_Real_t;


float PID_RealCalc(PID_Real_t *pid, float target, float measure)
{
    float p;
    float i;
    float d;

    // 当前误差
    pid->error = target - measure;

    // P项
    p = pid->kp * pid->error;

    // I项
    pid->integral += pid->error * pid->dt;

    pid->integral = limit_float(pid->integral,
                                pid->integral_min,
                                pid->integral_max);

    i = pid->ki * pid->integral;

    // D项:对测量值求微分,而不是对误差求微分
    d = -pid->kd * (measure - pid->last_measure) / pid->dt;

    // 输出
    pid->output = p + i + d;

    // 输出限幅
    pid->output = limit_float(pid->output,
                              pid->output_min,
                              pid->output_max);

    // 保存上一次测量值
    pid->last_measure = measure;

    return pid->output;
}

9. 增量式 PID

位置式 PID 算的是:

c 复制代码
当前应该输出多少

增量式 PID 算的是:

c 复制代码
这一次输出应该比上一次增加多少

公式:

\Delta u_k=K_p(e_k-e_{k-1})+K_iT_s e_k+\frac{K_d}{T_s}(e_k-2e_{k-1}+e_{k-2})

然后:

c 复制代码
output += delta_output;

工程代码:

c 复制代码
typedef struct
{
    float kp;
    float ki;
    float kd;

    float error;
    float last_error;
    float prev_error;

    float output;

    float output_max;
    float output_min;

    float dt;
} PID_Inc_t;


float PID_IncCalc(PID_Inc_t *pid, float target, float measure)
{
    float delta_output;

    pid->error = target - measure;

    delta_output =
        pid->kp * (pid->error - pid->last_error)
        + pid->ki * pid->dt * pid->error
        + pid->kd * (pid->error - 2.0f * pid->last_error + pid->prev_error) / pid->dt;

    pid->output += delta_output;

    pid->output = limit_float(pid->output,
                              pid->output_min,
                              pid->output_max);

    pid->prev_error = pid->last_error;
    pid->last_error = pid->error;

    return pid->output;
}

10. 位置式 PID 和增量式 PID 怎么选?

类型 特点 适合场景
位置式 PID 直接算最终输出 温度、位置、速度、电机控制常用
增量式 PID 算输出变化量 电机速度控制、执行器连续调节

你作为嵌入式初学者,建议先掌握位置式 PID


11. 工程里最重要的几个细节

1. PID 必须固定周期调用

比如:

c 复制代码
1ms 调一次
5ms 调一次
10ms 调一次

不能一会儿 2ms,一会儿 30ms,否则 D 项和 I 项都会乱。

例如:

c 复制代码
pid.dt = 0.01f; // 10ms

如果 10ms 调一次:

c 复制代码
dt = 0.01 秒

2. 输出必须限幅

例如 PWM 范围是 0~1000:

c 复制代码
pid.output_min = 0;
pid.output_max = 1000;

否则 PID 可能算出:

c 复制代码
output = 5000

但你的 PWM 根本输出不了 5000。


3. 积分必须限幅

如果积分不限幅,容易出现"积分饱和"。

比如电机堵转了,速度一直上不去:

c 复制代码
target = 1000
measure = 0
error = 1000

积分一直累加:

c 复制代码
integral += 1000;
integral += 1000;
integral += 1000;
...

最后积分巨大。等电机突然恢复后,输出会猛冲,系统会严重超调。

所以要加:

c 复制代码
integral = limit_float(integral, integral_min, integral_max);

4. D 项最好加滤波

传感器噪声会导致 D 项抖动。

简单低通滤波:

c 复制代码
d_filter = 0.8f * d_filter + 0.2f * d_raw;

然后用:

c 复制代码
D = d_filter;

12. PID 调参经验

最简单的工程调参顺序:

第一步:先只开 P

c 复制代码
Ki = 0;
Kd = 0;

慢慢加 Kp。

现象:

现象 说明
反应慢 Kp 太小
开始震荡 Kp 偏大
剧烈震荡 Kp 太大

第二步:加 D 抑制震荡

如果系统有明显超调和震荡,加 Kd。

现象 处理
超调大 增大 Kd
来回抖 适当增大 Kd
输出毛刺多 Kd 太大或传感器噪声大

第三步:加 I 消除静态误差

如果系统稳定后还差一点,比如目标 1000rpm,实际 950rpm,就加 Ki。

现象 处理
长期有误差 增大 Ki
慢慢来回晃 Ki 偏大
超调越来越严重 Ki 太大或积分饱和

13. 平衡车里的 PID 怎么用?

平衡车一般不是一个 PID,而是多个环。

常见结构:

text 复制代码
角度环 PID/PD
    ↓
速度环 PI
    ↓
电机控制 PWM / 电流环 / FOC

更具体一点:

text 复制代码
IMU 测角度
    ↓
角度 PID 算出需要的电机力矩
    ↓
编码器测速度
    ↓
速度 PID 修正目标角度或目标力矩
    ↓
输出 PWM / FOC Iq

平衡车角度环常见用法:

c 复制代码
angle_error = target_angle - real_angle;

output = Kp * angle_error
       + Kd * gyro_y;

很多平衡车角度环其实更像 PD:

c 复制代码
output = Kp * 角度误差 + Kd * 角速度;

其中:

c 复制代码
角度误差:车身倒了多少
角速度:车身正在倒得有多快

P 负责把车拉回来,D 负责提前刹车防止冲过头。


14. 一个平衡车角度环例子

c 复制代码
float Balance_PID(float target_angle, float real_angle, float gyro_y)
{
    float angle_error;
    float output;

    angle_error = target_angle - real_angle;

    output = balance_kp * angle_error
           + balance_kd * gyro_y;

    return output;
}

注意:

gyro_y 的正负号要根据你的 IMU 安装方向、电机方向实际调整。

有时候是:

c 复制代码
output = Kp * angle_error + Kd * gyro_y;

有时候是:

c 复制代码
output = Kp * angle_error - Kd * gyro_y;

这要看你的坐标系。


15. 电机速度环例子

c 复制代码
float Speed_PID(float target_speed, float real_speed)
{
    static float integral = 0;
    static float last_error = 0;

    float error;
    float output;

    error = target_speed - real_speed;

    integral += error * 0.01f;  // 10ms周期

    integral = limit_float(integral, -500, 500);

    output = speed_kp * error
           + speed_ki * integral
           + speed_kd * (error - last_error) / 0.01f;

    output = limit_float(output, -1000, 1000);

    last_error = error;

    return output;
}

速度环很多时候用 PI 就够了:

c 复制代码
output = Kp * error + Ki * integral;

因为速度信号本身可能有噪声,D 项不一定好用。


16. 实际工程推荐版本

对于你这种嵌入式开发,建议先这么用:

控制对象 推荐
温度控制 PI
普通电机速度控制 PI 或 PID
舵机/位置控制 PID
平衡车角度环 PD
平衡车速度环 PI
FOC 电流环 PI
FOC 速度环 PI
FOC 位置环 P / PID

17. 最重要的一句话

PID 不是玄学,它就是根据这三个东西控制输出:

text 复制代码
P:现在差多少
I:过去一直差多少
D:误差变化有多快

实际工程公式核心就是:

c 复制代码
error = target - measure;

P = Kp * error;
I += Ki * error * dt;
D = Kd * (error - last_error) / dt;

output = P + I + D;

但是工程上一定要加:

c 复制代码
固定周期
输出限幅
积分限幅
D项滤波
方向检查

不然 PID 很容易出现震荡、冲过头、输出乱跳、积分爆炸等问题。




PID 本质上就是一个闭环控制算法

它干的事情很简单:

我想要一个目标值,比如速度 1000rpm;

现在实际只有 800rpm;

PID 根据"差多少、差了多久、变化快不快"来算出一个控制输出,比如 PWM 占空比。


1. PID 里面的几个量

假设:

c 复制代码
target   // 目标值,比如目标速度、目标角度、目标温度
measure  // 实际测量值,比如编码器速度、IMU角度、温度传感器值
output   // 控制输出,比如PWM占空比、电机电压、阀门开度

误差:

c 复制代码
error = target - measure;

也就是:

c 复制代码
误差 = 目标值 - 实际值

例如:

c 复制代码
目标速度 = 1000 rpm
实际速度 = 800 rpm
error = 1000 - 800 = 200

说明速度低了,需要加大 PWM。


2. PID 三个部分分别干啥?

PID = P + I + D


P:比例控制

P 的公式:

c 复制代码
P = Kp * error;

意思是:

当前误差越大,控制力度越大。

例如速度差 200rpm,就多给点 PWM;差 20rpm,就少给点。

P 的特点:

Kp 太小 Kp 太大
反应慢,没力气 容易震荡、抖动、超调

比如平衡车:

c 复制代码
角度偏差越大,电机越用力往回拉

I:积分控制

I 的公式:

c 复制代码
integral += error * dt;
I = Ki * integral;

意思是:

误差如果长期存在,就不断累加,慢慢加大控制输出。

它主要解决静态误差

比如电机速度控制:

目标 1000rpm,实际一直稳定在 950rpm。

P 控制可能觉得误差 50 不大,输出不够继续增加,于是永远差一点。

积分 I 会发现:

c 复制代码
你一直差 50,一直差,一直差......

于是它会慢慢把输出补上去,最后让速度接近 1000rpm。

I 的特点:

Ki 太小 Ki 太大
消除误差慢 容易积分饱和、震荡、超调

D:微分控制

D 的公式:

c 复制代码
D = Kd * (error - last_error) / dt;

意思是:

看误差变化速度,用来提前刹车、抑制震荡。

比如平衡车快要倒下时,角度变化速度很快,D 项可以提前反应。

D 的特点:

Kd 太小 Kd 太大
抑制震荡不明显 对噪声敏感,输出抖动

在工程里,D 项很容易被传感器噪声影响,所以经常要滤波。


3. 连续 PID 数学公式

u(t)=K_p e(t)+K_i\int_0^t e(\tau)d\tau+K_d\frac{de(t)}{dt}

其中:

符号 含义
u(t) 控制输出
e(t) 当前误差
Kp 比例系数
Ki 积分系数
Kd 微分系数

但是单片机里不能直接用这个连续公式,因为 MCU 是周期性运行的,比如 1ms 算一次、5ms 算一次、10ms 算一次。

所以工程里一般用离散 PID


4. 实际工程常用:位置式 PID

这是最常见的写法。

公式

c 复制代码
error = target - measure;

P = Kp * error;

integral += error * dt;
I = Ki * integral;

D = Kd * (error - last_error) / dt;

output = P + I + D;

last_error = error;

完整公式可以理解成:

u_k=K_p e_k+K_i T_s\sum_{i=0}^{k} e_i+K_d\frac{e_k-e_{k-1}}{T_s}

其中:

符号 含义
k 当前第 k 次计算
Ts / dt 控制周期
e_k 当前误差
e_{k-1} 上一次误差

5. 工程代码模板:位置式 PID

c 复制代码
typedef struct
{
    float kp;
    float ki;
    float kd;

    float target;       // 目标值
    float measure;      // 测量值

    float error;        // 当前误差
    float last_error;   // 上一次误差

    float integral;     // 积分累加值

    float output;       // PID输出

    float output_max;   // 输出限幅
    float output_min;

    float integral_max; // 积分限幅
    float integral_min;

    float dt;           // 控制周期,单位:秒
} PID_t;


static float limit_float(float value, float min, float max)
{
    if (value > max)
        return max;
    if (value < min)
        return min;
    return value;
}


float PID_Calc(PID_t *pid, float target, float measure)
{
    float p;
    float i;
    float d;

    pid->target = target;
    pid->measure = measure;

    // 1. 计算误差
    pid->error = pid->target - pid->measure;

    // 2. P项:当前误差越大,输出越大
    p = pid->kp * pid->error;

    // 3. I项:误差累加,用来消除长期静态误差
    pid->integral += pid->error * pid->dt;

    // 4. 积分限幅,防止积分饱和
    pid->integral = limit_float(pid->integral,
                                pid->integral_min,
                                pid->integral_max);

    i = pid->ki * pid->integral;

    // 5. D项:误差变化速度,用来抑制超调和震荡
    d = pid->kd * (pid->error - pid->last_error) / pid->dt;

    // 6. PID总输出
    pid->output = p + i + d;

    // 7. 输出限幅,比如PWM只能是 0~1000
    pid->output = limit_float(pid->output,
                              pid->output_min,
                              pid->output_max);

    // 8. 保存当前误差,供下次计算D项
    pid->last_error = pid->error;

    return pid->output;
}

6. 实际用在 PWM 控制里

假设你控制电机速度。

c 复制代码
float target_speed = 1000.0f;   // 目标速度 1000rpm
float real_speed   = get_motor_speed();

float pwm = PID_Calc(&speed_pid, target_speed, real_speed);

motor_set_pwm((int)pwm);

PID 输出可以直接对应:

控制对象 PID 输出
直流电机速度 PWM 占空比
BLDC FOC 速度环 目标电流 Iq
温度控制 加热功率
舵机位置控制 PWM 或目标角速度
平衡车角度控制 电机力矩 / 电流 / PWM
水泵压力控制 电机转速
灯光亮度控制 PWM 占空比

7. 更工程化的写法:D 项不要对 error 微分

很多实际工程里,D 项不推荐这么写:

c 复制代码
D = Kd * (error - last_error) / dt;

因为目标值 target 突然变化时,error 会突然跳变,D 项也会突然很大,输出会猛冲。

更常用的是对测量值微分:

c 复制代码
D = -Kd * (measure - last_measure) / dt;

为什么前面有负号?

因为:

c 复制代码
error = target - measure

如果 target 不变,那么:

c 复制代码
error变化 = -measure变化

所以工程写法:

c 复制代码
d = -pid->kd * (measure - pid->last_measure) / pid->dt;

这种叫:

c 复制代码
微分先行

或者:

c 复制代码
Derivative on Measurement

它的优点是目标值突变时,不容易让 D 项产生尖峰。


8. 带测量微分的工程 PID

c 复制代码
typedef struct
{
    float kp;
    float ki;
    float kd;

    float error;
    float integral;

    float last_measure;

    float output;

    float output_max;
    float output_min;

    float integral_max;
    float integral_min;

    float dt;
} PID_Real_t;


float PID_RealCalc(PID_Real_t *pid, float target, float measure)
{
    float p;
    float i;
    float d;

    // 当前误差
    pid->error = target - measure;

    // P项
    p = pid->kp * pid->error;

    // I项
    pid->integral += pid->error * pid->dt;

    pid->integral = limit_float(pid->integral,
                                pid->integral_min,
                                pid->integral_max);

    i = pid->ki * pid->integral;

    // D项:对测量值求微分,而不是对误差求微分
    d = -pid->kd * (measure - pid->last_measure) / pid->dt;

    // 输出
    pid->output = p + i + d;

    // 输出限幅
    pid->output = limit_float(pid->output,
                              pid->output_min,
                              pid->output_max);

    // 保存上一次测量值
    pid->last_measure = measure;

    return pid->output;
}

9. 增量式 PID

位置式 PID 算的是:

c 复制代码
当前应该输出多少

增量式 PID 算的是:

c 复制代码
这一次输出应该比上一次增加多少

公式:

\Delta u_k=K_p(e_k-e_{k-1})+K_iT_s e_k+\frac{K_d}{T_s}(e_k-2e_{k-1}+e_{k-2})

然后:

c 复制代码
output += delta_output;

工程代码:

c 复制代码
typedef struct
{
    float kp;
    float ki;
    float kd;

    float error;
    float last_error;
    float prev_error;

    float output;

    float output_max;
    float output_min;

    float dt;
} PID_Inc_t;


float PID_IncCalc(PID_Inc_t *pid, float target, float measure)
{
    float delta_output;

    pid->error = target - measure;

    delta_output =
        pid->kp * (pid->error - pid->last_error)
        + pid->ki * pid->dt * pid->error
        + pid->kd * (pid->error - 2.0f * pid->last_error + pid->prev_error) / pid->dt;

    pid->output += delta_output;

    pid->output = limit_float(pid->output,
                              pid->output_min,
                              pid->output_max);

    pid->prev_error = pid->last_error;
    pid->last_error = pid->error;

    return pid->output;
}

10. 位置式 PID 和增量式 PID 怎么选?

类型 特点 适合场景
位置式 PID 直接算最终输出 温度、位置、速度、电机控制常用
增量式 PID 算输出变化量 电机速度控制、执行器连续调节

你作为嵌入式初学者,建议先掌握位置式 PID


11. 工程里最重要的几个细节

1. PID 必须固定周期调用

比如:

c 复制代码
1ms 调一次
5ms 调一次
10ms 调一次

不能一会儿 2ms,一会儿 30ms,否则 D 项和 I 项都会乱。

例如:

c 复制代码
pid.dt = 0.01f; // 10ms

如果 10ms 调一次:

c 复制代码
dt = 0.01 秒

2. 输出必须限幅

例如 PWM 范围是 0~1000:

c 复制代码
pid.output_min = 0;
pid.output_max = 1000;

否则 PID 可能算出:

c 复制代码
output = 5000

但你的 PWM 根本输出不了 5000。


3. 积分必须限幅

如果积分不限幅,容易出现"积分饱和"。

比如电机堵转了,速度一直上不去:

c 复制代码
target = 1000
measure = 0
error = 1000

积分一直累加:

c 复制代码
integral += 1000;
integral += 1000;
integral += 1000;
...

最后积分巨大。等电机突然恢复后,输出会猛冲,系统会严重超调。

所以要加:

c 复制代码
integral = limit_float(integral, integral_min, integral_max);

4. D 项最好加滤波

传感器噪声会导致 D 项抖动。

简单低通滤波:

c 复制代码
d_filter = 0.8f * d_filter + 0.2f * d_raw;

然后用:

c 复制代码
D = d_filter;

12. PID 调参经验

最简单的工程调参顺序:

第一步:先只开 P

c 复制代码
Ki = 0;
Kd = 0;

慢慢加 Kp。

现象:

现象 说明
反应慢 Kp 太小
开始震荡 Kp 偏大
剧烈震荡 Kp 太大

第二步:加 D 抑制震荡

如果系统有明显超调和震荡,加 Kd。

现象 处理
超调大 增大 Kd
来回抖 适当增大 Kd
输出毛刺多 Kd 太大或传感器噪声大

第三步:加 I 消除静态误差

如果系统稳定后还差一点,比如目标 1000rpm,实际 950rpm,就加 Ki。

现象 处理
长期有误差 增大 Ki
慢慢来回晃 Ki 偏大
超调越来越严重 Ki 太大或积分饱和

13. 平衡车里的 PID 怎么用?

平衡车一般不是一个 PID,而是多个环。

常见结构:

text 复制代码
角度环 PID/PD
    ↓
速度环 PI
    ↓
电机控制 PWM / 电流环 / FOC

更具体一点:

text 复制代码
IMU 测角度
    ↓
角度 PID 算出需要的电机力矩
    ↓
编码器测速度
    ↓
速度 PID 修正目标角度或目标力矩
    ↓
输出 PWM / FOC Iq

平衡车角度环常见用法:

c 复制代码
angle_error = target_angle - real_angle;

output = Kp * angle_error
       + Kd * gyro_y;

很多平衡车角度环其实更像 PD:

c 复制代码
output = Kp * 角度误差 + Kd * 角速度;

其中:

c 复制代码
角度误差:车身倒了多少
角速度:车身正在倒得有多快

P 负责把车拉回来,D 负责提前刹车防止冲过头。


14. 一个平衡车角度环例子

c 复制代码
float Balance_PID(float target_angle, float real_angle, float gyro_y)
{
    float angle_error;
    float output;

    angle_error = target_angle - real_angle;

    output = balance_kp * angle_error
           + balance_kd * gyro_y;

    return output;
}

注意:

gyro_y 的正负号要根据你的 IMU 安装方向、电机方向实际调整。

有时候是:

c 复制代码
output = Kp * angle_error + Kd * gyro_y;

有时候是:

c 复制代码
output = Kp * angle_error - Kd * gyro_y;

这要看你的坐标系。


15. 电机速度环例子

c 复制代码
float Speed_PID(float target_speed, float real_speed)
{
    static float integral = 0;
    static float last_error = 0;

    float error;
    float output;

    error = target_speed - real_speed;

    integral += error * 0.01f;  // 10ms周期

    integral = limit_float(integral, -500, 500);

    output = speed_kp * error
           + speed_ki * integral
           + speed_kd * (error - last_error) / 0.01f;

    output = limit_float(output, -1000, 1000);

    last_error = error;

    return output;
}

速度环很多时候用 PI 就够了:

c 复制代码
output = Kp * error + Ki * integral;

因为速度信号本身可能有噪声,D 项不一定好用。


16. 实际工程推荐版本

对于你这种嵌入式开发,建议先这么用:

控制对象 推荐
温度控制 PI
普通电机速度控制 PI 或 PID
舵机/位置控制 PID
平衡车角度环 PD
平衡车速度环 PI
FOC 电流环 PI
FOC 速度环 PI
FOC 位置环 P / PID

17. 最重要的一句话

PID 不是玄学,它就是根据这三个东西控制输出:

text 复制代码
P:现在差多少
I:过去一直差多少
D:误差变化有多快

实际工程公式核心就是:

c 复制代码
error = target - measure;

P = Kp * error;
I += Ki * error * dt;
D = Kd * (error - last_error) / dt;

output = P + I + D;

但是工程上一定要加:

c 复制代码
固定周期
输出限幅
积分限幅
D项滤波
方向检查

不然 PID 很容易出现震荡、冲过头、输出乱跳、积分爆炸等问题。

相关推荐
接着奏乐接着舞1 小时前
java 数据结构
数据库·redis·缓存
m0_609160491 小时前
如何用 some 检测数组中是否存在至少一个满足条件的项
jvm·数据库·python
|_⊙1 小时前
Linux 深入理解文件(Ext2文件系统:上)
linux·运维·数据库
情绪总是阴雨天~1 小时前
大模型 Function Call(函数调用)详解:原理、实践与数据库智能查询 Agent
前端·数据库·人工智能
m0_702036532 小时前
如何从Oracle Java调用外部API_HTTP请求在数据库Java Source中的实现
jvm·数据库·python
六月雨滴2 小时前
数据库权限管理(Privilege Management)
数据库·oracle·dba
aisifang002 小时前
企业级GPT-Image2实战测评:从生成到生产
大数据·数据库·人工智能
TO_WebNow2 小时前
MySQL 索引的相关知识
数据库·mysql
神明9312 小时前
如何处理ORA-01152报错_恢复未完成导致的数据文件仍需介质恢复
jvm·数据库·python