Skip to content
embedream edited this page Dec 4, 2022 · 1 revision

Welcome to the Arduino-PID-AutoTune-Library wiki! Sorry! My English is pool. So I only can wirte the Wiki with Chinese.

我花了一周时间消化、吸收此库,最终搞定,效果很好。

为了能真正理解,我逐行注释了作者的程序,并且做了以下改动:

  1. 将所有 Double 变量根据具体数据的需要,改为 int 或 float,对于单片机而言,内存还是有点紧张的。

  2. 改变了采样周期的算法,因为这个应该和对象的特征强相关,需要在相应的数量级,最好和输入数据的采样率一致。

  3. 改变了峰值从获取方式,采用采样过程中所有峰值的平均值,而非原来的最大值。我觉得这样更接近对象特性。

  4. 改变了震荡周期的获取方式,原来是用最后一个震荡周期的数值,改为取采样过程后 N 个震荡周期的平均值。

  5. 改变整定结束的判断条件,强制采集12个峰值(6高、6低),即6个震荡周期后停止。

  6. 修改了虚假峰值判断的处理,原来似乎少了一次 count + 1。

上述改变不一定合适,供大家参考,至少修改后应用于我的小车直流电机调速效果很好!

PID 算法使用的是 Arduino 的 PID_v2 库。

以下是修改后的代码:

PID_AutoTune_v1.h

/**

因为此库的说明比较少,故仔细阅读源代码,并结合作者的文章后,加上自己理解后的注释。

作者原文链接:http://brettbeauregard.com/blog/2012/01/arduino-pid-autotune-library/

有热心读者将其翻译:https://blog.csdn.net/foxclever/article/details/102645642

使用此库要注意以下几点:

1、确定要调节的对象输入(input)、输出(output)范围,从而确定合适的输入控制方向转换判断值(setpoint)、 回差值(NoiseBand),以及对应的输出中值(outputStart)、输出高低值(oStep)。

具体以我目前要尝试的小车对象为例:
输入值为:速度,范围大约是 0 ~ 650 mm/s
输出值为:PWM,按我目前程序设计,为 0 ~ 100,为避免控制饱和,作为速度调节控制,将范围控制在 10 ~ 90
为确定上述参数合适数值,应该先测试不同 PWM 下对应的速度,至少测试 20、50、80 三个 PWM 对应的速度,
将 50 作为 outputStart, 30 作为 oStep,
(最高速度-最低速度)/2 作为 setpoint,
(最高速度-最低速度)/10 作为 Noiseband,

2、setpoint、outputStart 是在启动自动整定的第一次初始化的,通过启动时的 input 、output 隐含确定, 编程时最好通过显性方式给 input 、output 赋值。 其余参数有初始化函数,比较容易理解

3、注意:这个库算法是默认输出控制值和输入值成正比的,即计算后的输出增加,控制产生的输入也增加, 如果你的控制对象不是如此,需要修改库函数中的相应计算!

4、库函数中使用了 millis() 函数确定计算间隔,所以必须保证函数返回的是 1ms,在 RTOS 中不能因修改 Tick 频率而改变! PID 库也是同样,利用了 millis() 确定计算间隔!

5、计算周期是由 sampleTime 确定,此值是隐性初始化的,在函数 SetLookbackSec 中确定,此函数是确定回溯峰值的时间, 最小值 1 秒,对应回查次数 10,sampleTime 为 100 ms; 小于 25 秒,对应 sampleTime 为 250ms,回查次数为:时间*4 大于等于 25 秒,对应 sampleTime 为:时间*10 ms,回查次数为:100

--------- 20221124 ---------- by Embedream
测试发现,原来的采样时间不合适,需要根据对象的时间常数确定合适的采样时间。
目前我所测试的对象采样时间 20ms 合适。修改了函数 SetLookbackSec().
回溯次数修改为最多 20 次,减少了输入指保存数组。
此外,修改了未到采样时间的返回值,原来是 false ,改为 2。
感觉使用 double 类型的变量有点多余,根据数据功能不同,修改为 int 或 float。
   将文件放在源程序目录下,作为程序的一个模块,便于管理。
*****************************************************************************/

