位置式与增量式PID算法详解:从原理到C语言工程实践
2026/8/26 9:10:58 网站建设 项目流程

1. 项目概述:从“玄学”到“科学”的PID控制实践

如果你玩过四轴飞行器、智能车,或者调试过温控系统,那么“PID”这个词对你来说一定不陌生。它被誉为控制领域的“万能公式”,但同时也是很多新手工程师眼中的“玄学”——参数调来调去,系统要么反应迟钝,要么疯狂振荡,就是达不到理想状态。今天,我们不谈复杂的理论推导,就从最接地气的角度,拆解PID算法的两种核心实现形式:位置式PID增量式PID,并附上可以直接“抄作业”的C语言实现代码。无论你是用STM32控制电机转速,还是用Arduino让小车走直线,或是用树莓派Pico调节一个恒温箱,这篇文章都能帮你把PID从概念变成手中实实在在可用的工具。我们会深入探讨为什么会有这两种形式,它们各自在什么场景下“称王”,以及在代码实现时有哪些教科书上不会写的“坑”。

2. PID控制的核心思想与两种算法形态

2.1 PID到底是什么?一个生活化的类比

抛开那些令人头疼的微分方程,我们可以把PID控制器想象成一位正在淋浴时调节水温的“智能管家”。

  • 目标(设定值):你想要的舒适水温,比如40°C。
  • 现状(当前值):当前水管里流出的实际水温。
  • 误差:目标水温减去实际水温。如果水太凉,误差为正;太烫,误差为负。

这位“管家”通过三方面的思考来调节热水阀门的开度:

  1. 比例(P)作用:“现在差多少就补多少”。当前水温35°C,差5°C,那我就按这个差距的比例开大热水阀门。这是最直接的反应,但单纯用P,系统永远会存在一个稳定的误差(静差),就像水温可能最终停留在38°C,怎么也到不了40°C。

  2. 积分(I)作用:“过去一直差多少”。如果P作用让水温停在了38°C,那2°C的误差会随着时间累积起来。I作用就是关注这个“累积的误差”,误差存在的时间越长,累积值越大,它给出的修正力就越强,最终能消除静差,把水温精准拉到40°C。但I作用太强,系统会变得“迟钝”且容易超调。

  3. 微分(D)作用:“未来会差多少”。它不关心误差的绝对值,而是关心误差变化的趋势。比如水温正在从35°C快速上升到39°C,虽然还有误差,但D作用能“预见”到即将超调,于是会提前减小阀门开度,抑制超调,增加系统稳定性。D作用对噪声非常敏感,用不好反而会引入振荡。

PID控制器的输出,就是这三位“管家”思考结果的加权和。而位置式和增量式,就是计算这个“加权和”的两种不同策略。

2.2 位置式PID:绝对权力的掌控者

位置式PID,顾名思义,它的输出直接对应的是执行机构的绝对位置。比如阀门开度(0%-100%)、舵机角度(0°-180°)、电机的目标位置。

它的算法公式是经典形式:u(k) = Kp * e(k) + Ki * Σe(j) + Kd * [e(k) - e(k-1)]其中:

  • u(k):本次计算出的控制量输出(绝对位置)。
  • e(k):本次误差。
  • Σe(j):从开始到现在所有误差的累加和(积分项)。
  • e(k)-e(k-1):本次误差与上次误差的差值(微分项)。

它的工作方式像一位事必躬亲的总经理,每次计算都要审视从项目开始到现在的所有历史业绩(积分项),然后下达一个绝对的指令:“阀门必须开到65%”。

优点

  • 概念直观,与理论公式完全对应。
  • 对于执行机构本身没有记忆功能的系统(如简单的继电器、开关阀),必须使用位置式输出。

缺点

  • 积分饱和(Integral Windup)问题严重:这是位置式最大的坑。想象一下,当系统启动或设定值突变时,误差瞬间很大。积分项会疯狂累积到一个巨大的值。即使后面误差减小了,这个巨大的积分值也需要很长时间才能“消化”掉,导致系统长时间超调甚至失控。就像总经理因为一次重大失误,不断自责积累负面情绪,影响了后续所有决策。
  • 对计算机精度要求高:需要存储并不断累加所有历史误差,可能存在数据溢出风险。
  • 输出突变大:当设定值阶跃变化时,输出u(k)也会阶跃变化,对执行机构冲击较大。

