FPGA实现PID闭环控制器:从Verilog代码到电机控制实践
发布时间:2026/9/19 13:57:17 锦皓数字建站

简介PID控制是工业自动化中最经典的控制算法广泛用于电机调速、温度控制等场景。然而基于MCU的软件实现受中断延迟和总线仲裁影响难以保证控制周期的确定性。FPGA凭借硬件并行和固定时钟延迟成为高实时性闭环控制的首选平台。从数字PID的离散化原理出发解析位置式与增量式算法的工程取舍详细阐述FPGA中的定点数Q格式设计、控制周期同步、Verilog可综合代码实现以及抗积分饱和策略。同时介绍参数整定流程与硬件在环验证方法并结合级联控制和前馈补偿等实践技巧为电机控制应用从单片机迁移到FPGA提供系统化参考。1. 把 PID 闭环控制器搬进 FPGA到底在解决什么问题做电机控制的人通常都有个直觉PID 这种经典算法拿单片机和 STM32 跑得挺好代码成熟、资料多、调参工具齐全为什么还要折腾 FPGA答案是“确定性”和“并行性”。STM32 的 PWM 输出靠定时器硬件实现但 PID 计算本身跑在中断里中断延迟、C 代码编译优化、总线仲裁都会让控制周期产生抖动。直流电机对电流和转速的响应是微秒到毫秒级的一个 10kHz 的电流环控制周期抖动 5% 以上电流波形就会出现明显的毛刺进而影响转速平稳性。FPGA 解决这个问题的方式跟软件完全不同。整个 PID 运算在硬件逻辑里完成从编码器计数到 PWM 占空比更新延迟是固定的几个时钟周期不会因为系统负载变化而波动。另一个优势是并行转速环和电流环可以在同一时刻独立计算不存在抢占和排队。这篇文章要做的事很明确把数字 PID 闭环控制器拆成 FPGA 内部的几个核心模块给出可综合的 Verilog 实现思路说清楚控制周期怎么设计、PID 参数怎么换算成 FPGA 定点数、编码器反馈和 PWM 输出怎么跟 PID 计算模块对接。目标读者是做过 FPGA 基础开发、想往电机控制方向深入的人或者已经在用 STM32 做电机控制、打算迁移到 FPGA 平台的工程师。整个过程不依赖任何厂商专用 IP纯 Verilog 逻辑加一个简单的积分器即可跑通位置式和增量式两种 PID。2. 数字 PID 的离散化与 FPGA 定点数表示2.1 从模拟 PID 到数字 PID位置式和增量式的工程取舍模拟 PID 的表达式 $u(t) K_p e(t) K_i \int e(t)dt K_d \frac{de(t)}{dt}$ 在连续域里描述控制规律但 FPGA 只能处理离散的采样序列。离散化的标准做法是把积分项用累加替代微分项用差分替代控制周期设为固定值 $T_s$于是得到位置式 PID$$u(k) K_p e(k) K_i T_s \sum_{i0}^{k} e(i) \frac{K_d}{T_s} [e(k) - e(k-1)]$$位置式的特点是输出直接对应执行机构的绝对位置对电机而言就是 PWM 占空比绝对值。但工程上更常用的是增量式$$\Delta u(k) u(k) - u(k-1) K_p [e(k)-e(k-1)] K_i T_s e(k) \frac{K_d}{T_s} [e(k) - 2e(k-1) e(k-2)]$$增量式输出的是占空比的增量每次在上一拍基础上累加。这个式子在 FPGA 里的优势是直接省掉了一个大位宽的累加器——积分项不需要单独存储历史累加值误差项天然被“遗忘”。更重要的是增量式在手动/自动切换、输出限幅处理上比位置式安全。位置式一旦积分饱和输出可能长时间停在极限值恢复需要较长时间增量式因为输出是增量限幅逻辑只需限制 $\Delta u$ 的幅度控制律本身更“温和”。还有一个容易忽略的点FPGA 实现微分项时测量噪声会被直接放大。位置式的微分项是 $e(k) - e(k-1)$增量式是 $e(k) - 2e(k-1) e(k-2)$。如果编码器反馈有量化噪声增量式的高频增益更高所以通常建议在微分项前面加一阶低通滤波这个在 FPGA 里实现很便宜——一个移位寄存器加一个乘加器。2.2 定点数格式设计Q 格式与参数溢出边界FPGA 做不了浮点除非用昂贵的 DSP 块做浮点 IP电机控制惯用的是定点数。定点数格式要同时解决两个问题数值范围和分辨率。范围不够会导致中间运算溢出分辨率不够会导致稳态误差。工程上最常见的做法是统一用 Q 格式。Q 格式的定义是 Qm.nm 为整数位含符号位n 为小数位总位宽 W m n。比如 Q3.12 表示 16 位有符号数数值范围是 -8 到 7.9997分辨率是 $2^{-12} 0.000244$。选择 Q 格式时我一般按这条路径推算明确物理量范围。电机转速用 16 位编码器计数表示比如编码器 2500 线4 倍频后每转 10000 个脉冲转速测量值用 16 位无符号数存范围 0 到 65535完全够用。确定 PID 参数的定点表示。Kp、Ki、Kd 这三个浮点参数在 FPGA 里都是乘常数关键是乘法器的位宽。如果 Kp 取值范围是 0 到 10精度要求 0.001那么 Kp 用 Q5.11 格式16 位误差约 $2^{-11} 0.00049$满足要求。检查中间运算的溢出。最危险的是累加路径误差 e(k) 是 16 位Kp 是 16 位乘法结果可能需要 32 位。加法结果再扩展一到两位作为溢出保护。FPGA 的 DSP 块比如 Xilinx 的 DSP48E1原生支持 25x18 乘法用 32 位累加器正好。一个实用经验中间量位宽比理论需要的多留 2 位。因为闭环调试时你可能会加大 Kp位宽不够就得全部重写。多留两位只增加一点 LUT 消耗但换来的是调参自由。信号物理含义定点格式位宽说明speed_ref目标转速Q0.1616无符号0 到 65535speed_meas编码器测量转速Q0.1616无符号0 到 65535err误差 e(k)Q1.1618有符号含两符号位kp, ki, kdPID 参数Q4.1216范围 0 到 15.999delta_u增量式输出Q1.1618最终截取为 PWM 占空比这里特别说明 err 为什么用 18 位16 位无符号数相减结果范围是 -65535 到 6553517 位刚好够但留一位做加法进位保护统一用 18 位。定点数的运算规则是Q4.12 乘 Q0.16 结果是 Q4.28需要把结果右移 16 位截断回 Q4.12这个截断——不是四舍五入而是直接截断——会引入最多一个 LSB 的误差电机控制里完全可接受。2.3 控制周期设计决定 PI 参数的物理基准控制周期 $T_s$ 在 FPGA 里由时钟分频或计数产生。直流电机控制里电流环典型值 10kHz 到 20kHz转速环 1kHz 到 2kHz。FPGA 的优势是可以同时跑两个环但前提是理解每个环的物理意义。电流环控制周期由电机的电气时间常数 $\tau_e L/R$ 决定。一个典型直流电机L 几毫亨R 几欧姆$\tau_e$ 在 1ms 量级。采样周期至少要比时间常数快 5 到 10 倍所以 10kHz 以上的电流环是合理的。转速环的机械时间常数 $\tau_m$ 通常在几十毫秒量级1kHz 的转速环已经足够快。FPGA 实现多个控制环不用像软件那样“挤时间”每个环是一个独立的 always 块各自用自己的周期计数器触发。代码结构上每个控制环封装成独立模块模块内部用使能信号ctrl_en控制是否执行计算。这个使能信号由顶层模块的定时器产生避免了软件中“一个循环里按顺序执行多个控制器”的串行延迟。需要强调的坑PWM 载波频率和控制周期必须满足整数倍关系。如果 PWM 频率是 20kHz控制周期也是 10kHz每 2 个 PWM 周期更新一次占空比那么占空比更新时刻必须落在 PWM 周期的固定相位上否则会产生循环抖动。常见做法是 PWM 模块产生一个pwm_period_done脉冲PID 模块用它做触发时钟确保占空比更新和 PWM 计数同步。3. FPGA 内部架构从编码器反馈到 PWM 输出的完整数据通路3.1 模块划分谁负责测量谁负责计算谁负责输出FPGA 里的 PID 控制器不是单一模块而是一条流水线。按数据流向拆成四块编码器接口模块负责把正交编码信号变成转速数字量PID 计算模块负责执行增量式算法PWM 生成模块负责把 PID 输出变成占空比信号顶层控制模块负责协调和时序。编码器接口模块的核心是正交解码。增量式编码器输出 A、B 两路相位差 90° 的方波通过判断 A、B 的相位关系可以判断转向四倍频后每转脉冲数变成原来的 4 倍。标准做法是用 FPGA 的时钟同步外部信号消除亚稳态然后检测边沿和方向。接口模块输出一个计数值encoder_cntPID 模块需要的转速值由这个计数的差分得到——即每两个控制周期之间的计数差值再乘以一个比例系数换算成实际转速。PWM 生成模块要处理死区。直流电机用 H 桥驱动同一桥臂的上下两个 MOS 管不能同时导通否则直通关断。PWM 模块要在上下管驱动信号之间插入死区时间典型值 1 到 2 微秒。FPGA 里实现非常直接检测到 PWM 翻转沿后用一个计数器延迟死区时间再翻转另一路信号。3.2 核心 Verilog 实现增量式 PID 的可综合代码下面这段代码实现了增量式 PID 的核心计算逻辑省略了编码器接口和 PWM 细节聚焦在控制律本身module incremental_pid #( parameter DATA_WIDTH 18, parameter COEF_WIDTH 16, parameter FRAC_BITS 12 )( input wire clk, input wire rst_n, input wire ctrl_en, input wire signed [DATA_WIDTH-1:0] speed_ref, // 目标转速 Q1.16 input wire signed [DATA_WIDTH-1:0] speed_meas, // 实测转速 Q1.16 input wire signed [COEF_WIDTH-1:0] kp, ki, kd, // PID 参数 Q4.12 output reg signed [DATA_WIDTH-1:0] u_out // 输出增量 Q1.16 ); // 中间信号扩展位宽防止溢出 reg signed [DATA_WIDTH-1:0] err_0; reg signed [DATA_WIDTH-1:0] err_1; reg signed [DATA_WIDTH-1:0] err_2; reg signed [DATA_WIDTHCOEF_WIDTH-1:0] mul_kp; reg signed [DATA_WIDTHCOEF_WIDTH-1:0] mul_ki; reg signed [DATA_WIDTHCOEF_WIDTH-1:0] mul_kd; reg signed [DATA_WIDTH16-1:0] delta_u; always (posedge clk or negedge rst_n) begin if (!rst_n) begin err_0 0; err_1 0; err_2 0; u_out 0; end else if (ctrl_en) begin // 更新误差序列 err_0 speed_ref - speed_meas; err_1 err_0; err_2 err_1; // 增量式 PID 三个分项 mul_kp kp * (err_0 - err_1); // 比例项 mul_ki ki * err_0; // 积分项 mul_kd kd * (err_0 - 2*err_1 err_2); // 微分项 // 合并右移 FRAC_BITS 截断回 Q1.16 delta_u mul_kp mul_ki mul_kd; u_out delta_u FRAC_BITS; end end endmodule逐段说明逻辑误差序列更新err_0是当前拍误差err_1、err_2是前两拍误差。控制系统状态小于等于 2所以三个寄存器就够。这里用了非阻塞赋值err_1 err_0取的是更新前的err_0值正好对应前一拍误差时序上完全正确。乘法器位宽kp是 16 位有符号(err_0 - err_1)是 18 位有符号乘积位宽是 161834 位。代码里声明为DATA_WIDTHCOEF_WIDTH-1即 33 位。这里实际应该声明为DATA_WIDTH COEF_WIDTH也就是 34 位避免极端情况下最高位溢出。硬件综合工具会自动处理位宽扩展但手写位宽能让仿真阶段就暴露问题。算术右移与截断delta_u FRAC_BITS用的算术右移保留符号位。如果直接用右移运算符在 Verilog 里默认是逻辑右移负数会变成错误的大正数这一点极其容易踩坑。输出限幅缺失问题这段代码没有处理输出饱和。实际工程中u_out必须经过限幅再送给 PWM 模块。限幅逻辑可以和 PWM 模块放一起也可以直接在 PID 模块输出端做推荐后者——因为限幅后的值需要反馈为err_1和err_2做抗积分饱和anti-windup而这个反馈逻辑必须紧贴 PID 核心。具体做法在 3.3 节展开。3.3 抗积分饱和FPGA 里的三种实现方式积分饱和是 PID 控制器最经典的工程陷阱。当电机长时间被限幅比如堵转积分项持续累加到一个很大的值等误差反向时积分项需要很长时间才能“退回来”导致严重的超调。软件里常用“积分分离”或“条件积分”处理FPGA 里同样能做而且能做得更精细。方式一条件积分conditional integration。只有误差在某个范围内才允许积分项累加超出范围冻结积分。代码上只需要把mul_ki ki * err_0包在一个条件判断里if (err_0 -ERR_LIMIT err_0 ERR_LIMIT) mul_ki ki * err_0; else mul_ki 0;这个方案最直接缺点是误差范围ERR_LIMIT需要根据实际系统调太小会削弱积分作用太大失去抗饱和意义。方式二反向积分back-calculation。检测到输出限幅时把限幅前后的差值负反馈到积分项。实现上要引入一个额外的寄存器存限幅差值再乘一个时间常数。代码量比条件积分多但效果更好尤其适合负载突变频繁的电机驱动场景。方式三积分项单独限幅。给积分项的累加结果单独设置上下限让积分项的绝对值小于输出限幅。这个办法在 FPGA 里实现成本最低只需在delta_u合并前对mul_ki做饱和处理即可。我通常的做法是方式一和方式三叠加先用条件积分防止误差过大时积分继续累积再用单独限幅限制积分项的最大幅度。双保险的代价只是多几个比较器和选择器LUT 用量增加不到 1%。4. 参数整定与硬件在环验证让 PID 从“能跑”到“跑得好”4.1 参数换算从浮点到定点数的完整推导MATLAB/Simulink 里调好一组浮点 PID 参数后不能直接抄进 FPGA。要做换算这里给一个完整的例子。仿真里得到 Kp 2.5Ki 0.8Kd 0.05控制周期 Ts 0.0001 秒10kHz。FPGA 里 Q4.12 格式的参数是浮点值乘以 $2^{12}$Kp: 2.5 × 4096 10240十六进制 0x2800Ki: 0.8 × 4096 3276.8取整 3277十六进制 0x0CCDKd: 0.05 × 4096 204.8取整 205十六进制 0x00CD检查量化误差Kp 的量化值是 2.5用 10240/4096 2.5精确无误差。Ki 用 3277/4096 0.7998误差 0.0002相对误差 0.025%。Kd 用 205/4096 0.05005误差 0.1%。这个误差水平对电机控制完全可接受。关键提醒Ki 的数值还隐含了采样时间。Simulink 里如果 PID 模块的积分系数是 $K_i T_s$ 的乘积形式那换算时要先乘 Ts 再做定点如果 Simulink 用的是独立 Ki那就直接换算。两种习惯都有人用务必确认。4.2 板级调试步骤从开环到闭环的推进路径直接上闭环调试是新手最容易犯的错误。合理的推进路径是四步第一步开环 PWM 验证。写一个固定占空比比如 50%的输出信号接上电机听声音、看电流。目的有二确认 PWM 频率和死区设置正确电机不啸叫、 MOS 管不发热确认 H 桥接线和逻辑正确电机能稳定转动。第二步编码器反馈验证。手动旋转电机轴在逻辑分析仪或 ILA 里观察encoder_cnt的变化。注意检查两点正转计数增加、反转计数减少四倍频后的脉冲数是否是理论上每转脉冲数的 4 倍。这一步有问题后面 PID 完全没法做。第三步纯 P 控制调试。给一个固定的目标转速从 Kp 0.1 开始换算成定点数后逐步增大。观察转速响应是否出现振荡。出现等幅振荡时的 Kp 就是临界增益 Kp_crit记录此时的振荡周期 T_crit这两个值是后续用 Ziegler-Nichols 查表的基础。第四步加入 Ki 和 Kd。按 Ziegler-Nichols 的经验公式PI 控制取 Kp 0.45 × Kp_critKi Kp / (0.85 × T_crit)。这些公式是经验值但作为起点非常有效比盲目试凑快得多。每步调试时用 FPGA 内置的逻辑分析仪ILA 或 SignalTap抓取speed_ref、speed_meas、u_out三个信号测量转速是否跟踪目标、控制输出是否抖动。没有内置逻辑分析仪的板子可以把这三个信号送到 DAC 或者直接用板上 LED 观察粗略状态。4.3 混合仿真Matlab ModelSim 的协同验证方法板上调试成本高、周期长更好的方式是先在仿真环境里验证 PID 逻辑。常见做法是 ModelSim/Questa 里跑 Verilog 仿真但 PID 闭环必须有电机模型否则无法验证闭环稳定性。两种方案方案 AVerilog 里建电机简化模型。用一阶惯性环节近似电机的电气模型$G(s) \frac{K}{Js B}$。离散化后用差分方程建模型这个模型和一个真实的电机行为不完全一样但在验证 PID 逻辑、检查位宽溢出、测试抗积分饱和逻辑上已经足够。方案 BSimulink ModelSim 联合仿真。Simulink 里建电机模型ModelSim 跑 FPGA 的 PID 逻辑通过 cosimulation 接口互连。验证精度高但配置复杂而且需要 license。考虑到多数项目不需要这种精度我一般先用方案 A 做单元级验证再上板做实测。仿真中最应该关注的是什么不是 PID 能不能跟随目标而是位宽溢出和时序冲突。比如delta_u的位宽是否在极限工况下溢出——把目标转速设成满量程的 120%观察内部信号有没有变成错误的正负数跳变。这种问题在仿真里一分钟定位上板调试可能要排查几个小时。4.4 必调的三个参数控制周期、输出限幅、死区时间参数典型值范围调大效果调小效果检查方法控制周期 Ts电流环 10-20kHz速度环 1-2kHz响应变慢相位裕度降低高频噪声被采样进来看速度阶跃响应的超调量输出限幅 u_max占空比 10%-95%启动电流过大带载能力不足看电机堵转时电流死区时间1-2 μs波形畸变效率降低MOS 管直通烧毁示波器看上下管交叉点输出限幅是最容易被忽略的参数。初学者经常只设 P、I、D 三项就完事但 FPGA 里如果不限幅PID 的输出直接数字溢出变成负数PWM 占空比跳变到反方向电机瞬间反转轻则剧烈抖动重则烧毁驱动。所以输出限幅不是可选项而是必选项。位宽留够限幅才是最后防线。5. 进阶实践多闭环级联与 FPGA 特有的性能边界5.1 速度环 电流环的级联架构单闭环 PID 在负载波动小的场景下够用但工程中直流电机经常面临负载突变比如机器人关节堵转这时单环的响应速度不够。标准方案是级联控制内环是电流环外环是速度环电流环响应速度远快于速度环10kHz vs 1kHz内环先抑制电流扰动外环再修正转速误差。FPGA 实现级联控制的优势在于两个环可以真正的并行。软件方案里速度环算完要写电流参考值电流环才能开始算总延迟是两个环的串行时间。FPGA 里速度环的输出直接连线到电流环的参考输入两个环各用各的时钟使能互不占用。代码结构上是实例化两个 PID 模块外层模块的输出作为内层模块的speed_ref输入。需要额外处理的是两个环的限幅关系速度环输出的电流参考值要限幅在电机允许的最大电流内电流环的输出限幅在 PWM 占空比可用范围内。两个限幅的层级关系搞反了超调时内环会瞬间饱和速度环失去对电流的控制力这是级联调试中最常见的坑。5.2 前馈补偿真正的工程加速技巧PID 本质是反馈控制有反馈就有滞后。对于频繁启停、正反转切换的工况加入前馈补偿能明显提升响应速度。前馈的思路是把目标转速的变化率直接转换成 PWM 占空比的预补偿量PID 只需要修正前馈的误差。// 前馈项根据加速度和系统增益直接计算占空比增量 wire signed [17:0] ff_term accel_ref * feedforward_gain; // 前馈 PID 增量叠加 u_out delta_u ff_term;前馈项的意义在于PID 的输出是在误差出现后才开始工作而前馈在误差还没出现时就提前给出控制量让系统“未动先调”。配合级联 PID电机对阶跃信号的跟踪时间可以减少 50% 以上这对于追求快速响应的应用场景价值非常大。5.3 混合仿真验证方法回到真实世界的最后一步把 PID 参数从仿真搬到真实电机之前有一个验证技巧值得重点推荐用信号发生器注入已知扰动。比如给电机轴加一个周期性负载同时用逻辑分析仪观察speed_meas的波动幅度对比 PID 开启前后的波动衰减倍数。这个方法比单纯看阶跃响应更能反映控制器的实际抗扰能力。最后要验证的是“硬件在环”整体行为具体步骤是先给目标转速 0 到额定值之间的阶梯波观察每个台阶上的超调量和稳态误差再做突变负载测试看转速跌落后恢复的时间。这两项测试通过了FPGA 的 PID 闭环控制器基本就能交付工程使用了。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。