如何用C语言实现PID控制算法?

99ANYc3cd6
预计阅读时长 38 分钟
位置: 首页 C语言 正文
  1. PID算法原理简介:快速回顾PID的三个核心作用。
  2. 离散化公式推导:如何将连续的PID公式转化为计算机可以执行的离散形式。
  3. C语言数据结构设计:用结构体优雅地管理PID控制器。
  4. C语言核心函数实现:分步讲解代码实现。
  5. 完整代码示例:一个可以运行的、带有注释的完整示例。
  6. 参数整定(Tuning)方法简介:如何找到合适的PID参数。
  7. 总结与扩展

PID算法原理简介

PID控制器是一种在工业控制应用中常见的反馈回路控制器,它根据设定值与实际输出值之间的误差,通过比例、积分、微分三个环节计算出控制量,来驱使被控对象的输出趋于设定值。

  • P (Proportional) - 比例

    • 作用:即时响应误差,误差越大,控制作用越强。
    • 优点:快速反应,能迅速减小误差。
    • 缺点:存在稳态误差(Steady-state Error),即系统稳定后,输出与设定值之间仍有微小差距。
  • I (Integral) - 积分

    • 作用:累积历史误差,只要有误差存在,积分项就会不断累加,直到误差为零。
    • 优点:消除稳态误差,使系统最终能精确达到设定值。
    • 缺点:可能增加系统的超调量和调节时间,甚至引起振荡。
  • D (Derivative) - 微分

    • 作用:预测误差的未来趋势,根据误差的变化率进行调节,有“阻尼”或“预见”作用。
    • 优点:能抑制超调,缩短调节时间,提高系统的稳定性。
    • 缺点:对噪声非常敏感,可能会放大系统中的高频噪声。

总输出 = Kp * P + Ki * I + Kd * D


离散化公式推导

计算机是数字系统,无法处理连续的微积分运算,因此必须将连续的PID公式离散化。

  • 连续公式Output(t) = Kp * e(t) + Ki * ∫e(t)dt + Kd * de(t)/dt

  • 离散化

    1. 时间:将连续时间 t 离散化为采样周期 k (k=0, 1, 2, ...)。
    2. 误差e(k) 是第 k 次采样的误差,e(k) = Setpoint(k) - Input(k)
    3. 积分项:积分用求和近似。∫e(t)dt ≈ Σe(i) (从 i=0 到 k)。
    4. 微分项:微分用差分近似。de(t)/dt ≈ (e(k) - e(k-1)) / ΔtΔt 是采样周期。
  • 离散PID公式Output(k) = Kp * e(k) + Ki * (Σe(i)) + Kd * (e(k) - e(k-1)) / Δt

为了方便编程,我们通常使用更实用的“位置式PID”和“增量式PID”,这里我们实现最常用的位置式PID

位置式PID公式Output(k) = Kp * e(k) + Ki * T * Σe(i) + Kd * (e(k) - e(k-1)) / T

T 是采样周期,为了简化计算,我们通常将公式整理为:

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

令:

  • Kp = Kp (比例系数)
  • Ki = Kp / Ti (积分系数,Ti为积分时间常数)
  • Kd = Kp * Td (微分系数,Td为微分时间常数)

在编程时,我们更直接地使用 Kp, Ki, Kd 这三个参数,并采样周期 T 结合使用。


C语言数据结构设计

为了封装PID控制器的所有状态和参数,使用一个结构体是最好的方式。

#include <stdio.h>
#include <stdint.h>
// 定义浮点数类型,可根据平台精度需求选择 float 或 double
typedef float pid_data_t;
// PID控制器结构体
typedef struct {
    // --- PID参数 (由用户设置) ---
    pid_data_t Kp;      // 比例系数
    pid_data_t Ki;      // 积分系数
    pid_data_t Kd;      // 微分系数
    // --- 内部状态 (由PID控制器维护) ---
    pid_data_t setpoint; // 设定值
    pid_data_t integral; // 积分累加项
    pid_data_t prev_error; // 上一次的误差,用于计算微分
    // --- 可选配置 ---
    pid_data_t output_limit_max; // 输出上限
    pid_data_t output_limit_min; // 输出下限
    pid_data_t integral_limit_max; // 积分项上限,防止积分饱和
    pid_data_t integral_limit_min; // 积分项下限
} PID_Controller;

