当前位置: 首页 > article >正文

STM32单片机开发(7).离散PID的程序实现

一、调控周期T

离散PID 程序实现的第一步就是,确定一个调控周期T ,每隔时间 T, 程序执行一次 PID 调控,至于调控周期设置多大比较好呢,这也是有说法的,调控周期一般取决于被控对象变化的快慢,比如倒立摆平衡车、四轴飞行器等,这些对象变化的很快,你总不能说我一秒才调控一次吧,要是等一秒才调控一次,那倒立摆、平衡车早就倒了,四轴飞行器早就掉下来了,所以这些对象调控一定要快,一般20 ms 、10 ms 、5ms甚至1ms就要进行一次调控

调控的越快一般来说对象就会越稳定,但是调控周期也不能无限制的快,因为会受硬件传感器等设备分辨率限制,比如姿态传感器每隔5 ms 才能更新一下数据,那么你 PID 1毫秒就调控一次也没意义。 而且,对于电机控速来说,调控周期不需要特别快,一般20到100 ms 调控一次就可以,电机控速调控也不能过快,因为会受编码器分辨率限制,调控周期越快,编码器测速的分辨率就越低,过快的调控周期,编码器测速根本不准确,这时再快的调控也没有意义。最后,对于某些很大的被控对象,比如给一个很大的锅炉加热,或者给一个游泳池加热,这些对象的变化非常缓慢,PID 调控周期也得慢下来,调快了没意义,比如几百毫秒、几秒甚至几十s调控一次都没问题,因此,调控周期 T 到底确定为多久,需要根据被控对象变化的速度来决定,没有一个固定的值,需要靠我们的实践经验来反复调节尝试

那在程序中,如何实现每隔时间 T 执行一次调控呢?下面给出了三种实现方案:

① 延时函数(最不稳定):这是最简单的,使用 Delay 延时实现调控周期 T ,我们 STM 32程序 main()函数进来,不断执行while(1)主循环,在 while 循环里,这里注释写了,我们可以直接在此处执行 PID 调控,然后呢,执行完 PID 调控后,主循环进行Delay延时,延时时间就是调控周期 T ,这样就能实现上面这一块 PID 调控程序每隔时间 T 执行一次了。

