3. Verilog基础语法(上):模块、端口、数据类型、连续赋值

好,咱们正式开始写代码了。这一章我带你过一遍Verilog最核心的几个概念——模块、端口、数据类型、还有连续赋值。这些东西你看着简单,但我在项目里见过太多人栽在细节上。咱们一个一个来。

3.1 模块——Verilog的基本单元

Verilog里所有的设计,说白了都是模块。一个模块就是一个黑盒子,有输入有输出,内部实现具体功能。你可以把模块想象成一个芯片——外面露着引脚,里面是电路。

模块的基本结构:

module 模块名 (
  端口列表
);
  // 内部信号声明
  // 功能描述
endmodule

我个人习惯把模块名写得长一点、有意义一点。比如 uart_rx 就比 rx 好得多。你想想看,三个月后回来看代码,看到 rx 你还记得是啥吗?

模块内部可以写什么?可以写组合逻辑、时序逻辑、还可以例化其他模块。这就是层次化设计——大模块套小模块,小模块再套更小的模块。我在做交易系统的时候,整个系统就是一层层模块堆起来的。

小技巧:每个模块只做一件事。一个模块如果既做加法又做乘法还做控制,趁早拆开。这是我在项目里踩过的坑——一个模块写了800行,最后改一个功能要翻半天。

3.2 端口——模块的对外接口

端口就是模块跟外界通信的通道。Verilog里有三种端口:input、output、inout。

端口类型 方向 说明
input 输入 只能从外部读,不能在里面赋值
output 输出 只能从里面赋值,外部不能写
inout 双向 既能读又能写,常用于总线

端口声明有两种写法。一种是ANSI风格,端口列表里直接写类型:

module adder (
  input  [7:0] a,
  input  [7:0] b,
  output [8:0] sum
);

另一种是传统风格,端口列表只写名字,类型在内部声明:

module adder (a, b, sum);
  input  [7:0] a, b;
  output [8:0] sum;

我建议你用第一种。为什么?少写几行代码,而且一眼就能看出端口方向。传统风格在大型项目里容易搞混——我曾经在一个200多端口的模块里,找了半天才发现某个信号方向写反了。

注意:inout端口要小心用。双向端口在FPGA里通常需要三态缓冲器,用不好容易出竞争。我一般只在SDRAM接口或者I2C总线这种场景才用inout。

3.3 数据类型——wire和reg

Verilog里最常用的两种数据类型就是wire和reg。很多人一开始搞不清它们的区别,其实很简单:

  • wire——连线。它不存储值,只是把两个点连起来。用assign赋值。
  • reg——寄存器。它能存储值,在always块里赋值。

但这里有个坑:reg不一定会综合成寄存器。如果你在组合逻辑的always块里用reg,它综合出来还是连线。说白了,reg只是语法上的概念,跟硬件没有必然对应关系。

我记得有个同事,写了一个组合逻辑的always块,里面用了reg,结果综合出来发现面积比预期大很多。查了半天,原来他把敏感列表写错了,漏了一个信号,导致综合工具把它推断成了锁存器。嗯,这里要注意——组合逻辑的敏感列表一定要写全。

常用数据类型速查:

wire       a;           // 1位连线
wire [7:0] bus;         // 8位连线
reg        b;           // 1位寄存器
reg  [31:0] data;       // 32位寄存器
integer    i;           // 整数,常用于循环
parameter  WIDTH = 8;   // 常量参数

还有一点:在模块端口声明里,input和inout默认是wire类型,output可以是wire也可以是reg。如果你在output后面没写类型,默认是wire。

3.4 连续赋值——assign语句

连续赋值用assign关键字,它描述的是组合逻辑。说白了,就是输入一变,输出立刻跟着变,没有延迟。

assign sum = a + b;
assign flag = (cnt == 10) ? 1'b1 : 1'b0;

assign的左边必须是wire类型,右边可以是任何表达式。多个assign语句是并行执行的——这一点跟软件完全不一样。软件是一行一行顺序执行,硬件是同时执行。

我在做低延迟交易系统的时候,大量使用assign。为什么?因为它延迟最小。assign直接映射成组合逻辑,没有时钟开销。但代价是——组合逻辑多了,路径延迟会变大,时序可能跑不过。

经验之谈:assign适合做简单的组合逻辑,比如加法、比较、选择。复杂的逻辑建议用always块,配合中间变量,时序更好控制。

连续赋值还有一个特点——它可以用于条件赋值。用问号表达式,一行搞定多路选择:

assign out = sel ? a : b;

这比写if-else简洁多了。但要注意,嵌套太多层问号表达式,代码可读性会下降。我一般不超过两层。

3.5 知识体系总览

下面这张图把本章的核心知识点串起来了。你看着它,就能知道模块、端口、数据类型、连续赋值之间的关系。

模块 (module) 端口 (port) 数据类型 连续赋值 (assign) input output inout wire reg parameter 组合逻辑 并行执行 条件赋值 模块 = 端口 + 内部逻辑(数据类型 + 连续赋值)

这张图你看懂了吗?模块是外壳,端口是接口,数据类型是内部信号的类型,连续赋值是描述逻辑的一种方式。四者缺一不可。

3.6 避坑指南

最后,我把自己这些年踩过的坑总结一下,你写代码的时候多留个心眼:

  • 端口方向搞反——input写成output,或者反过来。这种错误综合工具不会报错,但功能完全不对。我建议你写端口列表的时候,先写input再写output,养成习惯。
  • wire和reg混用——在assign左边用了reg,或者在always块里给wire赋值。编译器会报错,但新手经常看不懂错误信息。
  • 位宽不匹配——比如把一个8位的信号赋值给一个4位的信号,高位会被截断。我曾经因为这个原因,查了一个下午的波形。
  • 忘记声明信号——Verilog默认隐式声明wire,但位宽是1位。如果你想要8位总线,忘了声明,结果就是只有最低位有效。

强烈建议:在代码开头加上 `default_nettype none。这样任何未声明的信号都会报错,而不是隐式声明。这个习惯救过我很多次。

好了,这一章的内容就这些。模块、端口、数据类型、连续赋值——这四个概念是Verilog的基石。你先把它们吃透,后面写代码才能顺手。下一章咱们继续讲always块和时序逻辑,那才是真正开始做事情的地方。


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