3. Verilog基础(一):模块与端口、数据类型(wire/reg)、连续赋值(assign)、基础运算符
各位同学,欢迎来到Verilog硬件描述语言的第一课。说实话,每次讲到这个章节,我都会想起自己刚入行时对着代码发愣的样子——明明看着像C语言,怎么行为完全不一样?今天我们就来把这层窗户纸捅破。
3.1 模块与端口:芯片的“黑盒子”思维
Verilog里最基本的设计单元叫模块(module)。你可以把它想象成一个黑盒子——外面只看到几个引脚(端口),里面具体怎么实现的,外部不用管。这种封装思想,说白了就是硬件版的“高内聚、低耦合”。
一个模块的基本骨架长这样:
module adder (
input wire [3:0] a, // 4位输入
input wire [3:0] b, // 4位输入
output wire [4:0] sum // 5位输出(考虑进位)
);
// 内部逻辑
assign sum = a + b;
endmodule
这里有几个关键点:
- module/endmodule:模块的起止标志,成对出现
- 端口列表:写在括号里,用逗号分隔
- 端口方向:input、output、inout(双向端口,初学者先别碰)
- 端口类型:wire(线网)或reg(寄存器),后面细讲
我个人习惯把端口声明和类型声明写在一起,就像上面那样。这样代码更紧凑,也方便后期维护。我在项目中见过有人把端口声明和类型声明分开写,结果改端口时漏改类型,仿真跑了两天才发现——嗯,这种坑踩过一次就够了。
3.2 数据类型:wire与reg的“爱恨情仇”
这是初学者最容易懵的地方。我当年也纠结过:什么时候用wire?什么时候用reg?
简单粗暴的理解方式:
- wire:相当于一根导线。它不存储值,只是把驱动端的值传递出去。你想想看,导线本身能存东西吗?不能。
- reg:相当于一个存储单元。它能记住上一次被赋的值,直到下一次被改变。
但这里有个大坑——reg不一定是寄存器!在always块里赋值的变量,即使声明为reg,综合出来也可能只是组合逻辑。我刚开始做设计时就被这个坑过:明明写了reg,综合报告里却告诉我这是个wire。
来看个对比:
// wire用法:连续赋值
wire [7:0] data_out;
assign data_out = data_in + 8'h01;
// reg用法:过程赋值(always块内)
reg [7:0] counter;
always @(posedge clk) begin
counter <= counter + 1'b1;
end
- assign语句左边必须是wire
- always块内被赋值的变量必须是reg
- 模块的output端口可以是wire或reg,取决于你怎么驱动它
3.3 连续赋值(assign):硬件版的“即时生效”
连续赋值用assign关键字,它的特点是只要右边表达式变化,左边立刻更新。这跟软件里的赋值完全不同——软件是顺序执行,而硬件是并行、实时响应的。
举个例子:
wire [3:0] a, b;
wire [4:0] result;
assign result = a + b;
这段代码的意思是:result永远等于a + b。只要a或b变了,result马上跟着变,没有延迟,没有时钟控制。这就是“连续”二字的含义。
连续赋值有几个要点:
- 左边必须是wire类型(或者wire的向量)
- 右边可以是任何表达式(运算符、函数调用、条件表达式等)
- 多个assign语句是并行执行的,跟书写顺序无关
我记得有一次调试一个多路选择器,代码里写了三个assign,结果发现输出总是不对。后来才意识到,我把两个assign写反了顺序,以为后面的会覆盖前面的——但硬件里根本没有“覆盖”这个概念!
3.4 基础运算符:从算术到逻辑
Verilog的运算符跟C语言很像,但有些细节不同。我整理了一张表,方便你对照:
| 类别 | 运算符 | 说明 | 示例 |
|---|---|---|---|
| 算术 | + - * / % | 加减乘除取模 | a + b |
| 位运算 | & | ~ ^ | 与、或、非、异或 | a & b |
| 逻辑 | && || ! | 逻辑与、或、非(结果0或1) | a && b |
| 关系 | > < >= <= == != | 比较运算 | a == b |
| 移位 | << >> | 左移、右移 | a << 2 |
| 拼接 | { } | 将多个信号拼成一个 | {a, b} |
| 条件 | ? : | 三目运算符 | sel ? a : b |
这里有几个容易混淆的地方:
- 位运算 vs 逻辑运算:
&是按位与,&&是逻辑与。比如4'b1010 & 4'b1100结果是4'b1000,而4'b1010 && 4'b1100结果是1'b1(因为两个数都不为0)。 - 拼接运算符:
{a, b}可以把两个信号拼在一起。比如a=4'b1010, b=4'b0011,拼接后是8'b10100011。 - 条件运算符:
sel ? a : b相当于一个二选一多路器。sel为1选a,为0选b。
写一个简单的例子,把这些运算符串起来:
module alu_example (
input wire [3:0] a, b,
input wire [1:0] op, // 操作码
output wire [4:0] result
);
assign result = (op == 2'b00) ? a + b :
(op == 2'b01) ? a - b :
(op == 2'b10) ? a & b :
(op == 2'b11) ? a | b :
5'b0;
endmodule
这段代码实现了一个简单的ALU,支持加、减、与、或四种操作。注意输出是5位,因为加法可能产生进位。
4'b1010 + 4'b0011结果是5位(因为进位)。我建议在写代码时,显式指定结果位宽,避免综合工具自动推断出意想不到的宽度。
3.5 知识体系总览
下面这张图把本章的核心知识点串了起来,你可以对照着复习:
这张图展示了本章的四个核心知识点:模块与端口是骨架,数据类型是血肉,连续赋值是行为描述方式,基础运算符是具体实现手段。它们环环相扣,缺一不可。
好了,这一章的内容就到这里。记住:Verilog不是软件语言,它是描述硬件行为的工具。写代码时多想想“这根线连到哪里”、“这个值什么时候更新”,慢慢就会形成硬件思维。下一章我们会深入过程赋值和always块,那才是真正体现硬件描述语言精髓的地方。