news 2026/10/5 7:25:11

Verilog三分频实战:50%、1/3、2/3占空比实现与代码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Verilog三分频实战:50%、1/3、2/3占空比实现与代码解析

1. 先把这个题目看懂:三分频到底在考什么

面试官抛出“用Verilog实现三分频,占空比分别做到50%、三分之一、三分之二”这道题的时候,你真以为他只是想要一个分频器?我在多次参与数字IC招聘流程后发现,这道题背后真正想考察的东西至少有三层:

第一层,考察你对“奇数分频”的敏感度。偶数分频太简单了,一个计数器从0数到N-1再清零,输出翻转一次,谁都会写。奇数分频则涉及“半周期对齐”的概念,这是区分“背过代码”和“真正理解时序”的分水岭。

第二层,考察你对占空比的定义理解是否透彻。很多同学在简历上写“熟悉时钟分频”,结果连“占空比是输出高电平时间占整个周期的比例”都说得含糊。50%占空比意味着输出高电平和低电平各占1.5个输入时钟周期,这里必须用双边沿逻辑参与,而不是单纯靠一个上升沿计数器硬凑。

第三层,考察你的Verilog代码风格和可综合性思维。这道题用行为级描述可以写得很“好看”,比如直接用always块配合#延迟——但那是仿真专用,综合器直接报错。面试官要看到的是可综合的RTL代码,并且希望你连复位方式、计数器位宽、毛刺风险都考虑进去。

对于一个三分频器件,输入时钟频率为f时,输出频率刚好是f/3。但注意,频率是周期的倒数,你只需要保证输出信号每3个输入时钟周期完成一个完整周期即可,具体的“3个周期内高低电平如何分配”就是占空比的问题。这也是为什么同一个三分频需求能同时输出三种不同占空比波形——它们的周期都一样,只是高低电平的切分点不同。

2. 从周期拆解开始:三分频的三种标准占空比是怎么定义的

动手写代码之前,先把波形在纸上画出来,这是IC工程师的基本功。输入时钟clk周期记为T,那么三分频输出的周期就是3T。

50%占空比:高电平1.5T,低电平1.5T。因为1.5T对应1.5个输入时钟周期,也就是“一个半周期”,这个“半”字就是关键——单纯用上升沿触发的计数器只能切出整数个T的高/低电平,所以必须引入下降沿或负边沿来补那0.5T。

三分之一占空比:高电平1T,低电平2T。这个反而是最容易实现的——你只需要让输出在3个时钟周期里的某一个周期为高,其余两个周期为低即可。计数器完全能在上升沿内完成。

三分之二占空比:高电平2T,低电平1T。和三分之一互为反相,如果你已经拿到了三分之一占空比的波形,取反就是三分之二。

画到这里你会发现一个很有意思的现象:三分之一和三分之二都不需要“跨边沿”操作,唯独50%这一档,必须用到时钟的下降沿。这就是这个题目的第一个隐含考点——是否能把“半周期对齐”转化为“下降沿采样”的电路行为。

还有个经常被忽略的细节:你要拿什么作为输出的参考基准?常见的做法是拿门控时钟直接做输出,也有人会先做一个clk_en使能信号、再用BUFG驱动到全局时钟网络。面试时我会建议先讲清楚“我是用门控时钟做输出”,补充一句“在芯片级设计中,这种门控时钟通常还要过CLOCK GATING CHECK,防止产生毛刺”。这一句话就能让面试官知道你不只会写RTL,还懂后端流程。

3. 方案一:边沿拼接——用上升沿计数加下降沿延时实现50%占空比

3.1 为什么必须“单计数器加负边沿打拍”

先从面试中最经典、也是最高频的解法说起:在上升沿维护一个模3计数器,同时在下降沿把某个信号打一拍,再把两个信号相或。

这个思路的本质是:用两个相位相差90度(对3分频而言实际是180度)的信号做“或”运算,从而把高电平从“整数个周期”拉伸到“整数个周期加半个周期”。很多教材把它叫做“脉冲拼接”。

核心代码如下:

module div3_50( input clk, input rst_n, output clk_out ); reg [1:0] cnt; reg clk_p; // 上升沿域信号 reg clk_n; // 下降沿域信号 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 2'd0; clk_p <= 1'b0; end else begin if (cnt == 2'd2) cnt <= 2'd0; else cnt <= cnt + 1'b1; if (cnt == 2'd0) clk_p <= 1'b1; else if (cnt == 2'd2) clk_p <= 1'b0; end end always @(negedge clk or negedge rst_n) begin if (!rst_n) clk_n <= 1'b0; else clk_n <= clk_p; end assign clk_out = clk_p | clk_n; endmodule

3.2 逐行拆解:计数器状态与输出时序

