如何在FPGA上快速构建PID算法

作为一名工程师,在项目实施阶段多多少少会遇到需要使用控制理论的应用程序。
一种非常常用的算法是比例积分微分控制器(proportional-integral-derivative control)或 pid 控制器。pid 算法用于控制各种应用中温度、压力、电机位置和流量等变量。我经常看到的一个地方是高端图像处理系统(制冷型红外),为了减少图像中的噪点。它使用热电冷却器或其他冷却系统来冷却图像传感器。对于高端成像,较低的噪声可以带来更好的图像。
介绍pid 控制算法实现起来并不难,因为它只需要加法、乘法、除法和减法(dog)。但是,一旦算法实施,确保 pid 回路稳定的三个系数可能需要一点额外的时间来获取。
pid 主要使用三个术语。
比例(proportional) -测量期望值和测量值之间的差异。比例值是当前位置的量度。积分(integral) -会随着时间的推移对误差进行积分。积分项是误差的历史累积值。随着误差的消除,积分项停止增长。导数(derivative) -计算变化率并预测误差的未来趋势。每个术语还具有相关的增益 ki、kp 或 kd,可以帮助我们调整 pid 控制器算法的行为。d 项不是必须的,而且简单的情况下我们基本不使用,使用 pi 控制器也很常见。
pid经常使用浮点数来实现。因此,我们可以使用诸如 vhdl fixed/float 之类的库在 rtl 中实现。或者,我们可以使用hls来实现 pid,因为国内应用vhdl较少,所以我们今天的实例是使用hls构建我们的pid算法。使用hls能够使用浮点或任意精度的定点数。hls还能通过#pragma 快速的为ip添加通用控制接口(axi)。
在纯 fpga 实现类似系统时候,我们需要添加软核来控制ip。在较小的 zynq-7000 soc fpga(7007、7010、7020 等)中则可以通过硬核控制ip。或者,如果我们设计中不想使用处理器,那我们可以设计传统的矢量接口即可。
源码设计pid 的实际源代码非常简单,如下所示。
#include pid.hstatic data_type error_prev =0;static data_type i_prev=0;data_type pid (data_type set_point, data_type kp, data_type ki, data_type kd, data_type sample, data_type ts, data_type pmax){#pragma hls interface mode=s_axilite port=return#pragma hls interface mode=s_axilite port=sample#pragma hls interface mode=s_axilite port=kd#pragma hls interface mode=s_axilite port=ki#pragma hls interface mode=s_axilite port=kp#pragma hls interface mode=s_axilite port=set_point#pragma hls interface mode=s_axilite port=ts#pragma hls interface mode=s_axilite port=pmax data_type error, i, d, p; data_type temp; data_type op; error = set_point - sample; p = error * kp; i = i_prev + (error * ts * ki); d = kd * ((error - error_prev) / ts); op = p+i+d; error_prev = error; if (op > pmax) { i_prev = i_prev; op = pmax; }else{ i_prev = i; } return op;}已将previous error和previous integral声明为全局静态变量,以确保它们在迭代时候其值保持不变。
在算法方面,用户可以在应用程序运行时动态加载 kp、ki、kid、ts 和 pmax。我们可以轻松地添加积分值或使用附加寄存器重新启动控制器。这将使 pid 可以用于多个实现。
为了测试和配置 pid,测试文件罗列了一系列温度值,这些温度都远高于预期的目标设定点,并确保达到设定点。此示例中的 pid 设计用于提供功率(以瓦特为单位)维持光学床的温度。在这种情况下,我们需要加热而不是降低温度。
#include pid.h#include #define iterations 40int main(void){data_type set_point = -80.0;data_type sample[iterations] = {-90.000,-88.988,-87.977,-86.966,-85.955,-84.946,-83.936,-82.928,-81.920,-80.912,-80.283,-79.926,-79.784,-79.774,-79.829,-79.898,-79.955,-79.993,-80.011,-80.017,-80.016,-80.010,-80.005,-80.002,-80.000,-79.999,-79.999,-79.999,-79.999,-80.000,-80.000,-80.000,-80.000,-80.000,-80.000,-80.000,-79.999,-80.000,-80.001,-80.000};data_type kp = 19.6827; // w/kdata_type ki = 0.7420; // w/k/sdata_type kd = 0.0;data_type op;printf(testing cpp\\r\\n);for (int i =0; i在 vitis hls 中针对该 pid 算法进行c 仿真和协同仿真,结果完全符合预期。
算法按照预期运行,下一步是综合和导出 ip,最后就是添加到我们的 vivado 项目中。这次我们使用的是zynq fpga。
延迟性能和资源消耗下面的完整框图反映了添加到vivado项目中情况。
框图
总设计资源
pid 资源构建完成上面的vivado项目,接下来就是导出硬件(xsa)到 vitis 中开发驱动。
在 vitis 中开发驱动时候,我重用了 hls 仿真文件中的几个元素。
由于我们使用的是 axi 接口,vitis hls 在导出ip时候地为我们提供了一个可以在 vitis 中用于驱动 ip 核的驱动程序。但是,当在 ip 内核中使用浮点输入时,驱动程序则期望它们为 u32。如果我们在开发驱动时候从浮点数转换为 u32,我们将失去准确性。因此,解决这个问题的方法是使用指针(pointers)和强制转换。
本质上,我们将变量声明为浮点数,然后在函数中调用设置一个指向浮点变量地址的 u32 指针,并使用间接运算符读取该值。
xpid_set_set_point ( & pid , * ( ( u32 * ) & set_point ) ) ;整个应用程序是
#include #include platform.h#include xil_printf.h#include xpid.h#define iterations 40typedef float data_type;data_type set_point = -80.0;data_type sample[iterations] = {-90.000,-88.988,-87.977,-86.966,-85.955,-84.946,-83.936,-82.928,-81.920,-80.912,-80.283,-79.926,-79.784,-79.774,-79.829,-79.898,-79.955,-79.993,-80.011,-80.017,-80.016,-80.010,-80.005,-80.002,-80.000,-79.999,-79.999,-79.999,-79.999,-80.000,-80.000,-80.000,-80.000,-80.000,-80.000,-80.000,-79.999,-80.000,-80.001,-80.000};data_type kp = 19.6827; // w/kdata_type ki = 0.7420; // w/k/sdata_type kd = 0.0;data_type ts = 12.5;data_type pmax = 40;u32 op;xpid pid;int main(){ float result; init_platform(); disable_caches(); print(adiuvo pid example\\n\\r); xpid_initialize(&pid,xpar_xpid_0_device_id); xpid_set_set_point(&pid, *((u32*)&set_point )); xpid_set_kp(&pid, *((u32*)&kp)); xpid_set_ki(&pid, *((u32*)&ki)); xpid_set_kd(&pid, *((u32*)&kd)); xpid_set_ts(&pid, *((u32*)&ts)); xpid_set_pmax(&pid, *((u32*)&pmax)); u32 tst = xpid_get_set_point(&pid); for (int i =0; i运行,得到以下结果。
正如预期的那样,硬件中的实现与软件的工作方式相同。
当然,对于不同的应用程序,我们需要重新确定可用于应用程序的 kp、ki 和 kd 变量。
这样做的真正美妙之处在于,因为它是用 c 实现的,可维护性高,可以快速构建一个我们需要的pid算法。
完整项目在下面链接里。
参考https://www.cnki.com.cn/article/cjfdtotal-caiz201316188.htm
https://www.adiuvoengineering.com/
总结虽然上面的流程很简单,但是hls在调整资源和速度方面还是需要一些时间,并且浪费的资源还是比纯hdl多。
最后在说一下该方式的缺点,pid需要进行浮点运算,而fpga则不能进行浮点运算,如果想把上面的算法在逻辑中运行,则需要自己进行量化,但是如果像上面例程的方式在内核(硬核)中运行算法,则该方式简单且优雅~

