在数字IC验证中,数组无处不在。寄存器模型、TLM端口列表、Scoreboard 中的 expected data queue、覆盖率 bin 定义……几乎每一个验证组件都离不开数组。
SystemVerilog 提供了比 Verilog 丰富得多的数组类型和操作方法,用好了效率翻倍,用不好性能拉胯。
本文从头梳理所有 SystemVerilog 数组类型,并给出实战建议。
一、数组类型全景图
SystemVerilog 中有四大数组类型:
| 类型 | 声明关键字 | 大小 | 适用场景 |
|---|---|---|---|
| 定长数组 | [n] | 编译时确定 | 固定位宽的信号、寄存器组 |
| 动态数组 | [] + new[n] | 运行时可变 | 不确定数量的 transaction、配置表 |
| 关联数组 | [*] 或 [type] | 通过 key 索引 | 稀疏存储、地址映射表 |
| 队列 | [$] | 按需增减 | Scoreboard、FIFO、有序数据流 |
二、Packed vs Unpacked — 第一个分岔口
理解数组之前,先搞清楚一个核心概念:packed(压缩) 和 unpacked(非压缩)。
Packed Array(压缩数组)
// 位宽在名字左边 → packed,物理上连续存放
bit [7:0] byte_vec; // 单维度 packed,就是 8-bit 向量
bit [3:0][7:0] word_vec; // 二维 packed,4 个 byte,总共 32-bit
- 本质:一段连续的比特位,始终被视为一个整体
- 操作:可以整体参与算术/逻辑运算,也可以按位切片
- 存储:在内存中连续存储,没有额外开销
Unpacked Array(非压缩数组)
// 位宽在名字右边 → unpacked,每个元素在内存中独立
bit byte_mem [0:7]; // 8 个 bit 的 unpacked 数组
int data_buf [256]; // 256 个 int 的 unpacked 数组
- 本质:一组独立元素的集合
- 操作:只能逐个元素操作,不能整体运算
- 存储:每个元素在内存中可能不连续
⚠️ 常见坑
bit [7:0] a; // packed:一个 8-bit 变量
bit b [7:0]; // unpacked:8 个 1-bit 变量
a = 8'hFF; // ✅ 合法
b = 8'hFF; // ❌ 编译错误!不能整体赋值 unpacked 数组
bit [7:0] c [0:3]; // 混合:4 个 unpacked 元素,每个元素是 8-bit packed
经验法则:能 packed 尽量 packed,仿真效率高。但超过 64-bit 的综合效果可能变差。
三、定长数组(Fixed-Size Array)
最基本的数组类型,所有维度在编译时确定。
声明方式
// 一维定长
int arr1 [0:7]; // 8 个元素,下标 0~7
int arr2 [8]; // 等价于 [0:7]
int arr3 [6:1]; // 6 个元素,下标 6,5,4,3,2,1
// 多维定长
int matrix [0:3][0:7]; // 4x8 二维
int cube [0:1][0:3][0:7]; // 2x4x8 三维
初始化
int arr [0:3] = '{0, 1, 2, 3}; // 列表初始化
int arr2[0:3] = '{default: -1}; // 全部初始化为 -1
int arr3[0:3] = '{0:10, 1:20, default: 0}; // 指定部分,其余默认
int matrix[0:2][0:1] = '{ '{0,1}, '{2,3}, '{4,5} }; // 多维初始化
遍历
// C-style for(不推荐)
for (int i = 0; i < $size(arr); i++)
$display("arr[%0d] = %0d", i, arr[i]);
// foreach(推荐 ✅)
foreach (arr[i])
$display("arr[%0d] = %0d", i, arr[i]);
// 多维 foreach
foreach (matrix[i, j])
$display("matrix[%0d][%0d] = %0d", i, j, matrix[i][j]);
为什么推荐 foreach? 不需要自己算边界,不受声明维度顺序影响,代码更干净。
系统函数速查
| 函数 | 含义 | 示例 |
|---|---|---|
$size(arr) | 第一维度大小 | $size(arr) → 8 |
$size(arr, 2) | 指定维度大小 | $size(matrix, 2) → 8 |
$dimensions(arr) | 维度数 | 2 |
$left(arr) / $right(arr) | 左/右边界 | $left(arr[6:1]) → 6 |
$low(arr) / $high(arr) | 最小/最大下标 | $low(arr[6:1]) → 1 |
四、动态数组(Dynamic Array)
大小在运行时确定,通过构造函数 new[] 分配空间。
核心操作
int dyn[]; // 声明:空,大小为 0
dyn = new[10]; // 分配 10 个元素(默认值为 0)
dyn = new[20](dyn); // 扩容到 20,保留原数据 ✅
dyn = new[5]; // 缩小到 5,原数据丢失 ⚠️
dyn = '{1, 2, 3, 4, 5}; // 直接赋值(自动重新分配)
dyn.delete(); // 清空,size 归 0
$display("size = %0d", dyn.size()); // 获取当前大小
实用技巧:array → queue → array
int dyn[];
dyn = new[5];
foreach (dyn[i]) dyn[i] = i * 10;
// 动态数组 → 队列
int q[$] = dyn; // 直接赋值 ✅
// 队列 → 动态数组
dyn = new[q.size()];
dyn = {>>{q}}; // streaming operator
⚠️ 常见陷阱
// ❌ 错误:未分配就访问
int dyn[];
dyn[0] = 5; // 运行时 fatal error!
// ❌ 错误:resize 不保留数据
int dyn[] = '{1,2,3,4,5};
dyn = new[10]; // 前5个数据丢失!新元素全是0
// ✅ 正确:扩容并保留
dyn = new[10](dyn);
黄金法则:永远在访问动态数组前检查
dyn.size(),或者确保已经new[]过。
五、队列(Queue)
队列是 SystemVerilog 最灵活的数据结构,类似 C++ 的 std::deque。
声明与初始化
int q1[$]; // 空队列(无界)
int q2[$:255]; // 有界队列,最多 256 个元素
string names[$] = '{"alice", "bob", "charlie"};
插入与删除
int q[$] = '{10, 20, 30};
// 尾部操作
q.push_back(40); // {10,20,30,40}
q.push_back(50); // {10,20,30,40,50}
q.pop_back(); // {10,20,30,40}
// 头部操作
q.push_front(0); // {0,10,20,30,40}
q.pop_front(); // {10,20,30,40}
// 任意位置插入/删除
q.insert(2, 25); // {10,20,25,30,40} 在下标2处插入
q.delete(1); // {10,25,30,40} 删除下标1
q.delete(); // 清空整个队列
队列在验证中的经典用法
// Scoreboard:先进先出检查
class scoreboard;
transaction exp_q[$]; // 期望队列
function void add_expected(transaction tr);
exp_q.push_back(tr);
endfunction
function void check_actual(transaction tr);
if (exp_q.size() == 0) begin
`uvm_error("SB", "unexpected transaction!")
end
else begin
transaction exp = exp_q.pop_front();
if (!exp.compare(tr))
`uvm_error("SB", "mismatch!")
end
endfunction
endclass
// 滑动窗口:保留最近 N 个采样
int sample_window[$:15]; // 最多 16 个
function void add_sample(int val);
if (sample_window.size() == 16)
sample_window.pop_front();
sample_window.push_back(val);
endfunction
六、关联数组(Associative Array)
用 key(而非连续下标)来索引的数组,适合稀疏数据场景。
声明
int aa_wild [*]; // 通配符索引(key 类型任意 integral)
int aa_int [int]; // int 类型作 key
int aa_str [string]; // string 类型作 key
int aa_cls [some_class]; // class handle 作 key(少见)
bit [63:0] aa_addr [bit [63:0]]; // 大位宽 key:地址映射
基本操作
int aa [string];
aa["foo"] = 10;
aa["bar"] = 20;
aa["baz"] = 30;
$display("aa[bar] = %0d", aa["bar"]); // 20
// 检查 key 是否存在
if (aa.exists("foo"))
$display("foo exists");
// 删除
aa.delete("bar"); // 删除单个
aa.delete(); // 删除全部
// 遍历
if (aa.first(key)) begin
do
$display("aa[%s] = %0d", key, aa[key]);
while (aa.next(key));
end
// 大小
$display("size = %0d", aa.num());
典型场景:寄存器地址映射
class reg_model;
uvm_reg reg_map [bit [31:0]]; // 32-bit 地址为 key
function void build();
// 芯片只有 200 个寄存器,但地址空间是 4GB
// 用关联数组只存实际存在的
reg_map[32'h4000_1000] = reg_ctrl;
reg_map[32'h4000_1004] = reg_status;
reg_map[32'h4000_2000] = reg_data;
endfunction
function uvm_reg get_reg(bit [31:0] addr);
if (reg_map.exists(addr))
return reg_map[addr];
else begin
`uvm_error("REG", $sformatf("No register at addr 0x%h", addr))
return null;
end
endfunction
endclass
⚠️ 注意事项
exists()读不存在的 key 不会创建 entry- 直接读不存在的 key 会创建一个默认值 entry
[*]类型索引的 key 只能是 integral(非 class)- 关联数组不支持
foreach,只能用first()/next()遍历
七、数组方法大全
SystemVerilog 为 unpacked 数组(定长、动态、队列)提供了一组强大的方法。
7.1 缩减方法(Array Reduction)
对数组所有元素执行累积操作,返回单个值。
int arr[] = '{1, 2, 3, 4, 5};
arr.sum(); // 15 求和
arr.product(); // 120 累乘
arr.and(); // 按位与
arr.or(); // 按位或
arr.xor(); // 按位异或
7.2 定位方法(Array Locator)— find 全家桶
int arr[] = '{1, 3, 5, 7, 2, 4, 6, 8};
// find → 返回所有匹配元素的队列
// find_first → 返回第一个匹配元素的队列(单元素)
// find_last → 返回最后一个匹配元素的队列
// find_index → 返回匹配元素的下标队列
// find_first_index→ 第一个匹配元素的下标
// find_last_index → 最后一个匹配元素的下标
int q[$];
q = arr.find(x) with (x > 4); // {5, 7, 6, 8}
q = arr.find_first(x) with (x > 4); // {5}
q = arr.find_index(x) with (x > 4); // {2, 3, 5, 7}
q = arr.find_first_index(x) with (x > 4);// {2}
7.3 with 子句高级用法
// item 是默认迭代变量,可以自定义名字
q = arr.find(item) with (item > 5 && item < 8); // {7, 6}
// 多维条件
class packet;
int addr;
bit is_write;
endclass
packet pkt_q[$];
// 找到所有 write transaction 的地址
int addr_q[$];
addr_q = pkt_q.find(p) with (p.is_write).addr; // ⚠️ 注意:.addr 不可链式
// ✅ 正确做法:
addr_q = pkt_q.find(p) with (p.is_write);
foreach (addr_q[i]) addr_q[i] = addr_q[i].addr;
7.4 排序方法
int arr[] = '{5, 2, 8, 1, 3};
arr.reverse(); // {3, 1, 8, 2, 5} 原地反转
arr.sort(); // {1, 2, 3, 5, 8} 升序
arr.rsort(); // {8, 5, 3, 2, 1} 降序
arr.shuffle(); // 随机打乱
// 自定义排序(with 子句)
class packet;
int id;
int priority;
endclass
packet pkt_q[$];
// 按优先级降序排列
pkt_q.sort(p) with (p.priority);
pkt_q.rsort(p) with (p.priority); // 降序
7.5 其他常用方法
int arr[] = '{1, 2, 3, 4, 5};
arr.min(); // 1 最小值
arr.max(); // 5 最大值
arr.unique(); // 去重,返回去重后的队列
arr.unique_index(); // 返回唯一元素的下标队列
int idx = arr.min(item) with (item % 2); // 偶数最小值:2
八、foreach 循环进阶
多维遍历
int arr[3][4]; // 3行4列
// ✅ 推荐:foreach
foreach (arr[i, j])
arr[i][j] = i * 4 + j;
// ❌ 不推荐:双重 for
for (int i = 0; i < 3; i++)
for (int j = 0; j < 4; j++)
arr[i][j] = i * 4 + j;
foreach 与队列/动态数组
int dyn[] = '{10, 20, 30, 40};
int q[$] = dyn;
// 三种写法都合法
foreach (dyn[i]) $display(dyn[i]);
foreach (q[i]) $display(q[i]);
// ⚠️ 注意:遍历期间不要改变数组大小!会导致行为不确定
// ❌ foreach (q[i]) q.push_back(i); // 别这么干
九、综合 vs 仿真 — 关键差异
作为验证工程师,你写的大部分代码在仿真侧。但如果代码需要综合,下面几点很关键:
| 特性 | 仿真 | 综合 |
|---|---|---|
| 定长 packed array | ✅ | ✅ |
| 定长 unpacked array | ✅ | ✅ 有限支持 |
| 动态数组 | ✅ | ❌ 不可综合 |
| 关联数组 | ✅ | ❌ 不可综合 |
| 队列 | ✅ | ❌ 不可综合 |
foreach | ✅ | ✅ (SV 2017+) |
find/sort 等方法 | ✅ | ❌ 不可综合 |
$size / $dimensions | ✅ | ✅ |
仿真建议:大胆用动态数组和队列,性能足够,表达力强。
综合建议:只用定长 packed/unpacked 数组,慎用二维以上。
十、刷题时间 — 常见面试/笔试陷阱
陷阱 1:new[] 不带参数
int arr[] = '{1, 2, 3, 4, 5};
arr = new[10]; // arr 变成 {0,0,0,0,0,0,0,0,0,0},原数据丢失!
arr = new[10](arr); // ✅ 扩容且保留
陷阱 2:关联数组读不存在的 key
int aa[string];
$display("aa[key] = %0d", aa["nonexistent"]); // 输出 0,且创建了该 entry!
// aa.num() 现在是 1,不是 0!
陷阱 3:find 返回的是队列
int arr[] = '{1, 2, 3, 4, 5};
int result = arr.find_first(x) with (x > 3); // ❌ 类型不匹配!find 返回队列!
int result_q[$] = arr.find_first(x) with (x > 3); // ✅ 用队列接收
int result = result_q[0]; // ✅ 再取第一个
陷阱 4:packed array 维度顺序
bit [3:0][7:0] a; // 4 个 byte,每个 byte 8 bit
// a[3] = 高字节(MSB),a[0] = 低字节(LSB)
a = 32'hDEAD_BEEF;
$display("a[3] = %h", a[3]); // DE
$display("a[0] = %h", a[0]); // EF
$display("a[3][7] = %b", a[3][7]); // 1 (DE 的最高位)
十一、总结
| 需求 | 选型 |
|---|---|
| 固定数量信号/DUT 端口 | 定长 unpacked |
| 拼接成总线整体传输 | 定长 packed |
| 运行时才知道大小 | 动态数组 |
| 先入先出 / Scoreboard | 队列 [$] |
| 稀疏地址映射 | 关联数组 |
| 统计/搜索/排序 | 数组方法 (find, sort) |
| 遍历数组 | foreach(永远的首选) |
SystemVerilog 数组体系设计完善,掌握每个类型的适用场景和坑点,能让验证代码简洁数倍。别在不需要模拟的地方模拟 C 语言的双重 for 循环——SV 给了更好的工具,用起来。
如果你觉得这篇文章对你有帮助,欢迎在 B站 关注我,我会持续分享数字IC验证技术内容。