1. 项目概述与核心思路
在嵌入式控制领域,将经典的控制算法“硬化”到可编程逻辑器件中,一直是一个兼具挑战与魅力的方向。PID控制器,作为工业界的常青树,其原理看似简单,但要在FPGA上用VHDL语言实现一个稳定、高效且实用的数字版本,却需要跨越从连续域到离散域的思维转换,并妥善处理定点数运算、时序同步、接口驱动等一系列硬件设计特有的问题。这个项目正是源于一次毕业设计,目标很明确:用一块Basys 3 FPGA开发板,驱动一个自制的乒乓球悬浮装置,让小球稳定地悬浮在透明管道的某个设定高度。这不仅仅是一个算法仿真,更是一次从理论、代码到物理实物的完整闭环验证。
整个系统的核心逻辑链条非常清晰:红外传感器测量乒乓球的实时高度,经过ADC转换为数字量,这个值作为反馈信号。在FPGA内部,PID控制器模块将设定高度与反馈高度进行比较,计算出误差,并据此通过比例、积分、微分运算生成一个控制量。这个控制量最终被转换为PWM信号的占空比,输出给管道底部的风扇。风扇转速的变化会改变向上的气流,从而调整乒乓球的悬浮高度,形成一个闭环控制系统。这个项目适合两类朋友:一类是正在学习数字电路设计或嵌入式系统,希望将控制理论与硬件实践结合起来的在校学生;另一类是从事电机驱动、电源管理或需要快速原型控制系统的工程师,希望了解如何在FPGA上构建确定性的实时控制内核。接下来,我将拆解整个实现过程,分享其中关键的设计决策、踩过的坑以及一些优化思路。
2. PID控制器的数字离散化与VHDL建模要点
将连续的PID控制器方程转化为适合在FPGA时钟驱动下运行的离散形式,是设计的第一步,也是最容易出错的一步。连续时间的PID输出公式为:u(t) = Kp * e(t) + Ki * ∫e(t)dt + Kd * de(t)/dt。在数字系统中,我们需要在固定的采样周期Ts下,对其进行近似。
2.1 离散化公式的选择与推导
我采用了位置式PID算法,这是一种直观且常见的离散化方法。其核心思想是用求和代替积分,用差分代替微分。
- 比例项(P):最简单,直接使用当前采样时刻的误差
e(k)。P_out = Kp * e(k)。 - 积分项(I):积分是误差的累积。离散化后,用矩形法近似,即
I_out = Ki * Ts * Σ e(i),其中i从0到k。在代码中,我们需要一个寄存器来累加历史误差(即误差和error_sum)。 - 微分项(D):微分是误差的变化率。离散化后,用后向差分法近似,即
D_out = Kd * (e(k) - e(k-1)) / Ts。这需要存储上一个采样时刻的误差e(k-1)。
因此,完整的离散位置式PID公式为:u(k) = Kp * e(k) + Ki * Ts * Σ e(i) + Kd * (e(k) - e(k-1)) / Ts
这里有一个至关重要的细节:采样时间Ts必须显式地融入到积分和微分项的计算中。很多初学者实现的代码功能不正常,根源就在于忽略了Ts,导致积分和微分的作用与预期严重不符。在我的VHDL实现中,Ki和Kd参数在输入时,实际对应的是Ki * Ts和Kd / Ts,或者通过一个专门的“时间分频器”模块在计算时引入Ts因子。
2.2 VHDL实现中的关键设计决策
在VHDL中实现上述算法,需要将其映射到寄存器传输级(RTL)描述。
- 定点数运算:Basys 3 FPGA没有硬核浮点运算单元,使用浮点数会消耗大量逻辑资源且速度慢。因此,必须采用定点数。我选择了Q格式表示法,例如Q11.5(16位中,11位整数,5位小数)。这需要在精度和动态范围之间权衡。比例、积分、微分系数
Kp, Ki, Kd也都用定点数表示。 - 有符号数与误差极性:这是一个我早期遇到的坑。误差
e(k) = setpoint - feedback。当反馈值超过设定值时,误差应为负数,控制器应减小输出。必须使用VHDL的signed数据类型来表示所有涉及减法和可能为负值的信号,否则当误差为负时,无符号数运算会导致溢出或逻辑错误,控制器反而会继续增加输出,造成系统发散。 - 积分抗饱和与微分冲击:
- 积分抗饱和:当系统输出长时间处于极限值(如PWM占空比已达100%或0%)而误差仍未消除时,积分项会不断累积(“wind up”),导致系统恢复时产生大幅超调。一个简单的处理方法是,在计算积分项前,判断输出是否已饱和,若饱和则停止积分累加。
- 微分冲击:设定值的突变会导致误差微分项瞬间巨大,产生控制冲击。可以采用“微分先行”或对设定值变化进行滤波,但在本项目中,设定值变化不频繁,主要处理反馈噪声。
- 时序与控制流:PID计算不应在每个时钟周期都进行,而应在每个采样周期触发一次。我设计了一个
trigger进程,根据系统时钟和设定的采样频率生成一个脉冲信号pid_enable。只有当pid_enable为高时,才采样新的反馈值,计算误差,并更新积分、微分项及最终输出。这确保了控制器以正确的、离散的时间节奏运行。
注意:在仿真时,如果简单地用“反馈值是否变化”来触发计算,只能用于功能验证,不能反映真实的时间关系。实际系统中必须依赖精确的采样时钟。
3. 系统架构与FPGA外围接口设计
一个能工作的控制系统,PID算法核心只占一部分,更多的工作在于如何让FPGA与外部世界(传感器、执行器)可靠地对话。基于Basys 3开发板,我的系统架构如下图所示(在VHDL中体现为顶层实体和组件实例化):
+-----------------------+ | FPGA (Basys 3) | | | 设定值输入 ------->| 七段数码管显示模块 |<---- 当前反馈值 (通过拨码开关) | | | | | +-----------------+ | 红外传感器 ------->|->| ADC读取与预处理 |->| +-----------------+ (模拟电压0-1V) | | (XADC IP核) | | | | | +-----------------+ | | PID控制器 | | | | | (pid.vhd) | | v | | | | +-----------------+ | +--------+--------+ | | 滑动平均滤波 | | | | | (average.vhd) | | v | +-----------------+ | +-----------------+ | | | PWM生成模块 | | | | (控制风扇转速) | | | +--------+--------+ +-----------------------+ | v PWM信号 ------> 5V风扇3.1 模拟信号采集:XADC IP核配置与电压缩放
Basys 3板载了Xilinx的XADC模块,这是一个双通道12位ADC。红外传感器的输出通常是模拟电压,其范围可能超过XADC允许的0-1V输入范围。因此,必须设计一个电压分压电路。我使用了两个电阻(3kΩ和1kΩ串联),将传感器输出电压衰减到原来的1/4。例如,传感器输出0-4V,经分压后变为0-1V,完美匹配XADC。
在Vivado中,通过IP Catalog实例化XADC Wizard IP核,配置如下:
- 选择单通道模式(例如,使用VP/VN引脚对)。
- 设置���样率为1 MSPS(实际根据系统需求可降低)。
- 输出数据格式为12位二进制(0-4095对应0V-1V)。
- 在VHDL顶层,需要实例化该IP核,并将其
daddr_in端口连接到通道地址,den_in和dwe_in端口用于控制读写,do_out端口读取转换结果。需要编写一个状态机来周期性地启动转换并读取数据。
3.2 数字信号输出:PWM模块的精细控制
PID控制器的输出是一个数字量,需要转换为PWM波来控制风扇转速。PWM模块的核心是一个计数器和比较器。
- 设定一个PWM周期计数器,例如基于100 MHz系统时钟,计数到10000,则PWM频率为10 kHz。
- PID输出值(经过限幅处理,如0-10000)作为“比较值”。
- 在每个PWM周期内,当周期计数器小于比较值时,PWM输出高电平;否则输出低电平。高电平时间占总周期的比例即为占空比,直接控制风扇的平均电压。
实操心得:PWM频率的选择很重要。频率太低(如几十Hz),风扇可能会发出可闻噪音,且转速调节不平滑。频率太高(如上百kHz),可能超出风扇驱动电路的响应能力。对于普通直流风扇,1kHz到20kHz是一个常见的范围。我选择10kHz,效果较好。
3.3 人机交互与调试接口
调试是硬件项目的重中之重。Basys 3上的LED和七段数码管是宝贵的调试资源。
- 16个LED:我将PID计算出的原始输出值(或PWM占空比)的高位连接到LED上。这样,通过观察LED的亮灭模式,可以直观判断输出是否饱和(全亮或全暗),或者是否在合理范围内动态变化。这在初期排查控制器是否“活着”时非常有用。
- 4位七段数码管:我将其分为两组显示。前两位显示当前ADC读取的电压值(反馈值),后两位显示设定的目标电压值(设定值)。两者都显示到小数点后两位(例如,0.75V显示为“75”)。这让我能实时监控系统的“眼睛”(传感器)看到的和“大脑”(控制器)想要的之间差距,对于手动调节PID参数至关重要。
4. VHDL代码核心模块详解与实现
这里深入剖析几个关键VHDL模块的设计与编码细节。
4.1 PID控制器核心(pid.vhd)实体与架构
entity pid_controller is Port ( clk : in STD_LOGIC; reset : in STD_LOGIC; enable : in STD_LOGIC; -- 采样使能信号,来自trigger模块 setpoint : in STD_LOGIC_VECTOR (11 downto 0); -- 设定值,12位,对应0-4095 feedback : in STD_LOGIC_VECTOR (11 downto 0); -- 反馈值,12位 kp_num : in STD_LOGIC_VECTOR (15 downto 0); -- Kp分子,Q格式 kp_den : in STD_LOGIC_VECTOR (7 downto 0); -- Kp分母,用于缩放 ki_num : in STD_LOGIC_VECTOR (15 downto 0); -- Ki*Ts分子 ki_den : in STD_LOGIC_VECTOR (7 downto 0); -- Ki*Ts分母 kd_num : in STD_LOGIC_VECTOR (15 downto 0); -- Kd/Ts分子 kd_den : in STD_LOGIC_VECTOR (7 downto 0); -- Kd/Ts分母 output : out STD_LOGIC_VECTOR (15 downto 0) -- 控制器输出,16位 ); end pid_controller;架构内部的主要进程:
process(clk, reset) begin if reset = '1' then error_sum <= (others => '0'); last_error <= (others => '0'); output_reg <= (others => '0'); elsif rising_edge(clk) then if enable = '1' then -- 仅在采样时刻计算 -- 1. 计算当前误差(有符号数运算) current_error_signed <= signed('0' & setpoint) - signed('0' & feedback); current_error <= std_logic_vector(current_error_signed(11 downto 0)); -- 2. 计算比例项 P = Kp * e(k) p_term <= resize(signed(current_error) * signed(kp_num), p_term'length) / signed(kp_den); -- 3. 计算积分项 I = Ki * Ts * Σe(i) (简易抗饱和处理) if output_reg < OUTPUT_MAX and output_reg > OUTPUT_MIN then error_sum <= error_sum + current_error_signed; end if; i_term <= resize(error_sum * signed(ki_num), i_term'length) / signed(ki_den); -- 4. 计算微分项 D = Kd * (e(k) - e(k-1)) / Ts d_term <= resize((current_error_signed - last_error) * signed(kd_num), d_term'length) / signed(kd_den); last_error <= current_error_signed; -- 5. 求和并限幅 pid_sum <= p_term + i_term + d_term; if pid_sum > OUTPUT_MAX then output_reg <= OUTPUT_MAX; elsif pid_sum < OUTPUT_MIN then output_reg <= OUTPUT_MIN; else output_reg <= pid_sum; end if; end if; end if; end process; output <= std_logic_vector(output_reg);4.2 滑动平均滤波器(average.vhd)抑制传感器噪声
红外传感器输出易受环境光干扰,存在高频噪声。直接在数字域使用滑动平均滤波器是一种简单有效的平滑方法。
entity average is Generic ( DATA_WIDTH : integer := 12; AVG_WINDOW : integer := 8 -- 平均窗口大小,取2的幂次便于除法 ); Port ( clk : in STD_LOGIC; en : in STD_LOGIC; din : in STD_LOGIC_VECTOR (DATA_WIDTH-1 downto 0); dout : out STD_LOGIC_VECTOR (DATA_WIDTH-1 downto 0) ); end average;其原理是维护一个长度为AVG_WINDOW的移位寄存器组。每次新数据到来时,将其移入,最老的数据移出,并计算寄存器组内所有数据的和,然后右移log2(AVG_WINDOW)位(即除以AVG_WINDOW)得到平均值。这种方法能有效滤除随机噪声,但会引入一定的相位滞后。窗口越大,滤波效果越好,但滞后越严重,需要根据系统响应速度折中。我选择窗口大小为8,在平滑性和实时性之间取得了不错平衡。
4.3 顶层模块集成与时钟域管理
顶层文件(top.vhd)负责将所有模块像搭积木一样连接起来,并处理时钟和复位信号。
- 时钟分频:系统主时钟为100MHz。需要生成多个不同频率的时钟使能信号:
ADC_sample_en: 用于触发ADC采样,频率约50kHz(周期20us)。PID_enable: PID计算使能,与采样率同步,也是50kHz。PWM_clk_en: 用于更新PWM比较值,频率为PWM频率(10kHz)。display_refresh_en: 用于刷新七段数码管显示,频率约100Hz。 这些使能信号通常由计数器生成,是同步设计,比使用分频后的时钟信号更安全。
- 数据通路:明确数据流:ADC数据 -> 平均滤波 -> PID反馈输入端。PID输出 -> 限幅 -> PWM模块。设定值可以通过拨码开关或按钮设置,并传递给PID和显示模块。
- 复位同步:确保所有模块在系统上电或按下复位键时,能同步地初始化为已知状态。
5. 系统调试、参数整定与问题排查实录
将代码综合、实现并下载到板子后,真正的挑战才开始。乒乓球可能一动不动,也可能疯狂振荡。
5.1 PID参数整定实战:从零到稳定
对于这个二阶欠阻尼系统(乒乓球在气流中),我采用了经典的试凑法,并结合了一些观察经验。
- 纯比例控制(P):先将
Ki和Kd设为0,逐渐增大Kp。观察现象:Kp太小,小球无法到达设定高度;Kp增大,小球开始上升,但会在设定高度下方某个位置稳定(静差)。继续增大Kp,静差减小,但会出现振荡。记录下开始出现持续振荡的Kp值,记为Ku(临界增益)。 - 加入积分控制(PI):引入一个较小的
Ki值。积分作用能消除静差。但Ki太大会导致系统响应变慢,超调增大,甚至引发低频振荡。需要耐心微调Kp和Ki,目标是让小球能较平稳地到达设定点,即使有轻微过冲也能快速稳定。 - 加入微分控制(PID):微分能预测误差变化趋势,抑制过冲。但微分对噪声非常敏感。由于我们已经有了平均滤波,可以尝试加入较小的
Kd。观察效果:Kd有助于减小超调,让稳定过程更“干脆”。但Kd过大会放大高频噪声,可能导致控制输出高频抖动,反而使系统不稳定。
踩坑记录:最初调试时,我直接使用了未经滤波的ADC数据,且
Kd设置稍大,结果风扇转速疯狂跳动,小球根本无法稳定。后来意识到是微分项放大了传感器噪声。教训是:在引入微分项之前,必须确保反馈信号足够干净。
最终,我通过观察七段数码管上反馈值的跳动情况,以及LED显示的输出变化趋势,结合小球的实际运动,找到了一组相对稳定的参数。这个过程没有捷径,需要反复试验、观察和调整。
5.2 常见问题与排查速查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 小球完全不动,风扇不转 | 1. PWM输出引脚配置错误。 2. PID输出始终为0或最小值。 3. 电源或风扇连接问题。 | 1. 检查约束文件(.xdc),确认PWM输出引脚已正确分配到板载PMOD口或GPIO,并用示波器或逻辑分析仪检测该引脚是否有信号。 2. 使用ILA(集成逻辑分析仪)IP核,抓取 setpoint,feedback,error,output等内部信号,看计算逻辑是否正确。检查复位信号是否一直有效。3. 用万用表测量风扇供电电压。 |
| 小球剧烈上下振荡 | 1. PID参数不合理,尤其是Kp或Kd过大。2. 传感器噪声大,且微分项 Kd不为零。3. 采样频率过高或过低,与系统动态不匹配。 | 1. 回归纯比例控制,调小Kp直至振荡停止,再缓慢增加。2. 增大平均滤波的窗口,或尝试更复杂的滤波器(如一阶低通)。暂时将 Kd设为0。3. 尝试调整 trigger模块的采样分频系数,改变采样周期。通常采样频率应为系统带宽的5-20倍。 |
| 小球能稳定,但存在静态误差 | 积分作用不足或未生效。 | 1. 检查积分项Ki是否大于0。2. 检查积分抗饱和逻辑是否过于激进,导致在稳定点附近积分停止累加。 3. 适当增大 Ki,但需配合调整Kp。 |
| 响应速度慢,小球移动迟缓 | 1.Kp太小。2. 积分项 Ki主导,系统处于“软”控制状态。3. PWM频率过低,风扇响应跟不上。 | 1. 在保持稳定的前提下,逐步增大Kp。2. 适当减小 Ki,让比例项起主要作用。3. 提高PWM生成模块的计数频率,例如将PWM频率从1kHz提升到10kHz。 |
| 改变设定值后,系统发散 | 1. 设定值变化幅度过大,超出线性范围。 2. 参数是针对某个工作点优化的,系统非线性强。 | 1. 实现设定值斜坡函数,让其缓慢变化,而不是阶跃跳变。 2. 考虑在不同高度区间使用不同的PID参数集(增益调度),或者采用非线性PID。 |
5.3 使用Vivado仿真与调试工具
在烧录到板子前,仿真能避免很多低级错误。
- 编写Testbench:为
pid.vhd编写测试平台(tb_pid.vhd),模拟设定值阶跃变化和反馈值变化。观察output信号是否按预期响应。重点测试误差正负变化时,输出增减方向是否正确。 - 行为仿真:在Vivado中运行仿真,查看波形图。验证使能信号
enable触发时,计算是否发生。检查定点数运算有无溢出。 - ILA(集成逻辑分析仪):这是FPGA调试的神器。在设计中插入ILA IP核,将想要观察的内部信号(如
error_sum,p_term,i_term,d_term等)连接到其探针上。综合实现后,生成比特流文件并下载,在Vivado Hardware Manager中设置触发条件,即可像示波器一样实时捕获FPGA内部信号的变化,这对分析动态过程和无器件现象至关重要。
6. 项目优化与扩展思路
基础版本成功后,可以从多个维度进行优化和扩展,这体现了数字控制的灵活性。
6.1 进阶滤波:从平均滤波到数字滤波器
滑动平均滤波器是一种特殊的FIR(有限长单位冲激响应)滤波器。我们可以将其通用化,实现一个可配置系数的FIR滤波器IP核。通过MATLAB或Python的scipy.signal工具设计一个低通滤波器(如汉宁窗、凯泽窗),计算出滤波器系数,将其量化为定点数,写入VHDL代码的系数ROM中。这样可以实现更精确的频响控制,更有效地滤除特定频段的噪声,同时可能减少相位滞后。
6.2 串级PID控制器(Cascade PID)设计
对于这个乒乓球悬浮系统,一个更高级的控制策略是串级控制。其思想是设计两个嵌套的PID环:
- 外环(主环):以乒乓球高度为反馈,输出作为内环的设定值。这个设定值不再是高度,而是期望的风扇转速。
- 内环(副环):以风扇的实际转速(可通过测速计、编码器或反电动势估算获得)为反馈,控制PWM占空比,快速跟踪外环给出的转速指令。
这样做的好处是,内环可以快速抑制风扇电机本身的扰动(如电压波动、负载变化),外环则专心处理高度控制。内环的动态响应通常比外环快得多,整个系统的抗干扰能力和性能会得到提升。在VHDL实现上,需要实例化两个PID控制器模块,并正确连接它们的设定值和反馈通路。
6.3 自适应与参数自整定
能否让FPGA自己找到合适的PID参数?这是一个更前沿的方向。可以尝试实现简单的自整定算法,如继电器反馈法。思路是:先将控制器置于开关模式(类似Bang-Bang控制),使系统产生稳定振荡,测量其振荡周期和幅度,然后根据齐格勒-尼科尔斯(Ziegler-Nichols)等经验公式计算出一组初始PID参数。这需要额外的逻辑来检测振荡周期和幅值,并在线更新PID模块的Kp,Ki,Kd参数寄存器。
6.4 系统非线性补偿
实验中发现,在管道的不同高度,系统的增益(即风扇转速变化对高度的影响程度)是不同的。这是一个非线性系统。简单的固定参数PID在全程范围内性能可能不最优。可以在FPGA内实现一个查表法(LUT)的增益调度器。根据当前高度(反馈值)所在区间,从预先计算好的参数表中读取对应的PID参数组,动态加载到PID控制器中,从而在全范围内获得一致的良好性能。
这个基于VHDL的PID控制器项目,从理论推导、代码编写、仿真测试到硬件实现和物理调试,完成了一个完整的数字控制系统开发流程。它深刻地揭示了软件算法与硬件实现之间的差异,比如对时序的严格考量、定点数精度的把握、噪声处理的重要性等。最终看到乒乓球在管道中稳稳地悬浮在预设高度时,那种将抽象算法转化为物理现实的成就感,是纯软件仿真无法比拟的。希望这份详细的梳理和记录,能为你的FPGA控制之旅提供一块坚实的垫脚石。如果在具体的代码实现或调试中遇到问题,欢迎随时交流探讨。