3、Verilog基础(一):模块与端口、数据类型(wire/reg)、连续赋值(assign)、过程块(always)
各位同学,欢迎来到Verilog基础的第一讲。说实话,每次带新人时,我都会从这一章开始讲起。因为不管你以后是做验证还是做设计,这些概念就像你吃饭用的筷子一样,天天都要打交道。
我个人习惯把Verilog看作是一种「描述硬件行为的语言」。它不像C语言那样从上往下执行,而是描述一堆电路同时在工作。嗯,这个思维转变很重要,咱们慢慢来。
3.1 模块与端口:芯片的「黑盒子」
模块(module)是什么?说白了,就是你的设计单元。你可以把它想象成一个黑盒子——外面的人只关心它有什么输入、输出,不关心里面怎么实现的。
举个例子,一个最简单的与门模块:
module and_gate (
input wire a, // 输入端口
input wire b, // 输入端口
output wire y // 输出端口
);
assign y = a & b;
endmodule
这里有几个关键点:
- module/endmodule:模块的起止标志,就像C语言的{}一样
- 端口列表:写在括号里,告诉外界这个模块有哪些接口
- 端口方向:input、output、inout(双向端口,用得少)
端口类型还有一点要注意:端口默认是wire类型。如果你在端口声明时没写wire或reg,那它默认就是wire。我曾经见过新人因为没搞清楚这个,debug了一整天。
3.2 数据类型:wire vs reg
这是新手最容易搞混的地方。我当年刚学的时候也绕了好久,后来发现其实没那么复杂。
3.2.1 wire(线网类型)
wire代表的是物理连线。它本身不存储值,只是把驱动端的值传递到接收端。你可以把它想象成一根导线——导线本身能「存」电压吗?不能,它只是传递。
wire的特点:
- 只能被连续赋值(assign)或模块端口驱动
- 不能在always块中被赋值
- 如果没有驱动源,默认是高阻态(z)
wire w1;
assign w1 = a & b; // 正确:连续赋值驱动wire
wire w2;
always @(*) begin
w2 = a & b; // 错误!wire不能在always中被赋值
end
3.2.2 reg(寄存器类型)
reg代表的是存储单元。它可以在过程块(always)中被赋值,并且会保持这个值直到下一次被赋值。
这里有个坑:reg不一定综合成寄存器。我刚开始做项目时,以为reg就一定是触发器,结果综合出来发现是个组合逻辑。为什么会这样?因为reg只是语法上的变量类型,综合工具会根据你的代码风格来决定是寄存器还是锁存器。
reg r1;
always @(*) begin
r1 = a & b; // 正确:组合逻辑,综合成与门
end
reg r2;
always @(posedge clk) begin
r2 <= a & b; // 正确:时序逻辑,综合成D触发器
end
3.2.3 什么时候用wire,什么时候用reg?
我总结了一个简单规则:
- 模块的output端口:如果是在assign中赋值,用wire;如果在always中赋值,用reg
- 模块的input端口:永远是wire
- 内部信号:如果被assign驱动,用wire;如果被always驱动,用reg
| 场景 | 推荐类型 | 说明 |
|---|---|---|
| 模块输入端口 | wire | 只能被外部驱动 |
| assign驱动的输出 | wire | 连续赋值驱动 |
| always驱动的输出 | reg | 过程块赋值 |
| 内部连线 | wire | 模块间连接 |
| 内部寄存器 | reg | 存储状态 |
3.3 连续赋值(assign)
连续赋值,说白了就是「只要输入变了,输出立刻跟着变」。它描述的是组合逻辑,没有延迟,没有时钟。
assign y = a & b; // 与门
assign sum = a + b; // 加法器
assign sel = (cnt > 10) ? 1'b1 : 1'b0; // 条件赋值
assign的左边必须是wire类型,右边可以是任何表达式。我常用的assign场景:
- 简单的组合逻辑(与或非、加法、比较)
- 多路选择器(用条件运算符)
- 模块间的信号连接
3.4 过程块(always)
always块是Verilog中最灵活、也最容易出错的语法结构。它描述的是「在特定事件发生时,执行一段代码」。
3.4.1 always的触发方式
always块的触发方式由敏感列表决定:
// 组合逻辑:所有输入变化时触发
always @(*) begin
// 组合逻辑代码
end
// 时序逻辑:时钟上升沿触发
always @(posedge clk) begin
// 时序逻辑代码
end
// 时序逻辑:时钟上升沿+异步复位
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里有个重要原则:组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。这不是随便说的,而是有深刻原因的。
3.4.2 阻塞赋值 vs 非阻塞赋值
我当年理解这个花了不少时间。简单来说:
- 阻塞赋值(=):先算完右边的值,立刻赋给左边,然后才执行下一条语句。就像流水线上的工人,做完一个再做下一个。
- 非阻塞赋值(<=):先算好所有右边的值,然后统一赋给左边。就像大家同时举手,老师数完人数后一起放下。
// 阻塞赋值:顺序执行
always @(*) begin
a = b;
c = a; // c得到的是b的新值
end
// 非阻塞赋值:并行执行
always @(posedge clk) begin
a <= b;
c <= a; // c得到的是a的旧值
end
3.4.3 常见的always写法
我总结了三种最常用的always模板:
// 模板1:组合逻辑
always @(*) begin
// 用阻塞赋值
// 必须覆盖所有分支,否则会生成锁存器
end
// 模板2:同步时序逻辑
always @(posedge clk) begin
// 用非阻塞赋值
// 同步复位或不复位
end
// 模板3:异步复位时序逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位逻辑
end else begin
// 正常逻辑
end
end
3.5 本章小结
这一章的内容,说白了就是Verilog的「语法骨架」:
- 模块:你的设计单元,用端口和外界通信
- wire/reg:wire是连线,reg是变量,别搞混
- assign:描述组合逻辑,输入变了输出立刻变
- always:描述时序或组合逻辑,注意敏感列表和赋值方式
嗯,这些概念看起来简单,但真正用好需要大量练习。下一章我们会讲运算符和表达式,到时候你会看到这些基础概念是怎么组合起来构建复杂电路的。
记住:Verilog不是编程语言,是硬件描述语言。你写的每一行代码,最终都会变成实实在在的电路。带着这个思维去学习,你会少走很多弯路。