为什么需要限幅?

  • 输出限幅:执行机构(如电机、阀门)有其物理极限,PWM输出范围是0-100%,就不能输出-50%或150%。
  • 积分限幅:防止积分饱和,当系统长时间存在较大误差时,积分项会变得非常大,导致系统响应迟钝,甚至超调严重,通过限制积分项的大小,可以有效防止这种情况。

C语言核心函数实现

我们需要三个函数:PID_Init (初始化), PID_SetTunings (设置参数), 和 PID_Compute (计算输出)。

a. 初始化函数

/**
 * @brief 初始化PID控制器
 * @param pid 指向PID控制器结构体的指针
 * @param setpoint 设定值
 * @param Kp 比例系数
 * @param Ki 积分系数
 * @param Kd 微分系数
 * @param sample_time 采样周期,单位为毫秒
 * @param output_lim 输出限幅 [min, max]
 * @param integral_lim 积分限幅 [min, max]
 */
void PID_Init(PID_Controller *pid, pid_data_t setpoint, pid_data_t Kp, pid_data_t Ki, pid_data_t Kd,
              pid_data_t output_lim_min, pid_data_t output_lim_max,
              pid_data_t integral_lim_min, pid_data_t integral_lim_max) {
    pid->setpoint = setpoint;
    pid->Kp = Kp;
    pid->Ki = Ki;
    pid->Kd = Kd;
    pid->integral = 0;
    pid->prev_error = 0;
    pid->output_limit_min = output_lim_min;
    pid->output_limit_max = output_lim_max;
    pid->integral_limit_min = integral_lim_min;
    pid->integral_limit_max = integral_lim_max;
}

b. 参数设置函数 (可选)

如果需要动态调整PID参数,可以单独写一个函数。

/**
 * @brief 设置PID参数
 * @param pid 指向PID控制器结构体的指针
 * @param Kp, Ki, Kd 新的PID参数
 */
void PID_SetTunings(PID_Controller *pid, pid_data_t Kp, pid_data_t Ki, pid_data_t Kd) {
    pid->Kp = Kp;
    pid->Ki = Ki;
    pid->Kd = Kd;
}

c. 计算核心函数

这是PID算法的心脏。

/**
 * @brief 计算PID输出
 * @param pid 指向PID控制器结构体的指针
 * @param feedback 反馈值 (系统的当前实际值)
 * @return pid_data_t 计算得到的控制量
 */
pid_data_t PID_Compute(PID_Controller *pid, pid_data_t feedback) {
    pid_data_t error;
    pid_data_t derivative;
    pid_data_t output;
    // 1. 计算当前误差
    error = pid->setpoint - feedback;
    // 2. 计算积分项
    // 积分累加,但先不进行限幅,因为限幅通常在最后统一处理或在单独的积分抗饱和中处理
    // 这里我们采用对积分项本身进行限幅的方法
    pid->integral += error;
    // 积分限幅 (防止积分饱和)
    if (pid->integral > pid->integral_limit_max) {
        pid->integral = pid->integral_limit_max;
    } else if (pid->integral < pid->integral_limit_min) {
        pid->integral = pid->integral_limit_min;
    }
    // 3. 计算微分项
    // 微分 = (当前误差 - 上一次误差) / 采样周期
    // 注意:采样周期 T 是一个关键参数,必须在调用此函数时保持恒定。
    // 在此实现中,我们假设调用频率固定,T 被隐含在 Ki, Kd 的单位中。
    // T 是变化的,Kp, Ki, Kd 需要动态调整,或者将 T 作为参数传入。
    derivative = error - pid->prev_error;
    // 4. 计算总输出 (位置式PID公式)
    // Output = Kp * e + Ki * sum(e) + Kd * delta_e
    output = (pid->Kp * error) + (pid->Ki * pid->integral) + (pid->Kd * derivative);
    // 5. 输出限幅
    if (output > pid->output_limit_max) {
        output = pid->output_limit_max;
    } else if (output < pid->output_limit_min) {
        output = pid->output_limit_min;
    }
    // 6. 更新上一次的误差,为下一次计算做准备
    pid->prev_error = error;
    return output;
}

