离散PID公式
连续形式的PID,需要用模拟电路来实现,模拟电路时时刻刻都在工作着,对于单片机而言,我们需要用离散形式的PID,离散后,单片机需要设定一个调控周期T,单片机每隔T时间,才会进行一次PID计算和调控,单片机里的PID调控是间隔进行的。
连续形式PID:

离散形式PID:

其中k的取值是0,1,2 ……等等,表示的第几次调控。
那么error(k)的意思就是,第几次调控时,当前的误差值。
T是离散后的调控周期,也就是系统每隔T时间,调控一次。
error(j),这里的j是累加时候的临时变量,类似C语言中的for循环。
注意我们的第三个最简洁公式:在修改新的调控周期T时,Ki和Kd参数也要对应的更改,如果想要修改T,但是Ki和Kd不变的效果,那么就需要用第二个公式,这也是个不错的方案,但具体使用哪个方案,就要看自己的个人喜好了。
位置式PID/增量式PID
位置式PID:

大家也发现了,可以说离散式PID基本公式,就是位置式PID,这个位置式PID可以覆盖几乎所有需要用到PID的场景中。
然后还有一个比较流行的公式,那就是增量式PID,增量式PID是由位置式PID变换而来的,并且我们看到没有任何求和公式,只是本次、上次和上上次的error线性组合。
那么,增量式PID和位置式PID为什么要这样区分呢?有什么作用呢?
位置式PID由连续形式PID直接离散得到,每次计算得到的是全量的输出值,可以直接给被控对象。
增量式PID由位置式PID推导得到,每次计算得到的是输出值的增量,如果直接给被控对象,则需要被控对象内部有积分功能。
增量式PID也可在控制器内进行积分,然后输出积分后的结果,此时增量式PID与位置式PID整体功能没有区别。
位置式PID和增量式PID计算时产生的中间变量不同,如果对这些变量加以调节,可以实现不同的特性。这里的不同特性有很多,比如说,位置式PID的中间变量是误差积分,可以单独进行积分限幅,防止积分饱和,而内部积分的增量式PID没法单独对积分环节限幅。换一种说话,就是内部积分的增量式PID,只需要对输出值限幅,就可以同时实现积分限幅和输出限幅两个功能;而位置式PID,输出限幅和积分限幅得分开进行。另外,位置式PID每次计算得到的是全量输出值,如果信号有噪声干扰,不同输出值可能会相差很大,这会导致执行机构大幅度变化,而增量式PID或者内部积分的增量式PID,都可以单独对输出值增量进行限幅,防止执行机构大幅度变化。
PID程序实现
调控周期实现
第一步: 确定一个调控周期T,每隔时间T,程序执行一次PID调控。
至于调控周期设置多大比较好呢?这也是有说法的,调控周期一般取决于被控对象变化的快慢,比如倒立摆、平衡车、四轴飞行器等,这些对象变化的很快,你总不能1s才调控一次吧?要是1s一次,这些东西早就被摔坏了,所以这些东西的调控一定要快,一般20ms、10ms、5ms,甚至1ms就要进行一次调控。调控的越快,一般来说对象就会越稳定,但是,调控周期也不能无限制的快,因为会受到硬件传感器等设备分辨率限制,比如姿态传感器,每隔5ms才能更新一下数据,那么你PID就算1ms调控一次,也没有意义。
对于电机控速来说,一般不需要特别快,一般20~100ms调控一次就可以。电机控速也不能调节过快,因为会受编码器分辨率的限制,调控周期越快,编码器测速的分辨率就越低,过快的调控周期,编码器测速根本不准确,这时再快的调控也没有意义。
对于很大的被控对象,比如给锅炉加热,这些对象的变化非常缓慢,PID调控周期也得慢下来,调快了没意义,比如几百毫秒、几秒甚至几十秒调控一次都没问题。
那么在程序中我们如何实现每隔T时间进行一次调控呢?
第一种(最简单):使用delay延时实现调控周期T,使用简单但是弊端太多,比如Delay延时并不精确,Delay延时时间加上上面代码的执行时间,才是整个while循环执行的周期,这就会导致调控周期会比设定的T,长那么一点点,并且delay是一个阻塞态的函数,会阻止其他程序的运行。
int main()
{
while (1)
{
/* 在这里实现PID调节 */
delay_ms(20);
}
}
第二种:使用定时器进行定时中断,其实这种方法也很简单,并且逻辑上面更清晰。但是也有一定弊端,比如在中断里如果有操作硬件的函数,不能即在中断中调用,又在主程序中调用,因为中断函数和主程序,相当于多线程,多线程操作同一个硬件,会导致资源访问冲突。
int main()
{
Timer_Init();
while (1)
{
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Updata) == SET)
{
//在这里执行PID调控
TIM_ClearITPendingBit(TIM2,TIM_IT_Updata);
}
}
第三种:如果我们为了避免在中断即在中断中调用硬件,又在主程序中调用硬件,就要使用一个标志位,都在主程序中进行。 当然也有弊端,比如主程序的代码过多,或者主程序阻塞了,那么PID调控代码可能无法及时执行,这会导致调控周期T不准确,所以使用这种方法,一定要保证主程序不能阻塞。
uint8_t Flag;
int main()
{
Timer_Init();
while (1)
{
if(Flag == 1)
{
Flag = 0;
//执行PID调控
}
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Updata) == SET)
{
Flag = 1;
TIM_ClearITPendingBit(TIM2,TIM_IT_Updata);
}
}
位置式PID实现

