前面介绍的PID代码虽然能跑起来,但是还存在一些问题。
PID控制算法 – 0、PID原理_资深流水灯工程师的博客-CSDN博客
对应的代码也重新贴一下,方便比较
/*工作变量*/
unsigned long lastTime;
double Input, Output, Setpoint;
double errSum, lastErr;
double kp, ki, kd;
void Compute()
{
/*计算上次PID调用到这次调用之间的时间间隔*/
unsigned long now = millis(); //获得当前时间,这是Arduino的做法,其他平台自己可以去替换
double timeChange = (double)(now - lastTime); //计算时间间隔
/*计算误差、误差的积分、误差的微分*/
double error = Setpoint - Input;
errSum += (error * timeChange);
double dErr = (error - lastErr) / timeChange;
/*计算PID的输出*/
Output = kp * error + ki * errSum + kd * dErr;
/*保留一些变量,留着下次用,记录误差和时间*/
lastErr = error;
lastTime = now;
}
void SetTunings(double Kp, double Ki, double Kd)
{
kp = Kp;
ki = Ki;
kd = Kd;
}
主要有两个问题:
1、PID计算函数Compute()不是周期性的调用,相当于是轮询模式,调用的时间间隔不是一致的;
2、时间间隔的不一致,也就导致跟时间间隔相关的积分部分和微分部分每次都需要额外的计算;
解决方案
让PID计算函数Compute()周期性的调用,这计时所谓的采样频率,也叫采样周期。这样就会省很多事。怎么个省法?还是来进行一下时间分析。
PID算法调用时间的分析
积分和微分与时间是直接相关的,现在可不比之前,现在有固定的采样周期了,记录每次调用PID算法的时间间隔就是固定的采样周期SampleTime。
所以原先的累计误差errSum是这么表示:
那现在是固定的采样周期,累计误差errSum可以这么表示:
那整个积分部分
聪明的小朋友肯定知道把看成一个整体,因为Sampletime是一个固定的常量;
误差的微分dERR可以这么表示:
那整个微分部分可以这么表示:
聪明的小朋友肯定又会把看成一个整体了。
那比例系数、积分系数、微分系数可以直接把采样时间Sampletime绑定一起
void SetTunings(double Kp, double Ki, double Kd)
{
double SampleTimeInSec = ((double)SampleTime)/1000;
kp = Kp;
ki = Ki * SampleTimeInSec;
kd = Kd / SampleTimeInSec;
}
既然比例系数、积分系数、微分系数已经算上了时间,那误差、误差积分、误差微分就改变了
//误差还是当年的误差
error = Setpoint - Input;
//误差的积分已不是当年的积分
errSum += error;
//误差的微分也不是当年的微分
dErr = (error - lastErr);
PID的输出还是那个公式:
完整的周期性调用PID代码实现
/*工作变量*/
unsigned long lastTime;
double Input, Output, Setpoint;
double errSum, lastErr;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
void Compute()
{
unsigned long now = millis();//记录当前是时间
int timeChange = (now - lastTime);//计算时间间隔,是为了判断采样时间是否到了
if(timeChange>=SampleTime)//时间间隔大于采样时间就可以进行PID计算
{
/*计算误差、误差的积分、误差的微分*/
double error = Setpoint - Input;
errSum += error;
double dErr = (error - lastErr);
/*计算PID输出*/
Output = kp * error + ki * errSum + kd * dErr;
/*还是记录本次PID计算的误差和时间,留给下次使用*/
lastErr = error;
lastTime = now;
}
}
void SetTunings(double Kp, double Ki, double Kd)
{
double SampleTimeInSec = ((double)SampleTime)/1000;
kp = Kp;
ki = Ki * SampleTimeInSec;
kd = Kd / SampleTimeInSec;
}
void SetSampleTime(int NewSampleTime)
{//改变采样时间后,只需要将ki和kd等比例替换一下就行
if (NewSampleTime > 0)
{
double ratio = (double)NewSampleTime
/ (double)SampleTime;
ki *= ratio;
kd /= ratio;
SampleTime = (unsigned long)NewSampleTime;
}
}
在第10和11行,该算法可以决定是否需要进行PID计算。
我们现在知道采样之间的时间是相同的,因此不需要不断地乘以时间变化。 只需适当地调整Ki和Kd(第31和32行),结果在数学上是等效的,但效率更高。
虽然这样做有点瑕疵。 如果用户决定在操作过程中更改采样时间,则需要重新调整Ki和Kd以反映此新更改。 这就是第39-42行的全部内容。
在第29行将采样时间转换为秒。其实要不要没什么区别,但是这样允许用户以1 / sec和s的单位输入Ki和Kd,而不是1 / mS和mS。
上面的代码主要做了以下更改:
- PID算法会以固定的时间间隔进行计算【第11行】
- 不需要再乘除时间变化。采样时间是一个常数,可以将其从计算代码中移出【第15、16行】,然后将其于调整常数一起输入【第31、32行】。这样做省去了每次PID计算中的一次乘法和一次除法运算。
细心的小朋友肯定会发现,这种方法总是要搞个时间戳,记录每次调用PID算法的时间,通过判断时间间隔的大小来确定是否进行PID计算,在轮询系统中也不一定能保证采样时间是固定的,总归会有一点点偏差。通过定时器中断的方式周期性的调用PID计算函数不香吗?香,当然香,个人认为通过定时器中断来进行PID计算更好,也不复杂,在单片机的应用中,只需要掌握定时器中断就行了,有机会后面再展开吧。