1. 从“分频”说起:为什么我们需要三分频?
在电子电路的世界里,“分频”这个词听起来有点抽象,但它的应用却无处不在。简单来说,分频就是把一个频率较高的信号,通过特定的电路处理,变成一个或多个频率较低的信号。最常见的例子就是我们的数字时钟,一个高精度的石英晶体振荡器产生一个频率非常稳定的高频信号(比如32.768kHz),然后通过一系列分频电路,最终得到我们看到的秒、分、时信号。这就像一个大齿轮带动一系列小齿轮,每个小齿轮的转速(频率)都比前一个慢。
那么,为什么要“三分频”?这通常意味着我们需要将一个输入信号的频率精确地除以3。这个需求在数字逻辑设计、通信系统、音频处理甚至一些特殊的控制电路中都非常常见。比如,在一个系统中,主时钟是60MHz,但某个外设模块只需要20MHz的时钟,这时一个三分频电路就是最直接、最经济的解决方案。又或者,在音频领域,为了生成特定的和弦或音效,需要从基频衍生出其三倍频或三分之一频的信号。因此,掌握三分频电路的实现,是深入理解数字逻辑和时序电路设计的一块重要基石。
与简单的二分频(除以2)不同,三分频电路的设计需要一点“技巧”。二分频天然可以用一个D触发器实现,因为每个时钟上升沿翻转一次输出,自然就实现了频率减半。但三分频要求每三个输入周期才输出一个完整周期,这需要电路具备“计数”和“状态记忆”的能力。因此,实现三分频是学习状态机设计的一个绝佳入门案例。接下来,我将带你从最基础的原理开始,逐步深入,用几种不同的方法来实现它,并分析各自的优劣和适用场景。
2. 核心原理:状态机是三分频的灵魂
要理解三分频,必须抛弃“一个触发器搞定”的简单思维,建立起“状态”的概念。我们期望的输出波形,假设占空比为50%(即高电平和低电平时间各占一半),那么在一个完整的输出周期内,它需要对应输入时钟的三个周期。这意味着电路必须能区分“这是输入的第几个周期”。
我们可以用状态机来清晰地描述这个过程。假设我们使用上升沿触发的电路,目标是产生一个50%占空比的三分频时钟。那么,输出信号的一个完整周期(比如从低电平开始)会经历以下过程:
- 在第一个输入时钟周期,输出为低电平。
- 在第二个输入时钟周期,输出变为高电平。
- 在第三个输入时钟周期,输出保持高电平。
- 在第四个输入时钟周期(即下一个循环的第一个周期),输出回到低电平,如此循环。
你看,输出信号的变化并不是每个时钟沿都发生,它需要“记住”自己当前处于三个周期中的哪一个位置。这就是状态。我们可以定义三个状态:S0、S1、S2。
- S0状态:输出为0。当电路处于此状态时,下一个时钟沿到来后,它应该进入S1状态。
- S1状态:输出为1。当电路处于此状态时,下一个时钟沿到来后,它应该进入S2状态。
- S2状态:输出为1。当电路处于此状态时,下一个时钟沿到来后,它应该回到S0状态。
这个状态转换图清晰地描绘了三分频的逻辑。实现这个状态机,至少需要两个触发器(比如D触发器),因为要表示3个不同的状态(0, 1, 2),2个触发器可以提供2^2=4种状态组合,足够用了。我们可以用状态编码来实现,例如:
- S0 编码为
00(输出0) - S1 编码为
01(输出1) - S2 编码为
10(输出1) 11这个状态是无效的,我们需要在设计中确保电路不会进入这个状态,或者即使意外进入也能自动恢复(即设计一个自启动电路)。
有了状态机和编码,接下来就是设计组合逻辑电路,根据当前状态(两个触发器的当前输出Q1, Q0),来生成下一个时钟沿后触发器应该被赋予的值(D1, D0),以及最终的输出信号。这通常通过列真值表、画卡诺图、化简逻辑表达式来完成。这就是同步时序电路设计的标准流程。
注意:上面描述的是输出50%占空比的情况(高电平持续1.5个输入周期)。有时我们可能只需要一个脉冲信号,占空比为1/3(高电平持续一个输入周期),其状态机设计会有所不同,通常更简单。本文主要探讨更通用的50%占空比方案。
3. 经典实现一:基于D触发器的同步状态机设计
这是最标准、最可靠,也最易于在FPGA或ASIC中综合实现的方法。我们使用两个上升沿触发的D触发器,以及一些门电路构成组合逻辑网络。
3.1 设计步骤详解
首先,我们定义状态和输出。如前所述:
- 状态
Q1 Q0 = 00: 记为S0,输出CLK_out = 0。 - 状态
Q1 Q0 = 01: 记为S1,输出CLK_out = 1。 - 状态
Q1 Q0 = 10: 记为S2,输出CLK_out = 1。 - 状态
11: 无效状态,我们希望电路能避免或逃离它。
接下来,我们列出状态转换表。它描述了在当前状态和输入时钟沿作用下,下一个状态是什么。
| 当前状态 (Q1 Q0) | 下一状态 (D1 D0) | 输出 (CLK_out) |
|---|---|---|
| 00 (S0) | 01 (S1) | 0 |
| 01 (S1) | 10 (S2) | 1 |
| 10 (S2) | 00 (S0) | 1 |
| 11 (无效) | 00 (S0) [自启动] | X (无关) |
在最后一行,我们强制规定了如果电路进入无效状态11,下一个状态跳转到00,这实现了自启动功能。
现在,我们有两个需要求解的方程:D1 = f(Q1, Q0)和D0 = f(Q1, Q0)。同时,输出CLK_out = g(Q1, Q0)。 根据转换表:
- 对于
D1:当(Q1,Q0)为01时,D1=1;为10时,D1=0;为00时,D1=0;为11时,D1=0。通过卡诺图化简(这里省略化简过程),可以得到一个相对简单的表达式。一个常见的化简结果是:D1 = Q0。让我们验证一下:状态01时Q0=1,所以D1=1,正确;状态10时Q0=0,所以D1=0,正确;状态00时Q0=0,所以D1=0,正确;状态11时Q0=1,但我们需要D1=0,这里出现了矛盾!这说明简单的D1=Q0不能处理无效状态。
因此,我们必须严谨地使用卡诺图,将无效状态11的下一状态指定为00纳入考虑。重新绘制D1和D0的卡诺图并化简后,可以得到更准确的表达式(这里直接给出一种常见解):D1 = Q0D0 = ~Q1(注:~表示取反)
让我们验证这个解:
- 状态
00(Q1=0, Q0=0):D1=0,D0=1-> 下一状态01,正确。 - 状态
01(Q1=0, Q0=1):D1=1,D0=1-> 下一状态11?等等,这里出问题了。D0=~Q1=1,所以下一状态是11,这不是我们期望的10。所以这个解不对。
看来需要更仔细地求解。实际上,通过完整的卡诺图化简,一个可行的解是:D1 = (~Q1) & Q0(即:D1 = (not Q1) and Q0)D0 = ~(Q1 ^ Q0)(即:D0 = not (Q1 xor Q0),或者D0 = ~Q1 & ~Q0 | Q1 & Q0)
验证一下:
00:D1 = (1)&0 = 0,D0 = ~(0^0)=~0=1->01,正确。01:D1 = (1)&1 = 1,D0 = ~(0^1)=~1=0->10,正确。10:D1 = (0)&0 = 0,D0 = ~(1^0)=~1=0->00,正确。11:D1 = (0)&1 = 0,D0 = ~(1^1)=~0=1->01。这里没有回到00,而是去了01。但01是有效状态,所以电路可以从无效状态11经过一个周期进入有效状态01,这也是一种可接受的自启动方式。
对于输出CLK_out,从状态表可知,当状态为01或10时输出1,即CLK_out = Q1 | Q0?不对,00时Q1|Q0=0正确,01时Q1|Q0=1正确,10时Q1|Q0=1正确,11时Q1|Q0=1(但我们不关心)。所以CLK_out = Q1 + Q0(逻辑或) 是可行的。更简洁地,因为状态00时输出0,其他有效状态都输出1,所以CLK_out = ~( (~Q1) & (~Q0) ) = Q1 | Q0。
3.2 电路连接与仿真
根据上述方程,我们可以搭建电路:
- 两个D触发器,时钟端都接输入时钟
CLK_in,复位端可根据需要连接(实现全局复位到00状态)。 - 触发器1(输出Q1)的输入D1,由
(~Q1) & Q0这个组合逻辑驱动。 - 触发器0(输出Q0)的输入D0,由
~(Q1 ^ Q0)这个组合逻辑驱动。 - 最终输出
CLK_out = Q1 | Q0。
使用Verilog或VHDL等硬件描述语言可以轻松实现并仿真。一个简单的Verilog描述如下:
module clk_div3 ( input wire clk_in, input wire rst_n, // 低电平复位 output reg clk_out ); reg [1:0] state; always @(posedge clk_in or negedge rst_n) begin if (!rst_n) begin state <= 2'b00; end else begin case (state) 2'b00: state <= 2'b01; 2'b01: state <= 2'b10; 2'b10: state <= 2'b00; default: state <= 2'b00; // 处理无效状态 endcase end end always @(*) begin clk_out = (state != 2'b00); // 状态非00时输出高 // 或者 clk_out = state[1] | state[0]; end endmodule这段代码直接使用了状态机描述,比门级描述更直观,综合器会自动将其优化为相应的逻辑门和触发器。
3.3 优缺点与实战心得
- 优点:完全同步设计,时序分析简单,可靠性高,没有毛刺问题(输出经过触发器同步),非常适合FPGA和数字IC设计。
- 缺点:需要两个触发器,逻辑相对二分频稍复杂。
- 实战心得1:复位的重要性。在实际硬件中,上电后触发器的状态是随机的,可能直接进入无效状态
11。因此,一个可靠的全局复位信号(如上例中的rst_n)至关重要,它能将电路强制初始化为已知的有效状态(如00)。如果没有复位,依赖于前面推导的“自启动”逻辑,虽然理论上最终能进入有效循环,但上电后的初始几个周期输出可能是混乱的,对于某些敏感系统可能是不可接受的。 - 实战心得2:关注输出时序。
clk_out是组合逻辑产生的(always @(*)块),这意味着它的变化会略晚于时钟沿,因为要经过门电路的延迟。如果这个信号要作为其他同步电路的时钟,可能会带来时序风险。更稳健的做法是再用一个触发器寄存一下输出,生成一个与输入时钟沿完全同步的clk_out_reg,虽然这会引入一个时钟周期的延迟,但消除了毛刺和时序不确定性。
4. 经典实现二:利用计数器与数值比较
对于不熟悉状态机或者习惯用计数器思维的设计者,用计数器实现三分频是另一种直观的选择。其核心思想是设计一个模3计数器,然后根据计数器的值来控制输出电平。
4.1 模3计数器的构建
一个模3计数器,就是从0计数到2,然后归零,循环往复。同样需要两个触发器来存储计数值(0,1,2)。我们可以直接用上面状态机的状态当作计数值:00->0,01->1,10->2。那么状态转换(计数)的逻辑就变成了:
- 当前是0 (00),下一个是1 (01)。
- 当前是1 (01),下一个是2 (10)。
- 当前是2 (10),下一个是0 (00)。
这组转换关系可以用一个加法器(加1)和取模逻辑来实现,但在硬件上,我们仍然需要推导触发器的驱动方程。有趣的是,你会发现这个计数序列和前面为50%占空比设计的状态机序列完全一样!所以,硬件电路本质上是一致的。区别在于我们对输出的定义。
4.2 基于计数器值的输出生成
要得到50%占空比的输出,我们需要在计数器值为1和2时输出高电平,值为0时输出低电平。即CLK_out = (cnt >= 1)。对于0~2的计数范围,就是CLK_out = cnt[1] | cnt[0](因为cnt=1(01)和2(10)时,高位或低位总有一个为1)。这和状态机方案的输出逻辑完全一致。
如果只需要1/3占空比的脉冲,那就更简单了:CLK_out = (cnt == 2)。这样,每三个周期只在最后一个周期产生一个时钟宽的高脉冲。
4.3 硬件描述语言实现
用Verilog实现计数器版本非常简洁:
module clk_div3_counter ( input wire clk_in, input wire rst_n, output reg clk_out ); reg [1:0] cnt; // 计数范围0-2 always @(posedge clk_in or negedge rst_n) begin if (!rst_n) begin cnt <= 2'b00; end else begin if (cnt == 2'b10) // 如果计数到2 cnt <= 2'b00; else cnt <= cnt + 1; // 否则加1 end end // 50%占空比输出 always @(*) begin clk_out = (cnt != 2'b00); end endmodule4.4 方案对比与选型思考计数器方案和状态机方案在硬件实现上最终会综合成非常相似的电路。它们的核心区别在于设计思想层面:
- 状态机方案更侧重于“状态”的迁移和输出,适合描述控制密集型、状态转换复杂的逻辑。
- 计数器方案更侧重于“数值”的累加和判断,适合描述有明确计数、定时需求的逻辑。
对于三分频这个具体问题,两者等价。选择哪一种取决于设计者的思维习惯和代码的可读性。我个人在大多数情况下更倾向于计数器方案,因为它意图更明确,代码更简洁,直接表达了“数到3就归零”的核心逻辑。而在需要复杂输出模式(比如每个状态输出不同信号)时,状态机描述可能更清晰。
注意:上面的代码中
cnt != 2‘b00是一个组合逻辑比较。在高速时钟下,如果cnt的位宽增加,这个比较器可能会成为关键路径。对于高性能设计,有时会将输出也进行寄存器打拍,即always @(posedge clk_in) clk_out_reg <= (cnt != 2‘b00);,用clk_out_reg作为最终输出。
5. 进阶与变体:非50%占空比与小数分频
之前我们聚焦于50%占空比,这是最常用的情形。但实际需求可能更多样。
5.1 生成1/3占空比的脉冲信号
有时我们只需要一个周期性的脉冲,作为使能信号或触发信号。例如,每三个输入时钟周期产生一个单周期高脉冲。基于计数器方案,这极其简单:
// 产生占空比1/3的脉冲 always @(*) begin pulse_out = (cnt == 2'b10); // 仅在计数到2时输出高 end这个pulse_out信号非常适合用作下游模块的“时钟使能”(Clock Enable)。下游模块使用原始的快速时钟,但只在pulse_out为高时才采样或改变状态,这样既实现了三分频的效果,又避免了使用衍生时钟带来的时序问题,是更推荐的设计方法。
5.2 双边沿触发与奇数分频的占空比难题
我们之前讨论的都是基于时钟上升沿触发的同步电路。有没有可能用更少的器件实现三分频?有人会想到利用时钟的双边沿。例如,用三个串联的D触发器,通过巧妙的反馈,可以只用三个触发器实现三分频,但输出占空比往往不是50%。这类电路通常属于“行波计数器”或“约翰逊计数器”的变体,是异步电路。
一个经典的例子是使用约翰逊计数器(扭环形计数器)。一个3位的约翰逊计数器,其状态循环是:000->100->110->111->011->001->000…,循环长度是6。如果我们取其中某一位的输出,就能得到一个六分频且占空比为50%的时钟。但这并不是三分频。要得到三分频,需要对其中某些状态进行解码输出。这类设计通常会导致输出有毛刺,且对时钟的上升沿和下降沿都敏感,时序分析复杂,在高速或可靠性要求高的系统中不推荐使用。同步设计始终是首选。
5.3 小数分频简介
三分频是整数分频。有时我们需要小数分频,比如2.5分频、3.5分频。这通常通过两种技术实现:
- 双模分频:例如,要实现3.5分频(即输出周期是输入周期的3.5倍),可以在多个输入时钟周期内,让输出时钟高电平和低电平的时间不均匀。具体来说,可以设计一个序列,使得每7个输入时钟周期内,输出2个完整的周期(因为3.5*2=7)。这需要一个小型的状态机或计数器来控制。
- 相位累加器(DDS思想):这是更高级也更灵活的方法,常用于信号发生器。通过一个累加器在每个时钟周期累加一个步进值(与所需输出频率相关),累加器的溢出信号或最高位的变化就可以用来生成所需频率的时钟。通过调整步进值,可以实现任意频率的分频(包括非整数)。但这已经属于直接数字频率合成(DDS)的范畴,资源消耗比简单整数分频大得多。
对于绝大多数数字逻辑设计,整数分频(特别是2的幂次方和简单奇数分频)已经足够。小数分频通常只在特定的时钟生成芯片或高性能FPGA的时钟管理单元(如PLL、DLL)中才会直接用到。
6. 实际应用场景与设计考量
三分频电路不是一个孤立的练习题,它在实际系统中扮演着重要角色。
6.1 时钟域转换与时钟生成
这是最常见的应用。主控芯片(如FPGA)的外部晶振提供50MHz时钟,但外接的ADC芯片可能需要16.667MHz的采样时钟。一个简单的三分频电路(50MHz / 3 ≈ 16.667MHz)就可以满足需求。但这里有一个关键点:直接使用分频产生的时钟驱动外部芯片,必须考虑时钟的抖动(Jitter)和稳定性。我们上面设计的同步分频器,其输出时钟的边沿与输入时钟边沿是同步的,抖动性能较好。但如果经过的组合逻辑路径过长,可能会引入毛刺。因此,对于驱动片外器件,最好使用全局时钟缓冲器(如FPGA中的BUFG)来驱动分频后的时钟信号,并做好时序约束。
6.2 作为使能信号而非时钟
在现代同步设计中,一个重要的原则是:尽量减少时钟域的数量。随意使用分频时钟会产生新的时钟域,带来复杂的跨时钟域同步问题。因此,更优的做法是:使用原始高速时钟,但生成一个三分频的使能脉冲信号。
如前所述,我们可以生成一个每三个周期出现一次的高脉冲pulse_3。在系统顶层,所有模块都使用主时钟clk_50m。需要慢速操作的那个模块,在其内部逻辑中这样写:
always @(posedge clk_50m) begin if (pulse_3) begin // 只有当三分频使能有效时,才执行慢速逻辑 // ... 你的慢速逻辑代码 ... end end这种方法将所有逻辑都保持在同一个时钟域内,彻底避免了跨时钟域问题,是大型数字系统设计的推荐实践。
6.3 在音频与信号处理中的应用
在简单的数字音频合成或信号发生器中,三分频可以用来生成特定的谐波关系。例如,基频为f的信号,其三分频(f/3)是它的下谐波。通过组合不同分频比的信号,可以合成出复杂的波形。在这种情况下,对占空比可能没有严格要求,但对频率的准确性和稳定性有要求。此时,基于计数器的同步分频方案能提供非常精确的频率关系。
6.4 可靠性设计与验证要点
- 复位策略:必须设计可靠的全局复位电路,确保上电或复位后,分频器处于确定的状态(如
00)。 - 时序约束:在FPGA设计中,如果分频后的时钟
clk_out要作为时钟信号使用,必须为其创建相应的时序约束(create_generated_clock),告知时序分析工具它的来源和频率,否则静态时序分析(STA)会忽略该路径,可能隐藏建立/保持时间违例。 - 仿真覆盖:仿真时,不仅要测试正常计数循环,还要测试复位功能,以及如果可能,测试从无效状态(如
11)的自恢复能力。 - 功耗考虑:计数器或状态机中的触发器每个时钟周期都在翻转,会产生动态功耗。在低功耗设计中,如果不需要持续分频,可以考虑增加门控时钟逻辑,在不需要时关闭分频器的时钟输入。
7. 从理论到实践:在FPGA开发板上跑起来
让我们以一个具体的例子收尾,看看如何将三分频电路集成到一个小的FPGA项目中,并观察实际波形。
7.1 项目目标与平台假设我们使用一块带有50MHz晶振的FPGA开发板(如Altera/Intel Cyclone IV系列)。我们的目标是:
- 将50MHz时钟三分频,得到约16.667MHz的时钟。
- 同时,生成一个与之同步的、占空比1/3的使能脉冲。
- 用两个LED分别指示分频时钟和使能脉冲(由于频率太高人眼无法分辨闪烁,我们实际上用它们控制一个更慢的闪烁,以便观察)。
7.2 顶层模块设计
module top_div3 ( input wire clk_50m, // 板载50MHz时钟 input wire rst_n, // 板载复位按键(低有效) output reg led_clk, // 用于“观察”分频时钟的LED output reg led_pulse // 用于“观察”使能脉冲的LED ); wire clk_16m; // 三分频时钟信号 wire pulse_3; // 三分频使能脉冲 reg [23:0] counter_led; // LED闪烁用的慢速计数器 // 实例化三分频模块(采用计数器方案) clk_div3_counter u_div3 ( .clk_in(clk_50m), .rst_n(rst_n), .clk_out(clk_16m) ); // 生成使能脉冲(计数到2时有效) reg [1:0] cnt; always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt <= 2'b00; end else begin if (cnt == 2'b10) cnt <= 2'b00; else cnt <= cnt + 1; end end assign pulse_3 = (cnt == 2'b10); // 利用慢速计数器,让LED的亮灭反映clk_16m和pulse_3的“活动” // 这里我们让LED每约0.5秒翻转一次,当对应的信号有效时翻转更快一些(仅演示逻辑) always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin counter_led <= 24'd0; led_clk <= 1'b0; led_pulse <= 1'b0; end else begin counter_led <= counter_led + 1; // led_clk 在 clk_16m 为高时,翻转速率加快(模拟) if (clk_16m) begin if (counter_led[22]) // 使用计数器较高位,速度中等 led_clk <= ~led_clk; end else begin if (counter_led[23]) // 使用计数器最高位,速度最慢 led_clk <= ~led_clk; end // led_pulse 在 pulse_3 有效时,快速闪烁一次 if (pulse_3) begin led_pulse <= 1'b1; // 脉冲来时亮 end else if (counter_led[20]) begin // 脉冲过后,在下个慢速点灭 led_pulse <= 1'b0; end end end endmodule这个例子中,led_clk和led_pulse的行为并不直接显示16.667MHz的信号(那太快了),而是通过不同的闪烁模式来间接表明clk_16m和pulse_3信号在工作。真正的验证需要依靠仿真或逻辑分析仪。
7.3 仿真测试与上板验证使用ModelSim等工具进行仿真,编写测试平台(Testbench)提供clk_50m和rst_n激励。你会看到clk_16m的周期正好是clk_50m的3倍,且占空比为50%。pulse_3则是每三个时钟周期出现一个单周期高脉冲。
上板验证时,由于时钟频率高,直接观察LED无法确认。更可靠的方法是:
- 使用内部逻辑分析仪:如Xilinx的ILA或Intel的SignalTap,将
clk_50m、clk_16m、pulse_3等信号抓取出来,在软件中观察实际波形。 - 输出到GPIO用示波器测量:将
clk_16m信号分配到FPGA的普通IO引脚上,用示波器探头测量,可以直观看到16.667MHz的方波。这是最直接的验证方式。
7.4 可能遇到的问题与调试
- LED不亮或常亮:检查复位信号极性是否正确,开发板的按键是低有效还是高有效。检查引脚分配文件(.qsf或.xdc),确保时钟、复位、LED引脚分配正确。
- 示波器测量无信号或频率不对:首先用逻辑分析仪确认FPGA内部信号是否正确。如果内部正确但输出不对,可能是IO引脚约束问题,如未设置输出驱动强度、未指定IO标准(如LVCMOS3.3V)。也可能是探头接触不良或示波器设置不当(如触发模式、幅度档位)。
- 时序警告:如果工具报告
clk_16m路径有时序问题,而它又被用作其他触发器的时钟,请确保已经使用create_generated_clock命令对其进行了约束。更好的做法是如前所述,使用pulse_3使能信号替代衍生时钟。
通过这个完整的流程——从原理推导、代码编写、仿真验证到硬件调试,一个理论上的三分频电路就真正成为了你数字系统中的一个可靠部件。掌握它,你就掌握了处理非2的幂次方分频这一常见需求的基本技能。