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 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
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设计才能事半功倍。