可控分频器设计:从计数原理到Verilog实现与排错
简介围绕西南交通大学数电实验4可控分频器设计压缩包内包含完整作业参考适合正在完成该校FPGA实验的学生也适合自学分频器设计的初学者。实验需基于50MHz板载时钟设计可控分频器由sel选择输出频率为学号后四位或后五位对应的Hz值并要求占空比严格达到20%包内提供满足上述要求的Verilog源文件、Quartus工程配置以及实验测试记录可直接打开工程查看分频逻辑与参数换算。压缩包共113个文件大小3.92MB主要类型包括qpf/qsf工程文件、.v源码、.wlf仿真波形、.sig逻辑分析仪采集数据、.pdf/.docx说明文档等内容组织清晰便于对照修改与验收。目前已有2887人学习使用读者可从中获取完整设计思路、代码实现和验证方法尤其能借鉴如何设置分频系数并利用逻辑分析仪校准输出频率与占空比。1. 数电实验里的可控分频器到底在控制什么做数电实验4“可控分频器设计”时我见过最多的一类提交是代码里写了一个div参数仿真波形很好看下载到板子上用示波器一量出来频率是题目要求的一半甚至两倍。问题往往不在分频而在大家对“分频比”定义不一致计数器回绕一次对应输出翻转一次翻转前后合成两个完整半周期实际频率已经除以 2N 而不是 N。可控分频器真正要控制的不是时钟本身而是计数器模值也就是回绕点。下面从计数本质讲起给一个可综合的 Verilog 模块和配套仿真再列综合下载时容易翻车的 4 个具体问题。2. 可控分频器的计数原理模长、占空比和位宽怎么联动2.1 从 2 分频到 4 分频计数器回绕和输出翻转不是一回事一个 T 触发器接成 toggle 就是最简单的 2 分频每个输入时钟上升沿翻转一次输出两个半周期合成一个完整周期所以输出频率是输入的 1/2。这里已经埋伏了第一个概念陷阱“翻转”不是“输出一个周期”翻转两次才是一个输出周期。要做到 4 分频常见思路是用模 4 计数器。计数器从 0 走到 3一共 4 个输入时钟周期。如果把“计数到 3”当作一个窄脉冲输出脉冲频率确实是输入频率的 1/4但占空比只有 1/4不是 50%。如果题目要求 50% 占空比就得在 0 和 2 各做一次动作要么产生两个边沿要么直接把输出翻转一次得到的输出周期是 8 个输入时钟周期实际上做的是 8 分频。所以设计开始前先做一件被很多人跳过的事确认题目里的“N 分频”是指输出频率为 Fin/N还是输出半周期宽度对应 N 个输入时钟。前者是窄脉冲模式后者是翻转模式。下面的 Verilog 模块按翻转模式实现div_value表示输出半周期包含多少个输入时钟周期。2.2 数电组合逻辑电路负责模长比较同步时序负责状态更新可控分频器在数电课本里通常归入时序逻辑但真正让“可控”成立的是一小片组合逻辑电路比较器或者预置数译码逻辑。计数器本身只负责状态转移它不知道什么时候该回绕让它回绕的是cnt M-1这个组合判断或者 74 系列计数器里LD#引脚上的译码输出。这就在代码层面引出一个关键选择判断条件写在哪个敏感列表里。always (posedge clk or negedge rst_n)里写复位综合工具会推断为异步复位always (posedge clk)里写复位判断综合工具会把复位当成同步逻辑。数电课本 6 版的功能表里74161 的同步预置和异步清零是分开两行的实验只看真值表没问题写 Verilog 时如果不区分综合出来的复位网络可能和板级行为对不上。另一个常见误解是认为“可控”必须用拨码开关或寄存器改预置数。实际上比较器回绕也是可控把比较目标从一个固定常量换成寄存器变量div_value计数器每跑一轮就自动装载新目标。两种方式在毛刺行为上不一样预置数法更接近 74 系列的手工做法比较器法更适合写参数化模块。2.3 三类“可控”做法预置数、比较器回绕、查找表控制方式实现位置优点需要警惕的点预置数计数器的 D 输入或 LOAD 引脚模长范围宽改变值很直接LOAD 时序要满足建立保持时间上电预置值必须一致比较器回绕cnt div_value这类组合判断代码直观动态改成任意值比较目标变化瞬间可能组合毛刺需要同步处理查找表/译码输入先用 ROM 或译码器转成预置值手工实验排错直观档位固定只能选离散档位扩展要改表用 Verilog 做实验时比较器回绕是最容易下手的一种。它的核心关系是计数器状态数M等于div_value输出周期等于2 * div_value个输入时钟周期。位宽不能拍脑袋定要按最低要求频率反推下一章直接给出可复现模块。3. 用 Verilog 实现可动态配置的可控分频器模块3.1 端口和参数表DW、div_value 和最低输出频率怎么定“可动态配置”指的是运行过程中可以改分频比而不是编译前改参数。因此div_value是输入端口DIV这类 parameter 不出现。模块只需要一个参数DW它决定分频范围。端口/参数方向作用注意点clkinput输入参考时钟分频基准频率要已知rst_ninput全局复位低有效释放后等一个半周期再测输出div_value[DW-1:0]input输出半周期对应输入时钟数最小取 1取 0 时模块自动按 1 处理clk_outoutput分频输出占空比 50%半周期等宽DWparameter位宽决定最大分频比不是随便设的最低输出频率的计算公式是Fout_min Fin / (2 * (2^DW - 1))。举一个实际例子实验板晶振 50MHzDW8时最大半周期是 255 个时钟输出周期是 510 个时钟最低频率约 98kHz。如果实验要求输出 1HzDW至少要到 25因为2^25 - 1 33554431对应最低约 0.745Hz。所以 8 位不是默认值是按需求算出来的。3.2 可综合实现递减计数器在回绕沿翻转输出下面这段代码用递减计数器实现半周期结束后翻转同时重载div_value - 1。module clk_div_ctrl #( parameter DW 8 )( input wire clk, input wire rst_n, input wire [DW-1:0] div_value, // 输出半周期对应的输入时钟个数 output reg clk_out ); reg [DW-1:0] cnt; // div_value 为 0 时按 1 处理避免每个时钟沿都翻转 wire [DW-1:0] safe_div (div_value {DW{1b0}}) ? {{(DW-1){1b0}}, 1b1} : div_value; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt {DW{1b0}}; clk_out 1b0; end else if (cnt {DW{1b0}}) begin clk_out ~clk_out; // 半周期结束输出翻转 cnt safe_div - 1b1; // 装载后半周期计数目标 end else begin cnt cnt - 1b1; // 正常递减 end end endmodule逻辑说明只有三条递减计数到 0 时cnt 0成立模块把cnt重载为safe_div - 1并翻转clk_out。从计数 255 递减到 0 一共是 256 个状态也就是 256 个输入时钟周期输出翻转一次。用递减计数器而不是递增计数器是因为回绕判断只需要比较cnt 0零值比较在 FPGA 内部实现时面积最小很多厂商分频 IP 内部也是这个思路。safe_div防止div_value0时模块每个时钟沿连续翻转。如果没有这个保护输入 50MHz 会得到 25MHz 输出看起来像 2 分频实际是未定义行为。这段代码里div_value在非 0 状态改变效果是等到下一次cnt 0时才装载新目标天然避免了比较目标瞬变产生的毛刺。如果你拿到别人写的递增计数器版本cnt div_value里div_value一变比较器输出会立刻抖动这是两类实现最大的行为差异。3.3 配套 Testbench验证 div_value1/2/3 的三组频率仿真用下面这个 testbench输入时钟周期 20ns对应 50MHz。复位释放后先跑一段 2 分频再切换到 4 分频和 6 分频。module tb_clk_div_ctrl; reg clk 1b0; reg rst_n 1b0; reg [7:0] div_value; wire clk_out; always #10 clk ~clk; // 20ns 周期 50MHz clk_div_ctrl #(.DW(8)) dut ( .clk (clk), .rst_n (rst_n), .div_value(div_value), .clk_out (clk_out) ); // 复位释放 initial begin repeat (2) (posedge clk); rst_n 1b1; end task automatic set_div(input [7:0] d); (posedge clk); div_value d; repeat (30) (posedge clk); endtask initial begin div_value 8d1; (posedge clk); repeat (20) (posedge clk); // 2 分频阶段 set_div(8d2); // 4 分频阶段 set_div(8d3); // 6 分频阶段 $finish; end // 每次输出翻转时打时间戳 reg [63:0] last_toggle; always (posedge clk_out) begin if (last_toggle ! 0) $display(out period %0t ps, freq %0d kHz, $time - last_toggle, 1000000 / ($time - last_toggle)); last_toggle $time; end endmoduleset_div任务在时钟上升沿之后改变div_value目的是让测试激励贴近真实寄存器更新。如果直接在组合逻辑里赋值仿真看到的可能是理想时序板级复现不出来。用$display测两个clk_out上升沿的时间差对应关系如下div_value输出周期输出频率输入 50MHz实际分频比140ns25MHz2280ns12.5MHz43120ns8.33MHz6如果在课程实验里要求“N 分频”且 N4你要么把div_value写成 2要么在报告里明确写“半周期计数”。4. 综合下载与波形测试可控分频器最容易翻车的 4 个点4.1 综合前先查位宽、case 分支和复位极性在 Quartus 或 Vivado 里综合之前我一般会按固定顺序检查三个点。第一位宽是否够用。div_value如果来自拨码开关位宽由开关数量决定如果来自寄存器位宽要和最大分频比匹配。常见错误是把 4 位拨码接到 8 位模块端口工具自动补零高 4 位永远为 0实际可选档位只有低 4 位。查法是看综合报告里的 Warning出现 “truncated value” 时先把位宽统一。第二case 分支是否写全。如果实现里用case选择不同的分频档位必须有default。漏掉 default 时综合工具可能推断出 latch实验现象表现为某些档位输出卡住不动不是频率错而是寄存器被锁存。用if-else if链条时最后一个else也不能省。第三复位极性是否和板级复位按键一致。实验板常用低有效复位rst_n拉低后要保持在时钟沿之外释放时最好远离时钟边沿。如果复位信号抖动会看到分频输出在一串毛刺后才稳定。// 推荐写法复位分支必须完整不能只在 if 里清 cnt always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt {DW{1b0}}; clk_out 1b0; end else begin // ... end end很多复习模电数电基础知识面试的人会把这段行为描述背成“异步复位、同步释放”但实验板上复位按键没有同步器。严谨一点的做法是在顶层把rst_n打两拍再进分频模块避免复位释放瞬间进入亚稳态。4.2 从实验现象反推频率变 2 倍、切换毛刺、卡死的排错对照表时序实验的排错不能只看代码要连起来看现象。下面这张表覆盖了数电实验里最常见的三类现象。实验现象可能原因处理方式输出频率是题目要求的 2 倍翻转模式却被当成窄脉冲模式把div_value理解成半周期重新换算切换分频比瞬间出现极窄毛刺div_value没有等回绕就参与组合比较采用cnt 0回绕装载或打两拍同步输出一直保持高电平或低电平复位后寄存器停在未定义状态复位释放后先跑一个完整半周期再测量仿真正常、板级输出为固定值拨码开关引脚未约束或位宽不匹配检查 pin planner 和拨码开关电平第一行最容易出现在验收现场。学生把div_value4写成参数报告里写“4 分频”示波器一测发现周期是 8 个输入时钟。这不是代码错是文档里的定义错。正确做法是在模块注释里写明“半周期”报告里的表也按半周期填写。第二行值得展开有些人写的模块是always (posedge clk)里判断cnt div_value当div_value从 3 改成 4比较器输入端变化和时钟边沿竞争输出就可能出现窄到无法用示波器触发的小毛刺。用递减计数器加cnt 0判断能规避大部分情况因为新div_value只在回绕沿被装载比较目标始终是 0。4.3 如果实验板走 74 系列 TTL74161 预置数接法不是所有数电实验都上 FPGA。很多课程用 74161 做同价计数器可控分频器的接法也很固定。74161 是同步预置、异步清零的 4 位二进制计数器。要让模长为 N通常让计数器从16-N计到 15然后在Q3 Q2 Q1 Q0 1111时产生LD#低电平下一个时钟沿把16-N载入。比如做模 8 分频D 输入接8也就是D3D2D1D0 1000计数状态是 8 到 15一共 8 个状态。LD#的译码逻辑是assign ld_n ~(Q[3] Q[2] Q[1] Q[0]);ENP和ENT都接 1CLR#接 1 或复位按键。注意这个方案的输出是进位信号CO它只在计到 15 时高一个周期因此直接拿CO当输出得到的是占空比 1/N 的窄脉冲不是 50%。需要 50% 占空比时在CO后面再接一个 T 触发器做二分频这和前面 Verilog 里“翻转”的概念完全一致。要把这个 TTL 方案变成“可控”把 D 输入从固定常量换成拨码开关或 74373 锁存器即可。拨码表示的是预置数实际模长是16 - 拨码值验收时先算清楚这个减法关系。5. 从可控分频到可调占空比一个可复用库的写法5.1 high_cnt/low_cnt 双计数器的写法前面模块能动态控频率但占空比锁在 50%。做 PWM 或需要非对称输出时把单个div_value拆成high_cnt和low_cnt两个输入半周期可以不同模块就升级成一个通用时钟整形器。module pwm_clk #( parameter DW 16 )( input wire clk, input wire rst_n, input wire [DW-1:0] high_cnt, // 高电平保持的输入时钟数 input wire [DW-1:0] low_cnt, // 低电平保持的输入时钟数 output reg out ); reg [DW-1:0] cnt; reg phase; // 1: 当前处于高电平半周期 wire [DW-1:0] target phase ? high_cnt : low_cnt; wire timeout (cnt target); always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt {DW{1b0}}; out 1b0; phase 1b0; end else if (timeout) begin out ~out; phase ~phase; cnt {DW{1b0}}; end else begin cnt cnt 1b1; end end endmodule写这个模块时有一个容易看走眼的点target是组合逻辑由当前phase选择high_cnt或low_cnt。当timeout成立的那个时钟沿到来phase和out同时更新但cnt清零后下一次判断用的是新的target不会把上一轮的计数目标多保持一个周期。复用时可以直接把之前的clk_div_ctrl理解成high_cnt low_cnt div_value的特例。库里的分频模块和 PWM 模块共用同一套递减计数结构只是多了一个phase状态。最后一层工程约束是多级分频时不要把上一级out直接接下一级clk。课程实验允许这样级联但工程上这会拆分时钟树让静态时序分析失去意义。更稳妥的做法是保持单一时钟域用div_value 0时的时钟使能脉冲驱动下一级计数器。本文还有配套的精品资源点击获取