SystemVerilog合并数组与非合并数组:从语法到工程实践

发布时间:2026/10/4 17:16:51

SystemVerilog合并数组与非合并数组:从语法到工程实践 搞SystemVerilog的人迟早都要面对合并数组packed array和非合并数组unpacked array这两个概念。我印象特别深早年间第一次在面试里被问到“合并数组和非合并数组的区别是什么”当时我脑子里只有一句“一个是方括号在前面一个是在后面”结果被追问到内存布局、赋值语义、位选切片这些细节时直接就卡壳了。后来在实际项目里写验证环境、建寄存器模型、搭数据通路踩的坑多了才真正把这两个东西吃透。这篇文章就把我这几年的理解梳理一遍尽量用大白话讲清楚它们到底是什么、差别在哪、什么时候该用哪个以及那些文档里不会写但实际会坑你的细节。先说个结论性的框架SystemVerilog里数组的“合并/非合并”本质上描述的是数组元素在“比特层面”的排列方式和访问语义。合并数组是把所有元素在比特级别连续拼接成一个整体可以用一个整型值去读写非合并数组则更像是C语言里的数组每个元素是独立存储的元素之间没有比特级连续性。这个差别直接影响你能不能用位选、能不能直接做算术运算、能不能整体赋值甚至是仿真工具在内存里怎么给你分配存储空间。这个内容适合谁看主要两类人一类是刚接触SystemVerilog、正在写RTL或者验证环境的工程师需要把基础概念掰开揉碎另一类是已经写了几年代码但遇到数组相关报错、或者想优化仿真性能时想系统梳理一遍的人。不管你是哪个阶段这篇文章都尽量做到“原理讲透、例子能跑、坑点标全”。1. 从本质入手合并数组和非合并数组到底在描述什么1.1 名字已经说明了一切很多初学者最头疼的就是“packed”和“unpacked”这两个英文词翻译过来后的含义。我后来找到一个比较直观的理解方式把数组想成一排储物柜。合并数组packed array像是把储物柜全部焊死成一个整体柜与柜之间没有缝隙你想从中间某个位置直接抽出一个比特来看它属不属于某个元素需要自己动手算偏移量因为它本身就是一个“大比特串”。非合并数组unpacked array则像是正常的一排独立储物柜每个柜子都有自己的门和锁你直接用下标就能拿到一个完整的元素。这个类比虽然粗糙但方向是对的。合并数组强调的是“比特的打包”非合并数组强调的是“元素的独立”。所以在SystemVerilog标准里合并数组的声明方式是把位宽范围放在数组名之前比如logic [7:0] packed_data; // 合并数组8个比特连续打包整体就是一个8bit向量 logic unpacked_data [7:0]; // 非合并数组8个独立的logic元素注意看这两行声明的差异[7:0]写在类型后面、变量名前面时它表示这个变量本身是一个8比特的合并向量而[7:0]写在变量名后面的声明表示这个变量是一个拥有8个元素的非合并数组每个元素都是一个独立的logic。这个语法差异就是很多人一开始混淆的根源。你光看变量名packed_data和unpacked_data很难凭直觉判断哪个是合并的必须看方括号的位置。记起来也很简单logic [7:0] x这种方括号紧挨着类型是合并数组logic x [7:0]这种方括号紧挨着变量名是非合并数组。1.2 声明语法一个前缀一个后缀再往下扩展一层。合并数组可以用“向量维度”去理解声明的时候你可以写多个方括号但都放在变量名之前logic [3:0][7:0] packed_2d; // 4行 × 8bit 的合并二维数组整体是一个32bit的向量这种写法就相当于把4个8比特字节拼接成了一个32比特的整体。你既可以把packed_2d当作一个32位的向量来做算术运算也可以用packed_2d[1]取出第1个字节注意这里的索引是从左到右编号的也就是packed_2d[3:0][7:0]最左边的[3:0]是高维。非合并数组的声明则是在变量名之后logic [7:0] mem [0:3]; // 4个元素每个元素是8bit向量这一行是最常见的非合并数组写法常用于建模寄存器堆、存储器等。它声明了4个独立的8bit存储单元每个单元之间没有比特级的联系。你不能直接对mem做算术运算也不能用mem[1:0]这种“域切片”去取两个连续元素——非合并数组的索引规则和C语言一致一次只能通过下标取一个完整元素。这里还要注意非合并数组的元素类型本身可以是合并数组这就引出了“混合声明”。比如logic [7:0] matrix [0:3][0:5]; // 4×6个元素每个元素是8bit合并向量这种声明在验证环境里非常常见本质上是一个二维表每个格子存放一个8bit数据。整体看起来像“二维数组”但实际上它是由24个独立的8bit向量组成的访问方式为matrix[row][col]。搞清楚了语法布局下面就可以进入具体行为了。合并数组和非合并数组在“能干什么”上差别极大这直接决定你在什么场景下选谁。2. 合并数组适合做位操作和数据通路2.1 合并数组是“比特的连续拼接”合并数组最大的特点就是整个数组在比特级连续排布。这意味着你可以把它当成一个大的向量来做位选bit-select和部分选择part-select这是非合并数组做不到的。举个例子假设你有一个32bit的合并数组想提取其中第8到第15位logic [31:0] data; logic [7:0] byte1; assign byte1 data[15:8];这种写法在RTL设计里到处都是。如果你把data换成非合并数组logic data [32]那就不能这样操作了因为非合并数组的元素是独立的logic比特虽然每个元素是一个比特但从语义上讲它们不是打包在一起的向量不能直接做位选拼接。还有一点很关键合并数组可以直接参与算术运算。因为它在二进制层面就是一个完整的数你可以直接写sum a b其中a、b都是合并数组。这在数据通路设计、状态机计数器、地址计算等场景中非常方便。2.2 位选择、域切片与算术运算实际工程里合并数组最常见的是用于“把多个字段拼成一个数据”。比如做一个IP头解析你希望一次性声明一个32bit的合并数组然后分别提取其中的版本号、头长度、服务类型等字段logic [31:0] ip_header; logic [3:0] version; logic [3:0] header_len; assign version ip_header[31:28]; assign header_len ip_header[27:24];这就是典型的“打包”思维。反过来你也可以用拼接操作符构造一个合并数组logic [31:0] new_header; assign new_header {version, header_len, ttl, protocol, payload_field};这里{...}是SystemVerilog的拼接操作符它可以把多个合并向量和常量拼成一个大的合并数组。注意拼接操作符只能拼接“比特串”也就是合并类型所以非合并数组是不能直接放进{}里的——非要放的话得先遍历拷成合并数组。多维合并数组还有一个很常见的用途按字节索引。比如logic [3:0][7:0] dword; assign dword 32hA1B2C3D4; // dword[0] 8hD4dword[1] 8hC3这里有个容易踩坑的点合并数组的索引从左往右对应的是高位到低位。也就是声明[3:0][7:0]时[3:0]维度中下标0对应的是最低位的那个字节。所以dword[0]拿到的是低8位0xD4而不是0xA1。这个大小端问题在跨团队协作时经常导致bug后面我会单独展开。3. 非合并数组仿真里的“内存条”3.1 声明方式与访问语义非合并数组的定位更接近C语言中的数组。每个元素在仿真工具的存储空间里是独立分配的元素之间不存在比特级连续性所以它天然适合做“按地址访问”的存储结构。最常见的用法就是建模存储器。比如一个64深、每项8bit的寄存器堆logic [7:0] regfile [0:63];这一行声明了64个8bit的存储单元。我们可以用地址变量去读写always_ff (posedge clk) begin if (wr_en) regfile[wr_addr] wr_data; end assign rd_data regfile[rd_addr];这种写法在RTL里非常常见。为什么说“非合并数组适合建存储器”因为存储器的读写天然是按独立单元的每个地址对应一个独立的数据元素之间不需要比特级联系而且后面如果要加字节使能、掩码操作非合并数组每个元素是独立logic向量操作起来更灵活。3.2 为什么建模存储器、队列、关联数组都离不开它除了建模存储器非合并数组还有一层更重要的身份——它是SystemVerilog所有高级数据结构的“基座”。队列queue、动态数组dynamic array、关联数组associative array本质上都是非合并类型它们的每个元素都是独立存储的支持动态增删、遍历等软件化操作。比如验证环境里经常要写一个“收集所有激励地址”的队列logic [31:0] addr_queue [$];这里的[$]表示一个非合并类型队列每个元素是一个32bit的合并向量。你可以在仿真过程中不断push、pop、遍历这在C语言里很自然但在几乎所有的硬件描述语言里只有非合并数组支持这种动态结构。关联数组也类似typedef logic [7:0] byte_t; byte_t scoreboard [int key]; // 以int为键的动态存储关联数组的每个value都是独立的8bit数据按键访问非常灵活。这类数据结构在UVM环境里使用频率极高而它们都建立在“非合并类型”的基础上。这也就解释了为什么你在学SystemVerilog时如果不把非合并数组的概念搞清楚后面学队列、关联数组、动态数组都会感觉发虚。另外一点值得强调非合并数组的遍历通常配合foreach使用读取效率和代码可读性都更好foreach (regfile[i]) begin if (regfile[i] ! 0) $display(regfile[%0d] 0x%02h, i, regfile[i]); end这段代码会遍历所有64个元素并打印非零值。注意foreach对合并数组同样适用但行为不同合并数组的foreach是按变量声明时的维度和方向遍历比如foreach (packed_2d[i, j])。4. 深度对比布局差异、复制语义与类型兼容性4.1 内存布局上的根本差异先从“底层”角度看一下为什么这两个数组的差别不只是语法。对于仿真工具而言合并数组的存储是连续比特的它可以直接映射为一个整数、一个向量甚至在计算机仿真里可以按整数的形式存取。而非合并数组每个元素是独立的内存对象为了支持动态大小、独立索引工具会在内部做成类似“数组的数组”的存储结构。这个差异带来三个直接后果合并数组的整体赋值、比较、运算效率更高。因为它就是一个大的比特值工具可以直接用机器整数操作。非合并数组的元素可以独立初始化、独立修改但整体赋值时必须用“数组赋值模式”或者逐元素拷贝。合并数组可以使用$bits()很容易算出总比特数非合并数组的$bits()也有效但语义是“所有元素的总比特数”。4.2 赋值和复制的坑谈到赋值这里面的坑非常多。先说合并数组同类合并数组之间可以直接赋值、可以直接比较、可以整体做算术运算所有操作都跟普通向量一致。而非合并数组的赋值规则就复杂了。相同类型、相同维度的非合并数组可以直接整体赋值logic [7:0] mem_a [0:3]; logic [7:0] mem_b [0:3]; mem_a mem_b; // 合法整体赋值但如果维度不同、元素类型不同直接赋值会报错。你需要用循环或者赋值模式logic [7:0] mem_src [0:3]; logic [7:0] mem_dst [0:5]; // 这样写会报错维度不匹配 // mem_dst mem_src; // 正确做法逐元素拷贝 foreach (mem_src[i]) mem_dst[i] mem_src[i];数组赋值模式array assignment pattern是SystemVerilog中针对非合并数组的初始化/赋值语法但它要求你能精确匹配元素数量和类型logic [7:0] mem [0:3] {8h00, 8h11, 8h22, 8h33};花括号前的一撇是数组赋值模式的标志这个细节很容易被忽略。如果少写一撇就可能被理解成拼接操作符从而产生位宽不匹配的编译错误。4.3 类型兼容与隐式转换在类型兼容方面还有两个点特别容易搞混一是合并数组和整数之间的关系二是合并数组和非合并数组之间的互转。合并数组由于本质是比特串它和integer、bit向量之间可以很方便地互转logic [31:0] packed_val; int ival signed(packed_val); // 显式转换非合并数组不能这样直接转。如果你想拿到一个非合并数组里的所有比特拼成一个合并数组需要手动拼接或使用循环。在比较新的仿真工具里某些场景下也支持packed(unpacked_var)这种转换语法但可移植性不一定好还是建议写成显式循环。还有一个经典的报错场景是“在模块端口处传递数组”。SystemVerilog允许端口声明为非合并数组类型但必须使用ref或者显式声明方向而且端口类型的兼容性检查非常严格。很多新手在写接口模块时把合并数组和非合并数组混着连编译直接报类型不匹配。后面我会在常见问题里专门讲。5. 实际工程中的选型经验5.1 RTL设计场景怎么选在RTL可综合代码里选型的原则其实很清晰如果你的数据在逻辑上是一个“数”需要做算术、比较、移位、位提取那一定要用合并数组。因为可综合工具对合并数组的综合结果就是一堆连续的触发器或线网这和硬件数据通路的天然结构是一致的。如果你的数据在逻辑上是一个“存储阵列”按地址随机访问每个单元独立读写那用非合并数组比如寄存器堆、SRAM模型、FIFO的数据存储体。我见过不少新手在写FIFO时把存储体声明成合并数组// 不推荐存储体用合并数组声明 logic [15:0][7:0] fifo_mem; // 语义不对 // 推荐存储体用非合并数组声明 logic [7:0] fifo_mem [0:15];如果把存储体写成了logic [15:0][7:0] fifo_mem虽然从功能上也能访问但语义上把16个字节打包成了一个128bit的向量综合结果可能变成一个大的向量寄存器而不是一个可按地址读写的存储阵列这对于后续的时序优化、功耗分析都会产生误导。还有一个常见问题是“如何用参数化方式声明多维数组”。建议用localparam或parameter来定义深度和数据位宽parameter DEPTH 16; parameter DWIDTH 8; logic [DWIDTH-1:0] mem [0:DEPTH-1];这样后续如果深度或位宽变化只需要改参数不容易出错。在验证环境里建议用typedef定义一个“数据字”类型再用这个类型定义存储体让代码更清晰typedef logic [DWIDTH-1:0] word_t; word_t mem [0:DEPTH-1];5.2 验证环境场景怎么选在验证环境testbench里选型会更灵活因为不需要综合仿真的“运动方式”决定了你选哪种结构。如果你要维护一个“以地址为索引的镜像表”比如期望值存储、scoreboard里的参考数据用非合并数组/关联数组键值按地址或事务ID映射。如果你要实现一个数据流队列用非合并的队列[$]它可以动态增长非常适合FIFO模型的建模。如果你要处理一个数据包的包头大量字段需要位提取直接用合并数组更省事。举个验证场景的例子。假设你在写一个AXI总线的monitor需要收集每笔写事务的地址、数据、写strobes然后再校验数据你很可能定义这样一个结构体typedef struct packed { logic [31:0] addr; logic [31:0] data; logic [3:0] strobe; } axi_write_t; axi_write_t wr_queue [$];这里struct packed是合并结构体它可以被当成一个比特串来拼接、比较外层再用队列存储多笔事务这就是“合并结构体 非合并容器”的经典组合。5.3 多维数组的混合声明实际中还会遇到多维场景。比如你要建模一个二维的像素缓存每个像素有RGB三个通道每个通道8bittypedef logic [7:0] pixel_channel_t; typedef struct packed { pixel_channel_t r; pixel_channel_t g; pixel_channel_t b; } pixel_t; pixel_t frame_buffer [0:479][0:639];在这个例子里pixel_t是一个24bit的合并结构体frame_buffer是一个480×640的非合并数组。这样声明的好处是既可以用frame_buffer[row][col]按像素访问也可以把单个像素直接当作一个24bit的向量来处理比如做像素拼接、传输。这种“外层非合并、内层合并”的层级结构非常贴近硬件的数据组织方式。反过来如果你想按平面访问所有红色通道那就需要遍历每个像素取出.r字段存到一个新数组里。由于非合并数组之间没有比特级联系你不能用一个切片一下子把所有红色通道抽出来遍历是最可靠的办法。6. 常见问题与排查技巧实录6.1 赋值报错的典型场景我在带新人时发现数组相关报错是SystemVerilog编译错误里最多的几类之一。常见的问题有尝试把非合并数组直接赋值给合并数组logic [7:0] mem [0:3]; logic [31:0] packed_total; // packed_total mem; // 错误这种写法几乎不可能通过编译。正确做法是用循环拼接always_comb begin packed_total 0; for (int i 0; i 4; i) packed_total[i*8 : 8] mem[i]; end或者用拼接操作符packed_total {mem[3], mem[2], mem[1], mem[0]};注意拼接顺序非合并数组的下标0是低地址还是高地址取决于你的设计约定写前先确认。在模块端口连接数组时类型不匹配。端口若声明为logic [7:0] p [0:3]那么连接时也必须是完全一致的非合并数组类型。有人习惯在内部定义成logic [31:0] packed_data然后想把packed_data[31:0]逐位对应到端口的4个字节上这不能直接连需要通过中间变量转换。6.2 位宽不匹配与大小端合并数组的声明方向会影响索引对应关系。如果声明为logic [31:0] data那么data[0]是最低有效位如果声明为logic [0:31] data那么data[0]是最高有效位。虽然SystemVerilog允许降序和升序两种声明很多工程师习惯用降序[31:0]但在定义协议字段、数据结构时升序[0:31]也会遇到。这里的坑在于一旦有人用了升序声明位选和域切片的表现会和降序相反拼接出来的值容易和预期差一个字节序。多维合并数组更是重灾区。比如logic [3:0][7:0] data; data 32hA1B2C3D4;data[0]到底是0xA1还是0xD4答案是0xD4。因为[3:0]维度中最左边的是最高维data[0]是低维字节而整体向量又是大端风格高位字节在左。如果你没有意识到这一点在做字节重排时会得到和预期完全相反的结果。一个经验是写代码前先在注释里标明“index 0 对应最低有效字节”还是“最高有效字节”然后保持全工程一致。否则等综合、仿真结果出来再排查往往要花掉不少时间。6.3 仿真性能相关的坑非合并数组在仿真性能上也有讲究。如果你声明了一个超大的非合并数组比如深度很大的存储器仿真时会占用大量内存。某些仿真器对非合并数组的访问优化不如合并数组导致仿真变慢。性能建议是对于要频繁整体操作的数据比如做校验、加密、位变换尽量用合并数组仿真器能把它映射为机器字运算速度快。对于纯粹的存储建模用非合并数组但注意深度合理别一次性开一个亿级的数组。如果只是验证用可以改用关联数组只分配实际用到的条目。还有一个我踩过的坑在循环里反复拷贝大规模非合并数组。比如for (int i 0; i 10000; i) begin mem_tmp mem_large; // 每次迭代都拷贝整个大数组 end这种写法会让仿真工具在每次循环里做大内存复制如果mem_large有上万条目仿真会慢到让你怀疑人生。正确的做法是尽量用队列、关联数组等动态结构或者只拷贝需要的条目避免无意义的整体复制。最后再分享一个我在项目里惯用的小技巧如果你不确定某个变量是合并还是非合并类型可以在仿真里打印$bits()看它返回的是总比特数还是别的值同时用$typename()可以查出变量的完整类型字符串这在调试端口连接、typedef别名时非常有用。我在调试一个跨模块数组连接问题时就是靠$typename()发现两个模块里虽然写法类似但一个用了logic [7:0] a [4]另一个用了logic [7:0] a [0:3]看起来一样类型上报出的字符串却不同导致端口连不上。这种细节不真正在项目里跑一遍靠死记硬背很难记住。
延伸阅读

