FPGA实现PID闭环控制器:从Verilog代码到电机控制实践
2026/9/19 13:57:03 网站建设 项目流程

简介: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_{i=0}^{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.n,m 为整数位(含符号位),n 为小数位,总位宽 W = m + n。比如 Q3.12 表示 16 位有符号数,数值范围是 -8 到 7.9997,分辨率是 $2^{-12} = 0.000244$。

选择 Q 格式时,我一般按这条路径推算:

  1. 明确物理量范围。电机转速用 16 位编码器计数表示,比如编码器 2500 线,4 倍频后每转 10000 个脉冲,转速测量值用 16 位无符号数存,范围 0 到 65535,完全够用。
  2. 确定 PID 参数的定点表示。Kp、Ki、Kd 这三个浮点参数在 FPGA 里都是乘常数,关键是乘法器的位宽。如果 Kp 取值范围是 0 到 10,精度要求 0.001,那么 Kp 用 Q5.11 格式(16 位),误差约 $2^{-11} = 0.00049$,满足要求。
  3. 检查中间运算的溢出。最危险的是累加路径:误差 e(k) 是 16 位,Kp 是 16 位,乘法结果可能需要 32 位。加法结果再扩展一到两位作为溢出保护。FPGA 的 DSP 块(比如 Xilinx 的 DSP48E1)原生支持 25x18 乘法,用 32 位累加器正好。

一个实用经验:中间量位宽比理论需要的多留 2 位。因为闭环调试时你可能会加大 Kp,位宽不够就得全部重写。多留两位只增加一点 LUT 消耗,但换来的是调参自由。

信号物理含义定点格式位宽说明
speed_ref目标转速Q0.1616无符号,0 到 65535
speed_meas编码器测量转速Q0.1616无符号,0 到 65535
err误差 e(k)Q1.1618有符号,含两符号位
kp, ki, kdPID 参数Q4.1216范围 0 到 15.999
delta_u增量式输出Q1.1618最终截取为 PWM 占空比

这里特别说明 err 为什么用 18 位:16 位无符号数相减,结果范围是 -65535 到 65535,17 位刚好够,但留一位做加法进位保护,统一用 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_cnt,PID 模块需要的转速值由这个计数的差分得到——即每两个控制周期之间的计数差值,再乘以一个比例系数换算成实际转速。

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_WIDTH+COEF_WIDTH-1:0] mul_kp; reg signed [DATA_WIDTH+COEF_WIDTH-1:0] mul_ki; reg signed [DATA_WIDTH+COEF_WIDTH-1:0] mul_kd; reg signed [DATA_WIDTH+16-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_1err_2是前两拍误差。控制系统状态小于等于 2,所以三个寄存器就够。这里用了非阻塞赋值,err_1 <= err_0取的是更新前的err_0值,正好对应前一拍误差,时序上完全正确。

乘法器位宽kp是 16 位有符号,(err_0 - err_1)是 18 位有符号,乘积位宽是 16+18=34 位。代码里声明为DATA_WIDTH+COEF_WIDTH-1,即 33 位。这里实际应该声明为DATA_WIDTH + COEF_WIDTH也就是 34 位,避免极端情况下最高位溢出。硬件综合工具会自动处理位宽扩展,但手写位宽能让仿真阶段就暴露问题。

算术右移与截断delta_u >>> FRAC_BITS用的算术右移,保留符号位。如果直接用右移运算符>>,在 Verilog 里默认是逻辑右移,负数会变成错误的大正数,这一点极其容易踩坑。

输出限幅缺失问题:这段代码没有处理输出饱和。实际工程中,u_out必须经过限幅再送给 PWM 模块。限幅逻辑可以和 PWM 模块放一起,也可以直接在 PID 模块输出端做,推荐后者——因为限幅后的值需要反馈为err_1err_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.5,Ki = 0.8,Kd = 0.05,控制周期 Ts = 0.0001 秒(10kHz)。FPGA 里 Q4.12 格式的参数是浮点值乘以 $2^{12}$:

  • Kp: 2.5 × 4096 = 10240,十六进制 0x2800
  • Ki: 0.8 × 4096 = 3276.8,取整 3277,十六进制 0x0CCD
  • Kd: 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_crit,Ki = Kp / (0.85 × T_crit)。这些公式是经验值,但作为起点非常有效,比盲目试凑快得多。

每步调试时,用 FPGA 内置的逻辑分析仪(ILA 或 SignalTap)抓取speed_refspeed_measu_out三个信号,测量转速是否跟踪目标、控制输出是否抖动。没有内置逻辑分析仪的板子,可以把这三个信号送到 DAC 或者直接用板上 LED 观察粗略状态。

4.3 混合仿真:Matlab + ModelSim 的协同验证方法

板上调试成本高、周期长,更好的方式是先在仿真环境里验证 PID 逻辑。常见做法是 ModelSim/Questa 里跑 Verilog 仿真,但 PID 闭环必须有电机模型,否则无法验证闭环稳定性。两种方案:

方案 A:Verilog 里建电机简化模型。用一阶惯性环节近似电机的电气模型:$G(s) = \frac{K}{Js + B}$。离散化后用差分方程建模型,这个模型和一个真实的电机行为不完全一样,但在验证 PID 逻辑、检查位宽溢出、测试抗积分饱和逻辑上已经足够。

方案 B:Simulink + 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 闭环控制器基本就能交付工程使用了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询