ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于VHDL的数字频率计设计与实现:从测频原理到FPGA板级调试

基于VHDL的数字频率计设计与实现:从测频原理到FPGA板级调试 数字频率计FREQ是高校数字电路课程设计里出现频率最高的题目之一也是不少人第一次真正用 VHDL 在 EDA 工具上完成完整数字系统的入门项目。它的核心逻辑说穿了就一句话在已知闸门时间内数被测信号的脉冲个数算出频率。但实际做起来你大概率会遇到计数器不更新、ModelSim 仿真跑到天荒地老、数码管疯狂闪烁、综合出一堆 latch 警告这些破事。这篇文章把我带实验、带竞赛、自己画板调试过程中积累的一套能直接复现的设计方案和排错思路完整写出来从测频原理到 VHDL 代码从 Quartus II 建工程到板级验证最后再聊聊怎么优化成等精度测频希望能帮你少走几个月的弯路。1. 数字频率计的测频原理与方案选型1.1 从“数脉冲”到“测频率”三种常用测频思路频率的本质是单位时间内周期性信号重复的次数所以测频率最直白的做法就是“数脉冲”。但怎么数、数多久、怎么消除计数误差不同方案差别很大。第一种是测频法也叫计数法。固定一个闸门时间 T比如 1 秒用一个门控信号控制计数器对被测信号计数得到计数值 N那么频率 f N / T。这种方案实现最简单适合测高频信号因为高频下闸门时间内脉冲数多±1 个计数的误差占比很小。但测低频时会很尴尬比如测 1Hz 信号1 秒闸门只能数到 1 个脉冲误差直接到 50% 左右。第二种是测周法。测量输入信号的周期 T然后取倒数得到频率。实现上是用一个高频基准时钟去填充被测信号的一个周期数出基准时钟脉冲个数。这种方案适合低频测量高频时基准时钟填充个数太少误差反而变大。第三种是等精度测频也叫同步闸门测频。它把闸门信号同步到被测信号的上升沿上确保实际闸门时间是被测信号周期的整数倍这样被测信号的 ±1 误差就消除了。基准时钟仍然存在 ±1 误差但因为基准时钟频率很高这个误差非常小。这是目前通用频率计里最主流的方案后面我会专门讲。三种方案对比如下方案实现难度适合频段主要误差来源测频法低高频被测信号±1计数测周法中低频基准时钟±1计数等精度测频中高全频段基准时钟±1计数1.2 为什么课程设计先做测频法再用等精度优化我见过很多同学一上来就想做等精度结果被同步门控逻辑绕晕连基础功能都跑不通。我的建议是分两步走第一版先用测频法把完整链路跑通也就是“分频产生闸门 闸门内计数 下降沿锁存 译码显示”这一整套流程跑通之后再升级成等精度测频。这样每一步的问题都能定位清楚不至于一出错就是一团乱麻。本文的代码和调试记录主要围绕测频法展开最后会给出等精度测频的优化思路。测频法的核心价值在于让你把 VHDL 的时序逻辑、组合逻辑、状态机、信号与变量的区别这些基本功练扎实而这些恰恰是后面做任何复杂数字系统都绕不开的东西。2. 系统架构与关键模块设计2.1 四个模块各司其职整体数据流怎么走一个典型的数字频率计可以分成四个功能模块分频器把开发板自带的 50MHz 系统时钟分频成两路信号一路是 1Hz 左右的闸门信号另一路是数码管动态扫描用的 1kHz 左右刷新时钟。计数器受闸门控制闸门为高电平时对被测信号 freq_in 的上升沿计数闸门下降沿到来时把计数值锁存到内部寄存器然后计数器清零准备下一次测量。译码与显示驱动把锁存下来的二进制计数结果转换成十进制 BCD 码再通过扫描方式点亮数码管。因为系统时钟 50MHz 很快而计数结果在 1 秒内只刷新一次所以必须用锁存器在正确的时间点把数据“抓住”否则显示会闪烁或者干脆看不到稳定数字。顶层模块把上面几个模块通过端口映射连起来作为整个工程的顶层实体对应 FPGA 的顶层设计。数据流是单向的被测信号进计数器计数结果进锁存器锁存值进译码显示。我在带学生做项目时经常强调一点写代码前先把这个数据流图画清楚再动手写 VHDL否则很容易出现端口连错、信号赋值的时序混乱这类低级问题。2.2 闸门时间和计数器位宽怎么定闸门时间直接决定了测频分辨率和计数器位宽这一步需要算清楚不能拍脑袋。假设被测信号最高频率为 1MHz闸门时间 T 1s那 1 秒内最多计数 1,000,000 个脉冲。用二进制表示至少需要 20 位2^20 1,048,576所以计数器设计成 20 位就够用。如果你希望测到 10MHz那 1 秒内最大计数值是 10,000,000需要 24 位2^24 16,777,216。这里有个经验公式计数器位宽 W 要满足 2^W ≥ f_max × T。算完最好留 1~2 位余量因为你不知道闸门信号的占空比、边沿抖动会不会带来额外计数宁可多花几个触发器也别让计数器溢出。闸门时间的选择则要在分辨率和刷新速度之间做权衡。闸门时间越长被测信号的 ±1 计数误差占比越小分辨率越高但显示刷新越慢。1 秒闸门是课程设计最常用的折中方案。如果做竞赛或者实用仪器通常会做自动换挡低频时用较长的闸门高频时用较短的闸门这个放到优化部分细说。2.3 低位宽计数器省资源你的设计目标是什么很多初学者喜欢把计数器位宽压到刚好觉得这样省触发器资源。在 FPGA 上这个想法意义不大——现代 FPGA 的逻辑单元动不动几万个20 位和 24 位计数器占的资源差异几乎可以忽略。真正应该关注的是时序收敛和代码可读性。我见过有人为了省资源把计数结果截成 16 位然后只显示高 6 位结果低频信号显示全是 0自己又排查了半天。这里想提醒你设计目标是什么就按目标的指标来算位宽。课程设计追求的是功能正确、代码清晰、能讲明白原理不是省几个 LE逻辑单元。等以后做产品、做资源受限的小规模 CPLD 方案时再回来抠资源不迟。3. VHDL 代码实现与仿真验证3.1 分频器与闸门控制的 VHDL 写法分频器是所有时序逻辑的基础。下面这个分频器代码把 50MHz 系统时钟分频成 0.5Hz 的方波也就是周期 2 秒、高电平持续 1 秒高电平期间正好作为 1 秒闸门。注意常数 CNT_MAX 的取值50MHz 时钟下从 0 计数到 99,999,999 一共是 100,000,000 个时钟周期即 2 秒输出翻转一次。library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity clk_div is port ( clk_50m : in std_logic; rst_n : in std_logic; gate : out std_logic -- 1s 高电平闸门 ); end clk_div; architecture rtl of clk_div is constant CNT_MAX : integer : 99999999; signal cnt : integer range 0 to CNT_MAX : 0; signal gate_reg : std_logic : 0; begin process (clk_50m, rst_n) begin if rst_n 0 then cnt 0; gate_reg 0; elsif rising_edge(clk_50m) then if cnt CNT_MAX then cnt 0; gate_reg not gate_reg; else cnt cnt 1; end if; end if; end process; gate gate_reg; end rtl;这里有个很容易踩的坑整数类型的计数器最大值是奇数所以用if cnt CNT_MAX判断而不是if cnt CNT_MAX这样更安全也不容易在某些综合工具里产生额外比较器。另外复位用的是异步复位if rst_n 0这在 FPGA 里很常见但如果你在同一个工程里混用异步复位和同步复位Quartus II 会提示复位风格不一致建议统一成一种。3.2 计数器和锁存器的时序细节计数器必须在闸门高电平期间对被测信号计数闸门下降沿到来时锁存并清零。这里的关键是“下降沿锁存”这个动作。library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity counter is port ( clk_50m : in std_logic; -- 系统时钟用于同步检测 gate 边沿 rst_n : in std_logic; gate : in std_logic; -- 1s 闸门 freq_in : in std_logic; -- 被测信号 count : out std_logic_vector(19 downto 0); data_val : out std_logic_vector(19 downto 0) -- 锁存后的稳定计数值 ); end counter; architecture rtl of counter is signal gate_dly1 : std_logic; signal gate_dly2 : std_logic; signal gate_fall : std_logic; signal cnt_reg : std_logic_vector(19 downto 0) : (others 0); signal latch_reg : std_logic_vector(19 downto 0) : (others 0); begin -- 用两级触发器同步 gate消除跨时钟域亚稳态 process (clk_50m, rst_n) begin if rst_n 0 then gate_dly1 0; gate_dly2 0; elsif rising_edge(clk_50m) then gate_dly1 gate; gate_dly2 gate_dly1; end if; end process; gate_fall gate_dly2 and not gate_dly1; -- 闸门高电平期间对 freq_in 计数 process (freq_in, rst_n) begin if rst_n 0 then cnt_reg (others 0); elsif rising_edge(freq_in) then if gate_dly2 1 then cnt_reg cnt_reg 1; end if; end if; end process; -- 闸门下降沿锁存并清零 process (clk_50m, rst_n) begin if rst_n 0 then latch_reg (others 0); elsif rising_edge(clk_50m) then if gate_fall 1 then latch_reg cnt_reg; end if; end if; end process; count cnt_reg; data_val latch_reg; end rtl;这段代码里有三个值得细讲的点。第一为什么计数进程用freq_in做时钟而不是用系统时钟去采样freq_in因为被测信号频率相对系统时钟可能很低如果用系统时钟采样被测信号很容易丢掉高频脉冲。直接用freq_in作为计数时钟每个上升沿都能被捕捉到这在 FPGA 中是“异步计数器”的典型用法。代价是cnt_reg时钟域与被测信号有关后续读它的时候要小心。第二为什么 gate 要先打两拍因为 gate 来自系统时钟域而计数器在freq_in时钟域这是跨时钟域传递。如果不做同步处理gate 的边沿可能刚好落在被测信号时钟的建立保持时间窗口内导致计数使能条件判断出错。虽然 1 秒闸门很慢工程上直接采大概率也不出事但规范写法一定是打两拍。第三锁存用gate_fall信号它是在系统时钟域下检测 gate 下降沿得到的单周期脉冲。这样锁存动作只发生一次而不是持续整个低电平阶段。如果你写成if gate 0才锁存那在低电平的每一拍都会不停把当前计数器的值写进锁存器而计数器此时还没清零最终看到的结果会非常混乱。3.3 二进制转 BCD 与数码管动态扫描锁存下来的计数值是 20 位二进制数如果直接用二进制控制数码管需要转换十进制显示。最直观的方式是用整数运算因为最大计数值 1,048,575 完全在 VHDL 整数的范围内而且显示刷新频率很低综合工具不会在这里形成性能瓶颈。signal data_int : integer range 0 to 1048575; signal bcd0, bcd1, bcd2, bcd3, bcd4, bcd5 : integer range 0 to 9; bcd0 data_int mod 10; bcd1 (data_int / 10) mod 10; bcd2 (data_int / 100) mod 10; bcd3 (data_int / 1000) mod 10; bcd4 (data_int / 10000) mod 10; bcd5 (data_int / 100000) mod 10;然后通过 1kHz 扫描时钟选中当前要显示的位再根据 BCD 值查七段码表。共阴数码管的七段码对应关系如下数字a b c d e f g十六进制01 1 1 1 1 1 00x3F10 1 1 0 0 0 00x0621 1 0 1 1 0 10x5B31 1 1 1 0 0 10x4F40 1 1 0 0 1 10x6651 0 1 1 0 1 10x6D61 0 1 1 1 1 10x7D71 1 1 0 0 0 00x0781 1 1 1 1 1 10x7F91 1 1 1 0 1 10x6F动态扫描的核心是“人眼余晖”每一位数码管轮流点亮 1ms 左右循环刷新频率达到 1kHz人眼看起来就是六位数字同时稳定显示。扫描频率太高比如 100kHz会导致位选切换和段选驱动跟不上产生拖影太低比如 50Hz又会看到明显的闪烁。1kHz 是一个经过大量实践验证的安全值直接从 50MHz 分频得到即可。3.4 在 Quartus II 里建工程、写约束、跑 ModelSim 仿真环境准备这里只多说一句Quartus II 和 ModelSim 建议用同一 major 版本比如 Quartus II 13.0 配 ModelSim-Altera 13.0版本混搭容易在生成仿真库时出幺蛾子。安装好带授权版本之后用 Modelsim-Altera 而非独立版 ModelSim可以省去手动编译 altera 仿真库的麻烦。建工程时注意三点器件型号选择要和你的开发板一致比如 Cyclone IV E 系列 EP4CE10F17C8。选错器件后综合、适配都能过但引脚分配和时序约束会乱套。顶层实体名要和顶层文件名一致VHDL 对大小写不敏感但文件命名最好统一小写避免在 Linux 主机上编译时出问题。不要把所有模块都写在一个文件里。按功能拆成 clk_div.vhd、counter.vhd、display.vhd、top.vhd 四个文件后期调试和重用的成本低很多。写 testbench 是验证时序的关键环节。我强烈建议你在仿真时把分频参数改小否则 2 秒的闸门在 ModelSim 里要跑 100,000,000 个 50MHz 时钟周期虽然现代电脑仿真几十秒能跑完但每次改完代码都等这么久效率太低了。常见做法是单独用一个仿真专用顶层把闸门时间压缩成 10us 甚至 1us。-- testbench 片段产生 50MHz 系统时钟和 1kHz 被测信号 library ieee; use ieee.std_logic_1164.all; entity tb_freq is end tb_freq; architecture sim of tb_freq is signal clk_50m : std_logic : 0; signal rst_n : std_logic : 0; signal freq_in : std_logic : 0; signal gate : std_logic; begin clk_50m not clk_50m after 10 ns; -- 50MHz freq_in not freq_in after 500 us; -- 1kHz process begin rst_n 0; wait for 100 ns; rst_n 1; wait; end process; -- 这里不例化真实分频器直接用进程模拟 10us 高电平闸门加快仿真 process begin gate 0; wait for 1 us; gate 1; wait for 10 us; gate 0; wait; end process; end sim;仿真的目的是看三个关键信号gate 的边沿是否干净、cnt_reg 是否在 gate 高电平期间递增、latch_reg 是否在 gate 下降沿之后锁存了正确的值。如果这三个都对了再跑真实分频的长时间仿真确认显示逻辑就基本稳了。4. 板级调试中的常见问题与优化心得4.1 计数器乱跳、显示乱码先查异步信号和复位我在实验室见过最多的问题是把设计下载到板子上后数码管显示的数字毫无规律地跳动或者按下复位键之后显示仍然不对。这种问题 80% 出在异步信号没有同步处理上。一个典型场景是学生在计数器进程里用了freq_in作为时钟同时又用系统时钟的进程去读取cnt_reg做锁存。两个进程的时钟域不同锁存时cnt_reg可能刚好在翻转中间读到的就是亚稳态产生的不稳定值。解决办法就是我在代码里展示的两级同步链。另外如果开发板上的按键没有硬件消抖不要直接作为复位信号使用。我的做法是复位信号先用一个 20ms 的消抖模块处理再把消抖后的信号连到所有模块的rst_n。否则你按一次复位在 20ms 内可能产生几十次有效沿时序很容易错乱。还有一个小经验用 SignalTap II 在线调试时不要把freq_in直接接到逻辑分析仪的时钟上否则高频信号会把 SignalTap 的采样缓冲区灌爆。可以把freq_in分频或测量一段时间的脉冲数再抓拍。4.2 数码管闪烁、亮度不均怎么处理数码管闪烁往往不是代码逻辑错而是扫描时序不合理。最常见的问题是扫描时钟频率过低比如把 50MHz 分频成 100Hz 去扫描每一位点亮 10ms人眼就能明显看到数字在一跳一跳。另一个隐蔽问题是位选信号和段选信号不同步。如果位选切换和段选赋值用了不同的进程且中间存在组合逻辑延迟切换瞬间可能会短暂点亮前一位数字的段码看起来就是“拖影”。解决办法是把位选和段选放在同一个process里保证同一个时钟沿同时更新。还有一种情况是亮度不均。多位数码管动态扫描时如果某一位需要显示的字符笔画数明显多比如 8 比 1 的笔画多很多它的平均亮度会略微偏低。严格一点的驱动会在每个显示位做占空比补偿也就是笔画多的位点亮时间稍微长一点。但课程设计一般不用这么较真扫描频率上到 1.5kHz~2kHz肉眼几乎看不出差异。4.3 综合出现 latch 警告怎么定位和修改Quartus II 在 Analysis Synthesis 完成后如果代码里出现了条件不完整的分支会报告Inferring latch警告。很多人看到这个警告不以为然其实 latch 在 FPGA 中很容易导致时序问题因为它是电平敏感存储不是时钟边沿存储综合工具对它的优化策略也相对保守。常见的产生原因是在process中使用if但缺少else并且在条件为假时没有保持信号值。比如process (sel) begin if sel 00 then data d0; elsif sel 01 then data d1; end if; end process;这段代码在sel为其他值时data要保持之前的值综合工具就会推断出一个 latch。修改方法是补上else分支或者用一个默认赋值。如果确实需要锁存功能你应该明确使用时钟驱动的 reg 变量而不是隐式 latch。我在实际项目中处理综合警告的顺序是这样的先看 Error 和 Critical Warning再看数量较多的 Warninglatch 警告必须清零因为它是代码风格问题的直接反映。至于 PLL 未使用缓冲、引脚分配警告这类确认无影响后可以手动过滤掉。4.4 资源占用和布局布线优化当你的设计做完一轮基本功能后可以在 Quartus II 的 Compilation Report 里看资源占用。一个 6 位数码管 20 位计数器的频率计在 Cyclone IV 上通常也就是几百个逻辑单元完全不会成为问题。我见过有人在这么小的设计里去“优化”资源纯属浪费时间。真正值得关注的优化方向是时序收敛。如果你的设计要跑更高频率可以在 Assignment Editor 里给系统时钟添加约束比如create_clock -period 20.000 -name clk_50m [get_nets {clk_50m}]。有了时钟约束Quartus II 的 Fitter 才会认真去做布局布线优化否则它默认时钟是没有限制的布线很随意跑到高频时可能时序违例。另外如果你的板子上有 PLL 资源建议把 50MHz 输入时钟先经过 PLL 倍频或分频再供给内部逻辑。PLL 输出的时钟抖动和偏斜都更小对计数器这种对边沿敏感的设计更友好。5. 从课程设计到工程应用还能怎么升级5.1 从测频法升级到等精度测频基础版频率计做完之后我很建议你花点时间把它升级成等精度测频。这是面试和竞赛里非常加分的点也是从“会写代码”到“懂设计”的分水岭。等精度测频的核心是不直接用固定闸门去数被测信号而是先用被测信号对闸门做同步。具体做法是当外部闸门信号gate_in到达 FPGA 时等被测信号freq_in的下一个上升沿到来才真正打开两个计数器当gate_in结束时同样等到freq_in的上升沿才同时关闭两个计数器。这样实际闸门时间是“被测信号周期的整数倍”被测信号的 ±1 误差被彻底消除。此时频率计算公式变成freq_in N_freq / N_base × f_base其中N_freq是被测信号计数N_base是基准时钟计数f_base是基准时钟频率。由于基准时钟通常 50MHz 甚至更高它的 ±1 误差相对很小整个频率计的精度在不同频段上非常稳定。实现等精度需要两个计数器一个对被测信号计数一个对基准时钟计数。并且要在同一个被同步的门控信号控制下启停时序要求比基础版高不少。建议你用状态机来实现门控同步别再用简单的组合逻辑拼否则会引入毛刺。5.2 自动量程、串口回传和更多实用功能基础频率计只能测一个量程范围而实用仪器需要自动量程。思路是根据上一次测量的结果动态选择闸门时间计数结果偏小比如小于 100就把闸门时间增加一个档位计数结果接近满量程就缩短闸门。这样低频信号也能获得足够的分辨率高频信号又能快速刷新。如果想和上位机联调可以加一个 UART 发送模块把测量结果通过串口回传到 PC 端用串口助手直接查看频率值。这样一来频率计就从数码管显示的小工具变成了一个可远程监测的数据采集前端。UART 模块的波特率可以和系统时钟做分频工程上非常成熟也有大量现成的 VHDL 参考代码。做实物时我还推荐把设计迁移到带 MCU 的 SoC FPGA 上用 MCU 做数据解释和显示交互FPGA 只负责高速计数和门控这样性能和人机交互两不误。不过这种方案就超出本文“入门到进阶”的范畴了适合你做完频率计后作为下一个项目去挑战。5.3 从 FPGA 原型验证到独立仪器用 EDA 工具做板级设计如果你想把频率计从“开发板上跑通的工程”变成“一台独立的小仪器”那就需要设计 PCB 了。FPGA 最小系统板 数码管驱动 信号调理输入这几部分集成到一块板上。画板工具我比较推荐嘉立创 EDA免费、上手快中文资料多元件库也全画完可以直接导出 Gerber 打样。配合它的一键下单和在线元器件库从原理图到拿到板子通常一周内就能完成。画这类板子要特别注意三个点第一FPGA 的 IO 引脚分配要和原理图一致一定要在 Quartus II 的 Pin Planner 里核对别以为 EDA 软件里连的就是板上连的第二被测信号输入需要做阻抗匹配和电平调理不能直接把外部信号接到 FPGA 引脚上至少要有保护电阻和钳位二极管第三动态扫描电路对布局布线不敏感但电源和地要铺铜处理好减少数字噪声串扰到输入通道。我在实际做这个小仪器时最深的体会是软件仿真做得再漂亮都不如亲自把板子点亮、接上信号发生器、调几个频点来得踏实。很多问题是仿真环境里根本不会出现的比如信号源内阻导致的小幅值信号计数不稳定、长走线带来的反射、电源纹波对计数器的干扰等等。做完这一轮你对数字系统的理解会上一个台阶。5.4 仿真指标怎么设计才更有说服力如果你的频率计是用于课程答辩或者竞赛一定要提前设计好验证指标千万别在现场才临时接信号源乱调。我的习惯是准备一组有代表性的频点低频 1Hz、10Hz、100Hz中频 1kHz、10kHz高频 1MHz在计数器位宽允许范围内分别记录数码管或串口显示的测量值和信号源实际值的误差做一个表格。误差分析要能说清楚误差主要来自哪里是 ±1 计数误差还是基准时钟精度还是信号源本身就不准。如果你是升级成等精度测频的版本还可以展示同样频点下的误差对比这样才能体现出优化的价值。另外用示波器拍几组 gate 信号和实测频率的截图放到答辩 PPT 里比文字描述有说服力得多。根据我个人的经验数字频率计这个题目看着简单但把测频原理、VHDL 编码规范、时序约束、板级调试、优化迭代这一整套走完你的数字系统设计能力会比只做流水灯、跑马灯强出好几个档次。最后再分享一个小技巧遇到显示不对先不要反复改代码把 SignalTap 打开抓一下锁存后的latch_reg和原始cnt_reg对比一下就知道了省下的时间足够你再调三个模块。
返回列表