完整代码示例

下面是一个完整的、可运行的C语言示例,它模拟了一个简单的温度控制系统,目标是让温度稳定在设定值(例如100.0°C)。

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h> // for rand()
// --- PID控制器定义 (同上) ---
typedef float pid_data_t;
typedef struct {
    pid_data_t Kp, Ki, Kd;
    pid_data_t setpoint;
    pid_data_t integral;
    pid_data_t prev_error;
    pid_data_t output_limit_max, output_limit_min;
    pid_data_t integral_limit_max, integral_limit_min;
} PID_Controller;
void PID_Init(PID_Controller *pid, pid_data_t setpoint, pid_data_t Kp, pid_data_t Ki, pid_data_t Kd,
              pid_data_t output_lim_min, pid_data_t output_lim_max,
              pid_data_t integral_lim_min, pid_data_t integral_lim_max) {
    pid->setpoint = setpoint;
    pid->Kp = Kp;
    pid->Ki = Ki;
    pid->Kd = Kd;
    pid->integral = 0;
    pid->prev_error = 0;
    pid->output_limit_min = output_lim_min;
    pid->output_limit_max = output_lim_max;
    pid->integral_limit_min = integral_lim_min;
    pid->integral_limit_max = integral_lim_max;
}
pid_data_t PID_Compute(PID_Controller *pid, pid_data_t feedback) {
    pid_data_t error = pid->setpoint - feedback;
    pid->integral += error;
    if (pid->integral > pid->integral_limit_max) pid->integral = pid->integral_limit_max;
    else if (pid->integral < pid->integral_limit_min) pid->integral = pid->integral_limit_min;
    pid_data_t derivative = error - pid->prev_error;
    pid_data_t output = (pid->Kp * error) + (pid->Ki * pid->integral) + (pid->Kd * derivative);
    if (output > pid->output_limit_max) output = pid->output_limit_max;
    else if (output < pid->output_limit_min) output = pid->output_limit_min;
    pid->prev_error = error;
    return output;
}
// --- 模拟系统 ---
// 这是一个简化的被控对象模型
// 输入: control_signal (0-100)
// 输出: current_temperature
// 模拟了加热的延迟和惯性
pid_data_t simulate_system(pid_data_t control_signal, pid_data_t current_temp) {
    // 简单的一阶惯性环节模型
    // T_out = T_current + K * control_signal * dt - cooling_rate * (T_current - ambient_temp)
    pid_data_t ambient_temp = 25.0; // 环境温度
    pid_data_t gain = 0.5;         // 系统增益
    pid_data_t dt = 0.1;           // 模拟时间步长 (秒)
    pid_data_t cooling_rate = 0.05; // 自然冷却系数
    pid_data_t temp_change = (gain * control_signal * dt) - (cooling_rate * (current_temp - ambient_temp) * dt);
    return current_temp + temp_change;
}
int main() {
    // 1. 创建并初始化PID控制器
    PID_Controller temp_controller;
    // 设定温度 100°C
    pid_data_t target_temp = 100.0;
    // PID参数 (需要根据实际系统调整)
    pid_data_t Kp = 10.0;
    pid_data_t Ki = 0.5;
    pid_data_t Kd = 2.0;
    // 输出限制 (例如加热功率0-100%)
    pid_data_t output_min = 0.0;
    pid_data_t output_max = 100.0;
    // 积分限制 (防止积分过大)
    pid_data_t integral_min = -200.0;
    pid_data_t integral_max = 200.0;
    PID_Init(&temp_controller, target_temp, Kp, Ki, Kd, output_min, output_max, integral_min, integral_max);
    // 2. 模拟运行
    pid_data_t current_temp = 25.0; // 初始温度为环境温度
    pid_data_t control_signal;
    int time_step = 0;
    printf("Time(s)\tTemp(C)\t\tSetpoint(C)\tControl(%)\n");
    printf("------------------------------------------------\n");
    // 模拟运行60秒
    for (time_step = 0; time_step < 600; time_step++) {
        // a. 获取当前温度 (模拟传感器读入)
        // 在真实系统中,这里是从ADC或其他传感器读取的值
        // b. 计算PID输出
        control_signal = PID_Compute(&temp_controller, current_temp);
        // c. 将控制量作用于被控对象
        current_temp = simulate_system(control_signal, current_temp);
        // d. 打印结果 (每10步打印一次,避免刷屏)
        if (time_step % 10 == 0) {
            printf("%.1f\t\t%.2f\t\t%.2f\t\t%.2f\n",
                   (float)time_step / 10.0,
                   current_temp,
                   temp_controller.setpoint,
                   control_signal);
        }
    }
    return 0;
}

