SystemVerilog学习之路(4)— 动态数组、队列和关联数组

一、前言

SystemVerilog提供的动态数组类型,可以在仿真时分配空间或者调整宽度,这样在仿真中就可以使用最小的存储量。
SystemVerilog引进了一种新的数据类型—队列,它结合了链表和数组的优点。队列与链表相似,可以在一个队列中的任何地方增加或删除元素,这类操作在性能上的损失比动态数组小得多,因为动态数组需要分配新的数组并复制所有元素的值,队列与数组相似,可以通过索引实现对任一元素的访问,而不需要像链表那样去遍历目标元素之前的所有元素。
SystemVerilog提供了关联数组类型,用来保存稀疏矩阵的元素,这意味着当你对一个非常大的地址空间进行寻址时,SystemVerilog只为实际写入的元素分配空间。

二、动态数组

动态数组在声明时使用空的下标[],这意味着数组的宽度不在编译时给出,而在程序运行时在指定,数组在最开始时是空的,所以必须调用new[]操作符来分配空间,同时在方括号中传递数组,另外,也可以把数组名传给new[]构造符,并把已有数组的值复制到新数组里,编写代码如下所示,

module arr2;
	int dyn[],d2[];													// 声明两个动态数组
	
	initial begin
		dyn = new[5];												// 给dyn分配5个元素的容量
		foreach(dyn[i])
			dyn[i] = i;												// 对dyn中的元素进行初始化
		d2 = dyn;													// 复制一个动态数组给d2
		d2[0] = 5;													// 修改d2数组中第0个元素的值
		$display("@1: dyn[0] = %d, d2[0] = %d.", dyn[0], d2[0]);	// 显示dyn和d2两个数组中第0个元素的值
		dyn = new[20](dyn);											// 分配20个元素的存储空间给dyn,并将原dyn数组中的元素复制过来
		$display("@2: dyn: ", dyn);
		dyn = new[100];												// 分配100个元素的存储空间给dyn,此时旧值不复存你
		$display("@3: dyn: ", dyn);
		dyn.delete();												// 删除所有的元素
		$display("@4: dyn: ", dyn);
	end
endmodule

仿真运行如下所示,我们在对动态数组进行复制时,是将所有的元素重新复制了一份,改变新数组的元素并不会影响原数组的元素。我们使用dyn = new[20](dyn);重新生成数组时,会将原来数组的元素放在其低位,然后释放原有数组的空间,即我们可以通过new[0]实现和.delete()一样的效果。

systemverilog 空队列 systemverilog 二维队列_systemverilog 空队列


如果想声明一个常量数组但又不想统计其元素个数或是害怕会计算错误 元素个数时,可以使用动态数组并然后用常量数组进行赋值,只要基本数据类型相同,定宽数组和动态数组之间就可以相互赋值,编写代码如下所示,

module arr2;
	bit [7:0] mask[] = '{8'b0000_0000, 8'b0000_0001, 
						8'b0000_0011, 8'b0000_0111, 
						8'b0000_1111, 8'b0001_1111, 
						8'b0011_1111, 8'b0111_1111, 
						8'b1111_1111 };
	
	initial begin
		$display("@1: dyn: ", mask);	
		$display("@2: dyn.size: %d.", mask.size());
		mask.delete();
		$display("@3: dyn.size: %d.", mask.size());
	end
endmodule

仿真运行如下所示,使用动态数组的内建子程序.size()可以获得数组的宽度

systemverilog 空队列 systemverilog 二维队列_systemverilog 空队列_02

三、队列

队列的声明是使用带有美元符号的下标:[$],队列元素的编号是从0到$。队列是不需要new[]去创建空间的,只需要使用队列的方法为其增减元素即可,队列一开始的空间是不为零的,队列的一个简单使用即是通过其自带方法push_back()pop_front()的结合使用来实现FIFO的用法。编写代码如下所示,

module queue;
	int j = 1;
	int q2[$] = {3, 4};		// 队列的常量不需要使用” ' “
	int q[$] = {0, 2, 5};
		
	initial begin
		q.insert(1, j);				// 将j的值插入到第1位,插入后为{0,1,2,5}
		$display("@1: q: ", q);
		q.insert(3, q2[0]);			// 将q2的第0个元素插入到第3位,插入后为{0,1,2,3,5}
		$display("@2: q: ", q);
		q.insert(4, q2[1]);			// 将q2的第1个元素插入到第4位,插入后为{0,1,2,3,4,5}
		$display("@3: q: ", q);
		q.delete(1);				// 删除第1个元素
		$display("@4: q: ", q);
		
		q.push_front(6);			// 在队列前面插入一个元素值为6
		$display("@5: q: ", q);
		j = q.pop_back;				// 将队列末尾的元素弹出并赋值给j
		$display("@6: j= %0d", j);
		q.push_back(8);				// 在队列末尾插入一个元素值为8
		$display("@7: q: ", q);
		j = q.pop_front;			// 将队列最前面的元素弹出并赋值给j
		$display("@8: j= %0d", j);
		foreach(q[i])
			$display(q[i]);			// 打印整个队列
		q.delete;					// 删除整个队列 
	end
endmodule

仿真运行如下所示,

systemverilog 空队列 systemverilog 二维队列_SystemVerilog_03