2.3 增量式PID:专注于变化的改革者

增量式PID不再计算绝对的控制量,而是计算控制量的增量(变化量)。即,它只告诉执行机构:“比起上一次,这次你需要增加或减少多少”。

它的公式由位置式公式推导而来(令Δu(k) = u(k) - u(k-1)):Δu(k) = Kp * [e(k)-e(k-1)] + Ki * e(k) + Kd * [e(k) - 2e(k-1) + e(k-2)]而本次的实际输出为:u(k) = u(k-1) + Δu(k)

它的工作方式像一位专注于持续改进的部门主管。他不关心阀门现在具体开在多少,只关心“这次调整需要拧大一点还是关小一点,拧多少度”。

优点

  • 天然抗积分饱和:这是增量式最突出的优点。因为输出是增量,每次只在上次的基础上做调整。一旦系统进入饱和区(如阀门全开),误差符号不变,但Δu(k)由于微分项和比例项的变化可能为零或反向,从而能自动退出饱和,系统恢复更快。
  • 手动/自动切换无扰动:在工业控制中,如果需要从自动PID控制切换到手动调节,由于增量式输出只与最近几次误差有关,切换时不会引起输出的剧烈跳变。
  • 对执行机构更友好:输出的是变化量,相对平滑,冲击小。非常适合驱动步进电机、伺服电机等执行机构。
  • 代码实现更安全:不需要存储庞大的误差累加和,只需记住最近两次误差e(k-1),e(k-2),计算量小且不易溢出。

缺点

  • 概念上不如位置式直观。
  • 当执行机构本身需要绝对位置指令时(如某些通信协议要求发送绝对角度),仍需在增量式算法外维护一个绝对量u(k)

实操心得:选择位置式还是增量式,第一个判断依据是你的执行机构接受什么指令。如果它接受“绝对位置指令”(如PWM占空比、绝对角度),两种都可以用,但增量式需累加。如果它接受“相对步进指令”(如“正转10步”),增量式是天然适配。第二个判断依据是是否容易发生饱和。对于容易饱和的系统(如满功率加热),增量式的优势是决定性的。

3. 代码实现深度解析与避坑指南

下面,我们将分别用C语言实现一个具备工程实践价值的、带抗积分饱和和输出限幅的位置式PID和增量式PID控制器。代码将包含详细的注释,并重点解释那些容易出错的地方。

3.1 位置式PID的代码实现与关键细节

首先,我们定义一个PID结构体,用于存放所有参数和状态。这是模块化编程的基础,方便管理多个PID控制器。

