第二讲:Verilog语言入门——模块结构、端口定义、数据类型与基本语句
各位同学,欢迎来到第二讲。今天我们要聊的是Verilog语言最基础的部分。说实话,很多初学者一上来就被各种语法细节吓住了,其实大可不必。你想想看,Verilog说白了就是描述硬件的一种语言,它的核心思想很简单:告诉工具,你的电路长什么样。
我个人习惯把这一讲称为「搭积木入门」。因为模块就像积木块,端口就是积木的接口,而wire和reg就是积木块之间的连线。好,我们开始。
一、模块结构——你的第一个积木块
一个Verilog模块,从结构上看,就像是一个黑盒子。它有名字,有接口,有内部实现。我刚开始学的时候,老师告诉我一句话,我一直记到现在:「模块就是硬件电路的封装」。
来看一个最简单的模块例子:
module my_first_module (
input wire a, // 输入端口
input wire b, // 输入端口
output wire c // 输出端口
);
// 内部逻辑
assign c = a & b;
endmodule
这个模块做了什么?它把两个输入信号a和b做了与运算,结果送到输出c。就这么简单。
模块的基本结构包括:
- 模块名:就是
module后面的名字,比如my_first_module - 端口列表:括号里定义的所有输入输出
- 内部逻辑:
module和endmodule之间的所有代码
module1,三个月后连写它的人自己都忘了是干嘛的。
二、端口定义——给积木块装上接口
端口就是模块与外界通信的通道。Verilog里端口有三种方向:
| 方向 | 关键字 | 说明 |
|---|---|---|
| 输入 | input |
信号从外部流入模块内部 |
| 输出 | output |
信号从模块内部流出到外部 |
| 双向 | inout |
信号可以双向流动(初学者先了解即可) |
端口定义有两种写法。我个人习惯用ANSI风格,就是上面那种把端口类型和数据类型写在一起的:
module counter (
input wire clk,
input wire rst_n,
output reg [7:0] count
);
还有一种老式的写法,把端口列表和类型分开写。嗯,这里要注意,现在新工具都支持ANSI风格,咱们就用这种,清晰明了。
三、数据类型——wire和reg
这是初学者最容易搞混的地方。我当年也踩过这个坑,所以今天咱们把它彻底说清楚。
wire(线网类型):
- 说白了就是一根导线
- 只能被
assign语句驱动 - 不能存储值,值由驱动源决定
- 默认值是
z(高阻态)
reg(寄存器类型):
- 可以理解为一个存储单元
- 只能在
always块中被赋值 - 可以保持上一次的值
- 默认值是
x(未知态)
我曾经犯过一个错误:在always块里给wire赋值,结果编译报错。后来才明白,wire和reg的使用场景是严格区分的。
四、assign语句——连续赋值
assign语句,说白了就是「一直连着」。只要右边表达式的值变了,左边wire的值立刻跟着变。
assign c = a & b;
assign sum = a + b;
assign data_out = (sel) ? data_a : data_b;
这里有个关键点:assign的左边必须是wire类型。右边可以是wire、reg或者常量。
我建议初学者把assign想象成「硬件连线」。你写assign c = a & b,就相当于在电路里把a和b接到一个与门上,输出接到c上。就这么直观。
五、always块——过程赋值
always块是Verilog里最灵活、也最容易出错的语句。它的基本格式是:
always @(敏感列表) begin
// 过程赋值语句
end
敏感列表决定了always块什么时候执行。常见的写法有:
- 组合逻辑:
always @(*)或always @(a or b) - 时序逻辑:
always @(posedge clk)或always @(negedge clk)
来看一个典型的时序逻辑例子:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 8'd0;
end else begin
count <= count + 1'b1;
end
end
这个代码描述了一个带异步复位的8位计数器。注意我用了<=(非阻塞赋值),这是时序逻辑的标准写法。
always块里混用阻塞赋值和非阻塞赋值,结果仿真结果和综合结果完全不一样。记住:时序逻辑用<=,组合逻辑用=,别混着用。
六、知识体系总览
为了让大家对今天的内容有个整体认识,我画了一张图:
这张图把今天的内容串起来了。你看,一个Verilog模块,就是由端口定义、数据类型声明和内部逻辑三部分组成的。端口决定了模块怎么跟外界通信,数据类型决定了信号怎么存储和传递,而assign和always则描述了具体的逻辑行为。
七、实战小贴士
最后,分享几个我这些年积累的小经验:
- 写代码前先画框图。我习惯在纸上先画出模块的输入输出,再动手写代码。这样不容易漏掉端口。
- wire和reg的选择:如果你不确定用哪个,先问自己「这个信号是在always块里赋值吗?」如果是,用reg;如果不是,用wire。
- 敏感列表要完整:写组合逻辑时,用
always @(*),别手动列敏感信号。我见过有人漏掉一个信号,仿真结果怎么都不对。 - 代码风格要统一:比如端口定义用ANSI风格,赋值操作符统一用
<=或=,别混着来。
好了,这一讲的内容就到这里。记住,Verilog入门不难,关键是理解「硬件思维」——你写的每一行代码,最终都会变成实实在在的电路。下一讲我们会深入讨论组合逻辑和时序逻辑的区别,到时候见。
可可精品资料库