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(寄存器),后面细讲

我个人习惯把端口声明和类型声明写在一起,就像上面那样。这样代码更紧凑,也方便后期维护。我在项目中见过有人把端口声明和类型声明分开写,结果改端口时漏改类型,仿真跑了两天才发现——嗯,这种坑踩过一次就够了。

小技巧:端口名尽量用有意义的英文单词,别用a1、b2这种。三个月后你自己都看不懂。

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,取决于你怎么驱动它
避坑指南:我曾经在项目中把一个输出端口声明为wire,却在always块里给它赋值。综合工具直接报错,排查了半小时才发现是类型不匹配。记住:always块里只能给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写反了顺序,以为后面的会覆盖前面的——但硬件里根本没有“覆盖”这个概念!

经验之谈:写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基础(一):知识体系 模块与端口 数据类型 连续赋值 基础运算符 module/endmodule 端口方向:input/output 端口类型:wire/reg wire:导线,不存储 reg:存储,可综合 reg不一定是寄存器 assign关键字 左边必须是wire 并行执行,即时更新 算术:+ - * / % 位运算:& | ~ ^ 逻辑:&& || ! 拼接/条件:{} ? :

这张图展示了本章的四个核心知识点:模块与端口是骨架,数据类型是血肉,连续赋值是行为描述方式,基础运算符是具体实现手段。它们环环相扣,缺一不可。

好了,这一章的内容就到这里。记住:Verilog不是软件语言,它是描述硬件行为的工具。写代码时多想想“这根线连到哪里”、“这个值什么时候更新”,慢慢就会形成硬件思维。下一章我们会深入过程赋值和always块,那才是真正体现硬件描述语言精髓的地方。


专注资料整理