尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

数字电路三分频原理与FPGA实现:从状态机到工程实践

数字电路三分频原理与FPGA实现:从状态机到工程实践 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, 22个触发器可以提供2^24种状态组合足够用了。我们可以用状态编码来实现例如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)001 (S1)10 (S2)110 (S2)00 (S0)111 (无效)00 (S0) [自启动]X (无关)在最后一行我们强制规定了如果电路进入无效状态11下一个状态跳转到00这实现了自启动功能。现在我们有两个需要求解的方程D1 f(Q1, Q0)和D0 f(Q1, Q0)。同时输出CLK_out g(Q1, Q0)。 根据转换表对于D1当(Q1,Q0)为01时D11为10时D10为00时D10为11时D10。通过卡诺图化简这里省略化简过程可以得到一个相对简单的表达式。一个常见的化简结果是D1 Q0。让我们验证一下状态01时Q01所以D11正确状态10时Q00所以D10正确状态00时Q00所以D10正确状态11时Q01但我们需要D10这里出现了矛盾这说明简单的D1Q0不能处理无效状态。因此我们必须严谨地使用卡诺图将无效状态11的下一状态指定为00纳入考虑。重新绘制D1和D0的卡诺图并化简后可以得到更准确的表达式这里直接给出一种常见解D1 Q0D0 ~Q1注~表示取反让我们验证这个解状态00(Q10, Q00):D10,D01- 下一状态01正确。状态01(Q10, Q01):D11,D01- 下一状态11等等这里出问题了。D0~Q11所以下一状态是11这不是我们期望的10。所以这个解不对。看来需要更仔细地求解。实际上通过完整的卡诺图化简一个可行的解是D1 (~Q1) Q0即D1 (not Q1) and Q0D0 ~(Q1 ^ Q0)即D0 not (Q1 xor Q0)或者D0 ~Q1 ~Q0 | Q1 Q0验证一下00:D1 (1)0 0,D0 ~(0^0)~01-01正确。01:D1 (1)1 1,D0 ~(0^1)~10-10正确。10:D1 (0)0 0,D0 ~(1^0)~10-00正确。11:D1 (0)1 0,D0 ~(1^1)~01-01。这里没有回到00而是去了01。但01是有效状态所以电路可以从无效状态11经过一个周期进入有效状态01这也是一种可接受的自启动方式。对于输出CLK_out从状态表可知当状态为01或10时输出1即CLK_out Q1 | Q0不对00时Q1|Q00正确01时Q1|Q01正确10时Q1|Q01正确11时Q1|Q01但我们不关心。所以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 2b00; end else begin case (state) 2b00: state 2b01; 2b01: state 2b10; 2b10: state 2b00; default: state 2b00; // 处理无效状态 endcase end end always (*) begin clk_out (state ! 2b00); // 状态非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]因为cnt1(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 2b00; end else begin if (cnt 2b10) // 如果计数到2 cnt 2b00; else cnt cnt 1; // 否则加1 end end // 50%占空比输出 always (*) begin clk_out (cnt ! 2b00); 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 2b10); // 仅在计数到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*27。这需要一个小型的状态机或计数器来控制。相位累加器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 2b00; end else begin if (cnt 2b10) cnt 2b00; else cnt cnt 1; end end assign pulse_3 (cnt 2b10); // 利用慢速计数器让LED的亮灭反映clk_16m和pulse_3的“活动” // 这里我们让LED每约0.5秒翻转一次当对应的信号有效时翻转更快一些仅演示逻辑 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin counter_led 24d0; led_clk 1b0; led_pulse 1b0; 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 1b1; // 脉冲来时亮 end else if (counter_led[20]) begin // 脉冲过后在下个慢速点灭 led_pulse 1b0; 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的幂次方分频这一常见需求的基本技能。
返回列表