Arduino PID库 (6):初始化

news2024/9/30 5:26:11

Arduino PID库 (6):初始化

参考:手把手教你看懂并理解Arduino PID控制库——初始化
Arduino PID库 (5):开启或关闭 PID 控制的影响

问题

在上一节中,我们实现了关闭和打开PID的功能。我们关闭了它,但现在让我们看看当我们重新打开它时会发生什么:
NoInitialization

哎呀!PID 跳回到它发送的最后一个输出值,然后从那里开始调整。这会导致我们不希望有的输入凸起。
上一节,讨论的是 PID 控制由开转关的过程中存在的问题,那么紧接着上一节,如果在关闭后,突然再次开启,那么会产生什么问题呢?直观来看,对于被控量会出现图中,绿色线的一个 bump,这个 bump 是由于由关转开的一瞬间,输出突然放大,那么对于灵敏度高的系统,则会出现,滞后大的系统可能不会如此明显,但不管怎么说,为了杜绝一切不利因素,我们都应该想办法消除这个 bump。由于这个 bump 的根因是由于输出的突变而造成的,所以需要想办法控制这个输出的突变。

解决方案

想一想,所有此类问题都是发生在时间轴上的,那么 PID 控制中时间轴会影响的项只有积分项和微分项(可以想象,比例项为 Kp * (设定值 - 被控量),这是一个连续量,不存在突变的可能(除非是采样时间特别长,在改变输出前,被控量飞上天了),所以只需要从这两项上想办法,控制住这两项的突变,即可控制住输出的突变。首先,问题发生在 PID 关闭转开启的过程中,由于 PID 开启关闭控制的函数是 SetMode,所以在此函数中,增加一个 initial 函数用于控制积分项和微分项即可。具体做法是:

1、更新微分项上一次采样值为 PID 开启一瞬间的采样值,这样可以保持微分项维持在上一次开启结束的状态不变。

2、将积分项设置为当前的输出,为什么要这么做呢?积分项由于 PID 关闭长时间维持关闭前的状态,一旦开启,如果不改变积分项,输出会瞬间被拉回到上一次开启结束的状态,突变就这样产生了
这个很容易修复。由于我们现在知道何时打开(从手动到自动),我们只需要初始化即可平稳过渡。这意味着massaging2个存储的工作变量(ITerm和lastInput)以防止输出跳转。

《代码》

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
bool inAuto = false;
 
#define MANUAL 0
#define AUTOMATIC 1
 
void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      ITerm+= (ki * error);
      if(ITerm> outMax) ITerm= outMax;
      else if(ITerm< outMin) ITerm= outMin;
      double dInput = (Input - lastInput);
 
      /*Compute PID Output*/
      Output = kp * error + ITerm- kd * dInput;
      if(Output> outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      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)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
void SetOutputLimits(double Min, double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
    
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
 
   if(ITerm> outMax) ITerm= outMax;
   else if(ITerm< outMin) ITerm= outMin;
}
 
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto && !inAuto)
    {  /*we just went from manual to auto*/
        Initialize();
    }
    inAuto = newAuto;
}
 
void Initialize()
{
   lastInput = Input;
   ITerm = Output;
   if(ITerm> outMax) ITerm= outMax;
   else if(ITerm< outMin) ITerm= outMin;
}

我们修改了 SetMode(...) 以检测从手动到自动的转换,并添加了初始化函数。它设置 ITerm=Output 来处理积分项,lastInput = Input以防止导数出现峰值。比例项不依赖于过去的任何信息,因此不需要任何初始化。

结果

Initialization

我们从上图中看到,正确的初始化会导致从手动到自动的无颠簸转移:这正是我们所追求的。
下一>>

更新:为什么不是 ITerm=0?

我最近收到很多问题,问为什么我不在初始化时设置 ITerm=0。作为答案,我要求您考虑以下场景:pid 是手动的,用户已将输出设置为 50。一段时间后,该过程稳定到输入 75.2。用户设定值为 75.2 并打开 pid。应该怎么做?我认为切换到自动后,输出值应保持在 50。由于 P 和 D 项将为零,因此发生这种情况的唯一方法是将 ITerm 初始化为输出值。

