数字电路基础回顾(上):组合逻辑与时序逻辑

各位同学,欢迎来到第二章。这一章咱们要打打地基。别觉得基础枯燥,我做了十几年数字设计,见过太多项目翻车,根源往往就是基础概念没吃透。今天咱们就把组合逻辑、时序逻辑和Verilog基础语法捋一遍。

本章核心脉络:从最底层的与或非门,到多路选择器、加法器,再到D触发器、寄存器和锁存器,最后用Verilog把它们描述出来。说白了,就是让你知道“电路长什么样”以及“怎么用代码写出来”。

数字电路基础回顾(上)知识体系 组合逻辑 与或非门 多路选择器 加法器 时序逻辑 D触发器 寄存器 锁存器 Verilog HDL基础 module assign always 💡 学习路径建议 1. 先理解组合逻辑:输出只取决于当前输入,没有记忆功能。 2. 再掌握时序逻辑:输出不仅取决于当前输入,还取决于历史状态(有记忆)。 3. 最后用Verilog把上述电路描述出来,这是FPGA设计的基本功。 4. 记住:组合逻辑是“瞬间响应”,时序逻辑是“时钟驱动”。

2.1 组合逻辑基础

组合逻辑,说白了就是“输入一变,输出立马跟着变”。没有时钟,没有记忆,纯粹的门电路拼出来的逻辑。

2.1.1 与或非门

这三个门是所有数字电路的基石。你想想看,再复杂的CPU,拆到最底层也就是这些门的组合。

门类型 逻辑符号 Verilog运算符 真值表(2输入)
与门 (AND) Y = A & B & 00→0, 01→0, 10→0, 11→1
或门 (OR) Y = A | B | 00→0, 01→1, 10→1, 11→1
非门 (NOT) Y = ~A ~ 0→1, 1→0

我的经验:写Verilog时,与或非直接用 &、|、~ 就行。但要注意,综合工具对“多余的门”很敏感。我曾经在项目里为了图省事,把几个与门串在一起没用括号,结果综合出来的面积比预期大了30%。所以,写表达式时括号别省,清晰第一。

2.1.2 多路选择器 (MUX)

多路选择器,就是“数据选择开关”。从多个输入中选一个输出。2选1 MUX是最基本的,控制信号sel为0选a,为1选b。

// 2选1多路选择器 - 组合逻辑实现
module mux2to1 (
    input  wire a,      // 输入0
    input  wire b,      // 输入1
    input  wire sel,    // 选择信号
    output wire y       // 输出
);
    // 方式1:assign语句
    assign y = sel ? b : a;
    
    // 方式2:逻辑门实现(等价)
    // assign y = (~sel & a) | (sel & b);
endmodule

为什么我要强调MUX?因为FPGA的查找表(LUT)本质上就是一个可编程的MUX。你写的任何组合逻辑,最终都会被映射成LUT+MUX的结构。理解MUX,你就理解了FPGA的一半。

2.1.3 加法器

加法器是算术运算的核心。半加器只算两个bit,不考虑进位;全加器则把进位也算进去。

类型 输入 输出 逻辑表达式
半加器 A, B S(和), Co(进位) S = A ^ B; Co = A & B
全加器 A, B, Ci(进位入) S, Co S = A ^ B ^ Ci; Co = (A&B) | (A&Ci) | (B&Ci)
// 4位加法器 - 行为级描述
module adder_4bit (
    input  wire [3:0] a,
    input  wire [3:0] b,
    input  wire       cin,
    output wire [3:0] sum,
    output wire       cout
);
    assign {cout, sum} = a + b + cin;
endmodule

⚠️ 避坑指南:我曾经在项目里直接用“+”号做加法,结果综合后时序不满足。原因是工具默认生成了行波进位加法器,位宽一大,进位链太长。对于位宽超过16位的加法,建议用“超前进位加法器”或者直接例化DSP单元。别偷懒,该优化就得优化。

2.2 时序逻辑基础

时序逻辑和组合逻辑最大的区别是什么?它有记忆。组合逻辑的输出只看当前输入,时序逻辑的输出还看“之前的状态”。这个“记忆”靠的就是触发器。

2.2.1 D触发器

D触发器是时序逻辑中最基本的存储单元。每个时钟上升沿,它把输入D的值“抓”到输出Q上,然后保持住,直到下一个时钟沿。

