ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA定点增量式PID:Q格式、流水线与DSP资源映射

FPGA定点增量式PID:Q格式、流水线与DSP资源映射 简介围绕PID算法的FPGA实现整理的一份技术文章面向自动化、电气工程、嵌入式与FPGA方向的学习者和工程师重点解决传统单片机软件编程实现PID时顺序执行、实时性不足、抗干扰能力偏弱等问题。资源包共1个PDF文件约264KB内容以正文、公式推导、Verilog HDL代码片段与Quartus II仿真波形为主便于直接阅读和参考。文章从模拟PID离散化讲起给出增量式PID控制算法推导并展开误差计算、乘积求和模块的RTL实现以及K型热电偶配合MAX6675的冷端补偿、非线性校正与PWM输出控制电路设计。目前已有3247人学习下载适合用作课程设计、毕业设计或温控项目的方案参考也可为将PID封装成FPGA IP核、提升闭环控制实时性提供实现思路与仿真验证线索。1. 把PID递推式塞进FPGA先算清楚这三个数伺服驱动做电流环时只要控制频率上到 20kHz 以上主控的节奏就开始绷不住了一个 50 微秒的控制周期里要跑完 ADC 采样、Clarke/Park 变换、两个 PID 调节、SVPWM 更新再叠加中断进出栈开销留给单个 PID 的时间预算往往只剩一两个微秒。把 PID 挪进 FPGA图的不是主频有多高而是时序确定——一次乘加固定占用几个时钟周期抖动趋近于零配合流水线还能边采样边计算。代价也很直接浮点要改定点位宽要自己盯紧系数和状态量都得按寄存器资源重新规划。后面按离散化、定点化、流水线设计、Verilog 落地、上板联调的路径把增量式 PID 在 FPGA 里的实现走一遍代码和参数都能直接对照着改适合做运动控制、数字电源、温控这类延迟敏感场景的工程师。2. 位置式与增量式PID算法的差分方程与Q格式选型2.1 两条差分方程在硬件里差在哪里位置式 PID 的离散形式写成u(k) Kp·e(k) Ki·Σe(j) Kd·[e(k) − e(k−1)]增量式 PID 的离散形式写成Δu(k) Kp·[e(k) − e(k−1)] Ki·e(k) Kd·[e(k) − 2e(k−1) e(k−2)] u(k) u(k−1) Δu(k)把这两条式子放到寄存器视角看差别就出来了。位置式每次都要重新累加误差Σe 的历史值必须长期驻留位宽只增不减积分一旦饱和复位时要把累加器清零、输出跳变增量式只依赖最近三个误差天然就是一个三级移位链寄存器数量固定。我在 FPGA 里默认用增量式除了省资源还因为它的三个乘积项可以并行算——Kp、Ki、Kd 三个乘法器同时开工加法树一级合并一个时钟就能出 Δu。对比项位置式 PID增量式 PID误差状态寄存器只需 e(k)、e(k−1)需要 e(k)、e(k−1)、e(k−2)积分累加器必须独立维护位宽大隐含在 u(k) 累加里饱和处理位置输出端容易残留积分Δu 累加处恢复平滑适合 FPGA 的结构并行度低移位链 乘法树手动/自动切换需要预置 u切换时 Δu 清零即可需要说明的是增量式也不是没有饱和问题。u(k) u(k−1) Δu(k) 里的累加器如果一直加照样会溢出。区别在于你可以把饱和逻辑挂在累加这一级Δu 归零后 u 停下来不会像位置式那样越积越深。2.2 Q格式定点化与位宽预算浮点转定点第一步是确定 Q 格式写作 Qm.n其中 m 是含符号位的整数部分n 是小数部分总位宽就是 mn。误差信号 e 直接来自 ADC假设是 16 位带符号差值范围在 ±32767用 Q0.15 表示Kp、Ki、Kd 通常小于 1用 Q1.15 表示范围 −1 到 0.9999。两数相乘结果自然是 Q1.30总位宽 32 位。位宽预算我一般按下面这张表逐级推信号格式位宽说明e(k)Q0.1516设定值与反馈值之差Δe e(k)−e(k−1)Q0.1617差值会多出 1 位KpQ1.1516系数寄存器Kp·ΔeQ1.3133需要 33 位承载Σ 三项Q1.3133加法树合并后位宽可能再长 2 位Δu 截位后Q1.1516右移 1516 位u(k)Q1.1517累加后留 1 位余量防溢出关键的一步是截位。直接15会丢掉低位长期累加会出现稳态误差所以常见做法是先做舍入保留 32 位乘积取出低 15 位的高一位作为 round bit再右移。这个操作在 FPGA 里就是一个加法器加一个移位综合后大约多消耗 1 个 DSP 的级联资源很划算。注意系数 Q1.15 只能表示到 0.9999如果系统需要 Kp 大于 1要么改用 Q4.12要么在误差侧做前置缩放否则系数直接被截到 1 附近调参时就会觉得“怎么调都没反应”。2.3 抗积分饱和用什么硬件结构实现抗饱和在 FPGA 里的实现非常直接就是比较器 选择器// 输出限幅u_raw 为累加后原始值U_MAX/U_MIN 为 Q1.15 格式上下限 always (posedge clk) begin if (u_raw U_MAX) u_sat U_MAX; // 正向饱和 else if (u_raw U_MIN) u_sat U_MIN; // 反向饱和 else u_sat u_raw; end逻辑说明这段代码放在累加器之后、PWM 模块之前作用是把内部累加值硬钳在输出范围内。参数 U_MAX 和 U_MIN 按执行机构上限写比如 12 位 PWM 对应 Q1.15 的 0.9999 和 −1.0实际填 16sh7FFF 和 16sh8000。真正的积分抗饱和还需要一个反馈当输出被钳位时让积分增量停止叠加。实现上用一对比较结果做使能wire sat_high (u_raw U_MAX); wire sat_low (u_raw U_MIN); wire freeze (sat_high (du 0)) || (sat_low (du 0)); // freeze 为 1 时u 累加器保持不变这样 u 的累加只在需要的时候停下来退出饱和区后立刻恢复响应比单纯钳位输出要平滑得多。这也是增量式相比位置式在 FPGA 里更好落地的地方使能逻辑只挂在一个累加器上。3. 定点PID的FPGA流水线架构与DSP资源映射3.1 采样、误差、PID、限幅、PWM五级流水一个可综合的 PID 控制器顶层模块建议拆成五个module pid_top #( parameter W_ADC 16, parameter W_COEF 16 ) ( input wire clk, rst_n, input wire signed [W_ADC-1:0] adc_data, input wire adc_valid, input wire signed [W_ADC-1:0] ref, output wire pwm_out ); wire signed [W_ADC-1:0] err; wire signed [W_COEF-1:0] du; wire signed [W_COEF-1:0] u_sat; err_gen u_err (.clk(clk), .rst_n(rst_n), .ref(ref), .fb(adc_data), .valid(adc_valid), .err(err)); pid_core u_pid (.clk(clk), .rst_n(rst_n), .valid(adc_valid), .err(err), .du(du)); out_sat u_sat_inst (.clk(clk), .du(du), .u(u_sat)); pwm_gen u_pwm (.clk(clk), .rst_n(rst_n), .duty(u_sat), .pwm(pwm_out)); endmodule逻辑上adc_valid 是整条流水线的节拍源每个有效脉冲触发一次 PID 更新。参数 W_ADC 和 W_COEF 分别控制数据位宽和系数位宽改动时只要保证乘法器两端不超过 DSP 原语的输入上限即可。各段延迟我实测大致是这样分布的流水级功能典型延迟clk采样同步双触发器 边沿检测2误差计算ref 与 fb 相减1乘法三路 Kp/Ki/Kd3DSP 流水加法树 舍入合并三项2限幅比较 选择1PWM 更新占空比写入1合计—约 10在 100MHz 时钟下这 10 个时钟约合 100ns。如果控制周期是 20 微秒延迟占比只有千分之五完全能接受。确定性延迟反而比“延迟越小越好”更有价值调试阶段可以直接按固定拍数反推采样时刻。3.2 用DSP48风格原语推断乘法器各家 FPGA 都有专用乘法块Xilinx 7 系列的 DSP48E1 是 25×18 位带预加器Altera/Intel 的 DSP 块是 27×27 位加累加器。想让综合器正确推断写法上要守几个规矩// 属性只在 Vivado 里有效其他工具用对应原语约束 (* use_dsp yes *) reg signed [32:0] prod_kp; reg signed [16:0] de; always (posedge clk) begin de err - err_d1; // 打一拍给乘法器腾出输入寄存 prod_kp kp_q15 * de; // 16×17 → 33 位 end逻辑说明de 是误差差分先寄存一拍再进乘法可以让综合器把输入寄存器吸收进 DSP 原语的 A/B 端口减少一级 fabric 寄存。参数 kp_q15 用 Q1.15 格式定义localparam signed [15:0] kp_q15 16sd16384;对应 Kp 约等于 0.5。三个系数对应三份这样结构唯一要注意的是三者必须打同样拍数否则加法树对齐时会出问题。我一般在加法前统一再打一至两拍做 pipeline balance宁可总延迟多几拍也不要让某一支路路径过长导致时序违例。3.3 跨时钟域与valid脉冲同步ADC 采样往往有自己的时钟节拍尤其用 SPI 接口读外部 ADC 时采样有效信号属于另一个时钟域。直接把 valid 拉进 PID 模块会引入亚稳态模式是标准的两级同步加边沿检测reg v_d1, v_d2, v_d3; always (posedge clk or negedge rst_n) begin if (!rst_n) {v_d1, v_d2, v_d3} 3b000; else {v_d1, v_d2, v_d3} {adc_valid, v_d1, v_d2}; end wire valid_pulse v_d2 ~v_d3; // 上升沿在系统时钟域重建参数上没有特别要调的唯一要保证的是 ADC 有效信号的高电平宽度大于两个系统时钟周期否则边沿会被漏掉。如果 ADC 输出是并行总线数据侧也要打两拍再进 err_gen避免用同一个脉冲去同时采样和计算。注意复位信号同样要同步。异步复位直接接进 DSP 流水会破坏乘法器的专用寄存器结构常见做法是用复位同步器产生一个系统时钟域内的同步复位再分发给各子模块。4. 用Verilog写增量式PID核心与仿真验证4.1 参数寄存器组与Q格式乘法参数部分我用一组寄存器承载支持运行时通过 AXI-Lite 或简单并口改写现场调参不用重新综合module pid_param #(parameter W 16) ( input wire clk, input wire we, input wire [1:0] addr, input wire signed [W-1:0] wdata ); reg signed [W-1:0] kp, ki, kd; always (posedge clk) begin if (we) begin case (addr) 2d0: kp wdata; 2d1: ki wdata; 2d2: kd wdata; default: ; endcase end end endmodule逻辑说明addr 分别选择三个系数wdata 是上位机写入的 Q1.15 定点值。参数写成寄存器而不是 localparam 的好处是可以在线用示波器看阶跃响应、一边调一边看调完再固化进 bit 流。代价是多消耗几十个 LUT 和触发器对现在任何一颗 FPGA 都是可忽略的量级。4.2 增量式PID核心时序逻辑核心部分就是三个乘积加一个累加器配合误差移位链reg signed [15:0] e0, e1, e2; reg signed [32:0] p_kp, p_ki, p_kd; reg signed [32:0] du_raw; always (posedge clk or negedge rst_n) begin if (!rst_n) begin e0 0; e1 0; e2 0; end else if (valid_pulse) begin e2 e1; // 延迟链移动 e1 e0; e0 err; // 新误差进入 end end always (posedge clk) begin p_kp kp * (e0 - e1); // 比例项 p_ki ki * e0; // 积分项 p_kd kd * (e0 - (e1 1) e2); // 微分项注意移位写法 end always (posedge clk) begin du_raw (p_kp p_ki p_kd) 15; // 合并 截位 end逻辑说明三个乘积 p_kp、p_ki、p_kd 位宽取 33 位容纳 Q1.15 × Q0.16 的最坏情况。e0、e1、e2 组成移位链每个 valid_pulse 沿更新一次。(e0 - (e1 1) e2)就是 e(k) − 2e(k−1) e(k−2)用移位代替乘 2省一个乘法器。最后的15是算术右移把 Q1.30 截回 Q1.15注意用算术移位而不是逻辑移位否则负数会出错。累加和饱和单独一级always (posedge clk or negedge rst_n) begin if (!rst_n) u_acc 0; else if (valid_pulse !freeze) begin if (u_acc du_raw U_MAX) u_acc U_MAX; else if (u_acc du_raw U_MIN) u_acc U_MIN; else u_acc u_acc du_raw; end endfreeze 信号来自前面 2.3 节的饱和检测逻辑这样一写积分停止和输出限幅就都落在同一级里了。参数 U_MAX/U_MIN 按执行机构满量程填比如 16 位 PWM 用 16sh7FFF 和 16sh8000。4.3 Testbench与ModelSim波形检查仿真我一般用一个简单的阶跃激励把三条曲线拉出来对照误差、Δu、输出 u。initial begin rst_n 0; repeat (5) (posedge clk); rst_n 1; ref 16sd8192; // 目标值 Q0.15对应 0.25 repeat (10) (posedge clk); // 模拟反馈逐步逼近 for (int i 0; i 400; i i 1) begin (posedge clk); adc_valid 1; adc_data adc_data 16sd32; // 每拍逼近 32 end adc_valid 0; #2000 $finish; end逻辑说明这段激励每拍让反馈值增大 32模拟一个一阶惯性系统观察 u 是不是平滑上升并在接近设定值后稳定。重点看三个波形节点e0 的过冲、du_raw 的符号翻转时刻、u_acc 有没有贴到限幅。如果 du_raw 在稳态附近一直来回跳通常是微分项噪声过大考虑给 e0 加一级滑动平均或者在微分项上串一个一阶低通参数用kd_lpf一个移位实现即可。注意ModelSim 里观察 signed 信号时波形默认按二进制显示容易看错符号。右键信号改成 decimal并勾选 signed 显示才能看清负数走向。5. 上板联调ILA抓点、调参顺序与三个容易翻车的位置5.1 用ILA抓内部节点别只看输出上板之后光看 PWM 输出是看不出问题的要抓的是内部节点。在 Vivado 里给 e0、du_raw、u_acc 三个信号挂上标记采样深度设 2048触发条件放在 du_raw 的符号变化上# 在综合后的网表里标记待观测信号 set_property MARK_DEBUG true [get_nets {u_pid/e0[*]}] set_property MARK_DEBUG true [get_nets {u_pid/du_raw[*]}] set_property MARK_DEBUG true [get_nets {u_sat_inst/u_acc[*]}]抓到的波形重点看三件事。第一e0 是不是每拍都在动如果一直不动多半是 valid_pulse 没生成回头查 ADC 侧的同步逻辑。第二du_raw 在稳态时应该贴近零均值小幅抖动如果呈现出规律性的大幅摆动通常是 Ki 太大或微分项把噪声放大了。第三u_acc 是不是贴到 U_MAX/U_MIN贴住的时间占比超过三成说明限幅设太紧或者前向增益整体偏小。5.2 调参顺序和三个易翻车点FPGA 里调 PID 的顺序和软件一样先把 Ki、Kd 置零只留 Kp从小到大加到反馈开始出现小幅振荡再退回三分之二然后加 Ki观察稳态误差收敛的速度最后加 Kd主要压超调。区别在于每次改系数都要通过写寄存器不用重新综合这一点比改 localparam 效率高太多。下面这三个位置我踩过不止一次现象可能原因排查方向输出长期偏低怎么加 Kp 都不动系数右移做成了逻辑移位检查是否误写为上电后 u_acc 直接饱和复位没同步到 DSP 流水看复位同步器是否覆盖 pid_core稳态附近高频抖动微分项未滤波或 e0 抖动给微分支路加一阶低通最后一个技巧是关于定点化系数的调参时不要直接算 Kp 的十进制值填进去先把期望值乘以 32768 再取整写完寄存器后用 ILA 读回确认避免因为上位机做了错误的浮点转换导致系数差出一个数量级。整个 PID 环路在 FPGA 里的稳定性说到底就落在这几个定点位和几条同步逻辑上把它们盯住了剩下的就是常规的环路调试。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进