更多相关文章

2026/10/4 17:16:51

【灵神高频面试题合集17-20】动态规划(上)

基础算法精讲题目汇总:灵茶山艾府 - 【基础算法精讲】- GitHub 视频:灵茶山艾府的个人空间-灵茶山艾府个人主页-哔哩哔哩视频 力扣最全 DP 题单:分享丨【算法题单】动态规划(入门/背包/划分/状态机/区间/状压/数位/树形/优化&am…

2026/10/4 17:11:51

SSM+Vue汽车售票网站:从业务设计到并发数据一致性

做毕设那会儿,我周围不少同学都扎堆去做"网上商城""图书管理系统"这类选题,结果答辩时老师问两句并发控制就卡住了。我当时选了"基于JAVA的汽车售票网站",理由很简单:汽车票务这个场景天然包含车次…

2026/10/4 21:42:02

omofun动漫|安卓安装|官网入口和追番入门

第一次接触 OmoFun动漫,可以先把它看作一处面向动画爱好者的内容入口:打开后,不必急着寻找某一部作品,不妨先浏览首页推荐、分类栏目与专题信息,了解平台的页面布局,再按自己的兴趣逐步筛选。不同版本的界面…

2026/10/4 21:42:02

Ace Data Cloud 接入 GLM 实战:Chat Completion API 与流式输出全攻略

最近我一直在折腾怎么把大模型对话能力接到现有产品里,问得最多的问题就是“你的 GLM 接口怎么接的”“用了什么平台”。这篇我直接把我完整的接入过程交底:从 Ace Data Cloud 上开通 GLM 模型、拿到 Chat Completion API 的调用凭证,到 Pyth…

2026/10/4 21:42:02

芯片烧录本质:ISP、ICP、IAP三者原理与工程实践辨析

1. 芯片烧录不是“刷机”,而是给芯片装上第一行能跑起来的代码 很多人第一次接触单片机开发,看到“烧录”这个词,下意识联想到手机刷机、U盘拷文件——这其实是个危险的误解。我带过不少刚毕业的实习生,他们第一次用ST-Link往STM3…

2026/10/4 21:42:02

从零训练中文语言模型:手写Transformer与预训练微调全流程

把“AI engineering from scratch”当口号的人很多,真正从零手搓过一遍的人比例很低。我去年完整走过一遍:自己清洗数据、从零训练分词器、手写Transformer核心模块、把小模型喂到收敛、再做推理能力微调。整个过程如果用商业眼光衡量确实不划算&#xf…

2026/10/4 21:42:02

MIPI LP RX硬件设计实战:从信号完整性到FPGA实现

1. 项目概述:MIPI LP RX到底在解决什么问题?MIPI LP RX——这个缩写组合乍看像一串技术代号,实则直指一个高频、高痛、高门槛的硬件接口工程现场:低功耗(Low-Power)模式下的MIPI接收端(Receiver…

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