/** * 位置式PID控制器结构体 */ typedef struct { float Kp, Ki, Kd; // PID系数 float target; // 目标值(设定值) float measure; // 测量值(当前值) float error; // 当前误差 float last_error; // 上一次误差,用于微分 float integral; // 积分累加和 float integral_limit; // 积分限幅,抗饱和关键! float output; // 控制器输出 float output_limit_max; // 输出上限 float output_limit_min; // 输出下限 } Positional_PID;

接下来是初始化函数。务必在系统开始运行前调用,清零状态量,防止初始值乱飞。

/** * 位置式PID初始化 * @param pid PID结构体指针 * @param kp, ki, kd PID参数 * @param limit_i 积分限幅值(绝对值),例如1000 * @param limit_out_max 输出上限,例如100.0 * @param limit_out_min 输出下限,例如-100.0 */ void Positional_PID_Init(Positional_PID *pid, float kp, float ki, float kd, float limit_i, float limit_out_max, float limit_out_min) { pid->Kp = kp; pid->Ki = ki; pid->Kd = kd; pid->target = 0.0f; pid->measure = 0.0f; pid->error = 0.0f; pid->last_error = 0.0f; pid->integral = 0.0f; pid->integral_limit = limit_i; // 设置积分限幅 pid->output = 0.0f; pid->output_limit_max = limit_out_max; pid->output_limit_min = limit_out_min; }

核心计算函数。这里包含了抗积分饱和的关键处理。

/** * 位置式PID计算函数 * @param pid PID结构体指针 * @param measure 当前测量值 * @param target 当前目标值 * @return 控制器输出值 */ float Positional_PID_Calculate(Positional_PID *pid, float measure, float target) { pid->measure = measure; pid->target = target; // 1. 计算当前误差 pid->error = pid->target - pid->measure; // 2. 比例项 float proportional = pid->Kp * pid->error; // 3. 积分项(带抗饱和处理) pid->integral += pid->error; // 累加误差 // *** 关键抗饱和步骤:积分分离或积分限幅 *** // 方法一:积分限幅(更常用) if (pid->integral > pid->integral_limit) { pid->integral = pid->integral_limit; } else if (pid->integral < -pid->integral_limit) { pid->integral = -pid->integral_limit; } // 方法二:积分分离(当误差很大时,取消积分作用) // float integral = 0; // if (fabs(pid->error) < 50.0f) { // 误差阈值自己设定 // integral = pid->Ki * pid->integral; // } float integral = pid->Ki * pid->integral; // 4. 微分项(使用不完全微分可减少噪声影响,此处为标准微分) float derivative = pid->Kd * (pid->error - pid->last_error); // 注意:此处未除时间dt,因dt已融入Kd pid->last_error = pid->error; // 更新上次误差 // 5. 计算总输出 pid->output = proportional + integral + derivative; // 6. 输出限幅 if (pid->output > pid->output_limit_max) { pid->output = pid->output_limit_max; } else if (pid->output < pid->output_limit_min) { pid->output = pid->output_limit_min; } return pid->output; }

注意事项与实操心得

  1. Kd参数已经包含了dt:在离散系统中,微分项理论上是Kd * (e(k)-e(k-1)) / dt。我们在设定Kd值时,通常已经将采样周期dt考虑进去了。例如,如果理论Kd是2,采样周期是0.01秒,那么代码中使用的Kd应该是2 * 0.01 = 0.02。这是一个常见的混淆点。
  2. 积分限幅integral_limit如何设定:没有一个固定公式。一个实用的方法是:先关闭积分(Ki=0),调好Kp和Kd,使系统基本稳定但存在静差。然后估算消除这个静差大概需要多少“积分量”。例如,静差为5,Ki=0.1,那么积分项需要累积到5 / 0.1 = 50才能消除静差。可以将integral_limit设为100~150,给一个裕量,然后微调。
  3. 输出限幅必须要有:它对应你执行机构的物理极限。比如PWM输出范围是0-1000,那么output_limit_max/min就应该是1000和0。这是保证系统安全运行的底线。
  4. 采样周期dt要稳定:PID计算必须在一个固定的时间间隔内进行。最好用定时器中断来触发PID计算,而不是在循环里用delay。不稳定的dt会彻底破坏Ki和Kd的效果。

3.2 增量式PID的代码实现与优势体现

同样,先定义结构体。注意,这里不需要integral,但需要多记录一次误差。

/** * 增量式PID控制器结构体 */ typedef struct { float Kp, Ki, Kd; // PID系数 float target; // 目标值 float measure; // 测量值 float error[3]; // 当前误差e(k),上一次误差e(k-1),上上次误差e(k-2) float output; // 当前绝对输出(内部维护) float output_limit_max; float output_limit_min; float delta_output_limit; // 增量输出限幅,可选,用于进一步平滑 } Incremental_PID;

初始化函数。

/** * 增量式PID初始化 */ void Incremental_PID_Init(Incremental_PID *pid, float kp, float ki, float kd, float limit_out_max, float limit_out_min, float limit_delta_out) { pid->Kp = kp; pid->Ki = ki; pid->Kd = kd; pid->target = 0.0f; pid->measure = 0.0f; pid->error[0] = pid->error[1] = pid->error[2] = 0.0f; // 清零误差历史 pid->output = 0.0f; pid->output_limit_max = limit_out_max; pid->output_limit_min = limit_out_min; pid->delta_output_limit = limit_delta_out; // 增量限幅 }

核心计算函数。注意其输出是本次的绝对输出值,但内部计算的是增量。

/** * 增量式PID计算函数 * @return 本次的绝对输出值 u(k) */ float Incremental_PID_Calculate(Incremental_PID *pid, float measure, float target) { pid->measure = measure; pid->target = target; // 1. 更新误差历史队列(类似滑动窗口) pid->error[2] = pid->error[1]; // e(k-2) = old e(k-1) pid->error[1] = pid->error[0]; // e(k-1) = old e(k) pid->error[0] = pid->target - pid->measure; // e(k) = new error // 2. 根据增量式公式计算控制增量 Δu(k) // Δu(k) = Kp*[e(k)-e(k-1)] + Ki*e(k) + Kd*[e(k)-2e(k-1)+e(k-2)] float delta_u = pid->Kp * (pid->error[0] - pid->error[1]) + pid->Ki * pid->error[0] + pid->Kd * (pid->error[0] - 2.0f * pid->error[1] + pid->error[2]); // 3. (可选)对增量本身进行限幅,使控制更平滑 if (pid->delta_output_limit > 0) { if (delta_u > pid->delta_output_limit) { delta_u = pid->delta_output_limit; } else if (delta_u < -pid->delta_output_limit) { delta_u = -pid->delta_output_limit; } } // 4. 计算本次绝对输出 u(k) = u(k-1) + Δu(k) pid->output += delta_u; // 5. 对绝对输出进行限幅(保护执行机构) if (pid->output > pid->output_limit_max) { pid->output = pid->output_limit_max; } else if (pid->output < pid->output_limit_min) { pid->output = pid->output_limit_min; } // 注意:这里没有专门的抗积分饱和处理,因为增量式天然具有抗饱和特性。 // 当输出持续饱和时,误差e(k)符号不变,但e(k)-e(k-1)和e(k)-2e(k-1)+e(k-2)会趋于0, // 导致delta_u趋于0,输出不再增长,一旦误差反向,能迅速退出饱和。 return pid->output; }

实操心得

  1. 增量式PID的“积分”在哪?新手常问,增量式公式里的Ki*e(k)不就是积分项吗?是的,但它积的是当前误差,而不是历史误差和。它通过每次累加Δu(k)u(k)中,间接实现了对历史所有误差的积分效果,却避免了数值爆炸。
  2. delta_output_limit的妙用:这个参数在位置式中不常见,但在增量式中非常有用。它直接限制了每次调整的最大幅度。对于电机、舵机等惯性较大的系统,限制每次的增量可以避免过冲和机械冲击,让运动更平滑。调试时可以先不设,等系统基本稳定后再加上并调整。
  3. 手动重置输出:在某些情况下(如系统重启、模式切换),你可能需要将内部维护的pid->output重置为某个初始值(比如当前执行机构的实际位置),而不是0。可以增加一个PID_Reset()函数来设置这个值,确保切换无扰。
  4. 适用于“速度环”:在电机控制中,常用串级PID。外环(位置环)用位置式PID计算目标速度,内环(速度环)用增量式PID计算转矩或电流给定。因为速度控制更关注变化率,增量式非常合适。

4. 参数整定:从“瞎调”到“有章可循”

调参是PID应用的灵魂,也是新手最头疼的部分。网上流传的“口诀法”(先P后I再D)有一定道理,但这里分享一个更系统、可重复的工程方法:阶跃响应法

4.1 参数整定四步法

假设我们控制一个直流电机的转速。目标是从0加速到1000 RPM。

  1. 准备工作

    • 将Ki和Kd设为0。
    • 输出限幅设为安全范围(如PWM占空比0%-80%)。
    • 确保你能实时观测到电机转速(通过编码器)的响应曲线(用串口绘图工具或示波器)。
  2. 整定比例系数 Kp

    • 给一个很小的Kp(比如0.5)。
    • 发出一个阶跃指令(目标值从0变到1000)。
    • 观察现象:电机转速缓慢上升,最终稳定在一个远低于1000的值(存在静差)。
    • 逐步增大Kp:每次增加50%-100%,直到系统出现持续、小幅度的等幅振荡。记录此时的Kp值,称为Ku(临界比例增益),并测量振荡周期Tu
    • 确定Kp:将Kp设置为0.5 * Ku。此时系统响应较快,超调较小,但仍有一定静差。
  3. 整定积分系数 Ki

    • 保持上一步的Kp不变。
    • 引入一个很小的Ki(比如0.5 * Ku / Tu的十分之一)。
    • 观察现象:系统静差开始减小,但响应可能变慢或出现超调。
    • 逐步增大Ki:每次小幅增加,直到静差在可接受时间内被消除。关键观察点:系统从过阻尼状态变为有轻微超调(10%-20%)的衰减振荡状态时,Ki值就比较合适了。Ki太大必然导致系统振荡甚至不稳定。
  4. 整定微分系数 Kd

    • 保持Kp和Ki不变。
    • 引入Kd(比如0.125 * Ku * Tu的十分之一)。
    • 观察现象:系统超调量应减小,响应曲线变得更“平滑”,快速达到稳定。
    • 逐步增大Kd:直到超调被有效抑制,且系统对微小扰动(如用手轻触电机轴)的恢复能力增强。注意:微分对测量噪声极其敏感。如果引入Kd后系统出现高频抖动,说明噪声被放大,需要减小Kd,或者考虑在软件中对测量值进行低通滤波。

避坑技巧

  • “先P后I再D”是黄金法则,千万不要同时调整多个参数。
  • 采样周期很重要:对于电机速度控制,采样周期通常在1-10ms;对于温度控制,可能是100ms-1s。KiKd的值与采样周期强相关。
  • 记录你的参数:每次调整都记录下参数和对应的响应曲线(截图或描述)。这是最宝贵的经验。
  • 理解系统的物理限制:电机有最大转速和加速度,加热器有最大功率。PID参数再优化,也无法突破物理极限。输出限幅就是守护这个极限的阀门。

4.2 进阶技巧:变参数PID与抗积分饱和再讨论

在实际项目中,单一的PID参数可能无法覆盖所有工况。

  • 变积分系数:在误差很大时(系统启动),完全取消积分作用(Ki=0),防止积分饱和;当误差进入一个较小范围时,再引入积分以消除静差。这就是上文代码注释中提到的“积分分离”。
  • 死区控制:对于存在静摩擦或测量有噪声的系统,当误差绝对值小于某个阈值(死区)时,不进行PID调节。可以避免执行机构在平衡点附近频繁动作。
  • 输出滤波:将PID的输出结果再进行一次一阶低通滤波,可以进一步平滑控制指令,尤其适用于增量式PID或执行机构响应慢的系统。

关于抗积分饱和的补充:位置式PID中,除了对integral进行限幅,还有一种更优的方法叫“抗饱和积分(Anti-windup)”。其核心思想是:当输出达到限幅值时,只累积那些能实际减小误差的积分。具体实现是在积分项更新时增加一个修正项:integral += error + K_aw * (output_limited - output_before_limit)其中K_aw是抗饱和系数,output_before_limit是限幅前的计算输出。这能更快地“泄放”掉无效的积分累积。

5. 典型应用场景与代码适配要点

5.1 场景一:STM32/Arduino控制直流电机转速

这是最经典的应用。你需要:

  1. 测量:通过电机编码器获取实时转速(M法或T法测频)。
  2. 计算:在定时器中断(例如1ms)中调用PID计算函数。
  3. 执行:将PID输出(例如PWM占空比)写入定时器的CCR寄存器。

代码适配要点

  • 使用增量式PID:因为控制的是速度的变化率,且PWM本身也是占空比的绝对值,增量式输出平滑,适合驱动电机。
  • 注意数据类型:电机转速可能较高,误差和输出值较大,确保使用floatint32_t,避免计算溢出。
  • 加入死区:电机有启动静摩擦,可以在误差小于10RPM时,不更新PID输出,防止电机低速抖动。

5.2 场景二:恒温控制系统(如3D打印机热床)

这是一个大惯性、大滞后的系统。

  1. 测量:通过热电偶或热敏电阻(配合ADC)读取温度。
  2. 计算:采样周期可以较长,如100ms或1s。
  3. 执行:控制固态继电器(SSR)的通断时间(PWM)来控制加热棒功率。

代码适配要点

  • 使用位置式PID:加热控制本质是调节功率的“绝对大小”,位置式更直观。但必须加强抗积分饱和!因为加热系统从冷态到目标温度误差积分会非常大。
  • 输出限幅为0-100%:对应SSR全关到全开。
  • 微分项慎用:温度传感器噪声可能较大,微分项容易引入干扰。可以先调PI,如果超调严重,再尝试加入很小的D,或者使用“不完全微分”来平滑微分项。
  • 加入开关控制:当误差很大时(比如温差超过20°C),可以直接输出100%功率全速加热,绕过PID计算,加快响应。

5.3 场景三:平衡车/两轮差速小车姿态控制

这是一个快速、动态的系统。

  1. 测量:通过MPU6050等IMU获取车体倾角和角速度。
  2. 计算:需要高速运行,采样周期建议在5-10ms。
  3. 执行:PID输出作为左右电机的速度差或扭矩差。

代码适配要点

  • 串级PID是标配:外环是角度环(位置式PID),内环是角速度环(增量式PID)。角度环的输出作为角速度环的目标值。内环响应更快,负责抑制扰动。
  • 参数整定顺序:先断开外环,将内环(角速度环)调好,使其能快速、准确地跟踪给定速度。然后闭合外环,整定角度环参数。
  • 注意传感器融合:原始陀螺仪数据有漂移,加速度计数据有噪声。需要使用互补滤波或卡尔曼滤波融合两者,得到更准确的姿态角。滤波后的数据才能送给PID

6. 调试实战:常见问题排查清单

在实际焊接电路、下载代码后,PID系统可能表现异常。下面是一个快速排查清单:

现象可能原因排查步骤与解决方案
系统完全无反应1. PID输出未正确连接到执行机构。
2. 测量反馈信号错误或未更新。
3. PID计算函数未被定期调用。
1. 检查PWM输出引脚、电机驱动使能。
2. 打印或观测测量值measure,看是否随物理量变化。
3. 检查定时器中断配置,在中断服务程序里加一个翻转LED的测试代码。
输出持续最大或最小1. 误差符号始终不变,积分项饱和。
2. 传感器接线反了,导致正反馈。
3. 执行机构方向与控制逻辑相反。
1. 检查目标值与测量值大小关系。启用积分限幅或抗饱和积分。
2. 交换传感器两根线,或代码里对测量值取反。
3. 例如,电机应该正转时却反转,检查驱动芯片的输入逻辑。
剧烈振荡(发散)1. 比例系数Kp过大。
2. 微分系数Kd为负(弄反了符号)。
3. 采样周期dt不稳定或过长。
1. 大幅减小Kp,甚至从0开始重新调。
2. 检查Kd在公式中的符号,确保正确。
3. 确保PID在严格定时中断中执行,测量中断间隔时间。
低频等幅振荡积分系数Ki过大。逐步减小Ki,观察振荡是否衰减。
高频抖动/噪声大1. 微分系数Kd过大,放大了测量噪声。
2. 传感器本身噪声大或受到干扰。
1. 减小Kd,或尝试“不完全微分”算法。
2. 对测量值进行软件滤波(如滑动平均、一阶低通滤波)。检查硬件布线,做好电源去耦和信号屏蔽。
响应太慢,有静差1. 比例系数Kp过小。
2. 积分系数Ki过小或为0。
3. 输出限幅设得太低。
1. 适当增大Kp
2. 引入或增大Ki
3. 检查输出限幅值是否合理,是否达到了执行机构的物理极限。
增量式PID电机“卡顿”delta_output_limit设置过小,导致每次调整步长不足。适当增大delta_output_limit,或观察在匀速运行时delta_u的大小,将其限幅值设为该值的2-3倍。

最后,记住PID不是万能的。它适用于线性、时不变或变化缓慢的系统。对于非线性极强、模型复杂的系统(如四旋翼的姿态控制),可能需要更先进的控制算法,但PID仍然是其底层核心或调试起点。从理解这两个简单的公式和两段代码开始,亲手去调一个系统,观察参数变化如何影响那条响应曲线,你才能真正驾驭这个控制领域的基石工具。调试的过程,就是你和被控对象对话的过程,耐心和细致的观察比任何口诀都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询