如何编译和运行: 将上述代码保存为 pid_example.c,使用GCC编译:

gcc pid_example.c -o pid_example -lm
./pid_example

你会看到温度从25°C逐渐升高,最终稳定在100°C附近,控制信号会在初期较大,然后随着接近目标值而减小。


参数整定方法简介

PID参数 (Kp, Ki, Kd) 的设置是PID控制的核心和难点,通常称为“参数整定”,没有一套万能的参数,需要根据具体对象进行调试,以下是几种经典方法:

  1. 试凑法

    • 步骤1 (整定Kp):将 KiKd 设为0,从小到大增大 Kp,直到系统出现等幅振荡(临界稳定),记录此时的 Kp 值(称为 Ku)和振荡周期 Tu
    • 步骤2 (整定Ki和Kd):根据 Ziegler-Nichols 经验公式,代入 KuTu 得到一组 Kp, Ki, Kd 作为初始值,然后在此基础上微调。
    • 特点:简单直观,但依赖经验,可能需要多次尝试。
  2. Ziegler-Nichols (Z-N) 整定公式

    • 这是一组经验公式,基于临界比例度法(即上述试凑法的步骤1)。
    • 公式: | 控制器类型 | Kp | Ti | Td | |------------|------------|------------|------------| | P | 5 * Ku | - | - | | PI | 45 * Ku| Tu / 1.2 | - | | PID | 6 * Ku | Tu / 2.0 | Tu / 8.0 |
    • 注意:Z-N公式得到的参数往往会使系统响应较快,但超调量较大,它是一个很好的起点,但通常需要进一步减小 KpKi 以获得更平稳的响应。
  3. 软件工具

    在工业现场,常使用PLC或DCS自带的PID整定工具,这些工具可以自动分析系统响应并给出最优参数。

调试口诀

  • 比例快:增大 Kp 让系统响应变快,但容易超调和振荡。
  • 积分消差:增大 Ki 消除稳态误差,但会增大超调,使系统变慢。
  • 微分平波:增大 Kd 抑制超调,加快系统稳定,但对噪声敏感。

总结与扩展

我们成功地实现了一个功能完备的、结构清晰的C语言PID控制器,它包含了:

  • 标准的位置式PID算法。
  • 输出限幅和积分限幅,增强了鲁棒性。
  • 使用结构体封装,便于管理和复用。
  • 一个完整的模拟示例,展示了其工作过程。

扩展

  • 增量式PID:与位置式不同,增量式计算的是控制量的增量 ΔOutput,而不是直接计算 Output,它适用于需要平滑切换控制量的场合(如步进电机),并且更容易实现输出限幅。
  • 微分先行:将微分环节放在反馈回路中,而不是误差回路中,这样可以设定值的突变不会引起微分项的剧烈变化,从而减少超调。
  • 抗积分饱和:当输出达到限幅时,停止积分项的累加,或者根据误差的方向调整积分项,这是更高级的积分饱和处理方法。
  • 动态调整采样周期:如果系统无法保证固定的采样周期,需要在计算时将 T 作为参数传入,并对 KiKd 进行相应的缩放。

希望这份详细的教程能帮助你彻底理解并掌握PID控制算法的C语言实现!

-- 展开阅读全文 --
头像
织梦cms后台模板安装
« 上一篇 今天
没有更多啦!
下一篇 »

相关文章

取消
微信二维码
支付宝二维码

目录[+]