3. Verilog基础(一):模块与端口、数据类型(wire、reg)、连续赋值(assign)、运算符

各位同学,今天咱们正式进入Verilog的世界。说实话,每次带新人,我最怕的就是他们一上来就写代码,结果连最基本的模块结构都搞不清楚。别急,咱们一步一步来。

3.1 模块与端口——Verilog的“黑盒子”

Verilog里,模块(module)就是最基本的设计单元。你可以把它想象成一个黑盒子——外面的人只看到它的引脚(端口),里面怎么实现的,那是你的事。

一个模块长这样:

module counter (
    input  wire       clk,    // 时钟
    input  wire       rst_n,  // 复位,低有效
    output reg  [3:0] count   // 4位计数器输出
);

    // 内部逻辑写在这里

endmodule

嗯,这里要注意:moduleendmodule是成对出现的,少了哪个都不行。我在项目中见过有人把endmodule拼成endmodule——少了个字母,编译报错找了半天。

端口方向

端口有三种方向:

方向 关键字 说明
输入 input 信号从外部流入模块内部
输出 output 信号从模块内部流出到外部
双向 inout 既能输入也能输出(慎用)
注意:inout端口在实际项目中尽量少用。我吃过亏——双向总线没处理好驱动冲突,仿真没问题,上板就冒烟。后来老老实实改成单向端口加三态控制。

3.2 数据类型:wire vs reg

很多初学者搞不清wirereg的区别。说白了:

  • wire——代表一根导线,它本身不存储值,值由驱动源决定。
  • reg——代表一个寄存器,它能存储值,在always块里被赋值。

举个例子:

wire a, b;          // 两根导线
wire sum;           // 导线,用于连接加法结果

assign sum = a ^ b; // 连续赋值,sum的值由a和b实时决定

reg [7:0] data_reg; // 8位寄存器

always @(posedge clk) begin
    data_reg <= data_in;  // 时钟上升沿才更新
end

我个人习惯:组合逻辑用wire,时序逻辑用reg。但注意,在always块里赋值的变量必须是reg类型——哪怕它综合出来只是一根导线。

小技巧:如果你不确定用wire还是reg,先问自己:这个信号是在always块里被赋值吗?是→用reg;否→用wire。

3.3 连续赋值(assign)

assign是Verilog里最直接的赋值方式。它描述的是组合逻辑——只要右边信号变化,左边立刻更新。

assign y = a & b;   // 与门
assign z = ~(a | b); // 或非门

我曾经遇到一个bug:有人用assign给reg类型赋值,编译报错。记住,assign的左边必须是wire类型(或者wire的向量)。

连续赋值可以同时写多个:

assign {carry, sum} = a + b + cin;  // 全加器,carry是进位

嗯,这里要注意:assign并行执行的,没有先后顺序。你写10条assign,它们同时生效。

3.4 运算符

Verilog的运算符和C语言很像,但有些细节不一样。我整理了一张表:

类别 运算符 说明
算术 + - * / % 加减乘除取模。注意:除法综合出来很费资源
位运算 & | ~ ^ ^~ 与、或、非、异或、同或
逻辑 && || ! 逻辑与、或、非,结果只有0或1
关系 > < >= <= == != 比较运算符,结果也是0或1
移位 << >> 左移、右移,空位补0
拼接 { } 把多个信号拼成一个向量
条件 ? : 三目运算符,类似C语言

举个例子,看看实际怎么用:

wire [3:0] a, b;
wire [4:0] sum;
wire       eq;

assign sum = a + b;          // 算术运算,注意进位
assign eq  = (a == b);       // 关系运算
assign result = {a[3:0], b[3:0]}; // 拼接成8位
重点:位运算和逻辑运算的区别。位运算是对每一位操作,逻辑运算只关心整体是0还是非0。
比如:4'b1010 & 4'b1100 结果是 4'b1000(位与)
4'b1010 && 4'b1100 结果是 1'b1(逻辑与,因为两个都不为0)

3.5 知识体系图

下面这张图帮你理清本章的知识脉络:

Verilog基础 模块与端口 • module/endmodule • input/output/inout • 端口声明与位宽 数据类型 • wire:导线,组合逻辑 • reg:寄存器,时序逻辑 • 向量与标量 连续赋值 • assign关键字 • 组合逻辑描述 • 并行执行特性 运算符 算术:+ - * / % 位运算:& | ~ ^ 逻辑:&& || ! 关系:> < == != 移位:<< >> 拼接:{ } 条件:? :

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 端口类型不匹配——我曾经把一个output reg连到另一个模块的input wire上,结果仿真一直X态。检查了半天,发现是位宽不匹配。
  • assign左边用了reg——编译报错,新手常犯。记住,assign左边只能是wire。
  • 运算符优先级搞混——比如a & b | c,到底是(a & b) | c还是a & (b | c)?我建议:多用括号,别省那几下键盘。
我的习惯:写代码时,所有运算符都加括号。虽然看起来啰嗦,但可读性大大提升,而且不会因为优先级问题出bug。

好了,这一章的内容就到这里。记住,Verilog不是C语言,它是描述硬件的。多写、多仿真、多思考,慢慢就有感觉了。


公众号:蓝海资料掘金营,微信deep3321