一根线与多根线
到目前为止,我们见过的每个信号都只有 1 位宽。真实的设计几乎从来不是这样:地址是 16 位或 32 位,数据总线是 8 位或 64 位,RGB 像素是 24 位。Verilog 用同一种机制让任何信号变成多位:在方括号里加一个范围。
wire [7:0] data; // 8-bit wire, bit 7 is MSB, bit 0 is LSB
reg [15:0] address; // 16-bit reg
output reg [31:0] result; // 32-bit module output
方括号中的数字是最高位和最低位的位编号。[7:0] 表示“这个信号的位从 7 编号到 0”,总共 8 位。第一个数是高位索引,第二个数是低位索引。
偶尔你会看到 [0:7]:位数相同,只是截取时的端序相反。[high:low] 是业界压倒性的惯例,除非有充分的理由,否则就用它。
实例:8 位加法器
每个 + 把两个 8 位向量相加,得到一个 9 位结果。像 8'd10 这样的数字字面量表示“一个 8 位宽、值为 10 的十进制数”,我们会在数字字面量中详细介绍。
截取:取出某些位
有了向量之后,就可以取出单个位或连续的一段:
不必在意 testbench 里的 force 那一行,我们只是需要一种注入值的方式来演示截取。真正有意思的是截取本身。
几条规则:
- 截取方向必须与声明一致。如果声明的是
[7:0],就用[high:low]截取。方向反了会报语法错误。 - 越界截取在仿真中会得到
x(未知)。综合工具可能给出警告或报错。 - 位选择以你写的索引为准,从零开始:
data[0]就是编号为0的那一位,对于[7:0]的声明来说,它是最低位(LSB)。
起点可变的截取:+: 和 -:
一个常见需求是:“从第 N 位开始,给我 8 位”。你不能直接写 data[N+7:N],因为 Verilog 要求范围的两端都是常量。解决这个问题的语法是:
data[base +: width] // width bits starting at `base`, going UP
data[base -: width] // width bits starting at `base`, going DOWN
宽度是常量(我们每次取 8 位),但起点可以是运行时的表达式。按字节寻址的存储器、移位寄存器的抽头等场景需要的正是这个。
数组:比向量更进一步
向量是单个多位信号。数组则是一组可以分别索引的向量:
reg [31:0] mem [0:1023];
这个声明有两个范围,它们的含义不同:
[31:0]是 packed 维度:每个字的宽度。[0:1023]是 unpacked 维度:一共有多少个字。
所以 mem 是 1024 个独立的 32 位寄存器。用一个索引访问其中之一:
mem[5] = 32'hCAFE_BABE; // write word 5
data = mem[address]; // read the word at `address`
这是一个存放平方数的小存储器。真实的设计用同样的写法实现寄存器堆、查找表、FIFO,以及其他比单个向量更大的片上存储。
packed 与 unpacked:为什么重要
packed 维度和 unpacked 维度的区分随处可见。分清两者能省下大量调试时间:
- packed 向量是一个信号。 你可以把它整体当作一个数:
data + 1可以,data == 32'h0可以,data[7:0]也可以。 - unpacked 数组是许多个信号。 你不能把它整体当作一个数:
mem + 1是语法错误。必须先取出某一个具体的字。
多个 packed 维度也是合法的:
reg [3:0][7:0] regs; // 4 bytes packed together into a 32-bit signal
regs[0] 是一个字节(最低字节),regs 整体是 32 位。SystemVerilog 大量使用这种写法。
多个 unpacked 维度会构成一个二维存储器:
reg [31:0] frame [0:479][0:639]; // 480x640 of 32-bit pixels
用 frame[y][x] 访问单个像素。图像缓冲区在 HDL 中就是这个样子。
接下来
现在你可以声明并操作任意位宽的信号了。下一篇文档参数会介绍如何让这些位宽可配置,让同一个模块在一个实例中是 8 位,在另一个实例中是 32 位。之后我们会讲数字字面量的写法规则(8'b1010_1100、32'hDEAD_BEEF),以及信号没有被驱动时出现的 x/z 值。
常见问题
Verilog 中的向量是什么?
向量就是多位信号。给 wire 或 reg 加上一个范围就声明了向量:wire [7:0] data 是一个 8 位的 wire。方括号里的数字是位的编号,这里第 7 位是最高位,第 0 位是最低位。你可以截取单个位(data[3]),也可以截取连续的一段(data[7:4])。
Verilog 中的 [7:0] 是什么意思?
[7:0] 声明了从第 7 位到第 0 位(含两端)的范围,即一个以第 7 位为最高位的 8 位信号。第一个数是高位索引,第二个数是低位索引。也可以写成 [0:7],按小端顺序编号,但在业界代码中,[high:low] 这种写法远比它常见。
Verilog 中如何截取位?
用方括号索引。data[3] 取出单个位,data[7:4] 取出最高的四位,得到一个 4 位向量。截取的方向必须与声明一致:如果声明的是 [7:0],就用 [high:low] 截取。SystemVerilog 还增加了 data[3 +: 4],用于起点可变、宽度固定的截取。
Verilog 中 packed 数组和 unpacked 数组有什么区别?
packed 数组是一条连续的总线:reg [31:0] word 是一个 32 位信号。unpacked 数组(也叫“存储器”)是一组彼此独立的字:reg [31:0] mem [0:1023] 是 1024 个独立的 32 位寄存器。你可以读写 unpacked 数组中的一整个字,但不能把整个数组当作一个信号来运算。
Verilog 中如何声明存储器?
reg [31:0] mem [0:1023]; 声明了一个有 1024 项、每项 32 位宽的存储器。第一组方括号是字宽(packed),第二组是字的个数(unpacked)。用 mem[address] 访问某一项;启用 SystemVerilog-2005 的索引方式后,还可以用 mem[address][7:0] 读写该项中的一段位。