1. 从“手撕代码”到“三分频”:数字IC面试的硬核敲门砖
最近几年,数字IC设计岗位的面试里,“手撕代码”已经从一个加分项变成了必考题。面试官递过来一张白纸或者打开一个在线编辑器,让你现场实现一个功能模块,这种场景让不少候选人手心冒汗。而“三分频”电路,正是这个“手撕代码”题库里的经典明星,出场率极高。它考察的绝不仅仅是写几行Verilog那么简单,背后是对数字电路基础、时序逻辑设计、时钟域处理乃至代码风格的综合检验。很多朋友,包括我当年,第一次看到这个题目时,可能会觉得“不就是个计数器吗?”,但真动起手来,才发现从功能正确、到波形干净、再到面积和功耗优化,每一步都藏着细节。
“芯源”这个词,在IC圈里通常指代芯片设计的源头,也就是RTL(寄存器传输级)设计。所以,“数字IC手撕代码--芯源(三分频)”这个标题,精准地指向了数字IC设计工程师的核心技能:用Verilog这样的硬件描述语言,从源头构建一个可靠、高效的电路模块。三分频器,作为一个中等复杂度的时序电路,完美地承载了这个考察目标。它不像一个简单的计数器那样直白,也不像复杂的状态机或协议接口那样庞大,它恰到好处地卡在中间,让你必须清晰地理解时钟边沿、状态跳变和占空比这些概念。
这篇文章,我就结合自己多次面试和被面试的经验,以及在实际项目中设计时钟电路的体会,来彻底拆解这个“三分频”电路。我不会只给你一个“标准答案”,而是会带你走一遍完整的思考、设计、实现和优化的过程。我们会从最基础的需求分析开始,探讨几种不同的实现方案,比较它们的优缺点,然后深入到代码实现、仿真验证,最后还会聊聊在面试中如何应对可能的追问,以及在实际芯片设计中需要注意的陷阱。无论你是正在准备数字IC岗位面试的学生,还是希望巩固基础的初级工程师,相信这篇长文都能给你带来实实在在的收获。
2. 三分频电路的需求分析与设计目标拆解
在动笔写第一行代码之前,我们必须把需求彻底搞清楚。面试官说“实现一个三分频电路”,这其实是一个模糊的需求,就像客户说“我要一个好看的界面”一样。一个合格的工程师,第一步就是澄清需求,定义清晰的设计目标。
2.1 明确输入与输出
首先,最基本的接口:
- 输入:一个原时钟信号,我们通常命名为
clk_in。我们需要知道它的频率吗?对于纯粹的分频功能来说,不需要,我们只关心它的周期和边沿。还需要复位信号rst_n吗?在真实的芯片设计中,几乎百分之百需要,这是一个好习惯。所以,输入至少包括clk_in和rst_n。 - 输出:一个三分频后的时钟信号,命名为
clk_out。这里的关键问题是:输出时钟的占空比是多少?
2. 2 占空比:第一个设计抉择点
“三分频”意味着输出时钟的周期是输入时钟的三倍。但一个周期内高电平和低电平的占比,就是占空比,并没有被定义。这是面试官留给你的第一个发挥空间,也是考察你对电路理解深度的地方。
- 50%占空比:这是最理想、也是最常见的要求。因为占空比为50%的时钟信号在后续电路中驱动能力均衡,对时序分析友好。但实现一个精确的50%占空比的三分频,需要一点技巧,因为3是奇数,无法用简单的计数器直接二分得到。
- 1/3占空比(或2/3占空比):这种实现起来最简单,一个模3计数器,当计数器为某个值时输出高,其余为低即可。但占空比不是50%,在某些对时钟质量要求高的场景(如作为某些模拟模块的时钟源)可能不适用。
- 未指定占空比:如果面试官没有明确要求,你应该主动询问:“您对输出时钟的占空比有要求吗?” 这体现了你的严谨和专业。通常,如果对方说“都可以”或“你实现一个看看”,那么优先实现50%占空比的方案,因为它更能体现你的能力。
2.3 其他关键设计目标
除了功能正确,我们还需要考虑:
- 同步设计:所有触发器都应使用同一个
clk_in驱动,避免使用门控时钟或行波计数器等异步设计,以保证良好的可综合性、可测试性和静态时序分析(STA)友好性。 - 毛刺消除:输出时钟
clk_out必须是一个干净、无毛刺的信号。任何组合逻辑产生的毛刺如果被当作时钟使用,将导致电路功能错误。这意味着clk_out最好直接来自触发器的输出端(Q端),而不是经过组合逻辑。 - 面积与功耗:在满足功能的前提下,使用最少的触发器(Flip-Flop)和逻辑门。这对于低功耗设计尤为重要。
- 代码风格:代码应清晰、可读、可维护。良好的命名、合理的注释、规范的 always 块使用,都是面试的加分项。
明确了这些,我们就可以开始构思电路方案了。我们的核心目标是:在同步设计的前提下,产生一个周期为3倍clk_in、占空比为50%、无毛刺的clk_out信号。
3. 方案对比:从简单计数器到双边沿触发
实现三分频,至少有三种主流的思路。我们逐一分析,理解其原理和适用场景。
3.1 方案一:模3计数器与1/3占空比(最简方案)
这是最直观的思路。用一个2位宽的计数器,在clk_in的上升沿从0计数到2,然后归零,循环往复。
- 状态:
cnt可以为 0, 1, 2。 - 输出逻辑:例如,我们定义当
cnt == 0时,clk_out = 1;当cnt == 1或cnt == 2时,clk_out = 0。这样,clk_out在一个周期(3个clk_in周期)中,只有一个clk_in周期为高,占空比为 1/3。
module clk_div3_simple( input wire clk_in, input wire rst_n, output reg clk_out ); reg [1:0] cnt; // 模3计数器,0->1->2->0... always @(posedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt <= 2'b00; clk_out <= 1'b0; end else begin if (cnt == 2'b10) // 当计数到2时,下一个周期回到0 cnt <= 2'b00; else cnt <= cnt + 1'b1; // 输出逻辑:仅在cnt为0时输出高电平 clk_out <= (cnt == 2'b00); end end endmodule注意:在这个实现中,
clk_out是寄存器直接输出的,没有毛刺,这是优点。但占空比不是50%,是其主要局限。如果面试官可以接受非50%占空比,这个方案简洁高效。
3.2 方案二:错相叠加法实现50%占空比(经典方案)
这是实现奇数分频(如3、5、7分频)且要求50%占空比的经典方法。核心思想是:用两个占空比非50%的时钟信号进行逻辑运算,合成一个50%占空比的时钟。
具体到三分频:
- 我们产生两个时钟信号
clk_p和clk_n。它们的频率都是clk_in的1/3,但相位相差180度(即半个clk_out周期,或者说1.5个clk_in周期)。 clk_p在clk_in上升沿触发和控制,clk_n在clk_in下降沿触发和控制。这就是“错相”。- 将
clk_p和clk_n进行“或”运算(OR),得到的就是占空比为50%的clk_out。
为什么?我们可以画个波形图来理解。假设clk_p在计数器为0时拉高,持续一个clk_in周期。由于clk_n使用下降沿且相位差半个周期,它会在clk_p拉高结束后的半个周期(下降沿处)拉高。这样,两个信号的高电平脉冲在时间上刚好错开,但合并后,高电平的持续时间就变成了两个clk_in周期,低电平为一个clk_in周期,整体占空比就是 (2/3) / 3?不对,让我们重新计算:clk_out周期是3个clk_in,高电平持续了2个clk_in,所以占空比是 2/3?等等,这还不是50%。
这里有个关键点:对于三分频,50%占空比意味着高电平持续1.5个clk_in周期。单纯靠两个信号的“或”是得不到1.5的。实际上,更通用的操作是“异或”运算(XOR)。clk_p和clk_n的异或,当两者电平不同时输出高,相同时输出低。通过精心设计clk_p和clk_n的跳变沿,就可以合成出精确的50%占空比波形。
3.3 方案三:状态机与双边沿触发(思维拓展方案)
这个方案更能体现你对时序逻辑的深入理解。我们设计一个状态机,有3个状态 S0, S1, S2。状态转移发生在clk_in的每个上升沿。但输出clk_out的翻转,不仅发生在上升沿,也可能发生在下降沿。
具体实现时,我们使用clk_in作为状态机的时钟。在状态 S0,我们让clk_out在下一个clk_in的上升沿翻转(例如从0变1)。在状态 S1,我们保持。在状态 S2,我们让clk_out在下一个clk_in的下降沿翻转(从1变0)。这样,高电平持续时间就是 S0的上升沿到 S2的下降沿,刚好是1.5个周期。
这种方案的代码实现需要引入一个在下降沿采样的信号来控制部分逻辑,或者使用双边沿触发的触发器(这在ASIC中不常见且不推荐)。因此,它更多是一种理论上的思路,用于展示你对时钟边沿和状态机的掌控力,在实际工程中不如方案二可靠和通用。
综合来看,方案二(错相叠加法)是实现50%占空比三分频的最优选择,它结构清晰,完全同步,且易于扩展到其他奇数分频。接下来,我们就重点实现这个方案。
4. 手撕代码:50%占空比三分频器的Verilog实现与仿真
现在,我们进入核心环节,用Verilog实现方案二。我会给出完整的代码,并逐行解释其设计意图和注意事项。
4.1 模块定义与信号声明
module clk_div3_50percent( input wire clk_in, // 原时钟输入 input wire rst_n, // 低电平异步复位 output wire clk_out // 三分频(50%占空比)时钟输出 ); // 内部寄存器声明 reg [1:0] cnt_p; // 用于上升沿分频的计数器 reg [1:0] cnt_n; // 用于下降沿分频的计数器 reg clk_p_reg; // 上升沿产生的分频时钟寄存器 reg clk_n_reg; // 下降沿产生的分频时钟寄存器 wire clk_p, clk_n; // 对应的wire型信号,便于连接 // 将寄存器输出赋值给wire,这是良好的代码风格 assign clk_p = clk_p_reg; assign clk_n = clk_n_reg;首先定义模块接口。使用wire和reg类型要清晰。clk_out将是组合逻辑(异或)的输出,所以定义为wire。内部我们声明了两组计数器和寄存器,分别对应上升沿和下降沿的电路。
4.2 上升沿电路(产生clk_p)
这部分电路在clk_in的上升沿工作。
// 上升沿进程:生成cnt_p和clk_p_reg always @(posedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt_p <= 2'b00; clk_p_reg <= 1'b0; end else begin // 模3计数器 if (cnt_p == 2'b10) // 计数到2后归零 cnt_p <= 2'b00; else cnt_p <= cnt_p + 1'b1; // 生成clk_p_reg:在cnt_p为0和1时拉高,为2时拉低 // 这样clk_p的占空比是2/3,周期是3个clk_in case (cnt_p) 2'b00: clk_p_reg <= 1'b1; 2'b01: clk_p_reg <= 1'b1; 2'b10: clk_p_reg <= 1'b0; default: clk_p_reg <= 1'b0; // 避免锁存器 endcase end end这里的关键是clk_p_reg的生成逻辑。我们让它在计数器为0和1时输出高电平,为2时输出低电平。这样,clk_p是一个占空比为 2/3、周期为3倍clk_in的时钟。default语句是良好的编码习惯,确保在所有未列出的情况下(虽然理论上不会出现)也有确定值,防止综合出锁存器。
4.3 下降沿电路(产生clk_n)
这部分电路在clk_in的下降沿工作,其逻辑与上升沿电路类似,但计数器cnt_n的初态或相位需要与cnt_p错开半个周期,才能实现180度相位差。
// 下降沿进程:生成cnt_n和clk_n_reg always @(negedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt_n <= 2'b00; // 注意:这里初值设为0,但通过逻辑实现错相 clk_n_reg <= 1'b0; end else begin // 模3计数器,逻辑与上升沿一致 if (cnt_n == 2'b10) cnt_n <= 2'b00; else cnt_n <= cnt_n + 1'b1; // 生成clk_n_reg:为了实现与clk_p的180度相位差, // 我们让clk_n在cnt_n为1和2时拉高,为0时拉低。 // 这样,clk_n的占空比也是2/3,但跳变沿与clk_p错开。 case (cnt_n) 2'b00: clk_n_reg <= 1'b0; 2'b01: clk_n_reg <= 1'b1; 2'b10: clk_n_reg <= 1'b1; default: clk_n_reg <= 1'b0; endcase end end注意clk_n_reg的生成逻辑与clk_p_reg不同。clk_n在cnt_n为1和2时为高。通过仿真我们可以验证,这样设计的clk_n的上升沿,大致对应着clk_p高电平的中间点,从而形成了所需的相位关系。
4.4 合成最终输出
最后,将两个相位错开的时钟进行异或操作,得到50%占空比的最终时钟。
// 通过异或运算合成50%占空比的时钟 assign clk_out = clk_p ^ clk_n; endmodule异或运算的特性是“相异为1,相同为0”。clk_p和clk_n两个占空比2/3、相位差半拍的信号异或后,高电平的宽度恰好等于它们电平不同的时间宽度,通过精心设计的跳变沿,这个时间被调整为1.5个clk_in周期。
4.5 测试平台与仿真验证
设计完成,必须用测试平台(Testbench)进行仿真。这是“手撕代码”后不可或缺的一步,体现了你的验证意识。
`timescale 1ns/1ps module tb_clk_div3_50percent(); reg clk_in; reg rst_n; wire clk_out; // 实例化被测试模块 clk_div3_50percent uut ( .clk_in(clk_in), .rst_n(rst_n), .clk_out(clk_out) ); // 生成时钟信号,周期10ns(100MHz) initial begin clk_in = 0; forever #5 clk_in = ~clk_in; // 每5ns翻转一次,周期10ns end // 生成复位信号 initial begin rst_n = 0; // 初始复位 #20; // 复位保持20ns rst_n = 1; // 释放复位 #200; // 仿真运行200ns $finish; // 结束仿真 end // 将信号变化记录到VCD文件,便于查看波形 initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb_clk_div3_50percent); end endmodule在仿真波形中,你应该关注:
- 复位期间,所有信号是否为确定值(0)。
- 释放复位后,
cnt_p和cnt_n是否从0开始正确计数(0->1->2->0...)。 clk_p和clk_n的波形是否符合设计(占空比2/3,相位错开)。- 最关键:测量
clk_out的周期是否为30ns(3倍于clk_in的10ns),高电平持续时间是否为15ns(占空比50%)。
如果波形符合预期,那么恭喜你,一个稳健的50%占空比三分频器就设计成功了。
5. 深入探讨:关键细节、常见陷阱与面试应对
代码能跑通波形,只成功了80%。剩下的20%在于理解背后的细节,以及应对实际工程和面试中的挑战。
5.1 为什么使用“异或”而不是“或”?
这是理解这个方案的核心。我们可以用真值表来分析: 假设clk_p和clk_n的跳变沿完美对齐(实际有微小延迟),在一个clk_out周期内:
- 第1阶段:
clk_p=1,clk_n=0->clk_out = 1^0 = 1 - 第2阶段:
clk_p=1,clk_n=1->clk_out = 1^1 = 0 - 第3阶段:
clk_p=0,clk_n=1->clk_out = 0^1 = 1 - 第4阶段:
clk_p=0,clk_n=0->clk_out = 0^0 = 0(这实际上是下一个周期的开始) 但实际上,由于我们的设计,clk_p和clk_n不会同时为1持续整个阶段。通过精确控制它们跳变的时刻(cnt_p和cnt_n的状态变化点),clk_p和clk_n同时为1的时间极短(理论上是一个clk_in周期中的一部分),而同时为0的时间也极短。异或操作恰好能利用这种短暂的“相异”状态,合成出一个高、低电平各占1.5个clk_in周期的时钟。如果使用“或”运算,高电平时间会过长,无法得到精确的50%占空比。你可以尝试在仿真中修改代码,将^换成|,观察波形变化,理解会非常直观。
5.2 时钟偏移与毛刺风险
这是我们方案的一个潜在弱点。clk_out是由组合逻辑(异或门)产生的。虽然clk_p_reg和clk_n_reg是寄存器输出,无毛刺,但经过异或门后,如果两个输入信号到达异或门的时间有微小差异(由于布线延迟不同),就可能产生毛刺。
clk_p来自上升沿触发器,clk_n来自下降沿触发器。它们物理上到达异或门的路径延迟很可能不同。- 当两者状态本应同时变化时,一个快一点,一个慢一点,在变化的瞬间,异或门的输入端就会短暂地出现“0和0”或“1和1”之外的非预期状态,导致输出端产生一个非常窄的脉冲,即毛刺。
重要提示:在真实的ASIC或FPGA设计中,绝对不建议将组合逻辑产生的信号直接用作其他时序电路的时钟!这个
clk_out如果直接驱动其他触发器,毛刺可能导致严重的功能错误。那么怎么办呢?
5.3 解决方案:寄存器输出整形
标准的做法是对clk_out进行“再采样”或“整形”,用一个触发器来过滤掉毛刺。我们可以用原时钟clk_in的上升沿(或下降沿)来采样这个组合逻辑产生的clk_out_pre(我们重命名一下),得到一个干净的、寄存器输出的clk_out_final。
wire clk_out_pre; // 组合逻辑产生的、可能有毛刺的时钟 reg clk_out_reg; // 寄存器整形后的干净时钟 assign clk_out_pre = clk_p ^ clk_n; always @(posedge clk_in or negedge rst_n) begin if (!rst_n) clk_out_reg <= 1'b0; else clk_out_reg <= clk_out_pre; // 用clk_in采样 end assign clk_out = clk_out_reg; // 输出整形后的时钟这样,clk_out的跳变将只发生在clk_in的上升沿,其稳定性由触发器的建立保持时间保证,彻底消除了毛刺。代价是clk_out相对于clk_out_pre会有最多一个clk_in周期的延迟,并且其占空比可能不再是精确的50%(因为采样可能发生在跳变沿附近),但仍然是接近50%的、非常稳定的时钟。在绝大多数应用中,这种微小的偏差是可以接受的。
5.4 面试中可能被追问的问题
“除了这种方法,还能怎么实现50%占空比的奇数分频?”
- 可以提到状态机结合双边沿控制的思想(方案三),并指出其在实际综合中的潜在问题。
- 还可以提到使用更高频的时钟进行计数然后内插,但这需要PLL或更高频时钟源,不属于纯数字分频范畴。
“你的输出时钟
clk_out能直接用作时钟吗?有什么风险?”- 这正是展示你工程经验的地方。明确指出组合逻辑产生时钟有毛刺风险,并提出寄存器整形的解决方案。这会让面试官觉得你考虑问题很周全。
“如果要求输出时钟与输入时钟同相位(即第一个上升沿对齐),该怎么修改?”
- 这需要调整计数器
cnt_n的初始值或clk_n的产生逻辑。你可以通过分析波形,让clk_p和clk_n的初始相位满足对齐要求。这考验的是对波形和状态关系的调试能力。
- 这需要调整计数器
“如何将这个设计扩展到任意奇数分频(比如5分频、7分频)?”
- 原理相同:分别用上升沿和下降沿产生两个占空比为 (N-1)/N 的分频时钟(N为分频系数),并使它们相位差半个输出周期,最后异或。计数器的位宽需要调整,状态控制逻辑也需要相应修改。
6. 从三分频看数字IC设计素养
通过这个具体的“三分频”项目,我们可以提炼出数字IC设计工程师必备的几种核心素养,这些也是面试官通过“手撕代码”真正想考察的东西。
6.1 从需求到规格的转化能力
面试官给出一个模糊的“三分频”,你能主动识别出“占空比”、“同步/异步复位”、“有无毛刺”等关键规格缺口。在实际工作中,客户或系统工程师的需求文档(Spec)也常常是不完整的,需要设计者主动沟通、澄清、定义边界条件。这种“把模糊想法变成清晰电路指标”的能力,是资深工程师和新手的显著区别。
6.2 方案权衡与折中的意识
我们分析了三种方案。方案一简单但占空比不理想;方案二(错相叠加)功能完美但结构稍复杂且有毛刺风险;方案三思路巧妙但实现别扭。没有绝对的最优解,只有最适合当前场景的解。面试中,清晰地阐述不同方案的优缺点,并根据假设的“场景”(如“对面积极其敏感”、“对时钟质量要求极高”)做出选择,能体现你的工程判断力。在实际项目中,这种权衡无处不在:性能 vs 面积 vs 功耗 vs 交付时间。
6.3 对“时钟”的敬畏之心
时钟是数字电路的脉搏。通过这个设计,我们深刻体会到:
- 时钟质量至关重要:毛刺是时钟的毒药。任何可能产生时钟的组合逻辑都必须慎之又慎,最好通过寄存器输出。
- 时钟域:我们这个设计其实产生了一个新的时钟域
clk_out。如果后续有信号要从clk_in域传到clk_out域,就涉及到跨时钟域(CDC)处理,这是一个更复杂的话题。在面试中,如果你能主动提到“这个三分频模块的输出是一个新的时钟域,在实际系统集成时需要考虑CDC问题”,绝对是巨大的加分项。 - 时钟偏移与抖动:即使我们用了寄存器整形,
clk_out相对于clk_in的延迟(Latency)和偏移(Skew)也需要在系统级时序分析中考虑。
6.4 代码即电路的思想
写Verilog不是写软件。每一行代码都对应着综合后的实际电路元件(触发器、与门、或门、选择器等)和它们之间的连接。
always @(posedge clk)对应一个由时钟上升沿触发的触发器组。always @(negedge clk)对应下降沿触发的触发器组。在同一个模块中混合使用上升沿和下降沿敏感的逻辑,需要格外小心时序。assign语句对应连续赋值,是组合逻辑。if-else和case语句会被综合成多路选择器(MUX)。 在写代码时,脑子里要同步想象出大致的电路结构。这能帮你避免写出无法综合或综合结果很差的代码。
7. 举一反三:从三分频到通用奇数分频模块设计
掌握了三分频,我们可以很容易地将设计推广到一个参数化的通用奇数分频模块。这不仅体现了代码的复用性,也展示了你的系统思维。
7.1 参数化设计
我们可以使用Verilog的parameter来定义分频系数N,要求N为大于1的奇数。
module clk_div_odd_50percent #( parameter ODD_N = 3 // 分频系数,必须为奇数 )( input wire clk_in, input wire rst_n, output wire clk_out ); // 检查参数合法性(仿真时有用) initial begin if (ODD_N <= 1 || ODD_N[0] == 1'b0) begin $error("Error: ODD_N must be an odd number greater than 1. Current value is %0d", ODD_N); end end // 计算计数器位宽 localparam CNT_WIDTH = $clog2(ODD_N); reg [CNT_WIDTH-1:0] cnt_p, cnt_n; reg clk_p_reg, clk_n_reg; wire clk_p, clk_n; wire clk_out_pre; reg clk_out_reg; assign clk_p = clk_p_reg; assign clk_n = clk_n_reg; // 上升沿计数器与clk_p生成 always @(posedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt_p <= {CNT_WIDTH{1'b0}}; clk_p_reg <= 1'b0; end else begin if (cnt_p == (ODD_N - 1)) cnt_p <= {CNT_WIDTH{1'b0}}; else cnt_p <= cnt_p + 1'b1; // 生成占空比为 (ODD_N-1)/ODD_N 的 clk_p // 高电平持续 (ODD_N-1) 个 clk_in 周期 if (cnt_p < (ODD_N - 1)) clk_p_reg <= 1'b1; else clk_p_reg <= 1'b0; end end // 下降沿计数器与clk_n生成 // clk_n需要与clk_p错开 (ODD_N/2) 个计数周期(取整) // 对于N=3, (3/2)=1.5,取整后相位差1或2?这里需要精确计算。 // 更通用的方法是:让clk_n在cnt_n为 (ODD_N>>1) 到 (ODD_N-1) 时为高。 // 即高电平从中间点开始。 always @(negedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt_n <= {CNT_WIDTH{1'b0}}; clk_n_reg <= 1'b0; end else begin if (cnt_n == (ODD_N - 1)) cnt_n <= {CNT_WIDTH{1'b0}}; else cnt_n <= cnt_n + 1'b1; // 生成clk_n,高电平从计数中点开始 if (cnt_n >= (ODD_N>>1)) // (ODD_N>>1) 是整数除法,例如3>>1=1 clk_n_reg <= 1'b1; else clk_n_reg <= 1'b0; end end // 组合逻辑异或与寄存器整形 assign clk_out_pre = clk_p ^ clk_n; always @(posedge clk_in or negedge rst_n) begin if (!rst_n) clk_out_reg <= 1'b0; else clk_out_reg <= clk_out_pre; end assign clk_out = clk_out_reg; endmodule这个通用模块的关键在于clk_n生成逻辑的通用化。我们让clk_n在计数器值大于等于ODD_N>>1(即ODD_N除以2取整)时为高。对于 N=3,ODD_N>>1等于1,所以clk_n在cnt_n为1和2时为高,这与我们之前手写的三分频代码一致。对于 N=5,ODD_N>>1等于2,clk_n在cnt_n为2,3,4时为高。通过这种对称的设计,clk_p和clk_n的异或就能产生接近50%占空比的输出。需要注意的是,由于取整操作,当N不是2的幂次减一时,占空比可能不是精确的50%,但会非常接近,对于大多数应用已足够。
7.2 测试与验证
对于参数化模块,我们需要用不同的参数进行实例化测试。
module tb_clk_div_odd(); reg clk, rst_n; wire clk_out_3, clk_out_5, clk_out_7; clk_div_odd_50percent #(.ODD_N(3)) uut3 (.clk_in(clk), .rst_n(rst_n), .clk_out(clk_out_3)); clk_div_odd_50percent #(.ODD_N(5)) uut5 (.clk_in(clk), .rst_n(rst_n), .clk_out(clk_out_5)); clk_div_odd_50percent #(.ODD_N(7)) uut7 (.clk_in(clk), .rst_n(rst_n), .clk_out(clk_out_7)); initial begin clk = 0; forever #5 clk = ~clk; end initial begin rst_n = 0; #100 rst_n = 1; #500 $finish; end initial begin $dumpfile("wave_odd.vcd"); $dumpvars(0, tb_clk_div_odd); end endmodule在仿真中观察clk_out_3、clk_out_5、clk_out_7的波形,验证其周期和占空比是否符合预期。这种举一反三的能力,是面试官非常看重的。