如果您处于需要输出初始化为零的情况,则无需更改上面的代码。只需在调用例程中设置 Output=0,然后将 PID 从手动转换为自动。

方向

问题

PID连接的过程分为两组:直接作用和反向作用。到目前为止,我展示的所有例子都是直接行动。也就是说,输出的增加会导致输入的增加。对于反向作用过程,情况正好相反。例如,在冰箱中,冷却的增加会导致温度下降。为了使初学者 PID 使用反向过程,kp、ki 和 kd 的符号都必须为负数。

这本身不是问题,但用户必须选择正确的符号,并确保所有参数都具有相同的符号。

解决方案

为了使过程更简单一些,我要求 kp、ki 和 kd>=0。如果用户连接到反向进程,则使用SetControllerDirection 函数单独指定。这确保了所有参数都具有相同的符号,并希望使事情更加直观。

代码

/*working variables*/
unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int SampleTime = 1000; //1 sec
double outMin, outMax;
bool inAuto = false;
 
#define MANUAL 0
#define AUTOMATIC 1
 
#define DIRECT 0
#define REVERSE 1
int controllerDirection = DIRECT;
 
void Compute()
{
   if(!inAuto) return;
   unsigned long now = millis();
   int timeChange = (now - lastTime);
   if(timeChange>=SampleTime)
   {
      /*Compute all the working error variables*/
      double error = Setpoint - Input;
      ITerm+= (ki * error);
      if(ITerm > outMax) ITerm= outMax;
      else if(ITerm < outMin) ITerm= outMin;
      double dInput = (Input - lastInput);
 
      /*Compute PID Output*/
      Output = kp * error + ITerm- kd * dInput;
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
 
      /*Remember some variables for next time*/
      lastInput = Input;
      lastTime = now;
   }
}
 
void SetTunings(double Kp, double Ki, double Kd)
{
   if (Kp<0 || Ki<0|| Kd<0) return;
 
  double SampleTimeInSec = ((double)SampleTime)/1000;
   kp = Kp;
   ki = Ki * SampleTimeInSec;
   kd = Kd / SampleTimeInSec;
 
  if(controllerDirection ==REVERSE)
   {
      kp = (0 - kp);
      ki = (0 - ki);
      kd = (0 - kd);
   }
}
 
void SetSampleTime(int NewSampleTime)
{
   if (NewSampleTime > 0)
   {
      double ratio  = (double)NewSampleTime
                      / (double)SampleTime;
      ki *= ratio;
      kd /= ratio;
      SampleTime = (unsigned long)NewSampleTime;
   }
}
 
