
简介面向西南交通大学数字电路实验课程的可控分频器设计完整作业包覆盖 Quartus Prime 工程创建、Verilog 编写、引脚分配、编译下载和上板验证的完整链路。实验基于 50MHz 高频时钟设计可控分频器用 sel 开关选择分频系数sel0 时输出频率为学号后 4 位对应的赫兹值sel1 时为学号后 5 位对应值并保持 20% 占空比同时要求把 clk_out 接入逻辑分析仪验证实际频率与占空比误差小于 1%。压缩包共 113 个文件、约 3.92MB主要包含 Quartus Prime 工程文件qpf/qsf/sof、Verilog 源文件与测试激励、编译与布局布线报告、逻辑分析仪波形、实验说明文档及 README可对照工程代码与报告检查分频系数、约束配置和误差原因。目前已有 2887 人学习适合高校数电实验初学者理解分频器原理、掌握 FPGA 设计流程也可用于完善实验报告和快速定位 clk_out 频率或占空比偏差。1. 50MHz 到 Hz 级可控分频器为什么是数电实验里最容易被低估的一题很多人在拿到这份可控分频器设计时第一反应是50MHz 到几千赫兹计数器除一下就完事。真正动手才发现sel 切换两种频率、占空比 20%、误差小于 1%、还要在逻辑分析仪上复测任何一步算错都会让实测超差。这份资料是西南交通大学数电实验作业里的实验四压缩包里是 Quartus 编译中间文件和 .v.bak 源码备份。我第一次打开时也差点被 ly_2257_4.vpr.ammdb 这类文件带偏后来才发现关键是把备份后缀改回 .v重建工程。下文从分频系数取整、Verilog 计数比较、仿真到 CH0 逻辑分析仪实测把这条链路完整走一遍顺便说说那些最容易让有经验的工程师也在排错时多花半小时的细节。2. 分频系数与 20% 占空比先算清参数再写代码2.1 50MHz 时钟与 Hz 级输出的整数分频约束可控分频器的时钟输入是 FPGA 开发板上的板载 50MHz 晶振输出频率却只有几千赫兹中间差了四个数量级。比如学号 2017112345sel0 时目标频率是后四位 2345Hz理论分频系数是N 50_000_000 / 2345 21321.96但 Verilog 计数器在综合时只能处理整数分频计数器比较器对的是整数所以必须把 21321.96 变成 21321 或 21322。我一般会采用四舍五入也就是(50_000_000 f_out/2) / f_out因为这个实验里频率都是整数 Hz这样做误差最小。不过题目允许 1% 误差直接向下取整也完全够用。下面这张表是按四舍五入得到的参数后两列是实际输出频率与目标频率的相对误差。可以看到取整造成的频率误差通常在万分之几以内晶振本身的 ppm 级偏差早就覆盖掉了所以真正要盯的不是频率而是占空比。学号取值目标频率 (Hz)理论 N取整 N实际频率 (Hz)频率误差后四位2345234521321.96213222345.000.0002%后五位12345123454049.82405012345.680.0055%全零→2768276818063.58180642767.940.035%全零→32768327681525.88152632765.400.008%注意表格中“全零→2768”那行题目要求学号后四位如果为 0按 2768 取值后五位如果为 0按 32768 取值。这是为了避免除数为零而不是真的要输出 0Hz。实际 50_000_000 / 2768 算出的理论 N 是 18063.58四舍五入为 18064误差也很小。2.2 占空比 20%用计数比较实现脉宽而不是分频后再整形普通计数器分频输出的占空比通常是 50%因为输出在计数一半的地方翻转。题目要 20% 占空比也就是高电平时间占整个周期的 20%。最直接的做法是把计数器分成两个阶段从 0 数到 N-1当计数值小于某个阈值 HIGH 时输出高否则输出低。这样输出高电平的时间正好是HIGH * Tclk周期是N * Tclk占空比就是HIGH / N。HIGH 的取值我一般用整数除法localparam integer HIGH0 DIV0 / 5;例如 N21322 时HIGH04264实际占空比为 4264/21322≈19.998%和 20% 的相对误差只有 0.01%。即使 N 最小的 1526HIGH305占空比 19.987%误差也只有 0.06%离 1% 的限值还很远。这里要特别说明Verilog 整数除法会自动截断小数。如果你希望 HIGH 更接近 20%可以改成(DIV * 2 5) / 10这样的四舍五入写法但占空比本身是离散的只要 N 足够大截断的影响可以忽略。真正容易出错的是把 HIGH 写成浮点数综合工具不支持综合浮点最后会被截成 0导致输出永远为低。在数电实验里这类错误排查起来很奇怪因为波形看起来像是分频器没工作。2.3 sel 切换与学号全零特例把频率字算在编译期题目里 sel 为 0 时用学号后四位sel 为 1 时用后五位。这个后四位/后五位不是 Verilog 里的位选sn[3:0]而是十进制学号的末几位。例如学号 2017112345后四位是 2345后五位是 12345如果学号是 2017100000后四位是 0000按题目要替换成 2768。所以设计的第一步是在代码里解析学号并处理全零替换。常见做法是用取模运算localparam integer SN 32d2017112345; localparam integer F0 (SN % 10000 0) ? 2768 : (SN % 10000); localparam integer F1 (SN % 100000 0) ? 32768 : (SN % 100000);localparam只在编译时求值综合后这些量都是常量。SN % 10000取的是十进制后四位SN % 100000取的是后五位。三元表达式的条件在编译期就能确定所以 F0、F1 会直接变成常量不会生成选择器逻辑。需要注意如果把学号定义成parameter [31:0] SNSN % 10000的结果在 Verilog 里是无符号数实际学号不会超过 10 位十进制32 位足够。有些人会把后四位提前在纸上算好直接写parameter DIV0 21322这种做法也行但可读性差换学号时容易算错。用取模从学号推导能保证源码和实验报告里的学号一一对应验收时也容易解释。3. Verilog 实现从 .v.bak 恢复到 Quartus 工程3.1 压缩包里都是什么先找回备份源码下载的资源包文件名是【swjtu】数电实验4_可控分频器设计.zip里面大多是 Quartus Prime 编译过程中生成的中间文件比如ly_2257_4.vpr.ammdb、ly_2257_4.map.ammdb、ly_2257_4.cmp.bpm、ly_2257_4.cmp.cdb。这些数据库文件记录的是综合、布局布线的结果脱离了原工程后很难直接用并且也没有.qpf和.qsf这两个工程配置。真正可复用的是两个.v.bak文件ly_2257_4.v.bak test_ly_2257_4.v.bak它们分别对应可控分频器 RTL 和测试激励的备份。Quartus 不会自动把.bak当作源文件所以第一步是把它们重命名成.v。我一般会在终端里直接复制保留一份原始备份cp ly_2257_4.v.bak ly_2257_4.v cp test_ly_2257_4.v.bak test_ly_2257_4.v然后在 Quartus Prime 里新建工程器件型号按你手头的开发板选。如果下载包里没有.qsf直接打开.ammdb是打不开工程树的正确操作是 File → New Project Wizard选好器件后 Add File 把两个.v加进去再重新编译。遇到编译器报错说找不到某个工程宏时优先检查是不是 Quartus 版本差异导致的原语或 IP 核路径变化这种情况我一般会删掉旧的中间文件重新全编译。3.2 RTL 设计计数器、比较器与 sel 切换同步复位下面这段 RTL 就是可控分频器的主体。端口与题目一致学号用 parameter 传入默认值写成 2017112345。为了兼容不同开发板我没有在模块内部例化 PLL直接使用外部 50MHz 时钟做全局时钟输入。module clk_div_ctrl #( parameter integer SN 32d2017112345 )( input wire clk_in, input wire sel, output reg clk_out ); // 学号解析后4位/后5位全零时按题目替换 localparam integer F0 (SN % 10000 0) ? 2768 : (SN % 10000); localparam integer F1 (SN % 100000 0) ? 32768 : (SN % 100000); // 分频系数50MHz 除以目标频率 localparam integer DIV0 32d50000000 / F0; localparam integer DIV1 32d50000000 / F1; // 高电平计数20% 占空比 localparam integer HIGH0 DIV0 / 5; localparam integer HIGH1 DIV1 / 5; // sel 为 1 时选择后 5 位频率为 0 时选择后 4 位频率 wire [31:0] DIV sel ? DIV1 : DIV0; wire [31:0] HIGH sel ? HIGH1 : HIGH0; reg [31:0] cnt; reg sel_d 1b0; always (posedge clk_in) begin if (sel_d ! sel) begin // sel 变化时同步复位避免输出半个周期 cnt 32d0; clk_out 1b0; sel_d sel; end else begin if (cnt DIV - 1) cnt 32d0; else cnt cnt 1; clk_out (cnt HIGH) ? 1b1 : 1b0; sel_d sel; end end endmodule逻辑说明计数器的计数范围是 0 到DIV-1也就是一个输出周期包含DIV个输入时钟周期。当sel_d ! sel时说明拨码开关状态发生变化此时必须把计数器清 0同时把输出强制拉低否则可能产生宽度小于一个输入时钟周期的毛刺。clk_out在cnt HIGH时输出高HIGH约为DIV/5所以占空比约为 20%。参数说明DIV0和DIV1是 32 位常量但实际最大分频系数只有 50_000_00026 位二进制就够。使用 32 位是为了避免DIV-1在计数值最大时出现位数不够的情况也方便后面如果改成动态配置。HIGH0/HIGH1使用整数除法因为目标和实验误差都允许这种近似。需要注意这里不能用localparam DIV sel ? DIV1 : DIV0因为sel是运行时信号不是常量表达式综合器会直接报错。正确做法是用wire把两个常量接成数据选择器资源开销只有一个 32 位 2 选 1几乎可以忽略。3.3 引脚分配与综合配置RTL 写完还要分配引脚。开发板不同引脚编号也不同但对应关系是一致的clk_in必须接到板上的专用时钟引脚sel建议接拨码开关而不是按键因为按键需要消抖而拨码开关的电平是静态的clk_out接到扩展排针或 Pmod 接口方便用杜邦线连到逻辑分析仪的 CH0 通道。端口板级连接建议电气约束clk_in板载 50MHz 晶振输出时钟脚走专用时钟网络sel拨码开关未拨为 0拨下为 1加 10kΩ 下拉或依靠板载上拉clk_out扩展排针3.3V 逻辑电平在 Quartus Assignment Editor 里分配好引脚后编译前把 I/O Standard 统一设为 3.3-V LVTTL 或开发板手册指定的标准。不要把clk_out设为 OD开漏模式逻辑分析仪采到的会是一堆低电平。另外如果工程里只有.v.bak没有.qsf引脚全部处于自动分配状态直接烧录到板子上大概率不工作这一步不能跳过。3.4 顶层例化与学号替换在验证时我习惯单独建一个顶层文件把学号通过参数传进去比如测同学 A 的学号clk_div_ctrl #(.SN(32d2017100000)) u_div( .clk_in (clk_in), .sel (sel), .clk_out(clk_out) );这里SN2017100000是学号末尾五位全零的情况代码里的F0会变成 2768F1会变成 32768完全符合题目说明。如果直接在 RTL 里改localparam SN也行但顶层例化传参的好处是仿真时可以用initial block快速换学号不需要重新修改测试文件。4. testbench 仿真与逻辑分析仪实测误差到底从哪里来4.1 仿真用 task 统计 100 个输出周期更直接写 testbench 时除了观察波形我更喜欢加一段测量逻辑直接打印出输出频率。这样在 ModelSim 里跑完不用手动拉光标就能判断频率是否接近目标。下面这个 testbench 以 50MHz 时钟为基准用一个 task 测量 100 个clk_out上升沿之间的平均周期timescale 1ns/1ps module tb; reg clk_in 0; reg sel 0; wire clk_out; integer edge_cnt; real t_start; clk_div_ctrl #(.SN(32d2017112345)) dut( .clk_in(clk_in), .sel(sel), .clk_out(clk_out) ); always #10 clk_in ~clk_in; // 50MHz task measure_freq(input reg sel_value); begin sel sel_value; repeat(2) (posedge clk_out); // 跳过切换后的不完整周期 edge_cnt 0; (posedge clk_out); t_start $realtime; repeat(100) (posedge clk_out); $display(sel%0d, freq%.3f Hz, sel, 1e11 / ($realtime - t_start)); end endtask initial begin measure_freq(0); measure_freq(1); $finish; end endmodule逻辑说明measure_freq先改变 sel然后等两个输出上升沿让计数器进入新的稳定状态。之后再等一个上升沿记录t_start数 100 个上升沿时间差就是 100 个完整周期。$realtime返回的是带小数的仿真时间时间单位由timescale 1ns/1ps决定所以1e11 / 时间差得到的是 Hz。参数说明repeat(100) (posedge clk_out);是顺序执行的100 个上升沿之间不会插入其他语句因此测量结果很干净。如果你只是简单地把#500ms写在 initial 里等固定时间遇到输出频率很低的学号可能会错过很多周期而且切换瞬间的毛刺会把频率统计带偏。用边沿计数的方式可以避开这个问题。4.2 占空比误差采样点数量决定你能测多准逻辑分析仪测量占空比本质上是对被采样信号做过采样然后统计高电平采样点数占总点数的比例。它不像示波器那样有连续的模拟分辨率边界处的 ±1 个采样点会被折算成误差。以 2345Hz 输出为例周期约 426us高电平约 85us。不同采样率下的边界误差如下表采样率周期内采样点高电平采样点占空比测量误差1 MS/s42685±1.18%5 MS/s2132426±0.23%10 MS/s4264853±0.12%从表里能看到1MS/s 采样率下±1 个采样点的误差已经超过题目 1% 的要求。我一般会把逻辑分析仪采样率设成 5MS/s 以上并且连续采集 128 个周期让分析软件做平均这样边界误差被稀释到可忽略。注意如果输出频率是后五位 12345Hz周期缩短到 81us同样 5MS/s 采样率下周期内采样点只有 405 个高电平采样点约 81 个边界误差约 1.2%所以测 sel1 时要进一步提高采样率到 20MS/s 才比较稳。4.3 实测超差 1% 的排查顺序如果逻辑分析仪显示频率或占空比超过 1%先不要急着改 RTL。按顺序排查大概率是下面几个原因之一clk_in实际频率不是 50MHz。用示波器直接测晶振输出确认不是 48MHz 或 12MHz很多国产开发板会使用非标准晶振。clk_in没有接到 FPGA 的专用时钟引脚。接到普通 IO 时内部时钟走线会有几十到几百皮秒偏差计数器宽度越大累计误差越明显。逻辑分析仪采样率不够导致占空比边界误差超过 1%。把采样率提高重新采一段平均波形再判断。Quartus 综合时把DIV和HIGH的常量计算优化错。这一步比较少见但如果你看到的是完全不对的频率比如输出等于clk_in除以HIGH而不是DIV就要检查clk_out的赋值条件是cnt HIGH还是cnt DIV。实测时还有一种常见误操作用逻辑分析仪的“自动测量占空比”功能直接读单周期。由于触发位置和采样边沿是随机的单周期占空比可能偏到 25% 或 15%。正确做法是在软件里选中连续一段多周期波形用区域统计或周期平均功能或者干脆导出 CSV 自己数高电平点数。4.4 用 SignalTap 做内部观测如果实验室逻辑分析仪通道被占用我通常会绕道用 Quartus 内置的 SignalTap II 探clk_out和cnt。SignalTap 的采样时钟可以用clk_in但要注意它也会受到触发深度和采样点数限制。对于 Hz 级输出采样时钟 50MHz触发条件设为clk_out1采集深度 1K可以清楚看到计数器在DIV附近回绕。不过 SignalTap 观测的是芯片内部信号不能替代题目要求的 CH0 逻辑分析仪实测只能作为排错辅助。5. 进阶技巧把分频系数做成可配置寄存器动态切换学号频率5.1 从常量参数到运行时可写寄存器前面 RTL 用parameter SN把学号固定在了编译期改学号就得重新综合一次。实验验收时如果临时换学号重新编译烧录很耽误时间我一般会预留一个配置接口让DIV和HIGH可以由外部拨码或上位机写入。核心逻辑可以抽成一个更通用的模块module clk_div_cfg #( parameter W 32 )( input wire clk_in, input wire [W-1:0] div_cfg, input wire [W-1:0] high_cfg, input wire cfg_valid, output reg clk_out ); reg [W-1:0] div_r 32d100; reg [W-1:0] high_r 32d20; reg [W-1:0] cnt 32d0; always (posedge clk_in) begin if (cfg_valid) begin div_r div_cfg; high_r high_cfg; cnt 0; clk_out 0; end else begin if (cnt div_r - 1) cnt 0; else cnt cnt 1; clk_out (cnt high_r) ? 1b1 : 1b0; end end endmodule这个模块把div和high当作配置字cfg_valid高电平时写入并同步复位计数器。写入后第一个输出周期从低电平开始不会产生不完整的高脉冲。你可以在顶层用拨码开关编码两位数也可以接到 UART 或片上软核实验时直接在终端里写频率省掉重新布线。5.2 配置接口验证技巧动态配置的难点是验证写时序。常见做法是在 testbench 里模拟一次配置写入然后测量稳态后的频率。写入时要注意cfg_valid的边沿必须和clk_in上升沿对齐否则寄存器可能写入中间值。如果div_cfg比div_r小很多计数器复位后要跳过第一个输出周期再统计因为复位那拍会把clk_out强制拉低使第一个周期变短。上电时如果没有配置时序div_r会被声明时的初始值 100 接住所以我给了 100 分频的默认值保证在没有外部配置时也有一个可见输出。实际使用时至少要保证div_cfg 2否则cnt div_r - 1会在div_r1时恒成立计数器无法正常累加输出会异常。另一个容易踩的坑是配置字位宽不足。分频系数最大 50_000_000需要 26 位二进制高位不够会溢出导致频率变成 0 或随机。我把W默认设 32宁可多留几位也不要为了省触发器而缩短。如果你打算交付工程建议保留两组逻辑一组是题目的sel固定频率另一组是div_cfg动态配置通过一个模式选择开关切换。这样验收时既能快速演示题目要求又能临时修改频率而不重新综合。clk_out 接到逻辑分析仪前要确认输出引脚的电平标准和测量端匹配我见过不少 5V 逻辑分析仪把 3.3V 输出抬出假电平的情况最后测出来的占空比会明显偏低。本文还有配套的精品资源点击获取