SystemVerilog 第5章 面向对象编程基础
5.1概述
对结构化编程语言,例如 Verilog和C语言来讲,它们的数据结构和使用这些数据结构的代码之间存在很大的沟壑。数据声明、数据类型与操作这些数据的算法经常放在不同的文件里,因此造成了对程序理解的困难。
Verilog程序员的境遇比C程序员更加棘手,因为Ⅴ erilog语言中没有结构( structures),只有位向量和数组。如果你想要存储一个总线事务( bus transaction)的信息,你就需要多个数组:一个用于保存地址,一个用于保存数据,一个用于保存指令等等。事务( transaction)N的信息分布在这些所有的数组中。用来创建、发送和接收事务的代码位于模块( module)中,但这个模块可能连接到总线上,也可能根本没有连接到总线上。最糟糕的是,这些数组都是静态的,所以如果测试平台( testbench)只配置了100个数组项,而当前测试需要101个时,就需要修改源代码来改变数组的大小,并且重新编译。结果,数组的大小被配置成可以容纳最大数目的事务,但是在一个普通的测试中,大多数的存储空间却浪费了。
面向对象编程(OOP)使用户能够创建复杂的数据类型,并且将它们跟使用这些数据类型的程序紧密地结合在一起。用户可以在更加抽象的层次建立测试平台和系统级模型,通过调用函数来执行一个动作而不是改变信号的电平。当使用事务来代替信号翻转的时候,你就会变得更加高效。这样做的附加好处是,测试平台跟设计细节分开了,它们变得更加可靠,更加易于维护,在将来的项目中可以重复使用。
如果用户已经熟悉了面向对象编程(OP),则可以跳过这一章,因为 SystemVerilog相当严格地遵守OOP的规则。但你还是需要读一下5.18节,以便了解如何搭建一个测试平台。第8章给出了一些诸如继承等OOP的高级概念,以及更多的测试平台搭建技巧。
5.2考虑名词,而非动词
将数据和代码组合在一起可以有效地帮助你编写和维护大型测试平台。如何把数据和代码组合到一起?你可以先想想测试平台是怎么工作的。测试平台的目标是给一个设计施加激励,然后检查其结果是否正确。如果把流入和流出设计的数据组合到一个事务里,那么围绕事务及其操作实施测试平台就是最好的办法。在OOP中,事务就是测试平台的焦点。
传统的测试平台强调的是要做的操作:创建个事务、发送、接收、检查结果、然后产生报告。而在OOP中,你需要重新考虑测试平台的结构,以及每部分的功能。发生器( generator)创建事务并且将它们传给下一级,驱动器( driver)和设计进行会话,设计返回的事务将被监视器( monitor)捕获,记分板( scoreboard)会将捕获的结果跟预期的结果进行比对。因此,测试平台应该分成若干个块( block),然后定义它们相互之间如何通信。
5.3编写第一个类(Clas)
类封装了数据和操作这些数据的子程序。例5.1是一个通用数据包类。这个数据包包含了地址、CRC和一个存储数值的数组。在 Transaction类中有两个子程序:一个输出数据包地址的函数和一个计算循环冗余校验码〔CRC: cyclic redundancy check)的函数。
为了更加方便地对齐一个块的开始和结東部分,你可以在该块的最后放上一个标记(label)。在例5.1中,这些结束标记可能看起来是多余的,但是在具有很多嵌套块的真实代码中,标记可以很好地帮助你配对简单的结束或 endtask, endfunction和endaclass。
例5.1简单的 Transaction类
class Transaction;bit[31:0] addr ,crc, data[8];function void display;$display("Transaction :%h",addr);endfunction :displayfunction void calc_crc;crc=addr^data.xor;endfunction:calc_crcendclass :Transaction
5.4在哪里定义类
在 System Verilog中,你可以把类定义在 program、 module、 package中,或者在这些块之外的任何地方。类可以在程序和模块中使用。本书仅给出了类在程序块中使用的情况,如第4章所示。在此之前,可以将程序块当作一个包含了测试代码的模块,它含有一条测试、组成测试平台的对象及创建、初始化并运行测试的初始化块。当你创建一个项目的时候,可能需要将每个类保存在独立的文件中。当文件的数目变得太大的时候,可以使用 System Verilog的包( package)将一组相关的类和类型定义捆绑在一起。例如,可以将所有的SCSI/ATA事务组合到一个包中。这个包与系统的其他部分独立,可以单独地编译。其他不相关的类,例如事务、记分板或者不同协议( protocol)的类应该放在不同的文件中。
关于包的更详细的信息可以参见 System Verilog LRM。
5.5OOP术语
下面是一些OOP的术语定义以及它们与 Verilog-2001的大致的对应关系。
(1)类( class):包含变量和子程序的基本构建块。 Verilog中与之对应的是模块(mocule)
(2)对象( object):类的一个实例。在 Verilog中,你需要实例化一个模块才能使用它
(3)句柄( handle):指向对象的指针。在 verilog中,你通过实例名在模块外部引用信号和方法。一个OOP句柄就像一个对象的地址,但是它保存在一个只能指向单一数据类型的指针中。
(4)属性( property):存储数据的变量。在 Verilog中,就是寄存器(reg)或者线网(wire)类型的信号
(5)方法( method):任务或者函数中操作变量的程序性代码。 Verilog模块除了 initial和 always块以外,还含有任务和函数
(6)原型( prototype):程序的头,包括程序名返回类型和参数列表。程序体则包含了执行代码。
本书使用了更加传统的 Verilog术语:变量( variable)和程序( routine),而没有使用OOP中的属性( property)和方法( method)(本书实际上始终将 routine翻译为“程序或子程序—一译者)。
在 Verilog中,通过创建模块并且逐层例化就可以得到一个复杂的设计。在OOP中创建类并且例化它们(创建对象),你可以得到一个相似的层次结构下面是一个对这些OQP术语的比喻。将类视为一个房子的蓝图( blueprint),该设计图描述了房子的结构,但是你不能住在一个蓝图里,你需要建造一幢实际的房子。一个对象就是一个实际的房子。如同一组蓝图可以用来建造每个房子的各个部分,一个类也可以创建很多的对象。房子的地址就像一个句柄,它唯一地标志了你的房子。在你的房子里面,你有很多东西,例如带有开关的灯(开或者关)。类中的变量用来保存数值,而子程序用来控制这些数值。一个房子类可能具有很多盏灯,对 turn_on_porch_ light()的一个简单调用就可以将一个房子的走廊灯变量值置为ON。
5.6创建新对象
Verilog和OOP都具有例化的概念,但是在细节方面却存在着一些区别。一个 Verilog模块,例如一个计数器,是在代码被编译的时候例化的。而一个 System verilog类,例如个网络数据包,却是在运行中测试平台需要的时候才被创建。Ⅴerilog的例化是静态的,就像硬件一样在仿真的时候不会变化,只有信号值在改变。而 System Verilog中,激励对象不断地被创建并且用来驱动DUT,检查结果。最后这些对象所占用的内存可以被释放以供新的对象使用。O0P和Ⅴerilog之间的相似性也有一些例外。 Verilog的顶层模块是不会被显式地例化的。但是 System Verilog类在使用前必须先例化。另外, Verilog的实例名只可以指向一个实例,而 System verilog句柄可以指向很多对象,当然一次只能指向一个。
5.6.1实例化对象
在例5.2中,tr是一个指向 Transaction类型对象的句柄,因此tr可以简称为个 Transaction句柄。
例52声明和使用一个句柄
Transaction tr;//声明一个句柄
Tr=new();//为一个 Transaction对象分配空间
在声明句柄tr的时候,它被初始化为特殊值nu11。接下来,你调用new()函数来创建 Transaction对象。new函数为 Transaction分配空间,将变量初始化为默认值(二值变量为0,四值变量为X),并返回保存对象的地址。对于每一个类来讲, System Verilog创建一个默认的new函数来分配并初始化对象。有关new函数的更多细节,请参见5.6.2节。
5.6.2定制构造函数( Constructor)
有时候OOP术语会使一个简单的概念看起来复杂化。例化是什么意思?当你调用new函数例化一个对象的时候,你是在为该对象申请一个新的内存块来保存对象的变量。例如, Transaction类有两个32位的寄存器(addx和crc)以及一个具有八个元素(数据)的数组,总计包含10个长字( longword),或者说40个字节。所以当你调用new函数的时候, System verilog就会分配40字节的存储空间。如果你使用过C语言,你可以发现这个步骤跟mal1loc函数非常相似。(应当指出的是, System Verilog为四值变量使用更多的内存,并且会保存一些内部信息,例如对象的类型等。)
构造函数除了分配内存之外,它还初始化变量,在默认情况下,它将变量设置成默认数值——二值变量为0,四值变量为X,等等。你可以通过自定义new函数将默认值设成你想要的数值。这就是为什么new函数也称为“构造函数”,因为它创建对象,一如用木头和钉子建造你的房子。但是new函数不能有返回值,因为构造函数总是返回一个指向类对象的句柄,其类型就是类本身。
例5.3简单的用户定义的new()函数
class Transaction ;logic [31:0] addr ,crc, data[8];function new;addr=3;foreach (data[i])data[i]=5;endfunctionendclass
例5.3将addr和data设为固定数值,但是crc仍将被初始化位默认值X( Systemverilog自动为对象分配存储空间)。你可以使用具有默认值的函数参数来创建更加灵活的构造函数,如例5.4所示。这样你就可以在调用构造函数的时候给addr和data指定值或者使用默认值。
例54一个带有参数的new()函数
class Transaction ;logic [31:0] addr ,crc, data[8];function new(logic [31:0] a=3,d=5);addr=a;foreach (data[i])data[i]=d;endfunctionendclassinitial beginTransaction tr;tr=new(10); //data使用默认值end
System verilog怎么知道该调用哪个new()函数呢?这取决于赋值操作符左边的句柄类型。在例5.5中,调用 Driver构造函数内部的new()函数,会调用 Transaction的new()函数,即使 Driver的new函数的定义离它更近。这是因为tr是Transaction句柄, System verilog会做出正确的选择,创建一个 Transaction类的对象。
例5.5调用正确的new()函数
class Transaction ;...
endclass :Transactionclass Driver;Transaction tr;function new(); //Driver的new 函数tr=new();//调用Transaction 的New函数endfunctionendclass;Driver
5.6.3将声明和创建分开
你应该避免在声明一个句柄的时候调用构造函数,即new函数。虽然这样在语法上是合法的,但是这会引起顺序问题,因为在这时构造函数在第一条过程语句前就被调用了。你可能希望按照一定的顺序初始化对象,但是如果在声明的时候调用了new函数,你就不能控制这个顺序了。此外,如果你忘记了使用 automatic存储空间,构造函数将在仿真开始时,而非进入块的时候被调用。
5.6.4new()和new[ ]的区别
你可能已经注意到new()函数跟2.3节中用来设置动态数组大小的new[]操作看起来非常相似。它们都申请内存并初始化变量。两者最大的不同在于调用new()函数仅创建了一个对象,而new[ ]操作则建立一个含有多个元素的数组。new( )可以使用参数设置对象的数值,而new[ ]只需使用一个数值来设置数组的大小。
5.6.5为对象创建一个句柄
OOP的新手经常会混淆对象和对象的句柄。其实两者之间的区别是非常明显的。你通过声明一个句柄来创建一个对象。在一次仿真中,一个句柄可以指向很多对象。这就是OOP和 System verilog的动态特性。
在例5.6中,t1首先指向一个对象,然后指向另一个对象。图5.1给出了对象和指针最后的结果。
例5.6为多个对象分配地址
Transaction t1,t2;//声明两个句柄t1=new();//为第一个Transaction对象分配地址t2=t1; //t1 和t2 都指向该对象t1=new();//为第二个Transaction对象分配地址