// D触发器 - 上升沿触发,异步复位
module dff (
    input  wire       clk,
    input  wire       rst_n,   // 异步复位,低有效
    input  wire       d,
    output reg        q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;       // 复位
        else
            q <= d;           // 采样
    end
endmodule

注意这里的 always @(posedge clk or negedge rst_n),这是标准的时序逻辑写法。敏感列表里有时钟沿和复位沿。我个人的习惯是:所有时序逻辑都用非阻塞赋值 <= ,组合逻辑才用阻塞赋值 = 。这个习惯帮我避免了很多仿真时的竞争问题。

2.2.2 寄存器

寄存器其实就是一组D触发器。8位寄存器就是8个D触发器并排。在RISC-V处理器里,寄存器堆(Register File)就是由一堆寄存器组成的。

// 8位寄存器
module reg_8bit (
    input  wire       clk,
    input  wire       rst_n,
    input  wire [7:0] d,
    output reg  [7:0] q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 8'h00;
        else
            q <= d;
    end
endmodule

2.2.3 锁存器 (Latch)

锁存器,这是个“坑”。很多初学者不小心就综合出锁存器,然后发现电路行为不对。

锁存器和触发器的区别:

  • 触发器:时钟沿触发,边沿敏感。
  • 锁存器:电平触发,使能信号为高时透明(输出跟随输入),为低时保持。

⚠️ 我曾经踩过的坑:在always块里写组合逻辑时,if条件没写全,或者case没写default,综合工具就会“好心”地给你生成一个锁存器。结果仿真和实测对不上,查了两天才发现。记住:组合逻辑的always块里,所有分支都必须赋值,否则就是锁存器。

// 错误示例:会综合出锁存器!
always @(*) begin
    if (en)
        q = d;    // 没有else分支,en=0时q保持,综合出latch
end

// 正确写法:给默认值
always @(*) begin
    q = 1'b0;     // 先给默认值
    if (en)
        q = d;
end

2.3 Verilog HDL基础语法

好了,电路概念有了,咱们看看怎么用Verilog把它们描述出来。我挑三个最核心的语法讲:module、assign、always。这三个你掌握了,80%的代码都能写。

2.3.1 module — 模块化设计

Verilog的基本单元就是module。一个module对应一个硬件模块。端口定义、参数传递、模块例化,这些都是基本功。

// 模块定义
module counter #(
    parameter WIDTH = 8   // 参数化设计,方便复用
) (
    input  wire             clk,
    input  wire             rst_n,
    input  wire             en,
    output reg  [WIDTH-1:0] count
);
    // 功能描述
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 0;
        else if (en)
            count <= count + 1;
    end
endmodule

// 模块例化(在顶层模块中使用)
counter #(
    .WIDTH(16)              // 覆盖默认参数
) u_counter (
    .clk    (sys_clk),
    .rst_n  (sys_rst_n),
    .en     (count_en),
    .count  (counter_val)
);

我的习惯:模块例化时,端口连接用 .端口名(信号名) 的显式方式,别用位置关联。位置关联一旦端口顺序变了,代码全废。显式连接虽然多打几个字,但可读性和可维护性高得多。

2.3.2 assign — 连续赋值

assign用于描述组合逻辑。它右边的表达式一旦有变化,左边立刻更新。说白了,assign就是“连线”。

// 连续赋值示例
module comb_logic (
    input  wire [3:0] a, b,
    input  wire       sel,
    output wire [3:0] y,
    output wire       flag
);
    // 组合逻辑:多路选择 + 比较
    assign y    = sel ? a : b;
    assign flag = (a == b) ? 1'b1 : 1'b0;
endmodule

注意:assign左边必须是wire类型,不能是reg。这个很多初学者搞混。我刚开始学的时候也犯过这个错,报了半天错才发现类型不对。

2.3.3 always — 过程赋值

always块比assign灵活得多。它可以描述组合逻辑,也可以描述时序逻辑。关键看敏感列表怎么写。

always块类型 敏感列表写法 赋值方式 典型用途
组合逻辑 always @(*) 阻塞赋值 = 译码器、状态机下一状态
时序逻辑 always @(posedge clk) 非阻塞赋值 <= 计数器、移位寄存器
混合(带复位) always @(posedge clk or negedge rst_n) 非阻塞赋值 <= 带复位的寄存器
// always块完整示例
module alu_example (
    input  wire       clk,
    input  wire       rst_n,
    input  wire [7:0] a, b,
    input  wire [1:0] op,       // 00:加, 01:减, 10:与, 11:或
    output reg  [7:0] result,
    output reg        carry
);
    // 组合逻辑:运算结果
    always @(*) begin
        case (op)
            2'b00: {carry, result} = a + b;
            2'b01: {carry, result} = a - b;
            2'b10: {carry, result} = {1'b0, a & b};
            2'b11: {carry, result} = {1'b0, a | b};
            default: {carry, result} = 9'b0;
        endcase
    end
endmodule

核心原则:

  • 组合逻辑用 always @(*) + 阻塞赋值 =
  • 时序逻辑用 always @(posedge clk) + 非阻塞赋值 <=
  • 千万别混用!否则仿真和综合结果不一致,查错查到哭。

好了,这一章的内容就到这儿。组合逻辑、时序逻辑、Verilog基础语法,这三块是后续所有章节的基石。你把这些吃透了,后面写RISC-V处理器的时候就会顺手很多。