倍思GAMO游戏耳机体验 很好的表现了游戏耳机应有的水准
智慧路灯使用的传感器功能特点
宁波市新兴产业实现突破发展 制造业单项冠军居国内首位
5G移动签约数达到50亿还要多少年?丨爱立信发布最新《移动市场报告》
厉害了Word国!神威太湖之光实现三连冠,携手天河二号夺全球超级计算机500强一二名
如何在FPGA上快速构建PID算法
自学UI设计路上7点你要注意
RK3399|RK3399方案讲解如何实现双路mipi拼接屏+HDMI 三屏显示
新款Summit Lite是制造商模型的“入门版”
使用SiC提高工业应用的能源效率(1)
音响的分频器有什么样的作用?
阿里影业推出“锦橙合制计划”,五年合制20部优质电影赋能影视行业
微软对Windows10新版本重新设计图标 并在未来会有更多应用获得图标升级
UWB定位技术在自动驾驶中的应用
反接制动的优缺点
Maxim推出最小尺寸同步整流DC/DC转换器MAX15058
铠侠日本晶圆6厂起火,或影响NAND Flash的供货与价格
亚马逊收购机器人公司,建立机器人帝国
CQCC 2023赞助及展览合作方案发布
立迪思新科技为搭配LDS120P LDO控制器 推出两款新产品