SystemVerilog 基础

第 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;
end

mem 就是一个典型的存储器(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(intstring 等)按 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] 表示什么?