在数字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验证技术内容。