这种方式实现起来最简单,但是弊端也有很多:首先这个Delay延时并不精确,Delay 延时时间加上上面代码的执行时间,才是整个 WHILE 循环执行的周期,最后导致调控周期,会比设定的 T 长那么一点点,如果上面的代码又加入了其他程序逻辑(例如:打印等耗时的程序操作),那么调控周期就更不能保证精确了……另外,这里用了长时间delay,会阻塞主循环的运行,不利于程序其他代码的执行,因此,如果你的程序代码非常简单,只有 PID 调控这一个功能,其他啥代码都没有,那你可以使用这个方法,简单 Delay 一下就实现任务了,何乐而不为呢,但如果你的程序还有其他的代码需要运行,那这样的写法就非常不好了,所以当程序复杂一些后,我们可以使用下面这个方法来实现定时的 PID 调控(此外:freerots的绝对延时api

② 定时器中断(定时器足够的时候):这个方法就是用定时器,定时中断。 main 函数进来,首先初始化定时器,定时器设定好一个合适的时间。然后下面这个是定时器中断函数,每隔时间 T 程序执行到这里一次,然后在此处执行 PID 调控——这样的话 PID 调控的周期可以严格保证为设定的时间T, while(1)主循环也可以该干啥干啥,这样有利于程序多个功能互不干扰的运行一般来说这个方法用的最多啊,正常的项目也推荐用中间这个方法

当然使用这个方法也是有一些弊端的,就是在中断里执行 PID 调控的时候一定要注意,其中涉及硬件操作的函数,不能既在中断里调用,又在主程序中调用,因为中断函数和主程序相当于多线程,多线程操作同一个硬件,会导致资源访问冲突。比如你在主循环里读取了传感器,然后中断函数执行 PID 调控的时候,也读取了这个传感器,那么在执行主循环里读取传感器函数的时候,有可能产生中断,在中断函数里又读取了这个传感器,最后导致了冲突——因此这个方法一定要注意资源访问冲突的问题,如果你的程序特别复杂,为了避免产生资源访问冲突的问题呢,我们可以进一步用第三种方法实现定时。

(注:外部中断定时(定时器不够的时候)——例:当有外部传感器每隔10ms触发一次下降沿,就能使用外部中断)

③ 定时器中断(优化版):看第三种实验方案,同样是使用定时器实现定时,但是定时中断函数进来后,我们不直接在中断函数中执行 PID 调控,而是定义一个标志位 Flag ,定时时间到了Flag 就置1,之后中断函数退出,在主循环里不断检查,如果 Flag 为1了, 说明定时时间到,这样我们再清零Flag ,然后在此处执行 PID 调控(RTOS思想:模拟rtos),这样做的好处就是,所有涉及硬件的操作,我们都在主程序中进行,不会产生资源访问冲突。中断定时,我们使用一个标志位来传递信号即可,当然这样做也有弊端,就是如果主程序执行其他代码过多,或者主程序阻塞了,那么这个 PID 调用代码可能无法及时执行,这会导致调控周期 T 不准确,因此这种方法一定要保证主程序不能阻塞。

④ rots:……

好,这三种程序定时的方案就讲完了,每种方法都是有利有弊的,大家可以根据自己项目的复杂程度来选择,在大多数项目中,我们可以选择中间这个定时方法,后续程序还以中间这个为例进行讲解——也就是使用定时器,在定时中断函数中,每隔调控周期 T 的时间,执行一次 PID 调控的代码,那么每次执行的 PID 调控的主体代码该怎么写呢,具体有哪些步骤呢,继续看下面的内容。

二、位置式PID的程序实现

上图就是位置式 PID 的程序实现,我们看一下位置式 PID 的代码具体是怎么写的。

最上边是位置式PID的公式。

然后看左边这个图,程序第一行我们要先定义一些变量,这里变量可能存在负数或者小数,所以变量类型我都用的是 float 第一行定义的变量是target、Actural、Out,它们就是 PID 与外界交互的三个变量,分别表示:目标值、实际值和输出值。

第二行继续定义变量Kp 、 Ki 、 Kd 这三个是比例项、积分项和微分项的权重,也就是公式里的 Kp 、 Ki 和 Kd 这三个变量需要我们预设一个值,至于每个值到底是多少,这是我们 PID 调参时确定的,后续写程序的时候,再详细讲解调参技巧,现在知道 Kp 、 K i、 Kd 都是预设的定值就可以了。

最后还要定义变量Error0、 Error1、 ErrorInt,分别表示:本次误差、上次误差和误差积分。这三个就是 PID 计算时的中间变量,后续计算时会用到,我们也在这里定义好。

那需要用到变量定义好之后进入 main 函数,main 函数首先初始化定时器,这样定时中断函数每隔定时时间就会自动执行一次,然后主循环中可以执行这样的代码——用户在此处根据需求写入PID 控制器的目标值,也就是 Target = 用户指定的一个值,Target 是目标值,很显然是用户指定的,所以我们可以在主循环里把我们想要设定的目写入给 Target 变量,比如按键按下修改 Target,或者接收串口的数据,修改 Target 都可以,这里示例就只简单的写了一句,意思就是在主循环,根据自己的需求来写入 Target 指定目标值。 之后看右边定时中断函数里才是 PID 调控的核心代码,注释这里写的是每隔时间T 程序执行到这里一次,然后就可以执行 PID 调控了,下面是 PID 调控的完整流程,PID 调控第一步是获取实际值,因此代码是 actual = 读取传感器,读取传感器的数据的函数需要大家自己实现,比如可以读取 AD 值来获取温度,可以读取编码器来获取速度等等,读到的实际值存入事先定义好的 actual 变量里,之后,PID 第二步是获取本次误差和上次误差,本次误差 = 目标值 - 实际值,上次误差怎么得到呢,其实上次误差,就是上一次调控时的本次误差,本次误差就下一次调控时的上次误差,因此这里只需要进行一个变量传递的操作就行了。 但在第一次调控时不存在上一次,因此第一次调控时的 Error1没有意义,全局变量保持默认为零就行了。另外有的人也习惯把这个误差传递的代码放在这段程序的最后进行,这也是可以的哈,效果都一样,当然我习惯把误差传递都放在前面,这样代码看着比较规整。

好,本次误差和上次误差我们就得到了,之后进入下一步,代入位置式 PID 公式进行计算,PID 计算之前,我们得把积分项的累加单独处理一下,因此这一步是误差积分,也就是误差累加,ErrorInt 用于存储误差积分,所以每次调控ErrorInt 都加等于本次误差 Error0,这样 ErrorInt 表示的就是所有误差的积分了。

然后下一步正式代入位置式 PID 的公式,本次 PID 调控的输出值 Out 就可以参考一下上面的公式:第一项是 P 项——Kp * 本次误差 erROrK, 程序这里是 Kp 乘 Error0,第二项是 I 项——Ki * 历史所有 error 的累加,程序这里是 Ki * ErrorInt ,ErrorInt 是误差积分,我们在上面已经进行过历史所有 Error 累加这个操作了,所以下面公式的 I 项写 Ki * ErrorInt 就行了,最后第三项是 D 项——Kd * 本次误差 Error(k) -上次误差 Error(k-1),程序这里是 Kd 乘 Error0 - Error1。

这就是程序里 PID 的计算过程,是不是和公式都一一对应的,这个公式里面的一个难点,就是误差积分怎么实现,看一下程序,其实就是定义一个变量,然后每次的 Error 0都加等赋值给这个变量,这是误差积分啦,好, PID 计算到这里,完成之后进入下一步。

下一步这里写的是输出限幅,输出限幅是非常必要的,如果你电机输出的函数只能接收-100~100的参数,PID 计算出来一个200 肯定不能直接把200输出给电机啊,因此这里加一个输出值的限幅,程序逻辑也很简单,If out 大于上限,则 out 等于上限,if out 小于下限,则 out 等于下限,这样可以把 out 值的范围,限制在允许的范围之内,上限和下限的值到底是多少,取决于里的输出函数能接收多大范围的数。

最后, PID 调控最后一步就执行控制,我们需要调用“输出至被控对象”这个函数,把 PID 本次计算出来的结果 Out 写入进去,输出至被控对象这个函数也需要我们自己完成, 比如电机控速,那这个函数就设置占空比的函数,当然也包括方向控制啊。比如是锅炉控温,那这个函数就设置加热 / 散热功率的函数,这个函数需要根据实际项目来编写。

好,以上这些就是位置式 PID 执行一次调控的所有流程,简单来说就是读取传感器获取实际值actual、执行 PID 各种运算得到输出值 Out 、然后输入值给被控对象的执行机构,其中目标值 Target 我们可以在程序其他任何地方根据需求来指定,最后这整段程序在我们设定的调控周期 T 下重复不断的执行,这样结合 PID 的原理,被控对象的实际值就能又快、又准、又稳的跟踪到目标值了,这是位置式 PID 程序实现的思路。

三、增量式PID的程序实现

之后我们看增量式 PID 程序实现,大家注意,这个程序是控制器内积分输出全量,看左边图片的程序,首先还是定义变量Target、 actual、Out 、Kp、 Ki、 Kd ,这些变量和刚才一样,最后一行这里定义不太一样,是 Error0,Error1, Error2,表示的意思是本次误差、上次误差和上上次误差,之前位置式 PID ,这里是本次误差、上次误差和误差积分,这里没有误差积分的变量,取而代之的是 Error2,上上次误差,然后 main 函数里面和刚才一样,我们主要看中断函数里的调控过程。

第一步还是获取实际值,Actual = 读取传感器的函数,第二步这里变了,是获取本次误差、上次的误差和上上次误差,里面也是误差传递的过程,Error2 = Error1,Error1 = Error0,Error0 = Target - actual ,第一次执行 Error2和 Error1没有意义,默认为零,Error0得到本次误差,第二次执行 Error2没意义,默认为零,Error1得到上一次的 Error0,Error0得到最新的本次误差,第三次执行Error2得到上次的 Error1,也就是上上次的 Error0,Error1得到上次的 Error0,Error0得到最新的本次误差……

之后误差变量都这样传递,每次调控时,Error 2就是上上次误差,Error 1就是上次误差,Error 0就是本次误差。之后不需要误差积分,直接进行 PID 计算,对照公式,第一项,Kp 乘本次误差减上次误差,程序是 KP 乘 ERROR 0减 ERROR 1,没问题。第二项, KI 乘本次误差,程序是 KI 乘 ERROR 0,没问题,第三项,KD 乘本次误差减二倍的上次误差加上上次误差,程序是 KD 乘 Error0-2 * Error1+ Error2,没问题吧。注意,这样等号右边计算的结果是△OUT ,不直接是 out。

这里就有两种处理方式了哈,这里示例展示的是控制器内积分输出全量,因此可以看到增量式PID公式计算处理的结果是加等于赋值给 Out 变量的,这样相当于每次调控时,Out 都+=△ Out ,最终 Out 直接就是全量的输出值,这样的操作就是在控制器内积分输出全量,输出全量的 Out 后,剩下的部分和刚才的代码就完全一样了,也是输出限幅——If outT 大于上限,则 outT 等于上限,If outT 小于下限,则 outT 等于下限。最后全量的 Out 可以直接输出至被控对象,PID 调控过程结束。

当我们在控制器内积分后,这样的增量式 PID 和刚才展示的位置式 PID 整体功能就完全一样了,在同一个系统中两个代码互相替换也没问题。如果你只想实现纯粹的增量式 PID ,每次的结果都只是△ OUT ,那可以这样改,首先这个变量你可以改名叫做△ OUT 然后 PID 计算时,这里的 “out+=” 就改成“△ OUT =”, 这样△ OUT 的值就是增量输出,最后再进行输出至被控对象时,就直接把增量△ OUT 给它就行了。不过这样还是那句话,你一定要注意看看这个被控对象是否能接收并处理增量,如果不行,那还是直接给它输入全量比较好。

好,这就是位置式 PID 和增量式 PID 的代码实现,看到这里,其实发现 PID 也不是很难吧,核心代码也就这么几句对吧,但是虽然代码不多,PID 的基础理论我们还是得好好学的,理论理解的越深刻,PID 调试就会越容易,遇到问题了,才能更快的找到原因,更快的想到解决办法,理论指导实践,实践印证理论,这才是我们花长时间去学理论的目的。

那到这里,PID 学习的第一部分的基础理论就全部讲完了,下一节开始,我们就正式开始写代码,从设备的底层驱动开始,一步步构建到最终的 PID 闭环控制代码,那我们下节再见。


http://www.kler.cn/a/560396.html

相关文章:

  • Apache Pinpoint工具介绍
  • [实现Rpc] 客户端 | Requestor | RpcCaller的设计实现
  • JVM view(1)
  • rust笔记9-引用与原始指针
  • 浏览器JS打不上断点,一点就跳到其他文件里。浏览器控制台 js打断点,指定的位置打不上断点,一打就跳到其他地方了。
  • 精准识别IP应用场景
  • 【运维】内网服务器借助通过某台可上外网的服务器实现公网访问
  • 玩机日记 12 fnOS使用lucky反代https转发到外网提供服务
  • MTK Android12 预装apk可卸载
  • Flutter 上的 Platform 和 UI 线程合并是怎么回事?它会带来什么?
  • Gin从入门到精通 (七)文件上传和下载
  • 自定义SpringBoot Starter
  • 1.✨Java学习笔记
  • Win10登录Samba服务器报用户名密码错误问题解决
  • Windows 11【1001问】如何下载Windows 11系统镜像
  • 安装可视化jar包部署平台JarManage
  • 【排序算法】堆排序详解
  • 金融行业数据安全:KSP密钥管理系统如何保障支付交易与客户信息零泄露
  • springcloud负载均衡策略有哪些
  • 芯谷D1308:低成本、高性能的便携式音频解决方案