3、Verilog基础:模块与端口、数据类型、连续赋值与过程赋值、阻塞与非阻塞赋值
好,咱们今天聊聊Verilog最核心的几个概念。说实话,很多新手一上来就被这些概念绕晕了。我当年也是,看了三天书,写出来的代码还是跑不通。后来才发现,不是语法难,是没理解背后的设计思想。
这一节,咱们就把这些基础打扎实。你想想看,盖房子得先懂砖头和水泥,写Verilog也一样。
3.1 模块与端口:芯片的“黑盒子”
模块(module)是Verilog的基本单元。说白了,它就是一个黑盒子。你给它输入,它给你输出。至于里面怎么算的,那是你的事。
我习惯把模块想象成一个芯片引脚图。端口就是芯片的引脚,用来和外界通信。
模块的基本结构:
module counter (
input wire clk, // 时钟输入
input wire rst_n, // 复位输入,低有效
output reg [3:0] count // 4位计数器输出
);
// 内部逻辑写在这里
endmodule
这里要注意几点:
- 端口方向:input、output、inout。inout双向端口用得少,新手先别碰。
- 端口类型:wire和reg。后面会细讲。
- 位宽:[3:0]表示4位宽。我见过有人写成[0:3],也能用,但行业惯例是从高位到低位。
我的习惯:端口声明时,我会把input和output分开写,每个端口一行。这样代码可读性高,review时一眼就能看清接口。
3.2 数据类型:wire和reg,别搞混了
Verilog里最常用的两种数据类型就是wire和reg。很多初学者会问:什么时候用wire,什么时候用reg?
我直接给你一个简单粗暴的答案:
- wire:用连续赋值(assign)驱动,或者连接模块端口。
- reg:在always块里赋值。
举个例子:
wire a, b, c;
assign c = a & b; // 连续赋值,c必须是wire
reg [7:0] data;
always @(posedge clk) begin
data <= data + 1; // 过程赋值,data必须是reg
end
嗯,这里要注意:reg不一定是寄存器。它只是表示一个变量,可以被过程赋值。在组合逻辑的always块里,reg综合出来就是连线。我刚开始做项目时,以为reg一定是触发器,结果综合出来的面积大得吓人……
避坑指南:我曾经在项目中犯过一个低级错误——把组合逻辑的输出声明成wire,然后在always块里赋值。编译直接报错。记住:always块里赋值的变量,必须是reg类型。
3.3 连续赋值 vs 过程赋值
这两个概念,我当年花了整整一周才彻底搞明白。
连续赋值(assign):
- 用assign关键字
- 只能对wire类型赋值
- 赋值是“持续的”——只要右边变了,左边立刻更新
- 用于描述组合逻辑
过程赋值(always块内赋值):
- 在always块里进行
- 可以对reg类型赋值
- 赋值是“有条件的”——只在特定事件发生时执行
- 可以描述组合逻辑,也可以描述时序逻辑
看个对比:
// 连续赋值:组合逻辑
wire sum;
assign sum = a + b;
// 过程赋值:组合逻辑(always @(*))
reg sum_reg;
always @(*) begin
sum_reg = a + b;
end
// 过程赋值:时序逻辑(always @(posedge clk))
reg sum_ff;
always @(posedge clk) begin
sum_ff <= a + b;
end
你发现没有?同样的加法,三种写法都能实现。但综合出来的电路完全不同。第一个是纯组合逻辑,第二个也是组合逻辑(但用了reg类型),第三个是带触发器的时序逻辑。
核心区别:连续赋值是“线”的思维,过程赋值是“块”的思维。前者描述数据流,后者描述行为。
3.4 阻塞赋值 vs 非阻塞赋值
这是Verilog里最容易踩坑的地方,没有之一。我见过太多老手在这个问题上翻车。
阻塞赋值(=):
- “阻塞”的意思是:这条语句执行完,下一条才能执行
- 用于组合逻辑的always块
- 赋值是立即生效的
非阻塞赋值(<=):
- “非阻塞”的意思是:所有赋值同时计算,然后同时更新
- 用于时序逻辑的always块
- 赋值是延迟生效的(在块结束时统一更新)
看个经典例子:
// 错误示范:时序逻辑里用阻塞赋值
always @(posedge clk) begin
a = b;
c = a; // 这里a已经是新值了!
end
// 正确示范:时序逻辑里用非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a; // 这里a还是旧值!
end
第一个例子,a和c会同时变成b的值。第二个例子,a变成b,c变成a的旧值。仿真结果完全不同!
我曾经踩过的坑:有一次写一个移位寄存器,在always块里用了阻塞赋值。仿真波形看起来完全正确,但综合后的网表仿真死活不对。查了两天才发现是赋值方式的问题。从那以后,我给自己定了个规矩:时序逻辑用非阻塞,组合逻辑用阻塞。这个规矩救了我无数次。
总结一下规则:
| 场景 | 赋值方式 | 说明 |
|---|---|---|
| 组合逻辑(always @(*)) | 阻塞赋值(=) | 描述数据流,立即生效 |
| 时序逻辑(always @(posedge clk)) | 非阻塞赋值(<=) | 描述寄存器行为,并行更新 |
| 连续赋值(assign) | 不适用 | 只能用wire,没有赋值方式选择 |
小技巧:如果你不确定该用哪种,先问自己:这个always块综合出来应该是组合逻辑还是时序逻辑?组合逻辑用=,时序逻辑用<=。记住这个,能避开90%的坑。
好了,这一节的内容就到这儿。模块端口、数据类型、赋值方式,这些都是Verilog的基石。别急着往下赶,先把这些概念吃透。下一节咱们聊运算符和表达式,到时候你会发现,基础打牢了,后面学什么都快。