#ifndef PID_AutoTune_v0

#define PID_AutoTune_v0

#define LIBRARY_VERSION 0.0.1

class PID_ATune

{

public:

//commonly used functions **************************************************************************
PID_ATune(int*, int*);         // * Constructor.  links the Autotune to a given PID
int Runtime();	         // * Similar to the PID Compue function, returns non 0 when done
void Cancel();		 // * Stops the AutoTune
void SetOutputStep(int);	// * how far above and below the starting value will the output step?
int GetOutputStep();
void SetControlType(int); 	// * Determies if the tuning parameters returned will be PI (D=0)
int GetControlType();		//   or PID.  (0=PI, 1=PID)
void SetLookbackSec(int);	// * how far back are we looking to identify peaks
int GetLookbackSec();
void SetNoiseBand(int);	// * the autotune will ignore signal chatter smaller than this value
int GetNoiseBand();		//   this should be acurately set
float GetKp();		// * once autotune is complete, these functions contain the
float GetKi();		//   computed tuning parameters.
float GetKd();
float GetKu();		// 增加 Ku 输出,便于判断计算结果  221122
float GetPu();		// 增加 Pu 输出,便于判断计算结果  221122
 private:
   bool isMax, isMin;		// 运算中出现最大、最小值标志
   int *input, *output;
   int setpoint;		// 反向控制判断值,这个值需要根据对象的实际工作值确定!是通过第一次启动时对应的输入值带入的。
   int noiseBand;		// 判断回差,类似于施密特触发器,实际控制反向的比较值是 setpoint + noiseBand 或 setpoint - noiseBand
   int controlType;		// 计算 PID 参数时,选择 PI 或 PID 模式,输出 Kp Ki,或 Kp、Ki、Kd
   bool running;
   unsigned long peak1, peak2, lastTime;	// 峰值对应的时间
   int sampleTime;
   int nLookBack;
   int peakType;
   //double lastInputs[101];		// 保存的历史输入值, 最多存前 100 次
   int lastInputs[51];			// 保存的历史输入值, 改为 50 次。 20221124 by Embedream
   int peaks[13];			// 保存的历史峰值,最多存前 12 次,对应 6个最大、6个最小。20221124 by Embedream
   int peakCount;                       // 峰值计数
   int peakPeriod[7];                   // 保存前 6 次的最大值间隔时间 20221124 by Embedream
   int peakMaxCount;                    // 最大峰值计数 20221124 by Embedream
   bool justchanged;
   //bool justevaled;                   // 此标志没有使用
   //int absMax, absMin;		// 整个过程中采集的输入最大值、最小值
   int oStep;		// 这个值是用于计算控制高低值的,以 outputStart 为中值,输出高值用 outputStart + oStep, 输出低值用 outputStart - oStep
   int outputStart;	// 输出控制的基础值,这个需要结合对象特征确定,此值也是通过第一次启动时对应的输出值带入的。
   float Ku, Pu;
};
#endif

PID_AutoTune_v1.cpp

#if ARDUINO >= 100

#include "Arduino.h"

#else

#include "WProgram.h"

#endif

#include "PID_AutoTune_v1.h"

PID_ATune::PID_ATune(int* Input, int* Output) {

input = Input;

output = Output;

controlType =0 ; //default to PI

noiseBand = 1;

running = false;

oStep = 30;

SetLookbackSec(2);

lastTime = millis(); }

void PID_ATune::Cancel() {

running = false;

}

