第二章:数字电路基础回顾——布尔代数、组合逻辑、时序逻辑、同步与异步设计

各位同学,欢迎来到第二章。说实话,这一章的内容看起来像是“复习课”,但我得提醒你——很多流片失败的案例,根源恰恰是这些基础概念没吃透。我自己带项目时,最怕听到的一句话就是:“这个电路我凭感觉画的,应该没问题吧?”

嗯,咱们今天就把这些“感觉”变成“确定”。

2.1 布尔代数:数字电路的数学根基

布尔代数,说白了就是0和1的游戏。你别看它简单,当年我在做一款通信芯片的译码器时,就因为一个布尔化简没做彻底,导致组合逻辑路径多了两级,时序直接崩了。

核心运算就三个:与(AND)、或(OR)、非(NOT)。

  • 与门:全1出1,有0出0。我习惯记成“串联开关”——两个开关都闭合,灯才亮。
  • 或门:有1出1,全0出0。像“并联开关”——任何一个闭合,灯就亮。
  • 非门:取反。输入1输出0,输入0输出1。

还有几个常用定律,你写代码时经常用到:

定律表达式我的理解
交换律A·B = B·A顺序不重要
结合律(A·B)·C = A·(B·C)括号随便加
分配律A·(B+C) = A·B + A·C和普通乘法一样
德摩根定律~(A·B) = ~A + ~B取反时,与变或

我的小技巧:写Verilog时,如果组合逻辑特别复杂,先画卡诺图化简。我曾经因为少化简一项,多用了200个LUT,被领导叫去喝茶。

2.2 组合逻辑:没有记忆的电路

组合逻辑的特点很简单:输出只取决于当前输入,跟历史无关。就像一台自动售货机——你投币,它立刻出货,不会记得你昨天来过。

常见的组合逻辑模块:

  • 多路选择器(MUX):从多个输入中选一个。我常用它来做数据路径切换。
  • 译码器:把二进制编码转成独热码。比如3-8译码器,3位输入,8位输出。
  • 加法器:半加器、全加器、超前进位加法器。做DSP模块时,加法器的速度往往决定整个模块的Fmax。

写组合逻辑时,有个坑你必须注意——组合环路。我曾经在调试一个状态机时,发现仿真波形一直在跳,查了两天才发现是组合逻辑的输出直接反馈到了输入,形成了死循环。

避坑指南:组合逻辑的输出绝对不能直接连回自己的输入!除非你故意要做振荡器。否则,轻则仿真卡死,重则芯片烧毁。

2.3 时序逻辑:记住过去的状态

时序逻辑就不一样了。它有记忆功能,输出不仅取决于当前输入,还取决于之前的状态。核心元件就是触发器(Flip-Flop)

触发器有三种常见类型:

  • D触发器:最常用。时钟上升沿时,D端的数据被锁存到Q端。我90%的时序电路都用D触发器。
  • JK触发器:功能更丰富,可以置位、复位、翻转。但说实话,现在写Verilog很少直接例化JK触发器了。
  • T触发器:每来一个时钟沿就翻转一次。做分频器时很实用。

为什么时序逻辑这么重要?因为数字电路里,数据需要“对齐”。你想想看,如果两个信号到达触发器的时间不一样,那采到的数据就是错的。这就是后面要讲的建立时间保持时间问题。

// 一个简单的D触发器,Verilog描述
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

这段代码我写了不下千遍。注意看,posedge clk表示时钟上升沿触发,negedge rst_n表示异步复位。嗯,这里要注意:复位信号是低电平有效,所以叫rst_n,这是业界惯例。

2.4 同步设计与异步设计

这是面试必考题,也是实际项目中踩坑最多的地方。

同步设计:所有触发器都使用同一个时钟源。数据在时钟沿到来时统一更新。好处是时序分析简单,不容易出问题。我个人的习惯是:能同步就同步,绝不搞花里胡哨的东西。

异步设计:不同模块使用不同的时钟,或者同一个时钟但触发沿不同。比如一个模块用上升沿,另一个用下降沿。这样做的好处是灵活,但代价是——亚稳态

什么是亚稳态? 当数据变化刚好发生在时钟沿附近时,触发器可能无法判断是0还是1,输出会处于一个中间状态。这个状态可能持续几纳秒,然后随机稳定到0或1。更可怕的是,它可能传播到后面的电路,导致整个系统崩溃。

我曾经在一个项目中,为了省一个PLL,让两个模块分别用晶振的上升沿和下降沿。结果板子调试时,数据偶尔会错一位。查了三天,最后发现是异步时钟导致的亚稳态。从那以后,我再也不敢在关键路径上用异步设计。

解决亚稳态的标准方法:两级同步器

// 异步信号同步化,两级触发器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        sync1 <= 1'b0;
        sync2 <= 1'b0;
    end
    else begin
        sync1 <= async_in;
        sync2 <= sync1;
    end
end

assign sync_out = sync2;

原理很简单:第一级触发器可能进入亚稳态,但经过一个时钟周期后,它大概率会稳定下来。第二级触发器再采一次,基本就安全了。当然,如果时钟频率很高,可能需要三级甚至四级。

2.5 我的设计哲学

做了十几年FPGA,我总结了几条原则:

  1. 时钟域要清晰:每个模块只用一个时钟,跨时钟域必须用同步器或FIFO。
  2. 组合逻辑要简单:复杂的组合逻辑拆成多级流水线,别想着一个周期搞定所有事。
  3. 复位要统一:要么全部异步复位,要么全部同步复位。混用会出问题。
  4. 仿真要彻底:不要只仿真正常情况,还要仿真边界条件。比如时钟抖动、数据变化最坏情况。

我记得有一次,一个实习生写了个计数器,组合逻辑路径长达20级。综合后频率只能跑到50MHz,而设计要求是200MHz。我让他改成5级流水线,每级4位加法,频率直接飙到250MHz。这就是基础知识的威力。

好了,第二章就到这里。下一章我们开始讲Verilog语法,我会告诉你哪些语法是“能综合的”,哪些是“仿真用的”——这两者区别大了去了。