-
Notifications
You must be signed in to change notification settings - Fork 226
Home
Welcome to the Arduino-PID-AutoTune-Library wiki! Sorry! My English is pool. So I only can wirte the Wiki with Chinese.
我花了一周时间消化、吸收此库,最终搞定,效果很好。
为了能真正理解,我逐行注释了作者的程序,并且做了以下改动:
-
将所有 Double 变量根据具体数据的需要,改为 int 或 float,对于单片机而言,内存还是有点紧张的。
-
改变了采样周期的算法,因为这个应该和对象的特征强相关,需要在相应的数量级,最好和输入数据的采样率一致。
-
改变了峰值从获取方式,采用采样过程中所有峰值的平均值,而非原来的最大值。我觉得这样更接近对象特性。
-
改变了震荡周期的获取方式,原来是用最后一个震荡周期的数值,改为取采样过程后 N 个震荡周期的平均值。
-
改变整定结束的判断条件,强制采集12个峰值(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自整定]