int PID_ATune::Runtime() {

int i,iSum;
unsigned long now = millis();
if((now-lastTime) < ((unsigned long)sampleTime)) return 2;	// 原来返回值为 false  不符合函数定义,也无法区分,改为 2,by Embedream 20221122
// 开始整定计算
 lastTime = now;
 int refVal = *input;
 if(!running)                       // 首次进入,初始化参数
 {
    //initialize working variables the first time around
    peakType = 0;
    peakCount = 0;
    peakMaxCount = 0;
    peak1 = 0;
    peak2 = 0;
    justchanged=false;
    setpoint = refVal;
    running = true;
    outputStart = *output;
    *output = outputStart + oStep;
 }
//oscillate the output base on the input's relation to the setpoint
if(refVal > (setpoint+noiseBand)) *output = outputStart - oStep;
else if (refVal < (setpoint-noiseBand)) *output = outputStart + oStep;
//bool isMax=true, isMin=true;
isMax=true;
isMin=true;
//id peaks
/*
 以下循环完成,对回溯次数的输入缓存进行判断,如果输入值均大于或小于缓存值,则确定此次为峰值。
 峰值特征根据 isMax、isMin 哪个为真确定。
 同时完成输入缓存向后平移,腾出第一个单元存放新的输入值。
 这一段代码完成的噪声所产生的虚假峰值判断,应该没有问题!
*/
for(i = nLookBack-1; i >= 0; i--)
{
  int val = lastInputs[i];
  if(isMax) isMax = (refVal > val);              // 第一次是新输入和缓存最后一个值比较,如果大于,则前面的值均判是否大于
  if(isMin) isMin = (refVal < val);              // 第一次是新输入和缓存最后一个值比较,如果小于,则前面的值均判是否小于
  lastInputs[i+1] = lastInputs[i];               // 每采样一次,将输入缓存的数据向后挪一次
}
lastInputs[0] = refVal;                           // 新采样的数据放置缓存第一个单元。
/*
  以下代码完成峰值的确定,以及对应峰值的时间纪录。
  因为上述代码只是去掉噪产生的波动峰值,但如果是连续超过 nLookBack 次数的的上升或下降,
  则上述算法所确定的最大或最小值,并非是峰值,只能是前 nLookBack 次中的最大或最小值。
  但逐句消化程序后,发现这段处理有几点疑惑:
  1、peaks[] 的纪录好像不对,在执行最小到最大值转换时,peakCount 也应该+1,否则应该把
     纪录的最小值覆盖了!所以后面的峰值判断总是满足条件。
  2、峰值对应时间似乎也应该多次存放,取平均值,因对象没有那么理想化,目前应该是取的最后一组峰值的周期。
  3、后续计算 Ku 用的是整个整定过程的最大、最小值,这对于非理想的对象而言也不是很合适。
  考虑做如下改进:
  1)修改峰值纪录,设计12个峰值保存单元,存满12个峰值(6大、6小)后再计算。
  2)纪录 6 组最大值的间隔时间,作为最终计算 Pu 的数据。
*/
if(isMax)
{
  if(peakType == 0) peakType=1;                   // 首次最大值,初始化
  if(peakType == -1)                              // 如果前一次为最小值,则标识目前进入最大值判断
  {
    peakType = 1;                                 // 开始最大值判断
    peakCount++;                                  // 峰值计数   20221124 by Embedream
    justchanged = true;                           // 标识峰值转换
    if(peak2 !=0)                                 // 已经纪录一次最大峰值对应时间后,开始记录峰值周期  20221124 by Embedream
    {
      peakPeriod[peakMaxCount] =(int)(peak1 - peak2); // 最大峰值间隔时间(即峰值周期)
      peakMaxCount++;                             // 最大峰值计数
    }
    peak2 = peak1;                                // 刷新上次最大值对应时间
  }
  peak1 = now;                                    // 保存最大值对应时间 peak1
  peaks[peakCount] = refVal;                      // 保存最大值
}                                                 // 此段代码可以保证得到的是真正的最大值,因为peakType不变,则会不断刷新最大值
else if(isMin)
{
  if(peakType == 0) peakType = -1;                // 首次最小值,初始化
if(peakType == 1)                               // 如果前一次是最大值判断,则转入最小值判断
{
  peakType = -1;                                // 开始最小值判断
  peakCount++;                                  // 峰值计数
  justchanged=true;
}
  if(peakCount < 10) peaks[peakCount] = refVal;   // 只要类型不变,就不断刷新最小值
}
/*  20221124 by Embedream
  以下计算是作为判断采集数据是否合适的部分,如果 2 次峰值判断条件满足,就结束整定过程,感觉不甚合理。
  拟修改为:
  1)计满 12 次峰值后再计算(到第 13 次)。
  2)不再判断是否合理,因为对象如果特性好,自然已经稳定,如果不好,再长时间也无效果。
  3)将后面5次的数据作为素材,去掉第一组数据,因为考虑第一组时对象可能处于过渡过程。
  4)用后 10 点得到的 9 个峰值差平均值作为 Ku 计算值中的 A,取代原来的整个过程的最大、最小值差。
  5)用后 5 点峰值周期平均值作为 Pu 的计算值,取代原来用最后一组的值。
*/
if(justchanged && peakCount == 12)
{
  //we've transitioned.  check if we can autotune based on the last peaks
  iSum = 0;
  for(i = 2; i <= 10; i++)  iSum += abs(peaks[i] - peaks[i+1]);
  iSum /= 9;                                      // 取 9 次峰峰值平均值
  Ku = (float)(4 * (2 * oStep))/(iSum * 3.14159); // 用峰峰平均值计算 Ku
iSum = 0;
for(i = 1; i <= 5; i++) iSum += peakPeriod[i];
iSum /= 5;                                      // 计算峰值的所有周期平均值
Pu = (float)(iSum) / 1000;                      // 用周期平均值作为 Pu,单位:秒
	*output = 0;
  running = false;
  return 1;
}
  justchanged=false;
  return 0;
}