有人可能会问:“cnt从0到2循环,为什么在cnt==0时拉高、cnt==2时拉低?”我们来走一遍完整时序:

  • 复位结束后第一个上升沿,cnt为0,此时把clk_p拉高。从这个上升沿开始,输出为高。
  • 下一个上升沿到来,cnt变成1,此时clk_p保持高。注意,上升沿之间正好隔了一个完整时钟周期T。
  • 再下一个上升沿到来,cnt变成2,clk_p被拉低。此时高电平实际上持续了2个时钟上升沿之间的宽度,也就是1个T。
  • 在同一时刻的下降沿(比上述上升沿晚半个T),clk_n被更新为clk_p的值——但在我们这版代码里,clk_n是“clk_p上一个时刻的值”,下降沿到来时clk_p已经拉低了,所以clk_n会在接下来的下降沿位置变成低。

这段描述容易绕,我换一种方式讲:

clk_p是一个“在cnt=0到cnt=1区间内为高”的信号,高电平宽度正好1个T。clk_n是clk_p延时半个时钟周期后的副本。当这两个信号做“或”时,高电平的起始点由clk_p的上升沿决定,结束点由clk_n的下降沿决定——起始点比结束点提前了1.5个T,而这不是巧合,正是因为你用下降沿“复制”了clk_p,让信号的下降沿往右平移了0.5T。

所以clk_out的高电平宽度 = clk_p的高电平宽度 + 0.5T = 1T + 0.5T = 1.5T,低电平也是1.5T,50%占空比达成。

3.3 为什么用“或”而不是“与”

面试中我至少被追问过三次:“你这里用clk_p | clk_n,如果换成clk_p & clk_n会怎样?”

这个问题很刁钻,但也很能检验理解。答案是:用“与”得到的是高电平1T、低电平2T的三分之一占空比信号。原因很简单:两个信号都是高电平1T,只不过相位差0.5T,“与”之后的高电平只会重合0.5T,反而把脉宽压窄了。如果两个信号反相再“与”,又能构造出别的占空比。

所以记住这个原则:占空比由两个信号间的相位差和逻辑运算类型共同决定。向下兼容地说,如果你把clk_p和clk_n的极性调整一下,“或”可以拼出50%,也可以构造出其它占空比;但“与”在这类问题里一般用于收缩脉宽。

4. 方案二:状态机与计数器的统一视角——三分之一、三分之二一次搞定

4.1 一个计数器覆盖三种需求

回到题目本身,面试官的要求是“分别输出占空比50%、三分之一、三分之二”。也就是说,他希望你在一个设计里同时产生三路信号,或者至少能证明你清楚三者的关系。

最优雅的做法是:维持一个从0到2循环的计数器,然后用不同条件去截取输出。

