目录
很多人应该都听说过PID,它的运算过程简单,并能在大多情况下实现较好的控制效果,因此它是工程实践中使用最广泛的控制方法之一。
抛开公式,我将带你从案例出发,详细了解PID的工作原理和使用方法。
注:阅读本文不需要有过多的基础知识,只需中学物理和数学知识就能看懂(当然如果有高等数学知识和单片机知识的话理解起来会更容易)
我专门为本文搭了一个在线仿真环境,下面使用的案例都来自这个环境,读者可以搭配使用https://skythinker.gitee.io/pid-simulator-web/
https://skythinker.gitee.io/pid-simulator-web/
我们假设有一个一维的坐标轴(向右为正方向),在上面上有一个小球(可以看作质点),小球不受任何阻力,可以自由左右滑动;另外,我们还为小球规定了一个目标位置(图中的绿色标线):
现在我们有下述任务:
看到这里的你可以停下来想一想,应该用什么样的策略来计算这个力呢?
到这里大多数人应该都能想出来这样的方法:
“当小球在目标左边的时候向右施力,当小球在目标右边的时候向左施力,就可以保证小球一直在目标位置上了”
思路是非常正确的,但这个策略仍不够完善,因为小球存在惯性,我们施加的力将小球拉回目标位置后小球还会具有一定的速度继续运动,并不会直接停在目标位置。
接下来我们来看看如果使用PID,我们应该如何计算出这个控制力呢?
计算PID的第一步就是计算误差(Error):误差=目标值-反馈值,在这个例子中,误差就是小球当前位置与目标值间的距离。
接下来的运算我们都会围绕误差进行,分为三个步骤使用误差分别算出一个分力,并将三个分力一起施加在小球上。
第一个环节是比例环节P(Proportion),这个环节产生的分力是:
也就是说分力大小与误差成正比,当小球在目标左边的时候分力向右,当小球在目标右边的时候分力向左,其中是比例系数。
比例环节的计算方法其实就与上面大家通过直觉得出的方法差不多,如果只有这个分力作用的话,会产生什么效果呢?
大家可能会发现这不就跟中学物理里的弹簧滑块模型是一样的嘛,力与距离成正比,很明显小球会以目标位置为中心进行左右摆动(简谐振动)(注:图中蓝色短线表示控制力):
那么如何让小球能够静止在目标点呢?这就要请出PID的另一个环节:微分环节D(Differential)。
微分环节也会计算出一个分力,计算方法是:
也就是说,这个分力与误差的变化速度有关,在目标位置不变的情况下,小球向右运动时误差变化速度为负,分力向左;反之当小球向左运动时分力向右;综合看来,微分环节产生的分力始终阻碍小球的运动。
因此如果在刚刚的基础上加入微分产生的分力,就会产生一个阻尼效果,小球会仿佛始终受到一个阻力,因此左右摆动的幅度会逐渐减小,最终收敛到目标位置上:
由公式还可以看出,微分系数可以影响这个“阻力”的大小,因此如果我们把系数调大一些,就可以让小球的运动收敛得更快一些:
到这里,其实我们已经完成我们的目标任务了,小球可以在驱动力的作用下运动到目标位置。
但现在,我们更希望在小球有一些外部干扰时也能实现上面的效果,比如我们在小球上加上一个水平向右的恒力,此时会发生什么呢?
小球在运动过程中仍然会像之前一样接近目标点,但在最终停下来时我们会发现,小球无法精确停在目标点上,而是像上图一样停在离目标点有一定距离的地方,此时控制力与干扰恒力平衡,小球静止。
稍加分析我们就能发现,此时小球静止,微分环节产生的分力为零,控制力完全由比例环节产生,且若距离更小则比例环节的输出更小,更无法平衡干扰力,因此小球无法继续向目标点接近。
此时就需要我们的第三个环节出场了:积分环节I(Integral),它的计算方法是:
也就是说积分环节产生的分力输出正比于误差的积分,当误差持续存在时,这个分力会逐渐变大,试图消除误差。
加入积分作用,我们的PID就能完美实现在有恒力干扰的情况下对小球的控制了:
上图就是PID的信号框图,表示了PID的运行过程:
由控制小球案例我们可以总结出PID三个环节各自的主要作用和效应:
我们在设计PID时主要关注三个量:目标值、反馈值、输出值,PID会根据目标值和反馈值计算输出值。
需要强调的是,PID的整个计算过程其实与被控对象是什么完全没有关系,它只是负责进行数值计算,而我们——作为控制系统的设计者,就需要为PID指定这三个量所对应的实际物理量,这在不同的控制系统中是不一样的。
那么如何确定实际物理量呢,我为大家总结了一个常用准则:
接下来给出几个例子:
任务一:对小球进行速度控制
可用条件:已知小球的实时速度,并且可施加一个力来改变小球的速度
PID目标值:需要小球达到的速度
PID反馈值:小球的实时速度
PID输出值:施加在小球上的力
分析:小球加速度是小球速度的低阶物理量,而施加的力正比于小球加速度
任务二:对电机转速进行控制
可用条件:已知电机的实时转速,并且可控制电机中流过的电流大小
PID目标值:需要电机达到的转速
PID反馈值:电机的实时转速
PID输出值:电机中流过的电流大小
分析:电机中流过的电流大小近似正比于电机的扭矩,也就近似正比于电机角加速度的大小,是转速的低阶物理量,因此可以用电流大小作为输出值
任务三:液位高度控制
描述:容器有进水口和出水口,需要通过进水口的阀门控制容器内液位的高度
可用条件:容器内液位实时高度,可控制进水口阀门液体流速
PID目标值:需要达到的液位高度
PID反馈值:液位实时高度
PID输出值:阀门液体流速
分析:阀门液体流速正比于液位高度的变化速度,是液位高度的低阶物理量
根据上面的步骤,我们其实很容易就能得出程序的执行流程了,就是在计算误差后逐个进行PID各环节的计算就行了,要注意的是整个采样-计算-输出的流程需要定时执行,可以在每次流程运行完后延时一段固定的时间(一般而言计算频率不高于传感器反馈频率,而若频率过低可能使控制精度下降)。
有一个问题没有解决,积分和微分应该怎么算呢?毕竟微积分都是连续的,而我们采样得到的是离散的数据点。其实也很简单,离散状态下的积分计算其实就是把过去采样得到的所有误差加在一起,而微分计算其实就是把这一轮计算得到的误差与上一轮的误差相减。
最后,我们一般还会对PID的积分和输出进行限幅(规定上下限),积分限幅可以减小积分引起的超调,输出限幅可以保护执行机构或被控对象。
//首先定义PID结构体用于存放一个PID的数据
typedef struct
{
float kp,ki,kd;//三个系数
float error,lastError;//误差、上次误差
float integral,maxIntegral;//积分、积分限幅
float output,maxOutput;//输出、输出限幅
}PID;
//用于初始化pid参数的函数
void PID_Init(PID *pid,float p,float i,float d,float maxI,float maxOut)
{
pid->kp=p;
pid->ki=i;
pid->kd=d;
pid->maxIntegral=maxI;
pid->maxOutput=maxOut;
}
//进行一次pid计算
//参数为(pid结构体,目标值,反馈值),计算结果放在pid结构体的output成员中
void PID_Calc(PID *pid,float reference,float feedback)
{
//更新数据
pid->lastError=pid->error;//将旧error存起来
pid->error=reference-feedback;//计算新error
//计算微分
float dout=(pid->error-pid->lastError)*pid->kd;
//计算比例
float pout=pid->error*pid->kp;
//计算积分
pid->integral+=pid->error*pid->ki;
//积分限幅
if(pid->integral > pid->maxIntegral) pid->integral=pid->maxIntegral;
else if(pid->integral < -pid->maxIntegral) pid->integral=-pid->maxIntegral;
//计算输出
pid->output=pout+dout+pid->integral;
//输出限幅
if(pid->output > pid->maxOutput) pid->output=pid->maxOutput;
else if(pid->output < -pid->maxOutput) pid->output=-pid->maxOutput;
}
PID mypid;//创建一个PID结构体变量
int main()
{
//...这里有些其他初始化代码
PID_Init(&mypid,10,1,5,800,1000);//初始化PID参数
while(1)//进入循环运行
{
float feedbackValue=...;//这里获取到被控对象的反馈值
float targetValue=...;//这里获取到目标值
PID_Calc(&mypid,targetValue,feedbackValue);//进行PID计算,结果在output成员变量中
设定执行器输出大小(mypid.output);
delay(10);//等待一定时间再开始下一次循环
}
}
在完成控制器代码编写后,就要连接好系统进行调参了,我们需要确定最合适的使控制效果最优。
通常还是使用经验法调参,通俗而言就是“试参数”,测试多个参数选取最好的控制效果,一般的步骤如下:
此时大家可以使用上述的小球仿真环境体验一下各参数对系统的影响。到这里,我们就已经能够使用PID来控制各种对象了。
当我们在进行小球的位置控制时,我们可能经常会发现一个问题,就是如果小球与目标之间的距离较远的话,小球在运动过程中的速度会很快,同时也总是会导致较大的超调,而且不论怎么修改参数都很难让系统的表现更好一些。
这时你可能会想,如果运动过程中的速度没这么快就好了,这样就不会冲过头了。没错,这就要用到串级PID了。
我们上面所说的算法其实就是单级PID,目标值和反馈值经过一次PID计算就得到输出值并直接作为控制量,如果目标物理量和输出物理量直接不止差了一阶的话,中间阶次的物理量我们是无法控制的。比如:目标物理量是位置,输出物理量是加速度,则小球的速度是无法控制的。
而串级PID就可以改善这一点。串级PID其实就是两个单级PID“串”在一起组成的,它的信号框图如下:
图中的外环和内环就分别是一个单级PID,每个单级PID就如我们之前所说,需要获取一个目标值和一个反馈值,然后可以产生一个输出值。串级PID中两个环相“串”的方式就是将外环的输出作为内环的目标值。
如果将串级PID看作一个整体,可以看到他有三个输入和一个输出,而此时被控对象也需要提供两个反馈量,那么它们都应该对应些什么物理量呢?
首先我们回到最开始的小球案例中,如果用串级PID完成同样的任务,应该这样设计:
可用条件:小球实时位置、小球实时速度、施加在小球上的控制力
目标值:小球目标位置
外环反馈:小球实时位置
内环反馈:小球实时速度
输出值:施加在小球上的控制力
此时的信号框图会变成这样:
可以发现,内环与小球构成了一个恒速系统,PID内环负责小球的速度控制;而如果把内环和小球看作一个整体被控对象,外环又与这个对象一起构成了一个位置控制系统,外环负责位置控制;总体来说,外环负责根据小球位置误差计算出小球需要达到的速度,而内环负责计算出控制力使小球达到这个目标速度,两个环协同工作,就可以完成任务了。
如果不局限于这个案例来说,串级PID的内环一般负责低阶物理量的调节,而外环负责高阶物理量的调节并计算出低阶物理量的目标值,比如下面这个例子:
任务:对电机进行串级角度控制
可用条件:电机实时角度、电机实时转速、可以控制电机电流大小
外环目标值:需要电机达到的角度
外环反馈值:电机的实时角度
内环反馈值:电机的实时速度
输出值:电机电流大小
分析:外环负责电机角度控制,根据电机目标角度和反馈角度计算出目标转速;内环负责转速控制,根据速度反馈和目标转速计算出电流
回到我们的小球控制案例,之前说使用串级之后我们就可以对速度进行控制了,如何进行控制呢?其实就是对外环PID的输出进行限幅,因为外环PID输出的是目标速度,限制外环输出就相当于限制了小球目标速度的最大值,内环也就会维持小球的速度不超过这个最大值了。
可以看到,使用串级PID后小球不再像之前那样“着急”地奔着目标而去,由于位置误差很大,外环输出在大部分时间内都处于给定的最大值,因此小球在运动中接近匀速,这个速度就是所设定的外环输出最大值。而且由于运动速度变慢了,超调也几乎消失了。这就是我们想要的“控制位置的同时还能控制速度”的效果。
//此处需要插入上面的单级PID相关代码
//串级PID的结构体,包含两个单级PID
typedef struct
{
PID inner;//内环
PID outer;//外环
float output;//串级输出,等于inner.output
}CascadePID;
//串级PID的计算函数
//参数(PID结构体,外环目标值,外环反馈值,内环反馈值)
void PID_CascadeCalc(CascadePID *pid,float outerRef,float outerFdb,float innerFdb)
{
PID_Calc(&pid->outer,outerRef,outerFdb);//计算外环
PID_Calc(&pid->inner,pid->outer.output,innerFdb);//计算内环
pid->output=pid->inner.output;//内环输出就是串级PID的输出
}
CascadePID mypid;//创建串级PID结构体变量
int main()
{
//...其他初始化代码
PID_Init(&mypid.inner,10,0,0,0,1000);//初始化内环参数
PID_Init(&mypid.outer,5,0,5,0,100);//初始化外环参数
while(1)//进入循环运行
{
float outerTarget=...;//获取外环目标值
float outerFeedback=...;//获取外环反馈值
float innerFeedback=...;//获取内环反馈值
PID_CascadeCalc(&mypid,outerTarget,outerFeedback,innerFeedback);//进行PID计算
设定执行机构输出大小(mypid.output);
delay(10);//延时一段时间
}
}
一般而言,需要先断开两环的连接,手动指定内环目标值,进行内环调参,当内环控制效果较好后再接上外环进行外环调参,具体的调参方法与单级PID相同。
此时大家也可以使用小球仿真环境体验一下串级控制的效果。
原创文章,未经授权请勿转载
by skythinker
2022.2.21
如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby
在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has
我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru
我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的
几个月前,我读了一篇关于rubygem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:
我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur
前言作为一名程序员,自己的本质工作就是做程序开发,那么程序开发的时候最直接的体现就是代码,检验一个程序员技术水平的一个核心环节就是开发时候的代码能力。众所周知,程序开发的水平提升是一个循序渐进的过程,每一位程序员都是从“菜鸟”变成“大神”的,所以程序员在程序开发过程中的代码能力也是根据平时开发中的业务实践来积累和提升的。提高代码能力核心要素程序员要想提高自身代码能力,尤其是新晋程序员的代码能力有很大的提升空间的时候,需要针对性的去提高自己的代码能力。提高代码能力其实有几个比较关键的点,只要把握住这些方面,就能很好的、快速的提高自己的一部分代码能力。1、多去阅读开源项目,如有机会可以亲自参与开源
嗨~大家好,这里是可莉!今天给大家带来的是7个C语言的经典基础代码~那一起往下看下去把【程序一】打印100到200之间的素数#includeintmain(){ inti; for(i=100;i 【程序二】输出乘法口诀表#includeintmain(){inti;for(i=1;i 【程序三】判断1000年---2000年之间的闰年#includeintmain(){intyear;for(year=1000;year 【程序四】给定两个整形变量的值,将两个值的内容进行交换。这里提供两种方法来进行交换,第一种为创建临时变量来进行交换,第二种是不创建临时变量而直接进行交换。1.创建临时变量来
1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,
在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主