ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

三分频电路设计详解:占空比50%实现方法与Verilog代码

三分频电路设计详解:占空比50%实现方法与Verilog代码 你正在准备数字IC设计或FPGA相关的面试题三分频电路出现的概率有多高我参加过不少面试也帮人模拟过面试基本可以这么说只要简历里写了Verilog、时序逻辑、计数器这类关键词三分频就是绕不开的一道基础菜。出题方式也很有套路面试官通常不会只问一个“实现三分频”往往会补一句“占空比是多少”或者直接三个要求一起上占空比50%、三分之一、三分之二都写一遍。这篇文章我把这个题目完整的从原理到代码、从Testbench到仿真、从面试追问到避坑技巧全部过一遍。不管你是刚入门Verilog的FPGA初学者还是准备数字IC校招的应届生又或者想系统性复习时序分频的在职工程师这篇都能直接当参考笔记用。文章很长建议收藏把代码敲一遍。1. 为什么三分频能成为IC面试的“钉子户”1.1 一道题覆盖三类核心能力面试官挑题目不是随便挑的三分频这道题能火是因为它一个小模块就能考察三样东西基本时序逻辑是否掌握、占空比概念是否清晰、代码风格是否规范。先说基本时序逻辑。三分频要求对一个时钟进行每三个周期输出一个周期的分频信号核心就是计数器加比较器。这个如果不过关后面什么状态机、FIFO、接口时序基本都不用聊了所以这是一个很好的“筛选器”。面试官看到你拿笔写不出计数器基本就知道基础功在哪里。再说占空比概念。很多初学者会写一个三分频但问他“你这个输出占空比是多少”他就愣住了。分频结果什么时候为高、什么时候为低对应原始时钟的多少个周期这个必须能一眼看出来。面试官考察的其实不是记住占空比公式而是看你能不能从代码波形里推导出高电平持续时间和低电平持续时间的比例。最后是代码风格。同样是实现三分频不同写法直接反映你平时的工程习惯。有没有异步复位计数器位宽够不够有没有用阻塞赋值产生时序逻辑组合逻辑输出有没有额外打一拍这些问题在面试官眼里比“功能对不对”更值钱因为它们决定你写出来的东西能不能落到芯片上。1.2 奇数分频与偶数分频的差异才是重点我遇到过很多同学能把二分频、四分频写得非常熟练一到三分频就卡住。为什么因为偶数分频可以靠一个计数器加翻转实现50%占空比而奇数分频里50%占空比没法靠单个边沿计数直接得到。这个差异是面试官真正想考察的点。三分频里最直观的实现方式是用上升沿计数的模三计数器输出高电平一个周期、低电平两个周期得到1/3占空比。但50%占空比意味着高电平时间应该是1.5个原始时钟周期这在一个只靠上升沿更新的寄存器输出里是不可能的。你必须同时利用上升沿和下降沿把两个相位错开的信号组合起来。面试官把“占空比50%、三分之一、三分之二”三个放在一起问就是想看你能不能区分低占空比和高占空比都能靠单个沿实现50%反而要两个沿配合。能讲清楚这一层的候选人通常基础都是扎实的。2. 三分频的底层原理与方案选型2.1 整数分频的本质计数器加边沿选择先统一一个认知整数分频的本质就是把输入时钟按周期数来“数数”数到特定计数值就改变输出状态。分频系数N就是计数器的模值输出信号的周期等于N个输入时钟周期。对三分频来说N等于3计数器需要从0数到2再回到0总共三种状态。输出信号的高电平和低电平持续时间加起来正好3个输入时钟周期。至于高电平到底持续几个周期完全取决于你写代码时的赋值条件。我自己常用的画图方法是这样先把输入时钟的波形画出来标出每个上升沿和下降沿的位置然后假设计数器在每个沿跳变再把输出信号在哪个沿改变、改成什么值标上去。三分频这种小分频比画一遍波形基本就能确认代码对不对比空想靠谱得多。2.2 占空比1/3和2/3方案单边沿计数就能搞定先看1/3占空比。让计数器在上升沿从0数到2循环当计数到0时输出高电平其余时间输出低电平。这样每3个时钟周期里高电平持续1个时钟周期低电平持续2个时钟周期占空比就是三分之一。再看2/3占空比。同样的模三计数器把输出条件反过来当计数器不等于2时输出高电平等于2时输出低电平。这样每3个周期里高电平占2个周期、低电平占1个周期占空比正好三分之二。这两个方案的共同特点是只用了上升沿逻辑实现非常简单综合出来就是一组D触发器加一点点组合逻辑。很多面试题里所谓“三分之一、三分之二”就是考察这个简单条件翻转大家不要把它想复杂了。2.3 50%占空比方案为什么必须引入下降沿1/3和2/3都好办50%才是这道题的灵魂。占空比50%意味着在一个三分频周期里高电平持续1.5个输入时钟周期低电平也持续1.5个周期。可是单个寄存器的输出无论如何只能在同一个边沿更新也就是说它的高低电平持续时间只能是整数个时钟周期不可能出现半个周期。要解决这个问题思路是先用上升沿计数器生成一个1/3占空比的信号A再用下降沿计数器生成一个1/3占空比的信号BA和B在时间上错开半个时钟周期然后把A和B相或。这样原本每个信号只有1个周期高电平错开后叠加起来高电平就变成了1.5个周期低电平也是1.5个周期输出得到一个完整的50%占空比三分频信号。同样的原理也可以反过来用生成两个2/3占空比的信号错开半个相位后再相与得到的结果也是50%。两条路都可以走面试时写其中一种讲清楚即可。2.4 同步复位与异步复位的选择面试代码里我推荐用异步复位也就是always (posedge clk or negedge rst_n)这种写法。这种写法在FPGA和大多数数字IC设计流程里都很常见而且能确保在复位信号有效时输出立即回到已知状态仿真行为也比较直观。不过在面试讲解时最好补充一句实际芯片项目里到底是同步复位还是异步复位要取决于后端流程和时钟复位策略有些设计为了时序收敛会统一使用同步复位。能主动提到这一层面试官会觉得你不是只会背代码而是真知道工程习惯。3. Verilog代码实现与逐段解析3.1 占空比1/3的三分频器先给最简单的单边沿版本。这个版本用上升沿计数计数到0时输出高实现占空比三分之一module div3_1_3 ( input wire clk, input wire rst_n, output reg clk_out ); reg [1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 2d0; clk_out 1b0; end else if (cnt 2d2) begin cnt 2d0; clk_out 1b1; end else begin cnt cnt 1b1; clk_out 1b0; end end endmodule注意这里cnt只有两位因为只需表示0、1、2三种状态。如果你写成3位甚至更宽功能没问题但面试官可能会问“为什么位宽可以更省”。位宽按最大计数值2来确定用2位刚好这是能体现出你考虑资源消耗的地方。这段代码的时序逻辑在复位后cnt为0clk_out为0。第一个时钟上升沿来临时cnt从0加到1同时clk_out被赋为0第二个上升沿cnt加到2clk_out仍为0第三个上升沿cnt回到0clk_out才拉高。可以看到clk_out的高电平其实出现在计数状态的某个窗口里整体每3个周期出现1个周期高电平。3.2 占空比2/3的三分频器把输出条件简单翻转就得到2/3占空比版本。计数到2时拉低其余时间保持高module div3_2_3 ( input wire clk, input wire rst_n, output reg clk_out ); reg [1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 2d0; clk_out 1b0; end else if (cnt 2d2) begin cnt 2d0; clk_out 1b0; end else begin cnt cnt 1b1; clk_out 1b1; end end endmodule这个模块复位时输出为0。等到第一个上升沿cnt变成1clk_out拉高第二个上升沿cnt变成2clk_out仍为高第三个上升沿cnt回到0clk_out才拉低。于是高电平持续两个周期低电平持续一个周期占空比2/3。有个细节值得注意复位后先输出0要等第一个上升沿之后才变高这里存在一个“启动相位”。如果外部电路关心分频输出的第一个高电平出现在什么时候就需要和时钟做相位对齐处理。面试可能不会追问这个但实际用的时候要心里有数。3.3 占空比50%的三分频器经典双沿组合法这是整道题的核心也是最常被要求手写的版本。思路是用两套一模一样逻辑分别跑在上升沿和下降沿上再相或module div3_50 ( input wire clk, input wire rst_n, output wire clk_out ); reg [1:0] cnt_p; reg [1:0] cnt_n; reg clk_p; reg clk_n; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_p 2d0; clk_p 1b0; end else if (cnt_p 2d2) begin cnt_p 2d0; clk_p 1b1; end else begin cnt_p cnt_p 1b1; clk_p 1b0; end end always (negedge clk or negedge rst_n) begin if (!rst_n) begin cnt_n 2d0; clk_n 1b0; end else if (cnt_n 2d2) begin cnt_n 2d0; clk_n 1b1; end else begin cnt_n cnt_n 1b1; clk_n 1b0; end end assign clk_out clk_p | clk_n; endmodule这段代码里clk_p在上升沿控制下每隔3个周期输出1个周期的高电平clk_n则在下降沿控制下输出同样的脉冲但相位相对clk_p滞后半个时钟周期。两者相或之后原本两个分离的1个周期高电平会有一部分重叠实际输出的高电平从第一个信号的上升沿到第二个信号的下降沿正好1.5个原始时钟周期。这里有个容易搞混的点为什么用“或”而不是“与”因为每个信号都是低电平占多数只有1个周期高两个高脉冲错开半个周期后或的结果把高电平时间拉长。反过来如果两个信号都是高电平占多数的2/3占空比那就应该用“与”。面试时把这一句讲清楚基本就能让面试官点头。3.4 另一种可综合的50%写法上面版本用两个计数器分别跑上升沿和下降沿代码清晰但不是唯一写法。还有一种常见的做法是只用一个计数器在always块里同时监听posedge clk和negedge clk用system verilog的always_ff不太好写Verilog里也不推荐写双沿计数。因为双沿对时钟树的时序要求更严格而且很多综合工具对同时使用双沿的寄存器支持不友好。所以我还是推荐两个计数器分开的写法。唯一要注意的是clk_p和clk_n这两个信号本身都是内部寄存器输出最后的clk_out是组合逻辑这个结构综合之后会自然映射到标准单元没有什么黑魔法。有些面试官会问“为什么不复位clk_out”因为clk_out是assign出来的它由clk_p和clk_n决定只要这两个寄存器复位置零clk_out自然就是0。4. Testbench设计与仿真验证4.1 直接可跑的Testbench代码代码写完不仿真等于白写。我习惯用免费开源的Icarus Verilog命令行就能跑很适合面试前快速验证。下面是一份能直接跑的Testbench把三个模块都例化进来同时输出timescale 1ns / 1ps module tb_div3; reg clk; reg rst_n; wire clk_out_1_3; wire clk_out_2_3; wire clk_out_50; initial begin clk 1b0; forever #5 clk ~clk; // 10ns周期100MHz end initial begin rst_n 1b0; #25; rst_n 1b1; #1000; $finish; end div3_1_3 u_div3_1_3 ( .clk (clk), .rst_n (rst_n), .clk_out (clk_out_1_3) ); div3_2_3 u_div3_2_3 ( .clk (clk), .rst_n (rst_n), .clk_out (clk_out_2_3) ); div3_50 u_div3_50 ( .clk (clk), .rst_n (rst_n), .clk_out (clk_out_50) ); // 打印波形 initial begin $dumpfile(div3.vcd); $dumpvars(0, tb_div3); end endmodule如果你用Vivado或ModelSim直接把Testbench添加进工程跑行为仿真即可波形导出看VCD或者自带的WDB格式都行。用Icarus的话命令行是iverilog -o div3_tb div3_1_3.v div3_2_3.v div3_50.v tb_div3.v vvp div3_tb跑完后在gtkwave里打开div3.vcd查看波形。4.2 仿真波形应该看到什么手工验波形的时候我一般不看自动断言直接数时钟沿最踏实。对1/3占空比版本你需要在每3个时钟周期里看到1个周期的高电平高电平的起点最好对齐某个上升沿。对2/3占空比版本每3个周期里看到连续2个周期高电平。50%占空比版本要重点看输出高电平的起点是不是某个上升沿输出高电平的结束点是不是某个下降沿。从波形上量一下高电平宽度是输入时钟周期的1.5倍低电平宽度也是1.5倍。这里最容易看走眼因为一个3倍周期的高电平1.5倍加上低电平1.5倍看起来好像不对称但周期长度确实是3倍。如果我用gtkwave会把输入clk、clk_out_1_3、clk_out_2_3、clk_out_50放在同一屏再用标尺量。量完就发现clk_out_50相对clk_out_1_3其实有一个固定的相位偏移这是由下降沿计数启动造成的不影响功能。4.3 用Icarus本地跑仿真的几个小细节Icarus Verilog虽然免费但对System Verilog的支持一般如果你写的是纯Verilog-2001基本没问题。跑之前确认文件名和模块名对得上iverilog编译时不区分大小写这一点坑了很多人。模块例化端口建议用.clk(clk)这种显式连接方式不要用位置连接这样代码可读性好也不容易在改端口顺序时出错。还要提醒一点$finish会让仿真直接结束有些图形界面可能提示“finish called”这是正常的。如果想看特定时间段的波形可以把#1000改成更长的数值。reset释放时间我用的是25ns相当于2.5个时钟周期足够让所有状态归零也方便观察复位到首个输出的建立过程。4.4 综合之后的潜在风险行为仿真通过不代表物理实现没问题。如果你的最终输出不直接接触发器而是接后续逻辑组合输出的glitch可能带来误触发。在FPGA里这个问题相对容易处理因为内部路径短但在ASIC流程中一般建议在组合输出后再加一级寄存器打拍或者直接把分频逻辑放进同步逻辑里避免跨时钟域。这里我稍微展开一下clk_out是clk_p | clk_n的结果clk_p在上升沿变化clk_n在下降沿变化。由于两个信号变化沿不同相或时可能出现极窄的毛刺宽度通常在几十皮秒量级。如果负载是异步复位或者异步置位风险就很大。面试时我通常会主动提一句“最后需要一个寄存器同步”先把这个问题堵上。5. 面试官常追的三连问与变形题5.1 单边沿计数器的输出占空比能是多少面试官如果看到你写了1/3占空比版本很可能会追问只用一个上升沿计数器理论上能实现哪些占空比答案是这个输出占空比只能是m/3m为整数。因为每个状态持续一个完整时钟周期高电平持续m个周期低电平持续(3-m)个周期m只能取0、1、2、3。你能得到1/3和2/3得不到1/2。这个追问背后的逻辑是想考察你是否理解“占空比与沿的关系”。你可以顺便补充如果m取3那输出恒为高不能算分频m取0则恒为低。真正有意义的是1/3和2/3两个值。5.2 扩展到五分频、七分频怎么做这是三分频最常见的变形。我建议你在面试前自己推导过一遍通用N分频公式。对奇数N50%占空比的设计方法是用两个N计数器的逻辑一个跑上升沿一个跑下降沿每个产生宽度为(N-1)/2个周期的高电平或低电平最后相或或相与让高电平宽度变成N/2个周期。以五分频为例N等于5单边沿计数可以产生占空比1/5、2/5、3/5、4/5但得不到2.5/5。于是同样用双沿方案每个沿上的计数器产生2/5占空比信号两个信号错开半个周期相或得到3/5等等得仔细算单沿2/5信号高电平持续2个周期相位错开0.5个周期后相或高电平长度变成2.5个周期低电平也是2.5个周期于是50%五分频成立。能把这个通用方法总结成模板面试官基本就满意了。不用背每个分频器代码但要会套生成 m/N 占空比的双沿信号再组合成1/2。5.3 时钟非50%占空比会对结果有什么影响实际芯片里输入时钟不一定严格50%占空比。如果时钟高电平0.4个周期、低电平0.6个周期那么上升沿到下降沿的间隔为0.4个周期下降沿到上升沿为0.6个周期你组合出来的“1.5个周期高电平”其实会被拆成0.4加1.0加0.6波形看起来会变形。这个问题的标准回答是分频结果的占空比会受输入时钟占空比影响但分频周期不受影响。因为周期由相邻上升沿间隔决定只要频率稳定周期仍然是输入时钟周期的3倍。面试时能说出这层逻辑面试官会觉得你对实际电路有时序意识。5.4 相位对齐和重置策略另一个常被追问的细节是clk_p和clk_n两个计数器是否同时从0开始。如果两个计数器的复位释放不完全对齐或者其中一个没有复位输出波形开头可能不稳定。因此两个always块里都必须对cnt_p和cnt_n复位这也是为什么我在代码里没有省略任何一个复位分支。有时面试官会问“如果我想让输出上升沿和输入时钟上升沿严格对齐该怎么做”最简单的说法是让clk_p的拉高条件正好发生在某个上升沿之后同时用组合输出让clk_out的上升沿跟随clk_p。但严格来说组合路径有延迟不能说绝对对齐只能说由触发器到组合网络再经过buffer比直接时序输出会差一些。可以提到“输出先过一级寄存器同步”改善对齐精度。5.5 用状态机实现和用计数器实现的区别三分频也可以用三段式状态机实现三个状态分别代表0、1、2状态转移时控制输出电平。这种方式在功能上等价于计数器主要区别是代码可读性和扩展性。计数器代码更简洁状态机则方便加入额外控制逻辑比如使能、暂停、复位优先权。面试时首选计数器如果面试官问“能不能用状态机”你再现场画状态转移图。我个人觉得状态机写法在面试中反而更啰嗦计数器已经足够没必要为了炫技搞复杂。能一句话讲清楚计数器的原理比写10行状态机更能展示功力。6. 实操踩坑与自查清单6.1 最容易出错的三个代码细节第一个坑是计数器位宽。计数到2只需要2位但如果写成reg cnt这样的1位变量数值2会溢出整个分频逻辑立刻失效。面试手写代码时尤其容易在匆忙中把位宽写少写完先数一下最大计数值的二进制位宽。第二个坑是clk_p和clk_n的赋值条件。很多人在if (cnt 2d2)分支里既让cnt归零又让clk_p拉高但忘记在else分支里把clk_p拉低结果clk_p一直保持高电平输出变成常1。这就是典型的没有覆盖所有赋值路径导致锁存器隐含生成。所有分支都要对输出赋值这一点在面试笔试里几乎是必查项。第三个坑是复位信号极性。我用的是低有效异步复位negedge rst_n有的同学抄代码时把敏感列表写成posedge rst_n会导致仿真立刻报错或复位逻辑完全颠倒。面试前最好自己跑一遍仿真确认复位后输出为0。6.2 自查代码的快速检查方法不依赖波形我也有一套自查流程。先看计数器模值对不对3分频计数范围必须包含0、1、2三种状态复位后是0到2之后回到0。再看输出条件1/3版本在cnt等于几时拉高2/3版本在cnt等于几时拉低50%版本是否组合了两个沿的信号。最后看位宽和赋值完备性。如果画波形我推荐先把clk画出来然后画出cnt的状态跳变再根据cnt画输出。这样不跑仿真也能确定逻辑错误。面试现场如果时间紧直接在草稿纸上画6个上升沿、6个下降沿然后按照代码推导输出波形比盲写更稳。6.3 面试实战技巧如果这是你第一次面对这个题目我建议你提前在纸上写过一遍再在电脑上仿真过一遍。面试时哪怕紧张手也能按肌肉记忆写出来。写的时候边写边解释这是计数器、这是复位、这是占空比控制逻辑。先给1/3版本再给2/3版本最后给50%版本层层递进面试官一般都会满意。如果面试官让你把50%版本里的组合输出改成寄存器输出你可以用如下思路先把clk_p和clk_n相或的结果赋值给一个wire再接一级always (posedge clk)打一拍输出。这样毛刺被寄存器隔离但输出会相对原始时钟有额外一拍延迟。能现场改对基本就是加分项。最后再分享一个小技巧面试时在代码注释里写清楚// divide by 3, duty 50%然后顺手在输出声明里加(* keep true *)不是每个面试官都认识这个综合属性但认识的会觉得你确实有真实流片或上板的经验。这个小细节往往比多说十句话都管用。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进