float PID_ATune::GetKp() { return controlType==1 ? 0.6 * Ku : 0.4 * Ku; }

float PID_ATune::GetKi() { return controlType==1? 1.2*Ku / Pu : 0.48 * Ku / Pu; // Ki = Kc/Ti }

float PID_ATune::GetKd() { return controlType==1? 0.075 * Ku * Pu : 0; // Kd = Kc * Td }

float PID_ATune::GetKu() { return Ku; }

float PID_ATune::GetPu() { return Pu; }

void PID_ATune::SetOutputStep(int Step) { oStep = Step; }

int PID_ATune::GetOutputStep() { return oStep; }

void PID_ATune::SetControlType(int Type) { controlType = Type; }

int PID_ATune::GetControlType() { return controlType; }

void PID_ATune::SetNoiseBand(int Band) { noiseBand = Band; }

int PID_ATune::GetNoiseBand() { return noiseBand; }

/* 设置峰值回溯时间,单位 0.1 秒,最小 0.2秒, 最大 4 秒,by Embedream 20221124 */

void PID_ATune::SetLookbackSec(int value) {

if (value < 2) value = 2;
if (value > 40) value = 40;
  if(value < 20)
   {
      nLookBack = 6;                // 按目前实际周期约300ms、采样周期 20ms 考虑,一个周期只有 15 点,回溯 6 点即可。
      sampleTime = value*10;	    // 改为 Value*10 ms, 20、30、40 ~ 200ms
   }
   else
   {
     nLookBack = 10 + value;
     sampleTime = 200;
   }
}

/* 设置峰值回溯时间,最小 1 秒,同时,根据回查时间确定采样时间及回查次数。 这个函数重写 by Embedream 20221124 void PID_ATune::SetLookbackSec(int value) { if (value<1) value = 1; if(value<25) { nLookBack = value * 4; sampleTime = 250; } else { nLookBack = 100; sampleTime = value*10; } } */

int PID_ATune::GetLookbackSec() { return nLookBack * sampleTime / 1000; }

详细说明见:https://blog.csdn.net/embedream/article/details/128060621[掌上单片机实验室 - 实现PID自整定]

Clone this wiki locally