3. 数字逻辑基础回顾:组合逻辑与时序逻辑、同步设计原则、亚稳态与跨时钟域处理

各位同学,咱们今天聊点基础,但也是最重要的东西。做高速数据采集,说白了就是在跟时间和信号赛跑。如果数字逻辑基础不牢,后面调试起来,那真是欲哭无泪。我见过太多新手,上来就写代码,结果时序跑不过,或者数据莫名其妙就错了,最后发现是最基本的同步设计没做好。

嗯,咱们今天就把这三个核心问题掰开揉碎了讲清楚。

3.1 组合逻辑与时序逻辑:硬件的“肌肉”和“大脑”

先说说组合逻辑。它就像硬件的“肌肉”,输入一变,输出立马跟着变。没有记忆功能,纯粹是当前输入的函数。比如一个简单的与门、或门,或者一个加法器。

// 组合逻辑示例:一个简单的多路选择器
assign out = sel ? a : b;

这段代码,sel一变,out立刻跟着变,中间只有一点点门延迟。我在项目中遇到过一个问题,一个很大的组合逻辑链,从输入到输出经过了十几级门,结果延迟太大,导致时序违例。后来我把它拆成几级流水线,问题就解决了。

时序逻辑就不一样了。它像硬件的“大脑”,有记忆功能。它只在时钟沿到来的时候,才去采样输入,更新输出。说白了,就是靠时钟来“拍板”。

// 时序逻辑示例:一个D触发器
always @(posedge clk) begin
    q <= d;
end

这里有个关键点:非阻塞赋值<=)。我建议你写时序逻辑时,一律用非阻塞赋值。为什么?因为它模拟了硬件中寄存器的真实行为——所有寄存器在同一个时钟沿并行更新。如果你用了阻塞赋值(=),仿真时可能没问题,但综合出来的电路逻辑可能完全不是你想要的。我曾经在这个坑里摔过,仿真跑得挺好,上板子就乱套,查了两天才发现是赋值方式用错了。

核心区别一句话总结:

  • 组合逻辑:输出只取决于当前输入,无记忆,用 assign 或 always@(*) 描述。
  • 时序逻辑:输出取决于当前输入和之前的状态,有记忆,用 always@(posedge clk) 描述。

3.2 同步设计原则:让所有信号“步调一致”

做高速设计,我个人习惯遵循一个黄金法则:所有信号都同步到同一个时钟域。你想想看,如果电路里信号各跑各的,时钟沿一来,有的信号到了,有的还没到,那寄存器采到的数据就是乱的。

同步设计有几个核心原则:

  1. 单一时钟域:尽量让整个设计跑在同一个时钟下。如果必须用多个时钟,那就得做跨时钟域处理(后面会讲)。
  2. 避免组合逻辑环路:组合逻辑的输出不能直接或间接反馈到自己的输入,否则会形成振荡或锁存器。我见过有人为了省一个寄存器,把组合逻辑的输出直接连回输入,结果综合出一堆奇怪的锁存器,功耗和面积都上去了。
  3. 寄存器输出驱动:所有模块的输出,最好都经过一级寄存器打一拍。这样能保证输出信号干净、稳定,不会因为组合逻辑的毛刺影响到下一级。

我的小技巧: 写代码时,先画一个简单的时序图。把时钟、数据、控制信号画清楚,再动手写RTL。这样能避免很多低级错误。

3.3 亚稳态:数字电路的“薛定谔状态”

亚稳态,说白了就是寄存器在采样时,正好赶上输入信号在变化,导致输出既不是0也不是1,而是一个不确定的中间态。这个状态可能会持续一段时间,然后随机稳定到0或1。更可怕的是,它可能会传播下去,导致整个系统崩溃。

为什么会这样?因为寄存器对输入信号有建立时间和保持时间的要求。如果输入信号在这个窗口内变化,寄存器就会“不知所措”。

参数 含义 典型值(以Xilinx 7系列为例)
建立时间 (Tsu) 时钟沿到来前,数据必须稳定的时间 0.1 ns ~ 0.3 ns
保持时间 (Th) 时钟沿到来后,数据必须稳定的时间 0.1 ns ~ 0.2 ns

解决亚稳态的唯一办法就是同步。最经典的方法就是打两拍(或三拍)。

// 两级同步器,用于跨时钟域处理
reg sync_reg1, sync_reg2;

always @(posedge clk_dst) begin
    sync_reg1 <= async_signal;
    sync_reg2 <= sync_reg1;
end

assign synced_signal = sync_reg2;

这段代码里,async_signal是异步输入的信号,经过两级寄存器同步后,synced_signal就基本消除了亚稳态的风险。第一级寄存器可能会进入亚稳态,但第二级寄存器采样时,第一级的输出已经稳定了。

注意: 两级同步器只能降低亚稳态的概率,不能完全消除。对于超高速设计(比如GHz级别),可能需要三级甚至更多级同步。另外,同步器会增加延迟,设计时要考虑时序余量。

3.4 跨时钟域处理:让不同“世界”的信号安全对话

高速数据采集系统里,经常有多个时钟域。比如ADC采样时钟是100MHz,FPGA内部处理时钟是200MHz,DDR控制器又是另一个时钟。这些时钟域之间的信号传递,必须小心处理。

跨时钟域处理,我把它分为两类:

  • 单比特信号:用两级同步器就够了。比如复位信号、使能信号。
  • 多比特信号:比如数据总线。如果直接用同步器同步每一位,由于布线延迟不同,各位到达的时间可能不一致,导致采到错误的数据。

对于多比特信号,常用的方法有:

  1. 异步FIFO:最通用的方法。用双端口RAM加上读写指针,读写指针分别用各自的时钟同步。我项目中90%的跨时钟域问题都是用异步FIFO解决的。
  2. 握手协议:发送方先发数据,再发请求信号;接收方收到请求后,锁存数据,再发应答信号。这种方法可靠,但吞吐量低。
  3. 格雷码:对于地址或指针这类连续变化的信号,可以用格雷码编码。格雷码相邻两个值只有一位不同,即使跨时钟域时出现亚稳态,最多错一位,不会出现大错误。

避坑指南: 我曾经在一个项目里,直接用两级同步器同步一个8位的数据总线。结果数据偶尔会跳变,查了好久才发现是不同位的延迟差异导致的。后来换成异步FIFO,问题就解决了。所以,多比特信号不要直接用同步器,这是很多新手容易犯的错误。

好了,今天的内容就到这里。数字逻辑基础是FPGA设计的根基,把这些搞懂了,后面做高速数据采集系统才能游刃有余。下一章咱们聊聊ADC接口的时序分析与设计,那才是真正考验功力的时候。


公众号:蓝海资料掘金营,微信deep3321