第 6 章 · 共 16 章
数组:packed/unpacked、动态数组、关联数组与队列
认识 SystemVerilog 里的数组家族:packed 与 unpacked 数组的区别、运行时才能确定大小的动态数组、按任意 key 索引的关联数组,以及可动态增删的队列,并知道什么场景该选哪一种。
到目前为止,我们打交道的都是单个变量或者单个 struct。但无论是硬件设计(一块存储器)还是验证代码(一批等待处理的事务),几乎都需要处理一组数据,而不是孤零零的一个值。这一章认识 SystemVerilog 提供的几种数组,以及各自的适用场景。
packed 数组与 unpacked 数组:最根本的区分
其实上一章的 logic [7:0] 本身就是一个 packed 数组——8 个 bit 紧密排列在一起,整体可以当成一个 8 位向量来赋值、运算。packed 数组的维度写在变量名前面:
logic [7:0] byte_val; // 1 个 8 位 packed 数组(向量)
logic [3:0][7:0] nibble_bytes; // 2 维 packed 数组:4 个 8 位分组,连续排列而 unpacked 数组的维度写在变量名后面,表示"一组独立的元素",比较接近软件里常见的数组/内存模型:
logic [7:0] mem [0:255]; // 256 个元素组成的存储器,每个元素是 8 位
int scores [4]; // 4 个 int 元素(另一种等价写法)
initial begin
mem[3] = 8'hFF;
scores[0] = 100;
endmem 就是一个典型的存储器(ROM/RAM)建模方式:每个元素是一个 packed 的 8 位向量(logic [7:0]),而 256 个这样的元素排成一个 unpacked 数组,通过下标独立访问。这两种维度也可以组合使用——logic [7:0] mem [0:255] 本身就是"unpacked 数组的元素是一个 packed 向量"的例子。
位选择与域选择:从 packed 向量里取出一部分
packed 向量本身也可以按位取值——位选择(bit-select)vec[i] 取出单独一位,域选择(part-select)vec[msb:lsb] 取出一段连续的位:
logic [7:0] byte_val;
initial begin
byte_val = 8'hA5; // 二进制 1010_0101
$display("bit 0 = %b", byte_val[0]); // 1
$display("bit 7 = %b", byte_val[7]); // 1
$display("low nibble = %h", byte_val[3:0]); // 5
$display("high nibble = %h", byte_val[7:4]); // a
end域选择的上下界顺序要和声明时一致——byte_val 声明成 [7:0](高位在前),取值就要写 [7:4] 这种"高位:低位"的顺序,不能反过来写 [4:7]。位选择/域选择也能出现在赋值语句左边,只修改向量的一部分:
byte_val[3:0] = 4'hF; // 只改低 4 位,高 4 位保持不变这在验证代码里很常见——比如从一个更宽的总线字里拆出地址、数据等字段:
logic [31:0] bus_word;
logic [15:0] addr, data;
initial begin
bus_word = 32'h1234_5678;
addr = bus_word[31:16]; // 高 16 位
data = bus_word[15:0]; // 低 16 位
end动态数组:运行时才确定大小
前面的数组大小在声明时就写死了。但很多时候(尤其是验证代码),数组该多大,要等运行时才知道——这正是动态数组的用途:
int dyn_arr[]; // 先声明,暂不分配大小
initial begin
dyn_arr = new[10]; // 运行时分配 10 个元素
dyn_arr[0] = 42;
$display("size = %0d", dyn_arr.size()); // 10
dyn_arr = new[20](dyn_arr); // 扩容到 20,同时保留原有内容
$display("size = %0d", dyn_arr.size()); // 20
end声明时方括号留空(int dyn_arr[];),用 new[N] 分配大小;.size() 方法能查询当前元素个数。new[N](old_array) 这种写法在重新分配大小的同时,把旧数组的内容拷贝过来。
关联数组:按任意 key 索引,而不是顺序下标
有时候你想用的"下标"根本不是连续整数,甚至不是整数——比如按事务 ID、按字符串名字去查一个值。关联数组就是为这种场景准备的:
bit [31:0] expected_data [int]; // key 是 int 类型(比如事务 ID)
initial begin
expected_data[101] = 32'hDEAD_BEEF;
expected_data[205] = 32'hCAFE_BABE;
if (expected_data.exists(101))
$display("txn 101 expects %0h", expected_data[101]);
expected_data.delete(101);
$display("size after delete = %0d", expected_data.size());
end关联数组不需要提前声明大小——每次给一个新 key 赋值,就相当于新增了一条记录。方括号里的类型(这里是 int)决定了 key 的类型,也完全可以换成 string 等其他类型。常用方法:.exists(key) 判断某个 key 是否存在,.delete(key) 删除一条记录,.size() 查询当前记录数。这在验证代码里很常见:比如按事务 ID 记录"期望结果",收到实际结果时再去查表比对。
队列:可以动态增删的有序列表
队列(queue)介于"数组"和"链表"之间:像数组一样按顺序下标访问,又能像链表一样在两端动态增删元素:
int q[$]; // 声明一个 int 类型的队列
initial begin
q.push_back(1);
q.push_back(2);
q.push_front(0);
$display("q = %p, size = %0d", q, q.size()); // q = '{0, 1, 2}, size = 3
void'(q.pop_front()); // 丢弃返回值时,用 void' 显式表明"故意不用这个返回值"
$display("q = %p", q); // q = '{1, 2}
end常用方法:push_back/push_front 在尾部/头部插入元素,pop_back/pop_front 从尾部/头部取出并删除元素,.size() 查询长度,也可以像数组一样用 q[i] 直接访问某个下标的元素。验证代码里常用队列来实现"待处理事务列表"这类先进先出(或后进先出)的场景。
该选哪一种?
| 数组类型 | 大小 | 索引方式 | 典型用途 |
|---|---|---|---|
| packed 数组 | 编译期固定 | 位下标,整体可当向量运算 | 表示一个信号/寄存器的位域 |
| unpacked 数组(固定) | 编译期固定 | 连续整数下标 | 存储器建模、固定数量的元素集合 |
| 动态数组 | 运行时用 new[N] 决定 | 连续整数下标 | 大小依赖运行时输入的一组同类数据 |
| 关联数组 | 无需预先声明,按需增长 | 任意类型的 key(int、string 等) | 按 ID/名字查表,稀疏存储 |
| 队列 | 动态增长/收缩 | 连续整数下标,支持两端插删 | 待处理列表、FIFO/LIFO 场景 |
组合起来:一个数据包队列
把上一章的 struct 和这一章的队列放在一起,就是验证代码里很常见的写法——用队列表示"一批待处理的数据包":
typedef struct {
int addr;
int data;
} pkt_t;
pkt_t inbox [$]; // 待处理数据包组成的队列
initial begin
pkt_t p;
p.addr = 32'h2000;
p.data = 32'hCAFE_BABE;
inbox.push_back(p);
$display("queue size = %0d", inbox.size());
$display("first packet addr = %0h", inbox[0].addr);
void'(inbox.pop_front());
$display("queue size after pop = %0d", inbox.size());
end小结
- packed 数组(维度写在变量名前)是紧密排列的位,可整体当向量使用;unpacked 数组(维度写在变量名后)是一组独立寻址的元素,两者可以组合使用。
- 位选择
vec[i]取单独一位,域选择vec[msb:lsb]取一段连续的位,上下界顺序要和声明时一致;两者也能出现在赋值语句左边,只修改向量的一部分。 - 动态数组(
int arr[];+new[N])适合"元素类型固定、但数量要运行时才知道"的场景。 - 关联数组(
int arr[key_type];)适合按 ID、名字等非连续 key 做稀疏查表。 - 队列(
int q[$];)适合需要在两端动态增删元素的有序列表,常用于验证代码里的待处理列表。
logic [7:0] mem [0:255]; 这个声明,256 和 8 分别对应什么?
以下哪种场景最适合使用关联数组(associative array),而不是普通的固定大小数组?
给队列的尾部添加一个元素,应该调用哪个方法?(英文小写,不带括号)
logic [15:0] w; 声明之后,w[15:8] 表示什么?