PID控制算法 – 1、Sample Time(采样时间)

前面介绍的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 = \int e(t)d_{t}= e(0)*d_{t0} + e(1)*d_{t1} +e(2)*d_{t2}+...+e(t)*d_{t}

那现在是固定的采样周期,累计误差errSum可以这么表示:

errSum = \int e(t)d_{t}= (e(0)+ e(1)+e(2)+...+e(t))*Sampletime

那整个积分部分

K_{i}*\int e(t)d_{t} = K_{i}*errSum

K_{i}*errSum = K_{i}*Sampletime*(e(0)+e(1)+e(2)+...+e(t))

聪明的小朋友肯定知道把K_{i}*Sampletime看成一个整体,因为Sampletime是一个固定的常量;

误差的微分dERR可以这么表示: dErr=\frac{de(t)}{d_{t}} =(error - lastErr)/Sampletime

那整个微分部分可以这么表示:

K_{d}*\frac{de(t)}{d_{t}} =K_{d}*(error-lastErr)/Sampletime

聪明的小朋友肯定又会把K_{d}/Sampletime看成一个整体了。

那比例系数、积分系数、微分系数可以直接把采样时间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的输出还是那个公式:

Output = K_{p}*e(t) + K_{i}*\int e(t)d_{t} + K_{d}*\frac{de(t)}{d_{t}}

完整的周期性调用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。

上面的代码主要做了以下更改:

  1. PID算法会以固定的时间间隔进行计算【第11行】
  2. 不需要再乘除时间变化。采样时间是一个常数,可以将其从计算代码中移出【第15、16行】,然后将其于调整常数一起输入【第31、32行】。这样做省去了每次PID计算中的一次乘法和一次除法运算。

细心的小朋友肯定会发现,这种方法总是要搞个时间戳,记录每次调用PID算法的时间,通过判断时间间隔的大小来确定是否进行PID计算,在轮询系统中也不一定能保证采样时间是固定的,总归会有一点点偏差。通过定时器中断的方式周期性的调用PID计算函数不香吗?香,当然香,个人认为通过定时器中断来进行PID计算更好,也不复杂,在单片机的应用中,只需要掌握定时器中断就行了,有机会后面再展开吧。

PID (Proportional-Integral-Derivative) 控制算法是一种经典的反馈控制系统,常用于自动驾驶车辆的稳定控制。在C++中实现PID控制器,你需要定义三个基本的函数:比例(P),积分(I),和微分(D)。 以下是简化的C++11代码示例: ```cpp #include <iostream> #include <cmath> class PIDController { public: PIDController(float kp, float ki, float kd) : kp_(kp), ki_(ki), kd_(kd), prev_error_(0.0), integral_(0.0) {} void update(float error, float sample_time) { float derivative = (error - prev_error_) / sample_time; integral_ += error * sample_time; output_ = kp_ * error + ki_ * integral_ + kd_ * derivative; prev_error_ = error; } float get_output() { return output_; } private: float kp_, ki_, kd_; float prev_error_, integral_; float output_; }; int main() { // 初始化PID控制器 PIDController controller(0.5, 0.1, 0.01); // 模拟连续运行 for (float time = 0; time <= 10; time += 0.1) { float error = calculate_error(time); // 根据当前状态计算误差 controller.update(error, 0.1); // 更新控制器 std::cout << "Time: " << time << ", Error: " << error << ", Output: " << controller.get_output() << std::endl; } return 0; } // 这里假设calculate_error()是一个函数,返回当前状态下的期望值与实际值的差 float calculate_error(float time) { // 实际编写计算误差的逻辑 } ``` 在这个例子中,`update()`函数接收当前的错误和采样时间作为输入,并根据PID公式更新控制器的状态。`get_output()`用于获取当前的控制输出值。 注意,这只是一个基础版本,实际应用中可能还需要添加超限处理、反风向等高级特性,并考虑实时性能优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值