一、数字逻辑基础:组合逻辑与时序逻辑

说实话,很多初学者一上来就啃Verilog语法,结果写出来的代码要么综合出一堆莫名其妙的Latch,要么时序跑不过。我当年也踩过这个坑。所以咱们先花点时间,把数字逻辑的两种基本形态搞清楚。

1.1 组合逻辑

组合逻辑,说白了就是「输入一变,输出立马变」。没有记忆功能,没有时钟参与。就像你按开关,灯就亮——中间没有任何「等一等」的过程。

我在项目中遇到过最典型的组合逻辑就是地址译码器。输入一个地址,输出对应的片选信号。如果地址变了,片选信号几乎同时跟着变(当然,实际有门延迟)。

组合逻辑的特点:

  • 输出只取决于当前输入
  • 没有存储单元(没有寄存器)
  • 没有时钟边沿触发
  • 容易产生毛刺(glitch)

注意:组合逻辑如果没写全所有输入条件,综合工具会给你「补」一个锁存器(Latch)。这玩意儿在FPGA里通常不是你想要的东西。我曾经因为一个case语句没写default,白白浪费了两天调试时间。

1.2 时序逻辑

时序逻辑就不同了。它有个「记忆」功能,靠时钟边沿来更新状态。你想想看,如果整个芯片全是组合逻辑,那数据怎么流水?怎么实现状态机?

时序逻辑的核心是触发器(Flip-Flop)。每个时钟上升沿,它把输入采样进来,然后保持住,直到下一个时钟沿。嗯,这里要注意:建立时间和保持时间这两个概念,是时序逻辑的命根子。

时序逻辑的特点:

  • 输出不仅取决于当前输入,还取决于之前的状态
  • 有存储单元(寄存器)
  • 依赖时钟边沿触发
  • 可以消除大部分毛刺

我个人习惯把时序逻辑想象成「拍照」。时钟沿就是快门,按下的一瞬间,把当前输入定格下来。然后这张照片一直显示,直到下一次快门按下。

二、Verilog基本语法速览

Verilog其实不难,核心语法就那么几个。你只要搞懂module、wire、reg、assign、always这五个关键词,就能写出七八成的代码了。

2.1 module:你的设计单元

每个Verilog文件就是一个module。它就像芯片里的一个黑盒子,有输入引脚、输出引脚,里面装着你的逻辑。

module adder (
    input  [3:0] a,      // 4位输入
    input  [3:0] b,      // 4位输入
    output [4:0] sum     // 5位输出(考虑进位)
);

    // 内部逻辑写在这里

endmodule

2.2 wire与reg:连线 vs 变量

这两个初学者最容易搞混。我简单说:

  • wire:就是一根导线。它只能被assign赋值,不能存值。
  • reg:是一个变量。它可以在always块里被赋值,可以存值。

小技巧:别被名字骗了。reg不一定会综合成寄存器。如果你在组合逻辑的always块里用reg,它综合出来还是wire。只有时序逻辑的always块里的reg,才会变成真正的触发器。

2.3 assign:连续赋值

assign用来描述组合逻辑。它右边的表达式一变,左边立刻更新。

wire [3:0] y;
assign y = a & b;   // y = a与b,纯组合逻辑

2.4 always:过程赋值

always块是Verilog的灵魂。它有两种写法:

组合逻辑写法:

always @(*) begin
    // 所有输入变化时执行
    // 这里用 = 阻塞赋值
end

时序逻辑写法:

always @(posedge clk or negedge rst_n) begin
    // 时钟上升沿或复位下降沿触发
    // 这里用 <= 非阻塞赋值
end

踩坑提醒:我曾经在同一个always块里混用阻塞赋值和非阻塞赋值,结果仿真和综合结果完全不一样。记住一条铁律:时序逻辑用非阻塞(<=),组合逻辑用阻塞(=)。别问为什么,先记住。

三、实战:加法器与计数器设计

光说不练假把式。咱们直接上手两个经典设计。

3.1 4位加法器

加法器是数字电路的基础。最简单的就是行波进位加法器,每个位的进位像波浪一样传下去。

module ripple_adder_4bit (
    input  [3:0] a,
    input  [3:0] b,
    input        cin,      // 进位输入
    output [3:0] sum,
    output       cout      // 进位输出
);

    wire c1, c2, c3;  // 内部进位

    // 第0位
    assign {c1, sum[0]} = a[0] + b[0] + cin;
    // 第1位
    assign {c2, sum[1]} = a[1] + b[1] + c1;
    // 第2位
    assign {c3, sum[2]} = a[2] + b[2] + c2;
    // 第3位
    assign {cout, sum[3]} = a[3] + b[3] + c3;

endmodule

你看,这就是纯组合逻辑。输入一变,sum和cout立刻跟着变。但实际项目中,我们通常会在加法器后面加一级寄存器,把结果存起来,避免毛刺影响后面的电路。

3.2 二进制计数器

计数器是时序逻辑的经典例子。每个时钟沿,计数值加1。

module counter_8bit (
    input        clk,
    input        rst_n,    // 低电平复位
    output reg [7:0] cnt   // 8位计数器
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 8'd0;      // 复位清零
        else
            cnt <= cnt + 1'b1; // 每个时钟加1
    end

endmodule

个人经验:计数器溢出是FPGA里最常见的bug之一。比如你计数器最大到255,结果你判断条件写成了cnt == 256,那永远等不到。我建议用 cnt == 8'hFF 这种写法,一眼就能看出最大值。

四、知识体系总览

下面这张图,把本章的核心知识点串起来了。你对照着看,心里就有谱了。

数字逻辑与Verilog入门知识体系 数字逻辑基础 组合逻辑 时序逻辑 特点: • 输出只取决于当前输入 • 无存储单元 • 无时钟参与 特点: • 输出依赖历史状态 • 有寄存器存储 • 时钟边沿触发 Verilog基本语法 module / endmodule wire / reg assign always 实战设计:加法器 + 计数器 组合逻辑 + 时序逻辑 + Verilog语法 = FPGA设计基本功

五、本章小结

这一章我们干了三件事:

  1. 搞清楚了组合逻辑和时序逻辑的区别——一个没记忆,一个有记忆
  2. 学会了Verilog的五个核心关键词——module、wire、reg、assign、always
  3. 动手写了加法器和计数器——从理论到实践

嗯,到这里你已经有了FPGA设计的基本概念。下一章我们会深入时序逻辑的细节,讲讲状态机和流水线设计。但今天先消化这些,把加法器和计数器自己动手敲一遍,仿真跑一跑。纸上得来终觉浅,绝知此事要躬行。

课后练习:

  • 把4位加法器改成8位,仿真验证进位是否正确
  • 给计数器加一个使能信号(en),只有en=1时才计数
  • 试试用always块写一个组合逻辑的加法器,看看和assign版本有什么区别

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