void SetOutputLimits(double Min, double Max)
{
   if(Min > Max) return;
   outMin = Min;
   outMax = Max;
 
   if(Output > outMax) Output = outMax;
   else if(Output < outMin) Output = outMin;
 
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
 
void SetMode(int Mode)
{
    bool newAuto = (Mode == AUTOMATIC);
    if(newAuto == !inAuto)
    {  /*we just went from manual to auto*/
        Initialize();
    }
    inAuto = newAuto;
}
 
void Initialize()
{
   lastInput = Input;
   ITerm = Output;
   if(ITerm > outMax) ITerm= outMax;
   else if(ITerm < outMin) ITerm= outMin;
}
 
void SetControllerDirection(int Direction)
{
   controllerDirection = Direction;
}

PID 完成

这就结束了。我们已经把“初学者的PID”变成了我目前知道如何制作的最强大的控制器。对于那些正在寻找PID库详细解释的读者,我希望你得到你想要的。对于那些编写自己的PID的人,我希望你们能够收集一些想法,从而节省一些周期。

最后两点:

  1. 如果本系列中的某些内容看起来有问题,请告诉我。我可能错过了一些东西,或者可能只需要在我的解释中更清楚。无论哪种方式,我都想知道。
  2. 这只是一个基本的PID。为了简单起见,我故意省略了许多其他问题。比如:前馈,重置回扣,整数数学,不同的pid形式,使用速度而不是位置。如果有兴趣让我探索这些主题,请告诉我。

example

  1. Arduino PID 库 - 亮度控制

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1980114.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

最小二乘法求解线性回归问题

本文章记录通过矩阵最小二乘法&#xff0c;求解二元方程组的线性回归。 假设&#xff0c;二维平面中有三个坐标&#xff08;1&#xff0c;1&#xff09;、&#xff08;2&#xff0c;2&#xff09;、&#xff08;3&#xff0c;2&#xff09;&#xff0c;很显然该三个坐标点不是…

React(三):PDF文件在线预览(简易版)

效果 依赖下载 https://mozilla.github.io/pdf.js/getting_started/ 引入依赖 源码 注意&#xff1a;pdf文件的预览地址需要配置代理后才能显示出来 import ./index.scss;function PreviewPDF() {const PDF_VIEWER_URL new URL(./libs/pdfjs-4.5.136-dist/web/viewer.html, im…

12.SpringDataRedis

介绍 SpringData是Spring中数据操作的模块&#xff0c;包含对各种数据库的集成&#xff0c;其中redis的集成模块就叫做SpringDataRedis。 spring的思想从来都不是重新生产&#xff0c;而是整合其他技术。 SpringDataRedis的特点 1.提供了对不同redis客户端的整合&#xff08…

8.4 day bug

bug1 忘记给css变量加var 复制代码到通义千问&#xff0c;解决 bug2 这不是我的bug&#xff0c;是freecodecamp的bug 题目中“ 将 --building-color2 变量的颜色更改为 #000” “ 应改为” 将 #000 变量的颜色更改为 --building-color2 “ bug3 又忘记加var(–xxx) 还去问…

渗透小游戏,各个关卡的渗透实例

Less-1 首先&#xff0c;可以看见该界面&#xff0c;该关卡主要是SQL注入&#xff0c;由于对用户的输入没有做过滤&#xff0c;使查询语句进入到了数据库中&#xff0c;查询到了本不应该查询到的数据 首先&#xff0c;如果想要进入内部&#xff0c;就要绕过&#xff0c;首先是用…

C#中的TCP和UDP

TcpClient TCP客户端 UDP客户端 tcp和udp的区别 TCP&#xff08;传输控制协议&#xff09;和UDP&#xff08;用户数据报协议&#xff09;是两种在网络通信中常用的传输层协议&#xff0c;它们在C#或任何其他编程语言中都具有相似的特性。下面是TCP和UDP的主要区别&#xff1a;…

MySQL的基本使用

文章目录 MySQL的基本使用什么是SQLSQL学习目标SQL的SELECT语句SQL的INSERT INTO语句 SQL的UPDATE语句SQL的DELETE语句 SQL的WHERE子句可在WHERE子句中使用的运算符SQL的AND和OR运算符SQL的ORDER BY子句SQL的COUNT(*)函数 在项目中操作数据库的步骤安装mysql模块配置mysql模块测…

微服务设计原则——易维护

文章目录 1.充分必要2.单一职责3.内聚解耦4.开闭原则5.统一原则6.用户重试7.最小惊讶8.避免无效请求9.入参校验10.设计模式11.禁用 flag 标识12.分页宜小不宜大参考文献 1.充分必要 不是随便一个功能都需要开发个接口。 虽然一个接口应该只专注一件事&#xff0c;但并不是每个…

摩托罗拉刷机包和固件下载地址

发现了一个非常好的摩托罗拉刷机包和固件下载地址&#xff1a;https://firmware.center/ 里面包含了所有的摩托罗拉的刷机包和软件、电路图等等&#xff0c;非常多&#xff0c;我想镜像到本地网盘&#xff0c;但不知道怎么操作&#xff0c;有没有懂得朋友教我全部镜像到国内的…

Kafka生产者(二)

1、生产者消息发送流程 1.1 发送原理 在消息发送的过程中&#xff0c;涉及到了两个线程——main 线程和 Sender 线程。在 main 线程中创建了一个双端队列 RecordAccumulator。main 线程将消息发送给 RecordAccumulator&#xff0c;Sender 线程不断从 RecordAccumulator 中拉取…

Gamma AI:一键生成专业级PPT的智能工具

1. Gamma 简介 Gamma 是一个致力于通过非常简单的ai交互&#xff0c;制作好的视觉体验作品&#xff0c;它始终站在作者的视角新增功能&#xff0c;同时注重观众视角呈现作品。 突破了以往演示文档&#xff08;ppt、pdf、网站&#xff09;表现形式&#xff0c;能够借助ai的力量…

informer中的WorkQueue机制的实现分析与源码解读(1)

背景 client-go中的workqueue包里主要有三个队列&#xff0c;分别是普通队列Queue&#xff0c;延时队列DelayingQueue&#xff0c;限速队列RateLimitingQueue&#xff0c;后一个队列以前一个队列的实现为基础&#xff0c;层层添加新功能。 workqueue是整个client-go源码的重点…

每日学术速递8.5—1

1.SV4D: Dynamic 3D Content Generation with Multi-Frame and Multi-View Consistency 标题&#xff1a; SV4D&#xff1a;具有多帧和多视图一致性的动态 3D 内容生成 作者&#xff1a;Yiming Xie, Chun-Han Yao, Vikram Voleti, Huaizu Jiang, Varun Jampani 文章链接&…

LinuxC++(10):调用可执行程序

认识system函数 可以直接用system在代码中实现调用shell命令 /bin/ls -l /tmp表示执行ls -l命令&#xff0c;打开/tmp地址 而前面的/bin/表示这是shell命令&#xff0c;不可少&#xff0c;可以认为&#xff0c;/bin/后面的就是等价于shell里面输入的命令。 然后&#xff0c;cou…

* (头指针分离自 9822ba4) ,提交代码不能到分支——游离分支

背景 通过git checkout commitId(之前的一个版本); 基于这个版本修改提交代码推送代码&#xff0c;但是远端没有更新最新数据。 操作 通过git checkout commitId(之前的一个版本);通过git branch 查看分支情况&#xff0c;发现所处分支在游离分支&#xff1a;切换到master分…

连接池的原理

文章目录 1. 连接池的含义2. 连接池的作用2.1 不使用连接池的情况2.2 使用连接池的情况 3. 连接池和线程池的关系4. 连接池设计要点5. 使用实测 1. 连接池的含义 数据库连接池&#xff08;Connection pooling&#xff09;是程序启动时建立足够的数据库连接&#xff0c;并将这些…

《Unity3D网络游戏实战》学习与实践

纸上得来终觉浅&#xff0c;绝知此事要躬行~ Echo 网络上的两个程序通过一个双向的通信连接实现数据交换&#xff0c;这个连接的一端称为一个Socket “端口”是英文port的意译&#xff0c;是设备与外界通信交流的出口。每台计算机可以分配0到65535共65536个端口 每一条Sock…

Java | Leetcode Java题解之第322题零钱兑换

题目&#xff1a; 题解&#xff1a; public class Solution {public int coinChange(int[] coins, int amount) {int max amount 1;int[] dp new int[amount 1];Arrays.fill(dp, max);dp[0] 0;for (int i 1; i < amount; i) {for (int j 0; j < coins.length; j)…

基于springboot+vue+uniapp的智慧物业平台小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

c++ 容器 vector

vector的意思就是向量&#xff0c;就是一个顺序表的意思&#xff0c;这个顺序表可以存任意的类型&#xff0c;因为其线性的内存特点&#xff0c;所以在stl里是经常被使用的存在。 vector vector既然要能储存任意的变量&#xff0c;那么就必须使用模板: 这里的T就是变量类型&a…