2. Verilog基础回顾:模块结构、数据类型、连续赋值与过程赋值、组合逻辑与时序逻辑

各位同学,咱们直接进入正题。这一章我打算带大家快速过一遍Verilog的核心基础。别觉得基础就简单,很多坑恰恰是基础不牢导致的。我自己带项目时,发现不少老手也会在连续赋值和过程赋值上翻车。咱们今天就把这些掰扯清楚。

2.1 模块结构——Verilog的骨架

Verilog里,模块(module)是最基本的设计单元。说白了,一个模块就是一个黑盒子,有输入、有输出,内部实现具体逻辑。

一个典型的模块长这样:

module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 异步复位,低有效
    input  wire       en,       // 使能
    output reg  [3:0] count     // 4位计数器输出
);

    // 时序逻辑:计数器核心
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 4'd0;
        else if (en)
            count <= count + 1'b1;
    end

endmodule

嗯,这里要注意几点:

  • 端口声明:input/output/inout,每个端口都要明确方向。
  • 数据类型:wire和reg,后面会细讲。
  • 逻辑块:always块、assign语句,是模块的“血肉”。

我个人习惯,写模块时先把端口列清楚,再写内部逻辑。这样结构清晰,后期维护也方便。我曾经接手过一个项目,模块端口乱成一锅粥,光理清接口就花了两天……

2.2 数据类型——wire和reg,别搞混了

很多初学者会问:wire和reg到底啥区别?

简单说:

  • wire:线网类型,用于连续赋值(assign语句)或模块端口连接。它不存储值,只是“导线”。
  • reg:寄存器类型,用于过程赋值(always块内)。它可以在时钟沿或电平触发时存储值。

但注意!reg不一定综合成寄存器。你想想看,在组合逻辑的always块里,reg只是用来描述逻辑,综合出来还是组合电路。

举个例子:

// 组合逻辑:reg综合成组合电路
reg [3:0] sum;
always @(*) begin
    sum = a + b;
end

// 时序逻辑:reg综合成触发器
reg [3:0] count;
always @(posedge clk) begin
    count <= count + 1;
end

我在项目中遇到过有人把组合逻辑的reg误当成寄存器,结果仿真和综合对不上,查了半天。记住:综合工具看的是赋值方式,不是数据类型

2.3 连续赋值 vs 过程赋值——两种思维模式

这是Verilog里最容易混淆的地方。咱们分清楚:

特性 连续赋值(assign) 过程赋值(always块内)
使用场景 组合逻辑、数据流描述 组合逻辑、时序逻辑
赋值对象 必须是wire类型 必须是reg类型
执行方式 输入变化立即更新 敏感列表触发时执行
典型写法 assign y = a & b; always @(*) y = a & b;

连续赋值,说白了就是“硬件连线”。输入一变,输出立刻跟着变。适合描述简单的组合逻辑,比如加法器、多路选择器。

过程赋值,则是在always块里赋值。它有两种:

  • 阻塞赋值(=):顺序执行,适合组合逻辑。
  • 非阻塞赋值(<=):并行执行,适合时序逻辑。

我曾经犯过一个低级错误:在时序逻辑里用了阻塞赋值,结果仿真波形完全不对。后来养成习惯:组合逻辑用=,时序逻辑用<=,再也没出过问题。

警告:不要在同一个always块里混用阻塞和非阻塞赋值!综合工具会报错,或者综合出意想不到的电路。

2.4 组合逻辑 vs 时序逻辑——两种电路形态

这两个概念是数字设计的基石。咱们用一张图来理解:

组合逻辑 输出只取决于当前输入 无记忆功能 时序逻辑 输出取决于当前输入 + 历史状态 有记忆功能(触发器) 组合逻辑 + 时钟/复位 → 时序逻辑

组合逻辑:输出只由当前输入决定。比如与门、或门、加法器。没有记忆,没有时钟。

时序逻辑:输出不仅看当前输入,还要看之前的状态。比如计数器、状态机。它依赖时钟沿或电平触发。

在Verilog里,区分它们很简单:

  • 组合逻辑:用always @(*)assign,赋值用=
  • 时序逻辑:用always @(posedge clk),赋值用<=
小技巧:写组合逻辑时,敏感列表用@(*),工具会自动推导所有输入信号。别手动列,容易漏。

我记得有一次,一个同事写组合逻辑时手动列敏感列表,漏了一个信号,仿真结果死活不对。后来改成@(*),问题立刻解决。嗯,这种坑踩过一次就记住了。

2.5 实战建议——从基础开始养成好习惯

说了这么多,总结几条我自己的经验:

  1. 模块命名要规范:比如uart_txspi_master,一看就知道干啥的。
  2. 端口顺序要统一:我习惯先输入后输出,时钟复位放最前面。
  3. 组合时序分开写:别把组合逻辑和时序逻辑混在一个always块里,后期维护会疯掉。
  4. 仿真验证不能省:写一个模块,至少跑个简单的testbench,确认功能正确。

我曾经因为偷懒没仿真,直接上板调试,结果一个简单的计数器逻辑错了,浪费了整整一天。从那以后,我坚持“先仿真,后上板”。

好了,这一章的基础内容就到这里。记住:Verilog基础不牢,后面写复杂逻辑时会很痛苦。多写、多练、多仿真,慢慢就上手了。


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