我们先定义一些变量,因为PID调控会出先一些负数、小数,所以变量类型我们都使用float浮点类型。其中Target、Actual、Out 是PID与外界交互的3个变量。还需要定义PID中的系数项,还有要定义误差值,即本次误差、上次误差和误差积分。
float Target,Actual,Out;//目标值,实际值,输出值
float Kp,Ki,Kd;//PID权重
float Error0,Error1,ErrorInt;//本次误差,上次误差,误差积分
int main()
{
Timer_Init();
while (1)
{
Target = 设定目标值;
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Updata) == SET)
{
//读取传感器实际值
Actual = 读取实际值;
//更新误差
Error1 = Error0;
Error0 = Target - Actual;
ErrorInt += Error0;
//更新输出
Out = Kp*Error0 + Ki*ErrorInt + Kd(Error0-Error1);
//输出限幅
if(Out > 最大值)
{
OUT = 最大值;
}
if(Out < 最小值)
{
Out = 最小值
}
//控制硬件函数
控制硬件函数(Out);
TIM_ClearITPendingBit(TIM2,TIM_IT_Updata);
}
}
输出限幅是什么作用呢?
输出限幅是非常必要的,如果你电机输出的函数,只能接收-100~100的参数,PID计算出一个200,肯定不能直接把200输出给电机。
增量式PID实现

大家要注意,我们这个增量式PID,是在控制器内积分,输出的还是全量。
float Target,Actual,Out;//目标值,实际值,输出值
float Kp,Ki,Kd;//PID权重
float Error0,Error1,Error2;//本次误差,上次误差,上上次误差
int main()
{
Timer_Init();
while (1)
{
Target = 设定目标值;
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Updata) == SET)
{
//读取传感器实际值
Actual = 读取实际值;
//更新误差
Error2 = Error1;
Error1 = Error0;
Error0 = Target - Actual;
//更新输出
Out += Kp*(Error0-Error1) + Ki*Error0 + Kd*(Error0-2*Error1+Error2);
//输出限幅
if(Out > 最大值)
{
OUT = 最大值;
}
if(Out < 最小值)
{
Out = 最小值
}
//控制硬件函数
控制硬件函数(Out);
TIM_ClearITPendingBit(TIM2,TIM_IT_Updata);
}
}
虽然我们计算的是▲OUT,但是我们使用的OUT += ▲OUT,所以是实现了控制器内积分,整体功能和位置式PID相同。