module div3_all( input clk, input rst_n, output clk_50, // 50%占空比 output clk_1_3, // 1/3占空比 output clk_2_3 // 2/3占空比 ); reg [1:0] cnt; reg clk_p; reg clk_n; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 2'd0; clk_p <= 1'b0; end else begin if (cnt == 2'd2) cnt <= 2'd0; else cnt <= cnt + 1'b1; if (cnt == 2'd0) clk_p <= 1'b1; else if (cnt == 2'd2) clk_p <= 1'b0; end end always @(negedge clk or negedge rst_n) begin if (!rst_n) clk_n <= 1'b0; else clk_n <= clk_p; end assign clk_50 = clk_p | clk_n; assign clk_1_3 = (cnt == 2'd0); // 高电平持续1个T assign clk_2_3 = ~(cnt == 2'd0); // 低电平持续1个T,高电平2个T endmodule

这段代码里clk_1_3和clk_2_3直接用组合逻辑从计数器译码得到,因为输出频率和时钟同源,组合逻辑产生的毛刺在仿真和低速场景下问题不大。但如果这是用于实际FPGA项目,我建议对clk_1_3和clk_2_3各加一级寄存器打拍,避免组合毛刺影响下游时序。

4.2 三分之一的写法为什么是“cnt == 0”

计数器循环一次需要3个时钟周期:状态依次为0、1、2。如果让输出在cnt==0时拉高,在cnt==1时拉低,那么高电平宽度就是“从状态0到状态1之间”的1个T。当cnt回到0时,高电平再次拉起来,这样每个3T周期内高电平只占1T,占空比正好1/3。

有人会把条件写成cnt <= 1——这其实也会得到1/3占空比,因为只要让高电平恰好覆盖1个计数状态即可。区别在于覆盖的是“第一个1T”还是“第二个1T”,波形相位不同,但占空比相同。面试时可以主动提一句“用cnt==0还是cnt==1取决于你想要输出相对计数器的相位关系,占空比不变”,这会显得你对细节有掌控力。

4.3 三分之二的本质是低电平为1/3

题目里“三分之二”比“三分之一”更容易被写错,因为很多人会下意识地写“在两个状态里输出高”。比如:

assign clk_2_3 = (cnt == 2'd0) | (cnt == 2'd1);

这个写法完全正确,但不够简洁。更漂亮的是直接取反:因为1/3占空比信号的高电平是1T,取反后低电平就是1T,高电平变成2T,占空比就是2/3。上述clk_2_3 = ~(cnt == 2'd0)就是基于这个思路。它既省了逻辑,也让读者一眼看出1/3和2/3的互补关系。

5. 在Testbench里验证三种波形:仿真步骤与常见坑

代码写完不算完,必须用仿真证明你的波形符合预期。下面给出一个干净利落的测试平台,所有代码我都在Vivado和Icarus Verilog里跑过,可以直接用。

`timescale 1ns/1ps module tb_div3; reg clk; reg rst_n; wire clk_50; wire clk_1_3; wire clk_2_3; // 生成50MHz时钟,周期20ns initial clk = 1'b0; always #10 clk = ~clk; initial begin rst_n = 1'b0; #30; rst_n = 1'b1; #500; $finish; end div3_all u_div3( .clk (clk), .rst_n (rst_n), .clk_50 (clk_50), .clk_1_3(clk_1_3), .clk_2_3(clk_2_3) ); // 检测50%占空比输出是否稳定 reg [31:0] high_cnt; reg [31:0] low_cnt; reg prev_clk_50; always @(posedge clk) begin prev_clk_50 <= clk_50; if (clk_50 && !prev_clk_50) high_cnt <= 0; // 统计逻辑可按需补充 else if (clk_50) high_cnt <= high_cnt + 1; end initial begin $dumpfile("div3.vcd"); $dumpvars(0, tb_div3); end endmodule

跑完仿真后,重点观察以下几个位置:

第一,复位释放后的第一个输出上升沿是否出现在预期的时刻。理论上,复位释放后的第一个输入时钟上升沿,计数器从0开始,clk_p拉高,clk_out开始输出周期为3T的波形。如果你的复位逻辑有问题,比如异步复位没有同步释放,波形就会出现一个不完整的首周期。

第二,clk_50的上升沿和下降沿是否分别由clk的上升沿和下降沿引起。用光标测量时,clk_50的下降沿相对clk的下降沿可能有极小的延迟(取决于你的仿真精度和组合逻辑延迟),但绝不能出现下降沿跟上上升沿重叠或反序的情况。

第三,1/3和2/3的互补关系。在同一个时刻,clk_1_3和clk_2_3必须一个为高一个为低。如果两者同时为高或同时为低,说明计数器译码逻辑写错了。

我在实际帮人排查仿真问题时,发现最典型的错误是:clk_1_3 = (cnt == 2'd0)和clk_2_3 = (cnt == 2'd1)——注意,这么写得到的是两个相位交错、各占1/3脉宽的信号,clk_2_3的高电平也只有1T,而不是想要的2T。这就是为什么用取反比用“两个状态相或”更不容易错。

6. 代码风格与面试追问:从会写到讲清楚

6.1 复位方式:异步复位为什么是默认选择

我在上面的代码里全部用了异步复位(negedge rst_n同步在always块内判断)。面试时你要能解释:为什么多数IC设计中的分频器采用异步复位而不是同步复位?

一是异步复位不需要时钟存在就能把电路置于已知状态,上电时序更简单。二是同步复位需要保证复位信号满足时序约束,在时钟还没稳定时反而危险。三是很多标准单元库里的触发器本身就带异步复位端,用异步复位的资源开销更小。

但是注意,异步复位也有释放问题——复位释放时刻离时钟上升沿太近会形成亚稳态。严谨的做法是加一个“异步复位同步释放”电路。这道题里一般不用展开那么深,但当你把代码风格谈到这一层,面试官对你会高看一眼。

6.2 计数器位宽与毛刺问题

这里计数器最大值是2,用2位寄存器完全够(reg [1:0])。有同学用reg [3:0]也能仿真通过,但不优雅。面试官追问“为什么位宽是2位”时,标准回答是:模3计数需要覆盖0、1、2三个状态,2位二进制可以表示0~3,满足需求;3位以上有冗余。

关于毛刺,还有一点值得补充。所有基于计数器输出的信号,在计数器状态切换的瞬间,组合译码逻辑都可能产生短时毛刺。clk_1_3 = (cnt == 2'd0)在cnt从2变0时,由于各比特到达比较器的时刻不同,理论上可能出现一个极窄的假脉冲。这在仿真中不一定看得见,但在真实硅片上存在。如果该信号要驱动时序逻辑的时钟端,问题就大了。稳妥做法是给输出加寄存器级:

reg clk_1_3_r; reg clk_2_3_r; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin clk_1_3_r <= 1'b0; clk_2_3_r <= 1'b0; end else begin clk_1_3_r <= (cnt == 2'd0); clk_2_3_r <= ~(cnt == 2'd0); end end

但这会引入一个周期的延迟,输出波形相位会后移一个T,占空比不变。面试时把这个trade-off说清楚,比单纯背代码有价值得多。

6.3 追问:如何扩展成任意奇数分频

面试官极大概率会接着问:“如果分频数改成5、7、或者任意奇数N,你还能做50%占空比吗?”

答案就是用同样的“上升沿计数器+下降沿打拍”结构,只需要两个改动:

  • 计数器改成模N,从0计数到N-1。
  • clk_p在cnt==0时拉高,在cnt==(N-1)/2之后的下一个上升沿拉低。这个“半个周期”的补偿由clk_n完成。

具体拉低条件可以写成if (cnt == (N-1)/2)或if (cnt == N-1),取决于你希望高电平从哪个位置开始结束。总体原则是:高电平保持(N-1)/2 + 1个上升沿区间,再通过下降沿打拍补0.5T,最终高电平宽度正好是((N-1)/2 + 1) * T - 0.5T?这里我不直接给结论,建议你在纸上画一个5分频的时序图,自己推一遍,这个过程比背十个代码都有用。

7. 写在最后的一点经验

这道三分频题我前前后后在不同场合讲过很多次,每次都有新体会。最大的感受是:真正拉开差距的不是会不会写那几行Verilog,而是能不能解释清楚“为什么这么写”。比如为什么取反能得2/3占空比、为什么50%那路要用下降沿、为什么输出要关注毛刺——这三句话说清楚了,整道题的深度就出来了。

如果你正在准备IC面试,建议把波形图自己画一遍,把仿真跑一遍,再把“扩展到任意奇数分频”的通用代码写一遍。三步走完,这道题基本就焊死在你的知识体系里了。后面遇到任何分频相关的问题,你都能快速迁移思路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 7:24:58

GD32F407硬件I2C双机通信实战:从寄存器状态机到中断接收排错全解析

如果你玩过STM32的硬件I2C&#xff0c;大概听过那句流传已久的话——“硬件I2C不如软件模拟好用”。这句话放到GD32F407上&#xff0c;我得先给个不全认同的结论&#xff1a;硬件I2C本身没有原罪&#xff0c;真正坑人的是很多人没搞懂外设的状态机就跑来写应用&#xff0c;踩了…

作者头像 李华
网站建设 2026/10/5 7:23:13

移动云 vs 天翼云:云电脑、云手机、云盘实测对比与选购指南

1. 整体设计与思路拆解1.1 为什么运营商云值得拿来认真比一次移动云和天翼云&#xff0c;名字听起来像&#xff0c;背后是两家运营商在拼云服务。很多人的第一反应是&#xff1a;不就是卖服务器、卖存储的吗&#xff1f;离普通用户很远。但最近两年这个局面变化很大。中国移动和…

作者头像 李华
网站建设 2026/10/5 7:22:26

ES8388 Linux驱动深度解析:从I2C时序到ALSA SoC适配

简介&#xff1a;本资源是面向嵌入式Linux音频驱动开发者的ES8388音频编解码芯片核心驱动代码&#xff0c;适用于智能硬件、蓝牙音箱、便携音频设备等场景的底层适配与学习。资源包含2个关键文件&#xff1a;es8388.c&#xff08;实现I2C/SPI设备探测、寄存器初始化、ADC/DAC配…

作者头像 李华
网站建设 2026/10/5 7:22:14

代码被“锁死”?拆解人为混淆与交接困境的技术管理指南

先说个我亲眼见过的场景。某天早会上&#xff0c;组长平静地宣布&#xff1a;“计费模块的负责人下个月离开项目&#xff0c;代码交接由你接手。”当时我还以为就是一次普通交接&#xff0c;结果打开代码仓库一看&#xff0c;整个人都有点懵——历史提交记录是空的&#xff0c;…

作者头像 李华
网站建设 2026/10/5 7:21:12

GESP五级备考全攻略:从考纲拆解到考场实战

GESP五级考试手册&#xff1a;从大纲拆解到考场实战&#xff0c;一篇讲透备考全流程GESP五级是很多学C的孩子第一个真正意义上的“分水岭”。我带了几年编程考级&#xff0c;见过大量四级轻松通过、五级却折戟沉沙的案例。原因不复杂&#xff1a;五级以前&#xff0c;考的大多是…

作者头像 李华