为什么我们希望动态地创建对象?在一次仿真过程中,你可能需要创建成百上千个事务。 System Verilog使你能够在需要的时候自动创建对象。在 verilog中,你只能使用固定大小的数组,而且这个数组必须要大到能够容纳最大数量的事务。应当指出的是,这种动态的对象创建不同于 Verilog语言之前所提供的任何特性。Verilog模块的实例和它的名字是在编译的过程中静态地捆绑在一起的。即使是在仿真过程中产生和自动注销的 automatic变量,名字和内存也总是捆在一起的。
5.7对象的解除分配( deallocation)
你已经知道了如何创建一个对象,但是你知道怎么回收它吗?例如,你的测试平台创建并且发起了上千次的事务,例如发到DUT的事务。一旦你得知事务已经成功完成,并且也得到了统计结果,你就不需要再保留这些对象了。这时候,你需要回收内存。否则,长时间的仿真会将内存耗尽,或者运行得越来越慢。垃圾回收是一种自动释放不再被引用的对象的过程。 System verilog分辨对象不再被引用的办法就是记住指向它的句柄的数量,当最后一个句柄不再引用某个对象了System verilog就释放该对象的空间。
例5.7创建多个对象
Transaction t; //创建一个句柄
t=new(); //分配一个新的Transaction
t=new(); //分诶第二个,并释放第一个
t=null; //解除分配第二个
例5.7的第二行调用new()创建了一个对象,并且将其地址保存在句柄t中。下个new()函数的调用创建了一个新的对象,并将其地址放在t中,覆盖了句柄t先前的值。因为这时候已经没有任何句柄指向第一个对象, System verilog就可以将其解除分配了。对象可以立刻被删除,或者等上一小段时间再删除。最后一行明确地清除句柄,所以至此第二个对象也可以被解除分配了。
如果你熟悉C++,这些对象和句柄的概念可能看起来不陌生,但是这两者存在着些非常重要的区别。 System verilog的句柄只能指向一种类型,即所谓的“安全类型”。在C中,一个典型的无类型指针只是内存中的一个地址,你可以将它设为任何数值,还可以通过预增量(pre- Increment)操作来改变它。这时候你无法保证指针一定是合法的。C++的指针相对安全些但和C有类似的问题。 System Verilog不允许对句柄作和C类似的改变,也不允许将一种类型的句柄指向另一种类型的对象。( System Verilog的OOP规范比起C++来更加接近Java)。
其次,因为 System verilog在没有任何句柄指向一个对象的时候自动回收垃圾,这就保证了代码中所使用的任何句柄都是合法的。而在C/C++中,指针可以指向一个不再存在的对象。在这些语言中,垃圾回收是手动的,所以当你忘了手动释放对象的时候,代码就可能会存在内存泄露。
System verilog不能回收一个被句柄引用的对象。如果你创建了一个链接表,除非手工设置所有的句柄为null,清除所有句柄,否则 SystemVerilog不会释放对象的空间。如果对象包含有从一个线程派生出来的程序,那么只要该线程仍在运行,这个对象的空间就不会被释放。同样的,任何被一个子线程所使用对象在该线程没有结東之前不会被解除分配。关于线程的更多信息请参见第7章。
5.8使用对象
现在你已经分配了一个对象,那么如何来使用它呢?回到 Verilog模块的对比,可以对对象使用“.”符号来引用变量和子程序,如例5.8所示。
例5.8使用对象的变量和子程序
Transaction t; //声明一个Transaction句柄
t=new(); //创建衣蛾Transaction对象
t.addr=32'h44; //设置变量的值
t.display();//调用一个子程序
严格的OOP规定,只能通过对象的公有方法访问对象的变量,例如get()和put(),这是因为直接访问变量会限制以后对代码的修改。如果将来出现一个更好的(或者另种)算法,你可能因为需要改变所有那些直接引用变量的代码,而导致你不能采用这种新的算法。
5.9静态变量和全局变量
每个对象都有自己的局部变量,这些变量不和任何其他对象共享。如果有两个Transaction对象,则每个对象都有自己的addr、crc和data变量。但有时候你需要个某种类型的变量,被所有的对象所共享。例如,可能需要一个变量来保存已创建事务的数目。如果没有OOP,可能需要创建一个全局变量。然后你就有了一个只被一小段代码所使用,但是整个测试平台都可以访问的全局变量。这会“污染”全局名字空间(namespace),导致即使你想定义局部变量,但是变量对每个人都是可见的。
5.9.1简单的静态变量
在 System Verilog中,可以在类中创建一个静态变量。该变量将被这个类的所有实例所共享,并且它的使用范围仅限于这个类。在例5,9中,静态变量 count用来保存迄今为止所创建的对象的数目。它在声明的时候被初始化为0,因为在仿真开始不存在任何的事务。每构造一个新的对象,它就被标记为一个唯一的值,同时 count将被加1。
例5.9含有一个静态变量的类
class Transaction;static int count =0; //已经创建的对象的数目int id; //实例的唯一标志function new ();id=count++;//设置标志,count递增endfunction
endclassTransaction t1,t2;
initial begint1=new(); //第一个实例,id=0,count=1t2=new(); //第二个实例id=1,count=2$display("Second id =%d ,count=%d ",t2.id,t2.count);end
在例5.9中,不管创建了多少个 Transaction对象,静态变量 count只存在一个。你可以认为 count保存在类中而非对象中的。变量id不是静态的,所以每个 Transaction都有自己的id变量,如图52所示。这样,你就不需要为 count创建一个全局变量了。
使用ID域是在设计中跟踪对象的一个非常好的方法。在调试测试平台的时候,你经常需要一个唯一的值。 System Verilog不能输出对象的地址,但是可以创建ID域来区分对象。当你打算创建一个全局变量的时侯,首先考虑创建一个类的静态变量。一个类应该是自给自足的,对外部的引用越少越好。
5.9.2通过类名访问静态变量
例5.9中使用了句柄来引用静态变量。其实无需使用句柄,你可以使用类名加上::,即类作用域操作符,如例5.10所示。
class Transaction;static int count =0; //已经创建的对象的数目int id; //实例的唯一标志function new ();id=count++;//设置标志,count递增endfunction
endclass
`include "class_static.sv"
module tb;Transaction t1,t2;
initial begint1=new(); //第一个实例,id=0,count=1//$display("Second id =%d ,count=%d ",t1.id,t1.count);$display("first count =%d ,", Transaction::count);t2=new(); //第二个实例id=1,count=2//$display("Second id =%d ,count=%d ",t2.id,t2.count);$display ("second count is %d ",Transaction::count);endendmodule
5.9.3静态变量的初始化
静态变量通常在声明时初始化。你不能简单地在类的构造函数中初始化静态变量,因为每一个新的对象都会调用构造函数。你可能需要另一个静态变量来作为标志,以标识原始变量是否已被初始化。如果需要做一个更加详细的初始化,你可以使用初始化块。但是要保证在创建第一个对象前,就已经初始化了静态变量。
5.9.4静态方法
静态变量的另一种用途是在类的每一个实例都需要从同一个对象获取信息的时候。例如, transaction类可能需要从配置对象获取模式位。如果该位在 transaction类中定义成了非静态句柄,那么每一个对象都会有一份模式位的拷贝,造成内存浪费。例5.11举例说明了如何使用静态变量。例5.11句柄的静态存储
class Transaction;static Condig cfg;//使用静态存储的句柄MODE_E mode;function new ();mode=cfg.mode;endfunctionendclassConfig cfg;initial begincfg=new(MODE_ON);Transaction:: cfg =cfg;end
当你使用更多的静态变量的时候,操作它们的代码会快速增长为一个很大的程序。在 System Verilog中,你可以在类中创建一个静态方法以用于读写静态变量,甚至可以在第一个实例产生之前读写静态变量。
例5.12中含有一个简单的静态函数来显示静态变量的值。 System verilog不允许静态方法读写非静态变量,例如id。你可以根据下面的代码来理解这个限制。在例5.12的最后调用 display_static函数的时候,还没有创建任何 Transaction类的对象,所以还没有为变量id分配存储空间。
例5.12显示静态变量的静态方法
class Transaction;static Config cfg;static int count=0;int id;//显示静态变量的静态方法static function void display_statics();$display("Transaction cfg.mode %s ,count =%0d ",cfg.mode.name(),count);endfunction
endclassinitial begincfg=new(MODE_ON);Transaction ::cfg =cfg;Transaction :;display_statics();end
5.10类的方法
类中的程序也称为方法,也就是在类的作用域内定义的内部task或者 function。例5.13为类 Transaction和 PCI_Tran定义了 display()方法。 SystemⅤ erilog会根据句柄的类型调用正确的 display()方法。
例5.13类中的方法
class Transaction;bit [31:0] addr,crc ,data[8];function void display();$display("@ %0t :TR addr =%h ,crc=%h",$time,addr,crc);$write("\t data [0-7]=");foreach (data[i])$write(data[i]);$display();endfunction
endclassclass PCI_Tran;bit [31:0] addr,data;//使用真实的名字function void display();$display("@%0t :PCI :addr=%h,data=%h",$time,addr,data);endfunction
endclassTransaction t;
PCI_Tran pc;initial begint=new();//创建一个Transaction 对象t.display();; //调用Transaction的方法pc=new();//创建一个PCI事务pc.display(); //调用 PCI事务的方法end
类中的方法默认使用自动存储,所以你不必担心忘记使用 automatic修饰符。
5.11在类之外定义方法
在 System Verilog中你可以将方法的原型定义(方法名和参数)放在类的内部,而方法的程序体(过程代码)放在类的后面定义。
下面是一个如何创建一个块外声明的例子。复制该方法的第一行,包括方法名和参数,然后在开始处添加关键词 extern。然后将整个方法移至类定义的后面,并在方法名前加上类名和两个冒号(::作用域操作符)。上例中的类可以如下定义。
例5.14块外方法声明
class Transaction;bit [31:0] addr,crc ,data[8];extern function void display();endclassfunction void Transaction::display();$display("@ %0t :TR addr =%h ,crc=%h",$time,addr,crc);$write("\t data [0-7]=");foreach (data[i])$write(data[i]);$display();endfunctionclass PCI_Tran;bit [31:0] addr,data;//使用真实的名字extern function void display();
endclassfunction void PCI_Tran::display();$display("@%0t :PCI :addr=%h,data=%h",$time,addr,data);endfunctionTransaction t;
PCI_Tran pc;initial begint=new();//创建一个Transaction 对象t.display();; //调用Transaction的方法pc=new();//创建一个PCI事务pc.display(); //调用 PCI事务的方法end
方法的原型定义跟内容不相匹配是一个常见的编码错误。 SystemVerilog要求除了多一个类名和作用域操作符之外,原型定义跟块外的方法定义一致。此外有些OOP编译器(g十十和VCS)禁止在原型和类的内部指定参数的默认值。因为参数默认值对于调用该方法的代码非常重要,但是对于如何实现就不那么重要了,所以它们最好放在类定义部分。
另一个常见错误是在类的外部声明方法时忘记写类名。这样做的结果是它的作用范围高了一级(也许是在整个程序或包的范围内都可调用),当某个任务试图访问类一级的变量和方法的时候编译器就会报错如例5.15所示。
例5.15类的外部任务定义忘记类名
class Broken;int id;extern function void display;
endclassfunction void display;//忘记::分隔符$display("Broken :id =%0d ",id);//错误,找不到id
endfunction
5.12作用域规则
在编写测试平台的时候,需要创建和引用许多的变量。 System verilog采用与 Verilog相同的基本规则,但是略有改进。作用域是一个代码块,例如一个模块,一个程序、任务、函数、类或者 begin-end块。For和 foreach循环自动创建一个块,所以下标变量可以作为该循环作用域的局部变量来声明和创建。你可以在块中定义新的变量。 System Verilog中新增的特性是可以在一个没有名字的begin-end块中声明变量,例如for循环内定义了索引变量名字可以相对于当前作用域,也可以用绝对作用域表示,例如以$root开始。对于一个相对的名字, System Verilog查找作用域内的名字清单,直到找到匹配的名字。如果不想引起歧义,可以在名字的开头使用 $root。
例5.16在不同的作用域内使用了相同的名字。应当指出的是,在实际的代码里应当使用更加有意义的名字。例子中的limit被用作全局变量、程序变量、类变量,任务变量和初始化块中的局部变量。后者是一个未命名的块,所以最终创建的标记( label)取决于具体的工具。
例5.16名字作用域
program automatic p;int limit; //$root.p.limitclass Foo;int limit ,array[]; //$root.p.Foo.limit//$root.p.Foo.print.limitfunction void print(int limit)for (int i=0; i<limit;i++)$display("%m ::arrat[%0d]=%0d",i,array[i]);endfunctionendclassinitialbeginint limit =$root.limit;Foo bar;bar=new;bar.array=new[limit];bar.print(limit)end
endprogram
对测试平台来说,你可以在 program或者 initial块中声明变量。如果一个变量仅在一个 initial块中使用,例如计数器,应当在使用它的块中声明,以避免跟其他块出现潜在的冲突。注意:如果在一个未命名的块内定义变量,如例5.16中的 initia1块,那么最终在各种工具中的层次结构名字就可能完全不同。
类应当在 program或者 module外的 package中定义。这应当是所有测试平台都该遵守的,你可以将临时变量在测试平台最内部的某处定义,如果在一个块内使用了一个未声明的变量,碰巧在程序块中有一个同名的变量,那么类就会使用程序块中的变量,不会给出任何的警告。在例5,17中,函数Bad: display没有声明循环变量i,所以 System Verilog将使用程序级变量的i。调用该函数就会改变test.i的值,这可能不是你所希望的!
例5.17使用了错误的变量的类
program test;int i ;//程序级变量class Bad;logic [31:0] data[];//调用该函数将会改变程序级的变量ifunction void display;//在下面的雨具里忘了声明ifor (i=0; i<data.size;i++)$display("data[%0d ]=%x",i,data[i]);endfunction
endclass
endprogram
如果你将类移到一个 package中,那么类就看不到程序一级的变量了,由此就不会无意调用到它了。
例5.18将类移入 package来查找程序错误
package Mistake;class Bad;logic [31:0] data[];//未定义i,不会被编译function void display;for (i=0; i<data.size;i++)$display("data[%0d]=%x",i,data[i]);endfunctionendclass
endpackageprogram test;int i; //程序级变量import Mistake::*;endprogram
5.12.1this是什么
当你使用一个变量名的时候, System verilog将先在当前作用域内寻找,接着在上一级作用域内寻找,直到找到该变量为止。这也是 Verilog所采用的算法。但是如果你在类的很深的底层作用域,却想明确地引用类一级的对象呢?这种风格的代码在构造函数里最常见,因为这时候程序员使用相同的类变量名和参数名。在例5.19中,关键词“this”明确地告诉 System verilog你正在将局部变量 oname赋给类一级变量 oname。
例5.19使用this指针指向类一级变量
class scoping ;string oname;function new(string oname);this.oname=oname; //类变量oname=局部变量onmeendfunction
endclass
5.13在一个类内使用另一个类
通过使用指向对象的句柄,一个类内部可以包含另一个类的实例。这如同在 Verilog中、在一个模块内部包含另一个模块的实例,以建立设计的层次结构。这样包含的目的通常是重用和控制复杂度。例如,你的每一个事务都可能需要一个带有时间戳的统计块,它记录事务开始和结束的时间,以及有关此次事务的所有信息,如图5.3所示。
例5.20给出了 statistics类的定义。
例5.20 statistics类的声明
class Statistics;time startT,stopT;//事务的时间static int ntrans =0; //事务的数目static time total_elapsed_time=0;function time how_long;how_long =stopT -startT;ntrans++;total_elapsed_time+=how_long;endfunctionfunction void start;startT=$time;endfunction
endclass
现在你可以在另一个类中使用这个类。
class Transaction ;bit[31:0] addr,crc,data[9];Statistics stats;//Statistics 句柄function new();stats=new();//创建stats实例endfunctiontask create_packet();//填充数据包stats.start();//传送数据包endtask
endclass
最外层的类 Transaction可以通过分层调用语法来调用 Statistics类中的成员例如 stats. startT。一定要记得例化对象,否则句柄 stats是nu11,调用 start会失败。这最好在上层即 Transaction类的构造函数中完成。
5.13.2编译顺序的问题
有时候你需要编译一个类,而这个类包含一个尚未定义的类。声明这个被包含的类的句柄将会引起错误,因为编译器还不认识这个新的数据类型。这时侯你需要使用typedef语句声明一个类名,如下例所示。
例5.22使用 typedef class语句
typedef class Statistics;//定义低级别的类class Transaction ;Statistics stats;//使用Statistics类...
endclassclass Statistics ;//定义Statistics 类变量oname...endclass
5.14理解动态对象
在静态分配内存的语言中,每一块数据都有一个变量与之关联,例如 verilog中可能有一个wire类型的变量 grant,整数变量 count和一个模块实例i1。在OOP中,不存在这种一一对应关系。可能有很多对象,但是只定义了少量句柄。一个测试平台在仿真过程中可能产生了数千次事务的对象,但是仅有几个句柄在操作它们。如果你只写过verilog代码,对于这种情况你可能需要好好地适应一下。
在实际使用中,每一个对象都有一个句柄。有些句柄可能存储在数组或者队列中,或者在另一个对象中,例如链表。对于保存在邮箱( mailbox)中的对象,句柄就是 SystemVerilog的一个内部结构。关于邮箱的更加详细的信息参见7.6节。
5.14.1将对象传递给方法
当你将对象传递给一个方法的时候发生了什么?也许这个方法只需要读取对象中的值,例如上面的 transmit.。又或者你的方法可能会修改对象的值,例如创建一个数据包的方法。不管是哪一种情形,当你调用方法的时候,传递的是对象的句柄而非对象本身。
在图5,4中,任务 generator调用了 transmit。两个句柄 generator.t和transmit.t都指向同一个对象。
当你调用一个带有标量变量(不是数组,也不是对象)的方法并且使用ref关键词的时候, System verilog传递该标量的地址,所以方法也可以修改标量变量的值。如果你不使用ref关键词, System verilog将该标量的值复制到参数变量中,对该参数变量的任何改变不会影响原变量的值。
例5.23传递对象
//将包传送到一个32位总线上
task transmit (Transaction t);bus.rx_data<=t.data;t.stats.startT=$time;endtaskTransaction t;initial begint=new();//为对象分配空间t.addr=42;//初始化数值transmit(t);//将对象传递给任务end
在例5.23中,初始化块先产生一个 Transaction对象,并且调用 transmit任务,transmit任务的参数是指向该对象的句柄通过使用句柄, transmit可以读写对象中的值。但是,如果 transmit试图改变句柄,初始化块将不会看到结果,因为参数t没有使用ref修饰符。
方法可以改变一个对象,即使方法的句柄参数没有使用ref修饰符。这容易给新用户带来混淆,因为他们将句柄和对象混为一谈。如上例所示, transmit可以在不改变句柄t的情况下为对象加盖时间戳。如果你不想让对象在方法中被修改,那么就传递一个对象的拷贝给方法,这样原来的数据就保持不变。关于对象复制的更多信息参见5.15节。
5.14.2在任务中修改句柄
一个常见的编码错误是,当你想修改参数的值的时侯,忘记在方法的参数前加ref关键词,尤其是句柄。在例5.26中,参数tx没有被声明为ref,所以在方法内部对tx的修改不会被调用该方法的代码看到。参数tr默认的信号方向是 input。
例5.24错误的事务生成任务,句柄前缺少关键词ref
function void create(Transaction tr);//错误,缺少reftr=new();tr.addr=42;//初始化其他域endfunctionTransaction r;
initial begincreate(t);//创建一个transaction$display(t.addr);//失败,因为t=nullend
尽管 create修改了参数tr,调用块中的句柄t仍为nu11。你需要将参数tr声明为ref。
例5.25正确的事务发生器,参数是带有ref的句柄
function void create (ref Transaction tr);...
endfunction //create
5.14.3在程序中修改对象
在测试平台中,一个常见的错误是忘记为每个事务创建一个新的对象。在例5.26中, generator bad任务创建了一个有随机值的Transaction对象,然后将它多次传送给设计。
例5.26错误的发生器,只创建了一个对象
task generator_bad(int n);Transaction t;t=new();//创新一个新对象repeat (n)begint.addr=$rondom();//变量初始化$display("second addr =%h",t.addr);transmit(t); //将它发送到DUTend
endtask
这个错误的症状是什么?上面的代码仅创建一个 Transaction,所以每一次循环,generator bad在发送事务对象的同时又修改了它的内容。当你运行这段代码的时候,Display会显示很多不同的addr值,但是所有被传送的 Transaction都有相同的addx数值。如果 transmit的线程需要耗费几个周期完成发送,就有可能出现这种错误,因为对象的内容在传送的期间被重新随机化了。如果 transmit任务发送的是对象的副本,你就可以多次重复利用这个对象了。这种错误也会发生在邮箱中,如例7.31所示。
为避免出现这种错误,你需要在每次循环的时候创建一个新的 Transaction对象。
例5.27正确的产生器,创建多个对象
task generator_good(int n );Transaction t;repeat(n)begint=new();//创建一个新对象$display("sending addr=%h",t.addr);transmit(t);//将它发送到DUTend
endtask
5.14.4句柄数组
在写测试平台的时候,可能需要保存并且引用许多对象。你可以创建句柄数组,数组的每一个元素指向一个对象。例5.28给出了一个保存十个总线事务的句柄数组。
例5.28使用句柄数组
task generator();transmit tarry[10];foreach (tarry[i])begintarry[i]=new();//创建每一个对象transmit(tarry[i]);end
endtask
array数组由句柄构成,而不是由对象构成。所以需要在使用它们之前创建所有对象,就像你为一个普通的句柄创建对象一样。没有任何办法可以调用new函数为整个句柄数组创建对象。
不存在“对象数组”的说法,虽然可以使用这个词来代表指向对象的句柄数组。你应当牢记这些句柄可能并没有指向一个对象,也可能有多个句柄指向了同一个对象。
5.15对象的复制
有时候可能需要复制一个对象,以防止对象的方法修改原始对象的值,或者在一个发生器中保留约束。可以使用简单的new函数的内建拷贝功能,也可以为更复杂的类编写专门的对象拷贝代码。8.2节介绍了为什么需要创建一个复制方法。
5.15.1使用new操作符复制一个对象
使用new复制一个对象简单而且可靠,它创建了一个新的对象,并且复制了现有对象的所有变量。但是你已经定义的任何new()函数都不会被调用。
例5.29使用new复制一个简单类
class Transaction;bit[31:0] addr,crc,data[8];
endclassTransaction src,dst;initial beginsrc=new;//创建第一个对象dst=new src;//使用new 操作符进行复制end
这是一种简易复制( shallow copy),类似于原对象的一个影印本,原对象的值被盲目地抄写到目的对象中。如果类中包含一个指向另一个类的句柄,那么,只有最高一级的对象被new操作符复制,下层的对象都不会被复制。在例5.30中, ransaction类包含了个指向 statistics类的句柄,原始定义见例5.20。
例5.30使用new操作符复制一个复杂类
class Transaction;bit [31:0] addr ,crc, data[8];static int count =0;int id;Statistics stats;//指向Statistics对象的句柄function new;stats=new();//构造一个新的Statistics对象id=count++;endfunction
endclassTransaction src,dst;initial beginsrc=new();//创建一个transaction对象src.stats.startT=42;dst=new src;//用new操作符将src拷贝到dst中dst.stats.startT=96;//改变dst 和src的stats$display(src.stats.startT);end
初始化块创建第一个 Transaction对象,并且修改了它内部 statistics对象的变量,见图5.5。

当调用new函数进行复制的时候, Transaction对象被拷贝,但是 Statistics对象没有被复制。这是因为当你使用new操作符复制一个对象的时候,它不会调用你自己的new()函数。相反的变量和句柄的值被复制,所以现在两个 Transaction对象都具有相同的id值,如图5.6所示。
更糟糕的是,两个 Transaction对象都指向同一个 Statistics对象,所以使用src句柄修改 startT会影响到dst句柄可以看到的值。

5.15.2编写自己的简单复制函数
如果你有一个简单的类,它不包含任何对其他类的引用,那么编写copy函数非常容易。
例5.31含有copy函数的简单类
class Transaction ;bit[31:0] addr,crc,data[8];//没有Statistics句柄function Transaction copy;copy =new();//创建目标对象copy.addr=addr;//填入数值copy.crc=crc;copy.data=data;//赋值数组endfunction
endclass
例5.32使用copy函数
Transaction src dst;
initial beginsrc=new();//创建第一个对象dst=src.copy;//赋值对象end
5.15.3编写自己的深层复制函数
对于并非简单的类,你应该创建自己的copy函数。通过调用类所包含的所有对象的copy函数,可以做一个深层的拷贝。你自己的copy函数需要确保所有用户域(例如ID)保持一致。创建自定义copy函数的最后阶段需要在新增变量的同时更新它们。
例5.33复杂类的深层复制函数
class Transaction ;bit[31:0] addr,crc,data[8];Statistics stats;//指向Statistics对象的句柄static int count=0;int id;function new;stats=new();id=count++;endfunctionfunction Transaction copy;copy=new();//创建目标copy.addr=addr;//填入数值copy.crc=crc;copy.data=data;copy.stats=stats.copy();//调用Statstics::copy函数id=count++;endfunction
endclass
copy调用了构造函数new(),所以每一个对象都有一个唯一的id。需要为 statistics类和层次结构中的每一个类增加一个copy()方法。
例5.34 statistics类定义
class Statistics;time startT,stopT;//Transaction的事件戳function Statistics copy;copy=new();copy.startT=startT;copy.stopT=stopT;endfunction
endclass
这样一来当你复制一个 Transaction对象的时候,它会有自己的 statistics对象,如例5.35所示。例5.35使用new操作符复制复杂类
Transaction src,dst;initialbeginsrc=new();//创建第一个对象src.stats.startT=42;//设置起始时间dat=src.copy();//使用深层赋值将src赋值给dstdst.stats.startT=96;//仅改变dst的stats的值$display(src.stats.startT);//end

5.15.4使用流操作符从数组到打包对象,或者从打包对象到数组
某些协议,如ATM协议,每次传输一个字节的控制或者数据值。在送出一个 transaction之前,需要将对象中的变量打包成一个字节数组。类似的,在接受到一个字节串之后,也需要将它们解包到一个 transaction对象中。这两种功能都可以使用流操作符来完成,流操作符的例子见2.11.3节。
你不能将整个对象送入流操作符,因为这样会包含所有的成员,包括数据成员和其他额外信息,如时间戳和你不想要打包的自检信息。你需要编写你自己的pack函数,仅打包你所选择的成员变量。
例5.36含有pack和 unpack函数的 Transaction类
class Transaction;bit[31:0] addr,crc,data[8];//实际数据static int count =0;//不需要打包的数据int id;function new();id=count++;endfunctionfunction void display();$write("Tr :id =%0d ,addr =%x,crc =%x",id,addr,crc);foreach (data[i])$write("%x",data[i]);$display;endfunctionfunction void pack (ref byte bytes[40]);byte={>>{addr,crc,data}};endfunctionfunction Transaction unpack(ref byte bytes[40]);{>>{addr,crc,data}}=bytes;endfunctionendclass:Transaction
例5.37使用pack和 unpack函数
Transaction tr,tr2;
byte b[40];//addr+crc+data=40字节initialbegintr=new();tr.addr=32'h0a0a0a0;//填满对象tr.crc=1;foreach (tr.data[i])tr.data[i]=i;tr.pack(b);//打包对象到字节数组$write("Pack results:");foreach (b[i])$write("%h",b[i]);$display;tr=new();tr2.unpack(b);tr.display();end
5.16公有和私有
OOP的核心概念是把数据和相关的方法封装成一个类。在一个类中,数据默认被定义为私有,这样防止了其他类对内部数据成员的随意访问。类会提供一系列的方法来访问和修改数据。这也使得你能够在不让类用户知道的情况下修改方法的具体实现方式。例如,一个图形包可能会将它的内部表示法由笛卡儿坐标变成极坐标,而用户接口(访问的方法)的功能却不会改变。考虑一下 Transaction含有一个载荷( payload)和一个CRC,这样硬件就可以检测到错误。在传统的OOP中,你会定义一个方法设置载荷的值同时也设置CRC的值,这样它们就可以保持同步。这样你的对象就总是具有正确的数值。
但是测试平台不同于其他的程序,例如网页浏览器或者文字处理器。一个测试平台需要能够注入错误。你需要产生一个错误的CRC,以便测试硬件是如何处理错误的。OOP语言诸如C十十和Java使你能够制定变量和方法的可见性。默认情况下,任何成员都是局部的,除非加上了标记。
在 System verilog中,所有成员都是公有的,除非标记为local或者protected。你应当尽量使用默认值,以保证对DUT行为的最大程度的控制,这比软件的长期稳定性更加重要。例如,CRC公有将使你能够轻易地往DUT中注入错误。如果CRC是局部的,你可能需要编写额外的代码来避开数据隐藏机制,最终使测试平台变得更大更复杂。
5.17题外话
作为OOP的初学者,你可能不愿意将数据封装成类,而仅将数据存放在一些变量中。避免这种想法!一个简单的DUT监视器可能只在接口上采样几个数值,但不要将它们简单地保存在整数变量中然后传递给下一级。这样可能会在一开始节省一点时间,但最终你还是需要将这些数值组合到一起以构成一个完整的事务。这些事务中的几个可能需要被组合成更高级别的事务,例如DMA事务。所以,应该立刻将这些接口数值封装成一个事务类。这样,你就可以在保存数据的时候同时保存相关信息(端口号、接收时间),然后将该对象传递给测试平台的其他部分。
5.18建立一个测试平台
你现在已经离使用类创建一个简单的测试平台更近了一步了。下面是第1章中的图。显然,图5.9中的事务是对象,但是每一个块也代表了一个类。
图中的 Generator、 Agent、 Driver、 Monitor、 Checker和 Scoreboard都是类,被建模成事务处理器( transactor)。它们在 Environment类内部例化。为了简单起见,Test处在最高层,即处在例化 Environment类的程序中。功能覆盖( Functional Coverage)的定义可以放在 Environment类的内部或者外部。
事务处理器由一个简单的循环构成,这个循环从前面的块接受事务对象,经过变换后送给后续块。有一些块,例如 Generator,没有上游块,所以该事物处理器就创建和随机复制每一个事务,而其他的对象例如 Driver接收到一个事务然后将其作为信号发送到DUT中。
例5.38基本的事务类
class Transaction;//通用类Transaction tr;task run;forever begin//从前一个块中获取事务...//做一些处理//...//发送到下游块中//...endendtask
endclass
在块之间如何交换事务呢?在程序性的代码中,你需要在一个对象里调用另一个对象,或者使用FIFO之类的数据结构来保存块之间的事务。在第7章你将会学到如何使用邮箱,一种能够延迟一个线程直到有新的数值加入的FIFO。
相关文章:
SystemVerilog 第5章 面向对象编程基础
5.1概述 对结构化编程语言,例如 Verilog和C语言来讲,它们的数据结构和使用这些数据结构的代码之间存在很大的沟壑。数据声明、数据类型与操作这些数据的算法经常放在不同的文件里,因此造成了对程序理解的困难。 Verilog程序员的境遇比C程序员更加棘手,因为Ⅴ erilog语言…...
指针进阶(1)
指针进阶 朋友们,好久不见,这次追秋给大家带来的是内容丰富精彩的指针知识的拓展内容,喜欢的朋友们三连走一波!!! 字符指针 在指针的类型中我们知道有一种指针类型为字符指针 char* ; 使用方法如…...
蝶形运算法
蝶形运算法是一种基于FFT(Fast Fourier Transform)算法的计算方法,其基本思想是将长度为N的DFT分解成若干个长度为N/2的DFT计算,并通过不断的合并操作得到最终的结果。该算法也称为“蝴蝶算法”,因为它的计算过程中需要…...
day 48|● 583. 两个字符串的删除操作 ● 72. 编辑距离
583. 两个字符串的删除操作 dp的含义:指0开头,i- 1和j - 1为结尾的两个序列的删除最小数 递推公式方面: 初始化方面:前面0行和0列的初值要赋好 func minDistance(word1 string, word2 string) int {dp : make([][]int, len(wor…...
服务器(I/O)之多路转接
五种IO模型 1、阻塞等待:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。 2、非阻塞等待:如果内核没有将数据准备好,系统调用仍然会返回,并且会返回EWUOLDBLOCK或者EAGAIN错…...
后端面试话术集锦第 十三 篇:java集合面试话术
这是后端面试集锦第十三篇博文——java集合面试话术❗❗❗ 1. Java里常见的数据结构都有哪些以及特征 数组 数组是最常用的数据结构。 数组的特点是长度固定,可以用下标索引,并且所有的元素的类型都是一致的。 列表 列表和数组很相似,只不过它的大小可以改变。 列表一般都是…...
《微服务架构设计模式》第一章
逃离单体地狱 FTGO单体架构 作者用国外FTGO公司(一家做线餐饮外卖)的应用程序举例,阐述了单体架构的优缺点。FTGO应用架构如下: 应用程序是单体应用,具有六边形架构,最内侧是业务逻辑&…...
前端是如何打包的
前端项目的打包过程通常涉及将多个源文件(包括HTML、CSS、JavaScript等)合并、优化和压缩,以生成最终用于生产环境的静态资源。这个过程可以使用构建工具和打包工具来自动化完成。以下是前端项目的常见打包步骤: 1. **源代码编写…...
Qt 5.15编译(MinGW)及集成Crypto++ 8.7.0笔记
一、背景 为使用AES加密库(AES/CBC加解密),选用Crypto 库(官网)。 最新Crypto C库依次为:8.8.0版本(2023-6-25)、8.7.0(2022-8-7)和8.6.0(202…...
Qt 简单闹钟
//wiget.h#ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTime> //时间类 #include <QTimer> //定时器类 #include <QTextToSpeech> #include <QDebug> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPA…...
简单谈下Spring、Spring MVC和Spring Boot
Spring是一个开源的轻量级框架,用于构建Java应用程序。它提供了一种全面的编程和配置模型,可以帮助开发人员构建各种类型的应用程序,从简单的控制台应用程序到大型企业级应用程序。Spring框架的主要目标是提高应用程序的可维护性、可扩展性和…...
利用python进行视频下载并界面播放快速下载素材
工具:python designer(python自带):UI界面设计工具 VLC:视频播放工具 需要的库如下: import os,platform os.environ[PYTHON_VLC_MODULE_PATH] "./vlc-3.0.14" import vlc from 脚本 import Player from …...
[C++][pcl]pcl安装后测试代码3
测试环境: vs2019 pcl1.12.1 代码: #include<iostream> #include <thread>#include <pcl/common/common_headers.h> #include <pcl/features/normal_3d.h> #include <pcl/io/pcd_io.h> #include <pcl/visualizatio…...
在WSL下使用makefile运行modelsim进行混合编译
modelsim的图像界面加载缓慢,实际上modelsim可以在纯命令行环境下仿真,使用-c参数:vsim -c。可以在WSL下用makefile运行Windows下的modelsim: HDL_CODE . HDL_CODE ../../rtl/ MODELSIM_ROOT : /mnt/e/exe/modeltech64_10.4/win…...
idea 常用插件和常用快捷键 - 记录
idea 常用插件 记得下载插件完成后,点击 Apply 和 OK Alibaba Java Coding Guidelines 作用:使用该插件可以,自动提示相关的语法格式问题,格式参考 阿里巴巴代码规范 详情链接: 代码规范之Alibaba Java Coding G…...
IDEA报错:Plugin ‘org.springframework.boot:spring-boot-maven-plugin:‘ not found
问题: 使用IDEA新建spring boot项目,报错如下: Plugin org.springframework.boot:spring-boot-maven-plugin: not found解决办法: 1.在本地maven仓库中找到spring-boot-maven-plugin的版本号 2.在pom.xml文件中添加对应的版本…...
C++——Vector:push_back和emplace_back的区别,测试写入1GB大数据时的性能差距
什么是emplace_back emplace_back是C11引入的STL容器成员函数。emplace操作只执行构造而不执行拷贝构造。 如何理解上面这句话?先来看一个场景。 class test { public:test(){}test(int i){ std::cout << "test(int i)" << std::endl; }tes…...
C/C++/QT/Python/MATLAB获取文件行数的示例
1. C获取文件行数 #include <stdio.h>int main() {FILE *file fopen("path/to/your/file.txt", "r");if (file NULL) {printf("Failed to open the file!\n");return 0;}int lineCount 0;char ch;while ((ch fgetc(file)) ! EOF) {if…...
mysql的binlog參數詳解
mysql的binlog參數詳解 1. expire_logs_days expire_logs_days:這個參數用於設置binlog日誌文件的過期時間。默認情況下,binlog文件永不過期。如果將其設置為一個正整數值,則表示binlog文件在指定天數後會被自動刪除。 max_binlog_size m…...
【SpringSecurity】九、Base64与JWT
文章目录 1、base64编码2、Base64Url3、JWT的产生背景4、JWT介绍5、JWT组成5.1 Header5.2 Payload5.3 Signature 6、JWT的使用方式7、JWT的几个特点 1、base64编码 base64是一种编码方式,不是加密方式。 所谓Base64,就是说选出64个字符:小写…...
Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...
Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
Caliper 配置文件解析:config.yaml
Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...
uniapp 开发ios, xcode 提交app store connect 和 testflight内测
uniapp 中配置 配置manifest 文档:manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号:4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …...
【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...
