2. 数字设计基础回顾:组合逻辑与时序逻辑、同步设计原则、Verilog HDL核心语法速览

各位同学,欢迎来到第二章。说实话,每次开课讲到这一节,我都有点感慨。很多初学者一上来就追着RISC-V架构跑,结果写出来的代码在FPGA上一跑就冒烟。我自己刚入行那会儿也吃过这个亏——花了两周写了个流水线,综合时报了一堆时序违例,查了半天才发现是最基本的同步设计原则没搞对。

所以这一章,咱们把地基夯实。别嫌基础,你想想看,盖楼的时候地基要是歪了,后面装修得再漂亮也没用。

2.1 组合逻辑与时序逻辑

数字电路就两大块:组合逻辑和时序逻辑。说白了,组合逻辑就是「输入一变,输出立马变」,没有记忆功能。时序逻辑呢,就是「等到时钟沿来了,我才变」,它有状态,能记住东西。

核心区别一句话:

  • 组合逻辑:输出 = f(当前输入)
  • 时序逻辑:输出 = f(当前输入 + 之前的状态)

我在项目中遇到过一位同事,写了个计数器,组合逻辑里直接赋值 reg 变量,结果仿真怎么都对,上板子就乱跳。为什么?因为他忘了组合逻辑里不能有「记忆」——你想想看,没有时钟沿,它凭什么记住上次的值?

2.1.1 组合逻辑的典型写法

组合逻辑用 assign 或者 always @(*) 来实现。我个人习惯用 always @(*),因为可读性更好,尤其是处理复杂逻辑时。

// 组合逻辑:全加器
module full_adder(
    input  a, b, cin,
    output sum, cout
);
    assign sum  = a ^ b ^ cin;
    assign cout = (a & b) | (a & cin) | (b & cin);
endmodule

2.1.2 时序逻辑的典型写法

时序逻辑必须用 always @(posedge clk)always @(negedge clk)。这里有个坑——我见过有人把敏感列表写成 always @(clk),结果综合出来是个锁存器,不是触发器。嗯,这里要注意,时序逻辑必须指定时钟沿

// 时序逻辑:带同步复位的D触发器
module dff_sync(
    input  clk, rst_n,
    input  d,
    output reg q
);
    always @(posedge clk) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

⚠️ 避坑指南:我曾经犯过一个低级错误——在时序逻辑里用阻塞赋值 = 代替非阻塞赋值 <=。仿真时看起来没问题,但综合后电路行为完全不对。记住:时序逻辑用非阻塞赋值,组合逻辑用阻塞赋值。这是铁律。

2.2 同步设计原则

同步设计,说白了就是「所有时序逻辑都听同一个时钟的指挥」。为什么非要这样?因为异步设计太难控制了。我早期做过一个项目,用了两个不同频率的时钟,结果跨时钟域的数据老是出错,查了整整三天才找到问题。

同步设计的三个基本原则,我建议你刻在脑子里:

  1. 单时钟域原则:一个模块尽量只用一个时钟。如果必须用多个时钟,一定要做跨时钟域同步处理。
  2. 避免组合逻辑反馈:组合逻辑的输出不能直接连回自己的输入,否则会形成组合环路。这玩意儿综合器有时候检查不出来,但流片回来必死。
  3. 寄存器输出原则:模块的输出信号最好都经过一级寄存器打一拍。这样能有效减少毛刺,也方便时序收敛。

💡 小技巧:我习惯在顶层模块的每个输出端口后面都加一级 reg 打拍。虽然多了一个时钟周期的延迟,但换来了整个系统的稳定性。在FPGA上,这一拍通常不会成为瓶颈。

2.3 Verilog HDL 核心语法速览

Verilog 语法其实不多,常用的就那么几个。我把它分成三块来讲:模块结构、数据类型、过程块。

2.3.1 模块结构

每个 Verilog 文件就是一个 module,端口定义写在括号里。我个人习惯把输入和输出分开写,清晰明了。

module counter #(
    parameter WIDTH = 8   // 参数化设计,方便复用
)(
    input  clk,
    input  rst_n,
    input  en,
    output reg [WIDTH-1:0] count
);
    // 功能代码写在这里
endmodule

2.3.2 数据类型

Verilog 里最常用的数据类型就三种:wirereginteger。初学者最容易搞混的是 wirereg

类型 用途 赋值方式
wire 组合逻辑连线 assign 或模块端口连接
reg 时序逻辑变量 always 块内赋值
integer 循环变量或通用整数 一般用于 for 循环

注意:很多人以为 reg 就一定是寄存器,其实不是。在 always @(*) 里赋值的 reg 综合出来是组合逻辑。只有 always @(posedge clk) 里的 reg 才是真正的触发器。

2.3.3 过程块

Verilog 里最重要的过程块就是 always。我总结了三种常用模式:

  • 组合逻辑模式always @(*),敏感列表用 *,自动包含所有输入信号。
  • 时序逻辑模式always @(posedge clk),只对时钟上升沿敏感。
  • 锁存器模式always @(enable or data),这个要慎用,我建议你尽量别用。
// 组合逻辑:多路选择器
always @(*) begin
    case(sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        default: out = d;
    endcase
end

// 时序逻辑:移位寄存器
always @(posedge clk) begin
    if (!rst_n)
        shift_reg <= 8'b0;
    else
        shift_reg <= {shift_reg[6:0], data_in};
end

2.4 本章知识体系

下面这张图是我自己画的,把本章的核心知识点串起来了。你仔细看看,能帮你理清思路。

第二章:数字设计基础回顾 - 知识体系 组合逻辑 时序逻辑 同步设计原则 assign / always @(*) 阻塞赋值 = 无记忆、无时钟 always @(posedge clk) 非阻塞赋值 <= 有记忆、依赖时钟沿 单时钟域原则 避免组合逻辑反馈 寄存器输出原则 Verilog 核心语法:module / wire / reg / always 三者结合,构成数字芯片设计的基础骨架

这张图你看懂了吗?组合逻辑和时序逻辑是两大支柱,同步设计原则是连接它们的桥梁。而 Verilog 语法,就是实现这一切的工具。说白了,你只要掌握了 modulewireregalways 这四个东西,就能写出 90% 以上的数字电路。

💡 我的建议:初学者不要急着去背语法手册。你先拿一个小项目练手,比如写个 8 位计数器、一个简单的状态机。遇到不会的语法再查,这样记得更牢。我当年就是这么过来的。

专注资料整理