我们还可以使用字下标串联来替代队列的内建方法,如果把$放在一个范围表达式的左边,那么$将代表最小值,例如[$:2]就代表[0:2],如果$放在表达式的右边,则代表最大值,编写代码如下所示

module queue;
	int j = 1;
	int q2[$] = {3, 4};		// 队列的常量不需要使用” ' “
	int q[$] = {0, 2, 5};
		
	initial begin
		q = {q[0], j, q[1:$]};				// 将j的值插入到第1位,插入后为{0,1,2,5}
		$display("@1: q: ", q);
		q = {q[0:2], q2[0], q[3:$]};		// 将q2的第0个元素插入到第3位,插入后为{0,1,2,3,5}
		$display("@2: q: ", q);
		q = {q[0:3], q2[1], q[4:$]};		// 将q2的第1个元素插入到第4位,插入后为{0,1,2,3,4,5}
		$display("@3: q: ", q);
		q = {q[0], q[2:$]};					// 删除第1个元素
		$display("@4: q: ", q);
		
		q = {6, q };						// 在队列前面插入一个元素值为6
		$display("@5: q: ", q);
		j = q[$];							// 将队列末尾的元素赋值给j
		q = q[0:$-1];						// 删除最后一个数据
		$display("@6: j= %0d", j);
		q = {q, 8};							// 在队列末尾插入一个元素值为8
		$display("@7: q: ", q);
		j = q[0];							// 将队列最前面的元素赋值给j
		q = q[1:$];							// 删除第0个数据
		$display("@8: j= %0d", j);
		foreach(q[i])
			$display(q[i]);					// 打印整个队列
		q = {};								// 删除整个队列 
	end
endmodule

仿真运行如下所示,

systemverilog 空队列 systemverilog 二维队列_动态数组_04


队列中的元素是连续存放的,所以在队列的前面或后面存取数据非常方便,无论队列多大,这种操作所耗费的时间都是一样的;但是在队列中间增加或删除元素需要对已经存在的数据进行搬移以便腾出空间,相应的操作所耗费的时间会随着队列的大小线性增加。

另外,我们是可以将定宽或动态数组的值赋给队列的。

四、关联数组

使用关联数组类型时,可以在对一个非常大的地址空间进行寻址时,SystemVerilog只为实际写入的元素分配空间。如图所示,关联数组中只保留0…3、42、1000、4521和200000等位置上的值,这样用来存放这些值的空间就会比有200000个条目的定宽或动态数组所占用的空间要小得多。

systemverilog 空队列 systemverilog 二维队列_动态数组_05


关联数组采用在方括号中放置数据类型的形式来进行声明,例如[int][Packet],编写代码如下所示

module arr3;
	initial begin
		bit [63:0] assoc[bit[63:0]], idx  = 1;
		
		repeat(64) begin						// 对稀疏分布的元素进行初始化
			assoc[idx] = idx;
			idx = idx<<1;
		end
		
		foreach(assoc[i])						// 使用foreach遍历数组
			$display("@1: assoc[%h]=%h", i, assoc[i]);
				
		assoc.first(idx);
		assoc.delete(idx);						// 找到并删除第一个元素
		
		if(assoc.first(idx)) begin				// 使用函数遍历数组
			do
				$display("@2: assoc[%h]=%h", idx, assoc[idx]);
			while(assoc.next(idx));
		end 
		$display("The array now has %0d elements.", assoc.num);
	end 
endmodule

仿真运行的片段如下所示,

systemverilog 空队列 systemverilog 二维队列_动态数组_06


和Perl语言中的哈希数组类似,关联数组也可以使用字符串索引进行寻址,可以通过使用函数exists()来检查元素是否存在,如果试图读取尚未被写入的元素,SystemVerilog会返回数组的缺省值,例如双状态类型是0,对于四状态类型是X。编写代码如下所示

module arr3;
	int switch[string], min_addr, max_addr;
	
	initial begin
		int i, r, file;
		string s;
		file = $fopen("switch.txt", "r");
		while(!$feof(file)) begin
			r = $fscanf(file, "%s %d", s, i);
			switch[s] = i;
		end 
		$fclose(file);
		
		// 获取最小地址值,缺省为0
		min_addr = switch["min_addr"];
		$display("@1: min_addr=%0d", min_addr);
		
		// 获取最大地址值,缺省为1000
		if(switch.exists("max_addr"))
			max_addr = switch["max_addr"];
		else
			max_addr = 1000;
		$display("@2: max_addr=%0d", max_addr);
			
		// 打印数组所有的元素
		foreach(switch[s])						// 使用foreach遍历数组
			$display("@3: switch[%s]=%0d", s, switch[s]);
	end 
endmodule

然后在当前文件夹下新建switch.txt文件内容如下所示

Sunday 7
Monday 1
Tuesday 2
Wednesday 3
Thursday 4
Friday 5
Saturday 6

仿真运行如下所示,

systemverilog 空队列 systemverilog 二维队列_systemverilog 空队列_07

五、链表

SystemVerilog提供了链表数据结构,类似于标准模板库(STL)的列表容器,这个容器被定义为一个参数化的类,可以根据用户需要存放各种类型的数据。虽然SystemVerilog提供了链表,但应该避免使用它,C++程序员也许很熟悉STL,但是SystemVerilog的队列更加高效易用 。

六、附录