Verilog HDL基础回顾(一):模块结构、数据类型、连续赋值、过程块(always)

各位同学,欢迎来到第二讲。今天咱们要聊的,是Verilog最核心的骨架——模块结构、数据类型、连续赋值和always过程块。这些东西,说白了就是你写RTL的“砖瓦”和“水泥”。我刚开始学的时候,总觉得这些基础概念太枯燥,后来在项目里吃过亏才明白,基础不牢,地动山摇。

核心要点:Verilog是一种硬件描述语言,不是软件编程语言。你写的每一行代码,最终都会映射成实际的逻辑门和连线。脑子里时刻要有“电路”这个概念。

2.1 模块结构:RTL设计的“黑盒子”

模块(module)是Verilog的基本设计单元。你可以把它想象成一个黑盒子——有输入、有输出,内部实现具体的功能。我个人习惯,写模块之前先画个框图,把端口定义清楚,再动手写代码。

一个完整的模块结构长这样:

module module_name (
    input  wire  clk,      // 时钟输入
    input  wire  rst_n,    // 复位输入,低电平有效
    input  wire  data_in,  // 数据输入
    output reg   data_out  // 数据输出
);

// 内部信号声明
wire   internal_wire;
reg    internal_reg;

// 功能描述(连续赋值、过程块等)

endmodule

这里有几个关键点:

  • 端口方向:input、output、inout。inout我用的不多,只在总线接口里见过。
  • 端口类型:wire(线网)和reg(寄存器)。注意,input端口只能是wire类型,output可以是wire或reg。
  • 模块名:最好见名知意,比如i2c_master、uart_rx,别用a、b、c这种。

我的习惯:写模块时,我会把端口声明和内部信号分开写,并且加注释。这样代码可读性高,别人接手也容易看懂。我曾经在一个项目里看到过500行的模块,端口和信号混在一起,那叫一个酸爽……

2.2 数据类型:wire和reg的“爱恨情仇”

Verilog里最常用的数据类型就两个:wire和reg。很多新手搞不清它们的区别,其实很简单:

特性 wire reg
本质 物理连线 存储单元(触发器或锁存器)
赋值方式 连续赋值(assign) 过程赋值(always块内)
默认值 高阻态(z) 未知态(x)
能否综合 能 能(但要注意综合出的电路类型)
典型用途 组合逻辑、模块间连线 时序逻辑、状态机

你想想看,wire就像一根导线,信号从一端传到另一端,没有存储能力。而reg就像一个“小盒子”,能把值存起来,等到下一个时钟沿再更新。

避坑指南:我曾经犯过一个错误——在always块里给wire赋值,结果仿真报错。记住:wire只能用assign赋值,reg只能在always或initial块里赋值。反过来,assign的左边也不能是reg。

除了wire和reg,还有几个常用的数据类型:

  • integer:32位有符号整数,常用于循环变量。我一般只在testbench里用,RTL里很少用。
  • parameter:常量定义,比如定义数据位宽、状态编码。用parameter可以让代码更灵活。
  • localparam:局部常量,只在当前模块有效。我习惯用localparam定义状态机的状态。

2.3 连续赋值:assign语句

连续赋值用assign关键字,描述的是组合逻辑。它的特点是:只要右边表达式有变化,左边立刻更新。说白了,就是“实时连线”。

// 一个简单的与门
assign y = a & b;

// 带条件的选择器
assign out = sel ? data1 : data2;

// 三态门
assign data_bus = en ? data_out : 1'bz;

这里要注意几点:

  • assign的左边必须是wire类型,不能是reg。
  • 多个assign语句是并行执行的,没有先后顺序。
  • assign可以嵌套条件运算符(? :),但别写太复杂,否则可读性差。

我的经验:写组合逻辑时,能用assign就用assign。它比always块更直观,综合出来的电路也更干净。但如果是复杂的组合逻辑,比如多级MUX,我建议用always块配合case语句,代码更清晰。

2.4 过程块:always的“三种姿势”

always块是Verilog里最灵活、也最容易出错的构造。它有三种常用写法,对应三种不同的电路:

2.4.1 组合逻辑:always @(*)

// 组合逻辑:敏感列表用 * 表示所有输入信号
always @(*) begin
    if (sel)
        out = a;
    else
        out = b;
end

这里的@(*)是Verilog 2001的新语法,表示“对所有输入信号敏感”。我强烈建议你用这种写法,别手动列敏感列表,容易漏信号。

2.4.2 时序逻辑:always @(posedge clk)

// 时序逻辑:时钟上升沿触发
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

这是最常用的时序逻辑写法。注意:

  • 敏感列表里有时钟和异步复位。
  • 赋值用非阻塞赋值(<=),不是阻塞赋值(=)。
  • 复位逻辑写在最前面,优先级最高。

2.4.3 锁存器:always @(敏感列表) 但条件不完整

// 不完整的if-else,会生成锁存器
always @(*) begin
    if (en)
        q = d;
    // 缺少else分支!
end

这种写法会综合出锁存器(latch)。锁存器在数字电路里是个“坏东西”——时序难分析、容易受毛刺影响。我建议你尽量避免。

避坑指南:我曾经在一个项目里,因为if-else条件没写全,综合出了一堆锁存器。后仿的时候时序一片红,查了三天才找到原因。从那以后,我写组合逻辑always块,一定会把if-else写完整,或者用case加default。

2.5 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

Verilog基础回顾知识结构图 Verilog HDL基础回顾(一)知识结构 Verilog基础 模块结构 数据类型 连续赋值 过程块(always) 端口声明 内部信号 功能描述 wire reg parameter assign语句 组合逻辑 组合逻辑 时序逻辑 核心:模块是基本单元,wire/reg是基本数据类型,assign描述组合逻辑,always描述时序或组合逻辑 注意:避免生成锁存器,组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值

2.6 阻塞赋值与非阻塞赋值

这个问题太重要了,我单独拿出来说。阻塞赋值(=)和非阻塞赋值(<=)的区别,是Verilog新手最容易踩的坑。

  • 阻塞赋值(=):顺序执行,前面的赋值会阻塞后面的。用于组合逻辑。
  • 非阻塞赋值(<=):并行执行,所有赋值同时更新。用于时序逻辑。

举个例子:

// 阻塞赋值:结果是 a=1, b=2
always @(*) begin
    a = b;
    b = a;  // 此时a已经更新为b的值
end

// 非阻塞赋值:结果是 a=1, b=0(假设初始a=0, b=1)
always @(posedge clk) begin
    a <= b;
    b <= a;  // 两个赋值同时发生,交换了a和b的值
end

我的建议:写时序逻辑时,一律用非阻塞赋值。写组合逻辑时,一律用阻塞赋值。别混用,也别问为什么,这是无数前辈用血泪换来的经验。

2.7 本章小结

好了,今天的内容就到这里。我们回顾了Verilog的模块结构、wire和reg的区别、assign连续赋值、always过程块的三种写法,以及阻塞/非阻塞赋值的核心要点。这些东西,你写RTL的时候天天都会用到。

嗯,最后说一句:别觉得基础简单就跳过。我见过太多人,写了好几年Verilog,还在纠结“为什么我的always块综合出了锁存器”。基础打牢,后面学去噪算法RTL设计才能事半功倍。