3、Verilog基础(上):模块与端口、数据类型(wire/reg)、组合逻辑(assign/always)
各位同学,咱们今天聊聊Verilog最核心的几个概念。说实话,我见过太多新手一上来就啃语法书,结果越看越迷糊。其实Verilog没那么玄乎,你把它理解成「用代码画电路图」就行了。
我个人习惯把Verilog的学习分成三步:先搞清楚模块长什么样,再弄明白数据怎么存,最后学会描述逻辑行为。今天咱们就走完这三步。
3.1 模块与端口——芯片的「黑盒子」
模块是什么?说白了就是一个带接口的黑盒子。你给它输入信号,它给你输出结果。就像你家里的路由器,插上网线(输入),它就能吐出Wi-Fi信号(输出)。
来看一个最简单的模块定义:
module and_gate (
input wire a, // 输入端口a
input wire b, // 输入端口b
output wire y // 输出端口y
);
assign y = a & b; // 组合逻辑:y等于a与b
endmodule
这里有几个关键点:
- module/endmodule:模块的起止标志,所有代码都包在里面
- 端口方向:input(输入)、output(输出)、inout(双向)
- 端口类型:wire(线网)或reg(寄存器),后面细说
模块可以嵌套。你可以在一个模块里调用另一个模块,这叫「模块实例化」。就像搭积木,小模块拼成大模块,大模块再拼成系统。
module top (
input wire [3:0] a,
input wire [3:0] b,
output wire [3:0] sum
);
// 实例化一个加法器模块
adder u_adder (
.a(a),
.b(b),
.sum(sum)
);
endmodule
注意看实例化的写法:模块名 实例名 (端口连接)。这里的u_adder是我随便起的名字,你可以按自己的命名规范来。我个人喜欢用u_前缀,一眼就能看出是实例化的子模块。
3.2 数据类型——wire和reg的「爱恨情仇」
这是新手最容易搞混的地方。我当年也踩过坑,所以咱们好好捋一捋。
| 特性 | wire | reg |
|---|---|---|
| 本质 | 物理连线 | 存储单元(不一定对应寄存器) |
| 赋值方式 | 只能被assign连续赋值 | 只能在always块中被赋值 |
| 默认值 | 高阻态z | 未知态x |
| 综合结果 | 组合逻辑连线 | 可能是寄存器或组合逻辑 |
核心原则就一句话:用assign赋值的信号声明为wire,在always块里赋值的信号声明为reg。
举个例子你就明白了:
module example (
input wire clk,
input wire rst_n,
input wire [7:0] d,
output reg [7:0] q // 这里用reg,因为要在always里赋值
);
// 时序逻辑:reg综合成寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 8'd0;
else
q <= d;
end
endmodule
再看一个组合逻辑的例子:
module comb_example (
input wire [3:0] a,
input wire [3:0] b,
output reg [3:0] y // 虽然是reg,但综合出来是组合逻辑
);
// 组合逻辑:reg综合成连线
always @(*) begin
y = a + b;
end
endmodule
看到了吗?两个例子都用了reg,但综合结果完全不同。关键看always块的敏感列表和赋值方式。
3.3 组合逻辑——assign和always的「分工」
组合逻辑就是输出只取决于当前输入,跟历史状态无关。说白了就是「即时响应」,输入一变,输出立刻跟着变。
描述组合逻辑有两种方式:
3.3.1 assign语句——简单粗暴
适合简单的逻辑运算,一行搞定:
assign y = (a & b) | (c & d);
assign的左边必须是wire类型。它相当于在硬件上焊了一根线,信号变化实时传递。
3.3.2 always块——灵活强大
适合复杂的组合逻辑,比如多分支判断:
always @(*) begin
case (sel)
2'b00: y = a + b;
2'b01: y = a - b;
2'b10: y = a & b;
2'b11: y = a | b;
default: y = 8'd0;
endcase
end
注意这里的@(*),它表示敏感列表包含所有输入信号。我建议你写组合逻辑时都用@(*),别手动列信号。为什么?因为曾经有个同事手动列敏感列表,漏了一个信号,仿真怎么跑都对,上板子就出错...查了三天才发现。
- 用阻塞赋值
=(非阻塞赋值<=留给时序逻辑) - 所有输入信号都要出现在敏感列表里(用
@(*)最保险) - 每个分支都要赋值,否则会综合出锁存器
说到锁存器,这是新手最容易踩的坑。看这个例子:
// 错误示例:会综合出锁存器!
always @(*) begin
if (en)
y = a;
// 缺少else分支!
end
当en=0时,y要保持原值,但组合逻辑没有记忆功能,所以综合工具会给你插入一个锁存器。锁存器这东西,时序分析很难搞,我建议你尽量别用。
正确的写法是给每个分支都赋值:
always @(*) begin
if (en)
y = a;
else
y = 8'd0; // 给个默认值
end
3.4 知识体系总览
说了这么多,咱们用一张图把今天的内容串起来:
这张图把今天的内容分成了三大块。你仔细看看,其实它们之间是有逻辑关系的:模块是容器,端口是接口,wire和reg是数据类型,assign和always是描述方式。搞清楚了这些,Verilog就算入门了。
好了,今天的内容就到这里。记住:模块是骨架,数据类型是血肉,组合逻辑是行为。这三样搞明白了,后面的时序逻辑、状态机都是水到渠成的事。