简介:面向FPGA数字系统学习者与电子设计竞赛开发者,这份资料围绕Quartus 13.1和Verilog实现等精度数字频率计,覆盖1Hz至150MHz宽频段测量、分频与同步计数、误差校正、串口输出和Modelsim仿真验证等关键环节,可用于课程设计、项目复现和精度方案对照,适合从中级实验向完整工程过渡。压缩包共390个文件,17.6MB,包含Verilog源码、TDF设计文件、CDB/HDB编译数据库、仿真综合报告、工程配置及说明文档等,完整保留Quartus工程结构与中间产物,方便直接打开分析或二次修改。已有1412人学习下载。对于希望理解等精度测量原理、梳理高频计数时序、核对计数器位宽与闸门时间、排查仿真与综合问题的人而言,这套工程能提供从源码到报告的实际参考,适合作为FPGA频率测量项目的起步模板与二次开发基础。
1. 一块入门开发板上的频率计:等精度到底解决了什么
手里一块入门 FPGA 开发板,想测 1 Hz 到 50 MHz 的信号,多数人第一反应是开一个 1 秒闸门去数被测脉冲,这就是直接测频法。它在高频段好用,可信号掉到几十赫兹,1 秒里只剩几十个脉冲,±1 个计数误差被放大成百分之几,读数在 49 和 51 之间反复横跳。等精度数字频率计换了个思路:用被测信号去同步闸门,让闸门长度始终是被测周期的整数倍,误差只由标准时钟和闸门时间决定,跟被测频率无关。低频不用等十秒闸门也能读得准,范围广是它最实在的收益。这套东西用 Quartus II 13.1 加 Verilog 就能在一块的入门开发板上跑起来,下面把数学、模块、约束和踩过的坑一次拆开。
2. 等精度测量的数学底子:闸门怎么同步、误差怎么算
2.1 直接测频法在低频段为什么会崩
直接测频法的结构简单到不能再简单:开一个固定长度的时间窗 T,窗内数被测脉冲个数 N,频率就是 f = N / T。问题全出在那个 N 上——闸门的开和关跟被测信号毫无关系,窗的边界大概率落在两个被测脉冲中间,最后那个不完整的脉冲要么算进去要么漏掉,这就是 ±1 个计数的量化误差。
误差的绝对值永远是 1 个计数,但相对误差是 1/N。N 越大相对误差越小,N 越小相对误差越离谱。列个表看得更清楚,假设闸门都是 1 秒:
| 被测频率 | 1 s 闸门内的计数值 N | ±1 计数对应的相对误差 |
|---|---|---|
| 50 MHz | 50 000 000 | 2×10⁻⁸ |
| 1 MHz | 1 000 000 | 1×10⁻⁶ |
| 1 kHz | 1 000 | 1×10⁻³ |
| 100 Hz | 100 | 1×10⁻² |
| 10 Hz | 10 | 10% |
10 Hz 的时候误差 10%,这个读数没有任何意义。想把低频测准只能拉长闸门,测 1 Hz 要 1% 精度就得开 100 秒闸门,刷新率掉到 0.01 Hz,人眼看着就是一串不动的数字,完全没法调机。
传统方案里还有一种测周期法:低频信号测它的周期,用标准时钟去数一个被测周期里有多少个标准脉冲,T_x = N × T_s。这个方法低频精度高,但频率一高 N 就变小,误差又反向恶化。于是老仪表都是分段的——高频走测频、低频走测周,中间设一个切换点,切换点附近两种方法的误差曲线交叉,读数容易在两种算法之间打架。等精度法要干的事情,就是把这两条曲线压成一条平的。
2.2 多周期同步法里的三个时间量
等精度测量的学名叫多周期同步测量,核心动作只有一个:让闸门的边界去对齐被测信号的边沿。展开来说涉及三个时间量,这三个量搞混了后面代码一定写错。
第一个是预置闸门 T_pr,由板载标准时钟分频得到,可以是 1 s、0.1 s、10 ms、1 ms,它只是一个「意向」,表示「我打算测这么长」。
第二个是实际闸门 T_act,这是真正生效的测量窗口。它的起点不是预置闸门的上升沿,而是预置闸门变高之后到来的第一个被测信号上升沿;终点也不是预置闸门的下降沿,而是预置闸门变低之后到来的第一个被测信号上升沿。这样一来,T_act 里包含的被测周期数必然是整数个,记作 N_x。
第三个是标准时钟在这个实际闸门里数出来的个数 N_s。它跟着 T_act 走,闸门开它就数,闸门关它就停。
三者满足两个关系式:T_act = N_x × T_x,同时 T_act = N_s × T_s。把 T_act 消掉,得到
f_x = f_s × N_x / N_s
这个式子是整套设计的落脚点。注意 N_x 在这里是精确的整数,闸门的两端都卡在被测信号的上升沿上,不存在「半个脉冲」的问题,所以 N_x 没有 ±1 误差。误差全部跑到 N_s 身上了——标准时钟和闸门边沿之间仍然是异步的,开门的那个标准时钟沿和关门的那个标准时钟沿都可能差一拍。
提示:实际闸门一定比预置闸门长,最长会多出接近一个被测周期。1 Hz 信号配 1 s 预置闸门时,实际闸门可能到 1.999 s。做资源估算和刷新率估算时要按最坏情况留余量。
2.3 误差公式决定闸门时间怎么选
从 f_x = f_s × N_x / N_s 出发做误差传递,N_x 无误差,f_s 由晶振决定,唯一有量化误差的是 N_s:
δ = Δf_x / f_x = ±1 / N_s = ±1 / (f_s × T_act) ≈ ±1 / (f_s × T_pr)
这个式子里没有 f_x。这就是「等精度」三个字的全部含义:无论测 1 Hz 还是 50 MHz,相对误差都是同一个数,只跟标准时钟频率和闸门时间有关。
拿 50 MHz 标准时钟、1 s 闸门算:N_s ≈ 5×10⁷,δ ≈ 2×10⁻⁸。测 10 Hz 时 N_x = 10,f_x = 50 000 000 × 10 / 50 000 000 = 10 Hz,误差 2×10⁻⁷ Hz。对比一下 2.1 节里直接测频法那 10% 的误差,差距是六个数量级。
但闸门时间不是越长越好,它直接换算成刷新率:
| 预置闸门 | N_s @ 50 MHz | 量化误差 | 理论刷新率 | 典型用途 |
|---|---|---|---|---|
| 1 s | 5×10⁷ | 2×10⁻⁸ | 约 0.5 Hz | 校准、计量 |
| 0.1 s | 5×10⁶ | 2×10⁻⁷ | 约 5 Hz | 通用测量 |
| 10 ms | 5×10⁵ | 2×10⁻⁶ | 约 50 Hz | 观察漂移 |
| 1 ms | 5×10⁴ | 2×10⁻⁵ | 约 500 Hz | 只看高频 |
刷新率那一列之所以不是 1/T_pr,就是因为实际闸门会被拉长到接近两个 T_pr,取个保守值更接近真实观感。
这里有个血泪经验必须提前说:公式算出来的 2×10⁻⁸ 是「相对量化误差」,不是「整机精度」。整机精度最后被板载晶振的准确度卡死,普通开发板上的 50 MHz 有源晶振温漂加初始偏差通常在 ±20 ppm 到 ±50 ppm 之间,比量化误差大三个数量级。所以别指望换个更长的闸门能把读数变准,那个天花板在晶振那里。后面第 6 章会用三点校准把这个固定偏差修掉一部分。
2.4 等精度法的适用边界:被测信号得能当时钟用
这套架构有个容易被忽略的前提:被测信号要直接接到 FPGA 的时钟输入上,当成时钟来驱动计数器。这意味着两件事。
第一,被测信号必须是已经整形好的方波,边沿要干净。拿一个正弦波直接怼进时钟引脚,计数器会在过零点附近疯狂误触发,读数偏大好几倍。前面必须有一级比较器或者施密特触发器,把正弦、三角、甚至带噪声的信号整成陡峭的方波,这是模拟前端的事,FPGA 管不了。
第二,被测频率不能超过器件的时钟接收能力和计数器本身的 Fmax。入门级 Cyclone IV 的全局时钟网络跑到 200 MHz 以上没问题,但计数器是 32 位宽、带进位链的,综合出来的 Fmax 会打折;再加上输入引脚到全局时钟的布线延迟,实际能可靠计数的上限通常在 100 MHz 到 150 MHz 之间。想要更高的量程,要么换更快的外接时钟缓冲芯片,要么先做一级前置分频再乘回去,代价是低频端的分辨率变差。
所以「范围广」是有边界的:下限取决于闸门时间你愿意等多久,上限取决于被测信号能不能被当成时钟用。把这两个边界写进指标里,比空喊宽量程靠谱得多。
3. Verilog 模块拆解:双计数器、同步闸门与除法器怎么接
3.1 顶层结构与两个时钟域
整个设计只有两个时钟:板载 50 MHz 的 clk_ref,和被测信号 sig_in。这个划分必须在动笔写代码之前就定死,因为跨时钟域的信号一旦漏掉同步,仿真可能完全正常,上板就是随机跳数。
- clk_ref 域:预置闸门产生、标准计数器、除法运算、BCD 转换、数码管扫描。
- sig_in 域:闸门同步、被测计数器。
- 跨域信号一共两个:闸门信号从 sig_in 域回传到 clk_ref 域,被测计数值 nx 从 sig_in 域送到 clk_ref 域做除法。
顶层端口如下,位数按 8 位数码管设计:
module freq_meter ( input wire clk_ref, // 50 MHz 板载晶振 input wire rst_n, // 低有效复位 input wire sig_in, // 被测信号,需外部整形为方波 input wire [1:0] range_sel, // 拨码选择预置闸门 output wire [7:0] seg, // 段选 a~g + dp,低有效 output wire [7:0] dig // 位选,低有效 );被测计数值在位宽上要留够。1 s 闸门、50 MHz 输入时 N_x 最大 5×10⁷,需要 26 位,取 32 位留余量。N_s 同理,32 位足够。
3.2 预置闸门与同步闸门的生成
预置闸门就是一个计数器加一个翻转位,四个档位对应四个计数值,翻转出来的是 50% 占空比方波,高电平那段时间就是预置闸门。
// 预置闸门产生:50 MHz 分频出 1s / 0.1s / 10ms / 1ms 的方波 module gate_gen ( input wire clk_ref, input wire rst_n, input wire [1:0] range_sel, output reg gate_pre ); reg [31:0] cnt; reg [31:0] target; // 组合逻辑选档,避免在时钟沿里做多路选择 always @(*) begin case (range_sel) 2'b00: target = 32'd24_999_999; // 半周期 0.5 s -> 高电平 1 s 2'b01: target = 32'd2_499_999; // 高电平 0.1 s 2'b10: target = 32'd249_999; // 高电平 10 ms default: target = 32'd24_999; // 高电平 1 ms end end always @(posedge clk_ref or negedge rst_n) begin if (!rst_n) begin cnt <= 32'd0; gate_pre <= 1'b0; end else if (cnt >= target) begin cnt <= 32'd0; gate_pre <= ~gate_pre; // 翻转得到等宽方波 end else begin cnt <= cnt + 1'b1; end end endmodule这里的 target 是半个周期的计数,因为翻转一次才是一个半周期,1 s 高电平对应 0.5 s 的半周期,50 MHz 下就是 25 000 000 个时钟,减一是因为从 0 开始计。参数改动只动这一个表就行,别在别处散落魔数。
同步闸门在被测域生成,做三件事:把 gate_pre 打两拍消亚稳态、用打拍后的结果当实际闸门、从实际闸门的下降沿提取一个锁存脉冲。
// 实际闸门:与 sig_in 上升沿对齐,并产生下降沿锁存脉冲 module gate_sync ( input wire clk_meas, // 被测信号 input wire gate_pre, // 来自 clk_ref 域的预置闸门 output wire gate_act, // 实际闸门,供被测计数器使用 output wire latch_pulse // 闸门关闭时的一拍脉冲 ); reg s1, s2, s3; always @(posedge clk_meas) begin s1 <= gate_pre; // 第一级:可能亚稳态 s2 <= s1; // 第二级:大概率已恢复 s3 <= s2; // 第三级:仅用于取边沿 end assign gate_act = s2; assign latch_pulse = s2 & ~s3; // s2 的下降沿 endmodules2 的上升沿恰好是 gate_pre 变高之后的第一个被测上升沿,s2 的下降沿恰好是 gate_pre 变低之后的第一个被测上升沿,正好满足 2.2 节对实际闸门的定义。第三级 s3 只为了差分出边沿,不参与计数。
注意:gate_pre 是在 clk_ref 域产生的,宽度是整秒级;sig_in 频率最低可能只有 1 Hz。这种「慢信号采快信号」的跨域方向和「快采慢」不同,两级同步在绝大多数情况下够用,但如果你的信号源抖动很大,建议加到三级。
3.3 双计数器、锁存与除法
两个计数器分属两个时钟域,各自在自己的域里清零和累加。
// 被测域计数:闸门打开期间累加被测脉冲 module cnt_meas ( input wire clk_meas, input wire rst_n, input wire gate_act, output reg [31:0] nx ); always @(posedge clk_meas or negedge rst_n) begin if (!rst_n) nx <= 32'd0; else if (gate_act) nx <= nx + 1'b1; // 只在闸门内计数 else nx <= 32'd0; // 闸门关闭即清零,为下一轮准备 end endmodule标准计数器在 clk_ref 域,它的使能信号 gate_act 是跨域过来的,必须先在 clk_ref 域打两拍再用,不能直接接进敏感逻辑。
// 标准域计数 + 锁存:闸门在 clk_ref 域同步后驱动计数 module cnt_ref ( input wire clk_ref, input wire rst_n, input wire gate_act, // 来自 sig_in 域 output reg [31:0] ns, output reg ns_valid // 一轮计数完成,可以启动除法 ); reg g1, g2, g3; always @(posedge clk_ref or negedge rst_n) begin if (!rst_n) begin g1 <= 1'b0; g2 <= 1'b0; g3 <= 1'b0; ns <= 32'd0; ns_valid <= 1'b0; end else begin g1 <= gate_act; g2 <= g1; g3 <= g2; ns_valid <= g2 & ~g3; // 同步后闸门的下降沿 if (g2) ns <= ns + 1'b1; else ns <= 32'd0; end end endmodulens_valid 拉高的那一拍,ns 的值就是本轮的标准计数。nx 此时已经停止累加,数值固定在 cnt_meas 的输出上。因为闸门关闭到下一次打开至少隔了半个预置闸门周期(1 ms 档也有 500 µs),nx 在这段时间里完全不变,直接从 sig_in 域搬到 clk_ref 域做运算即可,不需要握手。这一点是整个设计里最省事、也最容易被人质疑的地方——它不是教科书式的严谨跨域,但在闸门周期的量级上足够安全。要更保险就加一级 nx_valid 握手,代价是多几个状态。
除法用参数化除法器,不要指望综合器把(nx * 50_000_000) / ns这种写法自动优化成合理电路。被除数 64 位、除数 32 位,直接组合除会吃掉几百个 LUT 且时序一塌糊涂。
// 除法核心:f_x = f_s * nx / ns,f_s = 50_000_000 // numer 用常数乘法,综合器会拆成移位加法;divide 由 MegaWizard 生成 module freq_calc ( input wire clk_ref, input wire rst_n, input wire ns_valid, // 一轮结束,启动计算 input wire [31:0] nx, input wire [31:0] ns, output reg [31:0] freq_bin, // 单位 Hz output reg freq_valid ); reg [63:0] numer; reg [31:0] denom; reg start; wire [63:0] quotient; wire div_done; always @(posedge clk_ref or negedge rst_n) begin if (!rst_n) begin numer <= 64'd0; denom <= 32'd1; start <= 1'b0; freq_bin <= 32'd0; freq_valid <= 1'b0; end else begin start <= 1'b0; freq_valid <= div_done; if (ns_valid) begin // 常数乘法,50_000_000 由综合器优化 numer <= nx * 64'd50_000_000; denom <= (ns == 32'd0) ? 32'd1 : ns; // 防除零 start <= 1'b1; end if (div_done) freq_bin <= quotient[31:0]; end end // 由 MegaWizard 生成的 LPM_DIVIDE,流水线 33 级,latency 约 33 拍 lpm_divide #( .lpm_widthn(64), .lpm_widthd(32), .lpm_nrepresentation("UNSIGNED"), .lpm_drepresentation("UNSIGNED"), .lpm_pipeline(33) ) u_div ( .clock(clk_ref), .clken(1'b1), .aclr(!rst_n), .numer(numer), .denom(denom), .quotient(quotient), .remain(/* 悬空 */) ); // div_done 可由 start 延迟 pipeline 拍数得到,也可用 lpm_divide 的 ready 引脚 reg [5:0] pipe; always @(posedge clk_ref or negedge rst_n) if (!rst_n) pipe <= 6'd0; else pipe <= {pipe[4:0], start}; assign div_done = pipe[5]; endmodule几个参数要盯住:lpm_pipeline设成 33 表示流水线深度,越深 Fmax 越高,但结果延迟越大;numer是 64 位,因为 50 MHz × 5×10⁷ 已经到 2.5×10¹⁵,32 位装不下;denom做了除零保护,否则上电瞬间 ns=0 会得到全 X,那个 X 会顺着流水线污染后面所有输出。
除法结果只取整数赫兹,小数部分要靠第 6 章的显示处理补上。
3.4 二进制转 BCD 与数码管动态扫描
频率值最大 50 000 000,用 8 位数码管显示到 1 Hz 就够。二进制转 BCD 用 Double Dabble,思路是从高位往低位逐位塞,每塞一位之前检查每个 BCD 位是否大于 4,大于就加 3。
// 32 位二进制转 10 位 BCD,纯组合。资源紧张时改成 32 拍状态机 function [39:0] bin2bcd; input [31:0] bin; integer i; reg [39:0] s; begin s = 40'd0; for (i = 31; i >= 0; i = i - 1) begin if (s[3:0] > 4'd4) s[3:0] = s[3:0] + 4'd3; if (s[7:4] > 4'd4) s[7:4] = s[7:4] + 4'd3; if (s[11:8] > 4'd4) s[11:8] = s[11:8] + 4'd3; if (s[15:12] > 4'd4) s[15:12] = s[15:12] + 4'd3; if (s[19:16] > 4'd4) s[19:16] = s[19:16] + 4'd3; if (s[23:20] > 4'd4) s[23:20] = s[23:20] + 4'd3; if (s[27:24] > 4'd4) s[27:24] = s[27:24] + 4'd3; if (s[31:28] > 4'd4) s[31:28] = s[31:28] + 4'd3; if (s[35:32] > 4'd4) s[35:32] = s[35:32] + 4'd3; if (s[39:36] > 4'd4) s[39:36] = s[39:36] + 4'd3; s = {s[38:0], bin[i]}; end bin2bcd = s; end endfunction这段是 32 次迭代全展开的组合逻辑,综合下来占的 LUT 不少,但只在 freq_valid 之后算一次,速度不敏感。如果器件资源吃紧,把 for 循环改成状态机分 32 拍跑,或者干脆把输入位宽缩到 26 位。
扫描部分用 1 kHz 左右的刷新率轮流点亮 8 位数码管,靠视觉暂留看起来是同时亮的。
// 8 位数码管动态扫描,每位点亮 1 ms module seg_scan ( input wire clk_ref, input wire rst_n, input wire [39:0] bcd, // 10 位 BCD,只用低 8 位 input wire dp_en, // 小数点使能 output reg [7:0] seg, // 段选,低有效 output reg [7:0] dig // 位选,低有效 ); reg [15:0] div_cnt; reg [2:0] pos; reg [3:0] cur; always @(posedge clk_ref or negedge rst_n) begin if (!rst_n) begin div_cnt <= 16'd0; pos <= 3'd0; end else begin div_cnt <= div_cnt + 1'b1; if (div_cnt == 16'd49_999) begin // 1 ms 换一位 div_cnt <= 16'd0; pos <= pos + 1'b1; end end end // 按位取出当前应显示的 BCD 数字 always @(*) begin case (pos) 3'd0: cur = bcd[3:0]; 3'd1: cur = bcd[7:4]; 3'd2: cur = bcd[11:8]; 3'd3: cur = bcd[15:12]; 3'd4: cur = bcd[19:16]; 3'd5: cur = bcd[23:20]; 3'd6: cur = bcd[27:24]; default: cur = bcd[31:28]; endcase end always @(*) begin dig = ~(8'b0000_0001 << pos); // 只点亮一位 case (cur) 4'd0: seg = 8'hC0; 4'd1: seg = 8'hF9; 4'd2: seg = 8'hA4; 4'd3: seg = 8'hB0; 4'd4: seg = 8'h99; 4'd5: seg = 8'h92; 4'd6: seg = 8'h82; 4'd7: seg = 8'hF8; 4'd8: seg = 8'h80; 4'd9: seg = 8'h90; default: seg = 8'hFF; // 灭 endcase if (dp_en && pos == 3'd4) seg[7] = 1'b0; // 第 5 位加小数点 end endmodule50 000 个时钟换一位,8 位一轮是 8 ms,刷新率 125 Hz,肉眼看不到闪烁也不会拖影。段码表是共阳极接法的低有效编码,接共阴极屏要把整张表取反,这个在引脚分配阶段就要跟板子对上,接反了看起来像全亮或全灭。
4. Quartus II 13.1 工程落地:建工程、约束、编译、下载
4.1 新建工程与器件配置
13.1 这个版本的界面和后续版本差别不小,步骤按它的菜单走。File → New Project Wizard,工程路径不要带中文和空格,这一点每年都有人栽。器件页选板子上实际那颗,入门板常见的是 Cyclone IV 系列的 EP4CE6 或 EP4CE15,封装和速度等级必须跟板子一致,选错了编译能过但引脚对不上。
文件添加页把 freq_meter.v、gate_gen.v、gate_sync.v、cnt_meas.v、cnt_ref.v、freq_calc.v、seg_scan.v 全部加进来,顶层实体设为 freq_meter。这一步漏文件是最常见的低级错误,综合时报「找不到模块 xxx」回头补加就行,但补加之后要重新跑一次 Analysis & Synthesis,不能只跑全编译。
Device and Pin Options 里有两个必改项。第一项是把双用途引脚设成 Regular I/O,否则某些配置引脚会被保留给配置功能,Pin Planner 里直接不出现。第二项是未用引脚设为 As input tri-stated,避免它们被驱动成输出之后跟外部电路打架。
工程建完之后先别急着写满所有模块,建议先放一个只驱动数码管的骨架跑通全编译和下载,确认工具链和板子的基本通路没问题,再往里填逻辑。这个习惯能省掉后来「到底是代码错还是下载线错」的扯皮。
4.2 引脚分配:哪些脚能当被测信号输入
引脚分配是这类设计翻车率最高的一环,因为它的错误在编译阶段完全静默,只有上板才暴露。分配之前先把板子的原理图摊开,确认三件事:晶振接在哪个专用时钟引脚、数码管的段选位选怎么接、被测信号从哪个脚进来。
| 信号 | 建议引脚类型 | 说明 |
|---|---|---|
| clk_ref | 专用时钟输入脚 | 必须走全局时钟网络,不能随便挑普通 I/O |
| sig_in | 专用时钟输入脚 | 作为时钟使用,走全局时钟网络延迟最小 |
| rst_n | 普通 I/O | 按键输入,建议外部加 RC 或内部做消抖 |
| range_sel | 普通 I/O | 拨码开关,注意上下拉 |
| seg / dig | 普通 I/O | 数量多,优先放在同一个 bank 便于布线 |
sig_in 一定要挑专用时钟脚(CLK0/CLK1 之类),这是硬性要求。把它接到普通 I/O 再去驱动计数器,Quartus 会报一条「No dedicated clock pin」的 warning,很多人扫一眼就过了。后果是这根线的布线延迟大、抖动大,低频时看起来还行,频率一上到几兆就开始丢数,读数偏小而且随温度变。两个专用时钟脚在板子上通常是留给晶振和外部时钟输入的,正好一个给 50 MHz 晶振、一个给被测信号。
分配方式有两种:Pin Planner 图形界面逐个填,或者直接写一个 qsf 片段。后者可读性好、便于版本管理,我一般用后者:
# freq_meter.qsf 片段,引脚号需按实际板卡替换 set_location_assignment PIN_E1 -to clk_ref set_location_assignment PIN_E2 -to sig_in set_location_assignment PIN_M1 -to rst_n set_location_assignment PIN_M2 -to range_sel[0] set_location_assignment PIN_N1 -to range_sel[1] # 数码管段选 a~g,dp set_location_assignment PIN_A1 -to seg[0] set_location_assignment PIN_A2 -to seg[1] # 位选 set_location_assignment PIN_B1 -to dig[0] set_location_assignment PIN_B2 -to dig[1] # 未列出的 seg[2..7] / dig[2..7] 按同样方式补齐 set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to *这段里的引脚号是示意,实际必须对着原理图改。IO_STANDARD 也要跟板子的电平匹配,3.3 V 的板子写 2.5 V 会导致输入阈值不对,表现为按键偶尔失灵、数码管亮度异常。
4.3 SDC 时序约束与 TimeQuest 读数
13.1 用的是 TimeQuest 加 SDC 约束,不再是老版本的 tsu/th 设置文件。这个设计里需要约束的时钟只有两个:clk_ref 是 50 MHz,周期 20 ns;sig_in 频率未知,要给它一个保守的上限约束。
# freq_meter.sdc # 板载 50 MHz 参考时钟 create_clock -name clk_ref -period 20.000 [get_ports clk_ref] # 被测信号:按最高 100 MHz 约束,实际测试频率低于此值时余量更大 create_clock -name sig_in -period 10.000 [get_ports sig_in] # 跨时钟域路径设为伪路径,避免工具在异步路径上白费力气 set_false_path -from [get_clocks sig_in] -to [get_clocks clk_ref] set_false_path -from [get_clocks clk_ref] -to [get_clocks sig_in] # 输入输出延迟:数码管是纯输出,给个宽松值即可 set_output_delay -clock clk_ref -max 5.000 [get_ports {seg[*] dig[*]}]set_false_path 那两行是这个设计的关键。闸门信号和 nx 都是异步跨域的,工具如果按同步路径去算建立保持时间,会报一堆红色违例,而且这些违例本身没有意义。把它们标成伪路径之后,工具只关心各自域内的时序,报告才读得懂。
注意:伪路径不代表电路一定安全,它只是告诉工具「这条路径我自己负责」。真正的安全保障靠 3.2 节和 3.3 节里的两级同步器。约束和电路设计是两件事,别把伪路径当成免死金牌。
编译完成后打开 Tools → TimeQuest Timing Analyzer,跑一遍 Report All Summaries。重点
本文还有配套的精品资源,点击获取