第12章 让对象像数值一样工作

本文详细介绍了自定义类中的隐式类型转换、友元函数以及模板成员函数的概念与应用。通过实例演示了如何在构造函数中避免隐式转换,以及如何使用友元函数访问私有成员。此外,还展示了模板成员函数的实现方式,并解释了重载操作符的重要性。文章最后以简化版字符串类为例,综合运用上述概念,强调了编码中遵循惯用法的重要性。

自定义类产生的隐式类型转换:当构造函数有且仅有一个未指定默认值的参数的时候,可能会产生参数类型到自定义类型的隐式转换。例如:

class Test{

    int v_;

public:

    Test(int i) : v_(i){}

    int GetV() const{return v_;}

};

 

void fun(const Test& test)

{

    std::cout<<test.GetV();

}

 

int main()

{

    fun(1);

    return 0;

}

这样的代码就产生了一个从整型数值1到自定义类型Test的隐式转换,当然这里例子没有实际意义。

当自定义类定义了隐式转换操作符的时候,也可能会产生自定义类型到其他类型的隐式转换,这里的其他类型就是隐式转换操作符的时候的类型。例如:

class Test{

    int v_;

public:

    Test(int i) : v_(i){}

    operator int() const{return v_;}

};

 

void fun(int i)

{

    std::cout<<i;

}

 

int main()

{

    fun(Test(1));

    return 0;

}

这样的代码就产生了一个从自定义类型Test到整型数值1的隐式转换,当然这里例子没有实际意义。

在实际编程中,隐式转换会给程序员带来很多困扰,因为是隐式的,所以往往发生在程序员不知情的情况下,更是难以调试。

为了防止这两种方式产生的隐式类型转换,我们可以在有且仅有一个未指定默认值的参数的构造函数前加上explicit关键字修饰,这个关键字的作用是让这个构造函数只能显示的调用,避免发生隐式转换。我们也尽量不去定义隐式转换操作符,除非不会给使用者带来任何困扰和问题。

 

友元函数:有些时候我们或许会希望授权某个外部函数能够访问我们的私有成员,但是其他未被授权的函数不能访问,这个时候就可以用到友元函数了。将希望授权的函数声明为自定义类的友元函数,那么它就能访问自定义类的私有成员了。例如:

class Test{

    int v_;

public:

    explicit Test(int i) : v_(i){}

    friend void fun(const Test&);

};

 

void fun(const Test& test)

{

    std::cout<<test.v_;

}

 

int main()

{

    fun(Test(1));

    return 0;

}

 

模板成员函数:自定义类的成员函数也可以是模板函数,这样类似于定义了一系列相同名字的成员函数。例如:

class Printer{

    std::ostream& o_;

public:

    explicit Printer(std::ostream& o) : o_(o){}

    template<typename T>

    void Print(const T& v)

    {

       o_<<v;

    }

};

 

int main()

{

    Printer printer(std::cout);

    printer.Print(1);

    return 0;

}

 

重载操作符:自定义类可以通过重载操作符来让自己可以通过操作符来完成一定的功能。例如:std::string就重载了+操作符,完成了字符串的拼接功能。

 

以上就是本章的一些概念,教材中通过实现一个简化版的字符串类Str,来对这几个概念分别讲解并指出了一些编码中应该遵守的惯用法和如何避免可能会带来的问题,课堂上将不再赘述。

请总结《System verilog验证测试平台编写指南》第4-连接设计和测试平台主要的知识要点,并将其中的各个要点分别以表格的形式给出,结合以下内容:第四. 连接设计和测试平台 4.1 测试平台和DUT之间通信 DUT和测试平台(Test)通常是分开的模块(module,描述硬件),可以在顶层(top)中将DUT和Test例化,然后根据对应信号进行连接(注意信号的方向)。 //待测DUT module arb_port (output logic [1:0] grant, input logic [1:0] request, input logic rst, input logic clk); ... always@(posedge clk or posedge rst) begin if(rst) grant <= 2'b00; else ... end endmodule //测试模块(测试平台定义在另一个模块中,与设计所在的模块相互独立) module test(input logic[1:0] grant, output logic[1:0] request, output logic rst, input logic clk); initial begin @(posedge clk) request <= 2'b01; $display("@%0t:Drove req = 01", $time); repeat (2) @(posedge clk); if(grant != 2'b01) $display("@%0d: a1: grant != 2'b01", $time); $finish; end endmodule //在顶层中例化DUT和Test并进行连接(不使用接口) module top; logic [1:0] grant, request; bit clk, rst; //系统时钟发生器 always #5 clk = ~clk; //clk在DUT和Test都为输入,由顶层clk驱动 //DUT中grant为输出,Test中grant为输入,则由顶层模块连接后,驱动由DUT发送到Test arb_port a1 (grant, request, rst, clk); test t1(grant, request, rst, clk); endmodule AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 在一个真实的设计中往往含有数百个端口(port,相对于硬件DUT来说,一个端口就是一个信号)信号,需要数页代码来声明信号和端口。所以这种连接方式是极易出错的,因为一个信号可能流经几个设计层次,它必须一次又一次的被声明和连接。如果相对现有的信号进行拓展新信号,必须在多个文件中定义和连接,所以SV引入接口(interface,相对于Test来说,抽象化概念)来简化连接。 4.2 接口 SV使用接口为程序块之间的通信建模,接口可以看作一捆智能的连线。接口包含了连接,同步,通信的功能。 4.2.1 使用接口简化连接 //定义接口 interface arb_if(input bit clk); logic [1:0] grant, request; logic rst; endinterface //使用arb_if定义DUT module arb(arb_if arbif); ... always @(posedge arbif.clk or posedge arb_if.rst) begin if(arbif.rst) arbif.grant <= 2'b00; else arbif.grant <= next_grant; ... end endmodule //使用arb_if定义Test module test(arb_if arbif); ... initial begin ... @(posedge arbif.clk); arbif.request <= 2'b01; $display("@%0d: Drove req = 01", $time); repeat(2) @(posedge arbif.clk); if(arbif.grant != 2'b01) $display("@%0d: al: grant != 2'b01", $time); $finish; end endmodule //使用接口在顶层模块中连接DUT与Test module top; bit clk; always #5 clk = ~clk; arb_if arbif(clk); arb a1 (arbif); test t1(arbif); endmodule AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 与端口相比,使用接口连接可以看出来能使代码变得简洁不易出错,如果想拓展接口中的信号,只需在接口中定义和使用这个信号的模块做修改,不需要其他操作,这种特性极大降低了连接出错的几率。 使用接口时需要确保在你的模块和程序块(program block,软件建模)之外声明接口变量(就是说不能将接口定义在module和program内)。有些编译器不支持在模块中定义接口,及时某些编译器支持,接口也只是所在模块的局部变量,对设计的其他部分来说是不可见的。 4.2.2 连接接口和端口 如果DUT中端口使用4.1中的方式定义(不使用接口,端口声明),而Test使用4.2.1中的方式声明(使用接口),此时连接就涉及到如何连接接口与端口,接口与端口的连接是以点对点的连接方式进行的。实际开发过程中常用此方式进行连接。 //顶层模块连接接口与端口 module top; bit clk; always #5 clk = ~clk; arb_if arbif(clk); //DUT实例化过程中'.'代表一个端口,后面名字代表端口名称,括号内为与该端口连接的信号 arb_port a1(.grant(arbif.grant), .request(arbif.request), .rst(arbif.rst) .clk(arbif.clk)); test t1(arbif); endmodule AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 12 13 4.2.3 使用modport将接口中的信号分组并指定方向 在4.1中端口信号的声明包含了方向,编辑器会依次来检查连线方向是否发生错误。在接口中使用modport结构能够将信号分组并指定方向。 //带有modport的接口 interface arb_if(input bit clk); logic[1:0] grant, request; logic rst; modport TEST(output request, rst, input grant, clk); modport DUT(input request, rst, clk, output grant); modport MONITOR(input request, grant, rst, clk); endinterface //在接口中使用modport module arb(arb_if.DUT arbif); ... endmodule module test(arb_if.TEST arbif); ... endmodule //顶层例化与4.2中相同,因modport只需要在模块定义时指明,而模块例化时不需要指明。使用modport更确切得代表了一个真实的设计,尤其是信号的方向 AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 在设计中可以通过两种方法使用modport名:一种是在接口信号的程序和模块中使用modport名;另外也可以在顶层模块中使用modport名,然后把接口放到程序和模块的端口表中(module,program中使用interface声明,top层使用interface.modport声明)。正常情况下,使用第一种连接方式,因为modport是接口实现的细节,不应该出现在顶层模块中。特殊情况,如果一个模块需要多次例化,每次例化需要连接到不同的modport,那么此时应该使用第二种连接方式。 并非接口中的每个信号都是必须连接的,某些模块可能只关注接口的某些信号而忽略其他信号。 4.2.4 接口的优缺点 优点:便于设计重用,减少连接错误,可拓展性强,modport允许多个信号捆绑到一起,也可以通过定义方向借助编译器进行自动检查。 缺点:对于点对点连接,使用modport接口描述跟使用信号列表的端口一样冗余。必须同时使用信号名和接口,可能会使模块变得冗长。如两个连接的模块是一个不会被重用的专用协议,使用接口需要做比端口连接更多的工作。连接两个不同的接口很困难,需正确拆分独立的信号并正确的驱动。 4.3 激励时序(难点) 4.3.1 使用时钟块控制同步信号的时序 **接口块可以使用时钟块来指定同步信号相对于时钟的时序。时钟块中的任何信号都将同步地驱动或采样,这就保证了测试平台在正确的时间点与信号交互。**一个接口可以包含多个时钟块,因为每个块中都只有一个时钟表达式,所以每一个对应一个时钟域。可以在时钟块中使用default语句指定一个时钟偏移。一旦定义了时钟块,测试平台就可以用@arbif.cb表达式等待时钟,而不需要描述确切的式中信号和边沿@(posedge arbif.cb)。这样即使改变了时钟块中的时钟或者边沿,也不需要修改测试平台的代码。 //带时钟块的接口 interface arb_if(input bit clk); logic [1:0] grant, request; logic rst; clocking cb @(posedge clk); output request; input grant; endclocking modport TEST (clocking cb, output rst); modport DUT (input request, rst, output grant); endinterface //测试平台 module test(arb_if.TEST arbif); initial begin arbif_cb_request <= 0; @arbif.cb; $display("@%0t: Grant=%b", $time, arbif.cb.grant); end endmodule AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 4.3.2 接口中的logic和wire对比 cb块中声明了块中的信号在时钟的上升沿有效,**信号的方向是相对于modport(也就说方向相对于采样的模块)的,接口中的信号建议定义为logic,因logic易用。如果测试平台在接口中使用过程赋值语句驱动一个异步信号,那么该信号必须是logic类型。wire类型变量只能被连续赋值语句驱动,时钟块中的信号始终是同步的,可以定义为logic或者wire。logic信号可以直接被驱动,而wire需要使用额外的代码。**建议使用logic的另一个原因是,如果你无意中使用了多个元件的驱动源,编译器会自动报错。 //接口中驱动logic和wire信号 interface asynch_if(); logic l; wire w; endinterface module test(asynch_if ifc); logic local_wire; //接口中的wire类型不能直接驱动,需在module中定义logic类型驱动信号,使用assign赋值语句将信号线连接,module中通过logic类型驱动 assign ifc.w = local_wire; initial begin ifc.l <= 0; //直接驱动异步logic信号 local_wire <= 1; //借助logic驱动wire类型 end endmodule AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 4.3.3 V的时序问题 在实际的硬件设计中,DUT中的存储单元在时钟的有效沿锁存输入信号,这些数值由存储单元输出,然后通过逻辑块到达下一个存储单元。从上一个存储单元的输入到下一个存储单元的输入延时必须小于一个时钟周期。所以测试仪需要在时钟沿之后驱动芯片的输入,然后在下一个时钟之前读取输出。测试平台应该模拟测试仪这种行为,应该在有效时钟边沿或边沿之后驱动待测设计,然后在有效时钟沿到达之前,在满足协议时序的前提下,尽可能晚地采样。 如果DUT和测试程序仅由V模块构成,这几乎是不可能实现的。如果测试平台在时钟边沿驱动DUT,就会存在竞争状态。如果时钟到达一个DUT的时间快于测试平台的激励,但是到达另一个DUT的时钟又晚于这个激励,这种情况会导致DUT的外部时钟沿都是在相同的仿真时间达到,DUT内部有一些输入在上个时钟周期采样,但是其他的输入却在当前时钟周期采样。 解决此问题的一种方法是给系统添加延时。比如#0,0时延会强迫V代码的线程停止并在所有其他代码完成之后被重新调度执行。但在一个大型的设计中,往往不可避免地存在多个线程都想最后执行,#0带来的结果是每次运行结果不确定,所以要避免使用#0以免代码不稳定并且不可移植。 另外一个解决方法是使用一个较大的延时,#1。 RTL代码除了时钟沿之外没有其他时序信息,所以逻辑电路在时钟沿之后一个时间单位就会稳定。但是如果一个模块使用1ns时间精度,而其他仅使用10ps的时间精度呢,那么#1意味着1ns,10ps还是其他的时间长度呢。你需要在时钟的有效沿之后,并且是在任何事件发生之前,而非在一段时间之内,尽快地驱动设计。所以应当避免使用#1延时解决时序问题。 对DUT输出信号的采样存在着相同的问题,希望在时钟有效沿到来之前的最后时刻捕获信号的值,你可能直到下个时钟沿会出现在100ns,但是不能在100ns出现时钟边沿的时钟采样,因为设计的输出值可能已经改变了,应当在时钟沿到达之前的Tsetup时间上采样。 4.3.4 程序块和时序区域(难点) 竞争问题的根源在于设计和测试平台的事件混合在同一个时间片(time slot)内,即使在纯RTL程序中也会发生同样的问题。如果存在一种可以将时间轴上分开这些时间的方法,如在100ns时刻,测试平台可以在时钟信号变化或者设计产生任何活动之前采样设计的输出信号。在所有的事件执行完毕后,测试平台开始下一个动作。在SV中,测试平台的代码在一个程序块中,但是程序块中不能有任何的层次级别,例如模块的实例,接口或者其他程序。 SV引入了一种新的时间片的划分方式,在V中,大多数的时间在有效区域执行。在一个时间片首先执行Active区域,在这个区域中运行设计事件,包括RTL,门级代码和时钟发生器。第二个区域是Oberved区域,执行断言。接下来就是执行测试平台的Reactive区域。注意时间并不是单向向前流动,Observed和Reactive区域的事件可以触发本时钟周期内Active区域中进一步的设计事件。最后是Postponed区域,他将在时间片的最后,所有设计活动都结束后的只读时间段采样信号。 4.3.5 仿真的结束 在V中,仿真在调度事件存在的时候会继续执行,直到遇到$finish。SV中增加了一种结束仿真的方法。SV把任何一个程序块都视为含有一个测试,如果仅有一个程序块,那么当完成所有的initial块中的最后一个语句时,仿真就结束了,因为编译器认为这就是测试的结尾。即使还有模块或者程序块的线程在运行,仿真也会结束。所以,当测试结束时无需关闭所有的monitor和driver。如果存在多个程序块,仿真在最后一个程序块结束时结束,这样最后一个测试完成时仿真就会结束。可以执行$exit提前中断任何一个程序块,也可以使用$finish来结束仿真。 4.3.6 指定设计和测试平台之间的时延 时钟块的默认时序是在#1step延时之后采样输入信号,在#0延时之后驱动输出信号。#1step延时规定了信号在前一个时间片的Postponed区域,在设计有任何新的动作之前被采样,这样就可以在时钟改变之前捕获输出值。因为时钟模块的原因,测试平台的输出信号是同步的,所以他们直接被送入设计中,在Reactive区域运行的程序块在同一个时间片再一次触发Actie区域。可以想象时钟块在设计和测试平台中插入了一个同步器来理解。(不理解) 4.4 接口的驱动和采样 测试平台需要驱动和采样设计的信号,这主要是通过带有时钟块的接口实现的。异步信号通过接口时没有任何时延,比如rst,而时钟块中的信号将得到同步。 4.4.1 接口同步 可以使用V中的@和wait来同步测试平台中的信号。 //信号同步 program automatic test(bus_if.TB bus); initial begin @bus.cb; //时钟块的有效时钟沿(posedge,negedge)继续 repeat(3) @bus.cb; //等待3个有效时钟沿 @bus.cb.grant; //在任何时钟沿继续 @(posedge bus.cb.grant) //上升沿继续 @(negedge bus.cb.grant) //下降沿继续 wait (bus.cb.grant == 1); //等待表达式被执行,如果已经是真,不做任何延时 @(posedge bus.cb.grant or negedge bus.rst); //等待几个信号 end endprogram AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 12 13 14 4.4.2 接口信号采样 当从时钟块读取一个信号时,是在时钟沿之前得到采样值。 `timesacle 1ns/1ns program test(arb_if.TEST arbif); initial begin $monitor("@%0t: grant = %h", $time, arbif.cb.grant); # 50ns $display("End of test"); end endprogram module arb(arb_if.DUT arbif); initial begin # 7 arbif.grant = 1; //@7ns # 10 arbif.grant = 2; //@17ns # 8 arbif.grant = 2; //@25ns end endmodule AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 时序图如下,在仿真时刻25ns时刻,采样值为2而不是3,说明信号的新值不是在下一个时钟周期(25ns~35ns)传递给测试平台的。 4.4.3 接口信号驱动 当在时钟块中使用modport时,任何同步接口信号多必须加上接口名(arbif)和时钟块名(cb)的前缀。 4.4.5 通过时钟块驱动接口信号 在时钟块中应当使用同步驱动,接口信号必须使用非阻塞赋值来驱动。信号在赋值后并不会立刻改变(在时钟沿赋值除外)。例如测试平台在100ns的时候同时产生了arbif.cb.request和arbif.cb,那么request信号在100ns的时候就会改变。如果测试平台在101改变arbif.cb.request信号,那么该变化直到下一个时钟沿才会传递给设计。 busif.cb.request <= 1; //同步驱动 busif.cb.cmd <= cmd_buf; //同步驱动 AI生成项目 systemverilog 1 2 如果测试平台在时钟的有效沿改变接口信号,那么其值会立刻传递到设计中,这是因为时钟块的默认输出延时是#0。如果测试平台在时钟有效沿之后驱动输出,那么该值直到时钟的下一个有效沿才会传递到设计中。 program test(arb_if.TEST arbif); initial begin #7 arbif.cb.request <= 3; //@7ns #10 arbif.cb.request <= 2; //@17ns #8 arbif.cb.request <= 1; //@25ns #25 finish; end endprogram module arb(arb_if.DUT arbif); initial $monitor("@%0t: req = %h", $time, arbif.request); endmodule AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 12 13 14 时序图如上,第二个周期产生的值2永远不会被DUT捕获,因为第三个周期结束时测试平台产生了值1。异步的驱动时钟块信号会导致数值丢失,应该使用时钟延时前缀以保证在时钟沿驱动信号。 ##2 arbif.cb.request <= 0; //等待两个时钟周期后赋值 ##3 //非法,必须跟赋值语句同时使用 AI生成项目 systemverilog 1 2 如果想在驱动一个信号前等待两个时钟周期,可以使用repeat(2)@bus.cb;或将时钟周期延时##2.。后一种方式只能在时钟块里作为驱动信号的前缀来使用,因为它需要知道使用哪个时钟来做延时。 4.4.6 接口中的双向信号 V中,如果想要驱动一个双向信号,比如一个过程代码中的双向端口,需要用一个连续赋值语句来将reg连接到wire。 SV中,当你在程序对线网赋值时,SV实际上将值写到了一个驱动该线网的临时变量中,所有驱动器输出经过判断后程序可以直接通过连线读取该值。 interface master_if(input bit clk); wire [7:0] data; //双向信号 clocking cb @(posedge clk); inout data; endclocking modport TEST (clocking cb); endinterface program test (master_if mif); initial begin mif.cb.data <= 'z; //三态总线 @mif.cb; $display(mif.cb.data); //从总线读取 @mif.cb; mif.cb.data <= 7'h5a; //驱动总线 @mif.cb; mif.cb.data <= 'z; //释放总线 end endprogram AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 SV中没有明确定义如何驱动接口中的异步双向信号,驱动双向信号有两种解决方式:使用一个跨模块引用或连续赋值语句。 4.6.7 为什么在program中不允许使用always块 在一个设计中,一个always块可能从仿真的开始就会在每一个时钟的上升沿触发执行。但是一个测试平台的执行过程是经过初始化,驱动和响应设计行为等步骤后结束仿真的。在测试平台中,一个连续执行的always模块不能正常工作。当program中最后一个initial块结束的时候,仿真实际上也默认结束了,就像执行了$finish一样,如果加入了一个always块,它将永远不会结束,这样就不得不显式调用$exit来发出程序块结束的信号。如果确实需要一个always块,可以使用initial forever来完成。 4.6.8 时钟发生器 时钟发生器跟设计结合地更加紧密,所以时钟发生器应当定义一个模块。不应该把时钟发生器放在程序块里。如下clk和out_sig信号都从Reactive区域开始传递,在Active区域进入设计,根据这两个信号到达的先后不同可能会引起竞争状态。将时钟发生器放在一个模块中可以避免竞争状态。 program bad_generator(output bit clk, out_sig); initial forever #5 clk <= ~clk; initial begin forever @(posedge clk) out_sig <= ~out_sig; end endprogram AI生成项目 systemverilog 1 2 3 4 5 6 7 如下是一个正确的时钟发生器,他有意避免了0时刻的边沿以免竞争情形的发生。所有的时钟边沿使用阻塞赋值生成,他们将在Active区域触发事件的发生。如果确实需要0时刻产生一个时钟边沿,那么可以使用非阻塞赋值语句设置初始值,这样所有的时钟敏感逻辑电路比如always块都会在时钟变换之前执行。 module clock_generator (output bit clk); initial always #5 clk = ~clk //在时间0之后生成时钟沿 endmodule AI生成项目 systemverilog 1 2 3 4 4.5 连接模块 module top; bit clk; always #4 clk = ~clk; arb_if arbif(.*); arb al(.*); test t1(.*); endmodule AI生成项目 systemverilog 1 2 3 4 5 6 7 快捷符号.*(隐式端口连接),能自动在当前级别自动连接模块实例的端口到具体信号,只要端口和信号的名字和数据类型相同。端口列表中的接口必须连接,否则SV编译器不通过。 4.6 顶层作用域 有时需要在仿真过程中创建程序或者模块之外的对象,以便参与仿真的所有对象都可以访问他们。在V中,只有宏定义可以跨越模块的边界,而且经常被用来创建全局变量。 SV中引入了编译单元,它是仪器编译的源文件的一个组合。任何module,macromodule,interface,program,package或者primitive边界之外的作用域被称为编译单元作用域,也称为$unit。这个作用域内的任何成员,比如parameter,都类似于全局变量,因为它可以被所有低一级的块访问。但是它们又不同于真正的全局成员,例如parameter在编译时其他源文件不可见。 有些工具,比如Synopsys VCS,它同时编译所有的SV代码,所以$unit是全局的。但是Synopsys Design Compiler一次编译一个模块或者一组模块,这时$unit可能只包含了一个或者几个文件的内容。其他供应商的EDA工具可能一次编译所有的文件或者只是一个子集,结果导致**$unit是不可移植的。** 实例名$root允许你从顶层作用域开始明确地引用系统中的成员名。此时,$root类似于Unix文件系统中的根目录/。对于VCS这样一次编译所有文件的工具。$root和$unit是等价的。当你的代码引用另一个模块中的成员时,编译器首先在本作用域内查找,然后在上一层作用域内查找,如此往复直到到达顶层作用域。可以通过使用$root指定绝对路径明确地引用跨模块的变量。 4.7 程序-模块交互 程序块可以读写模块中的所有信号,可以调用模块中的所有例程,但是模块看不到程序块。程序可以调用模块中的例程执行不同的动作,这个例程可以改变内部信号的值。因为当前SV标准没有定义怎样在程序块内改变信号的值,所以需要在设计中写一个任务来改变信号的值,然后在程序中调用这个任务。 4.8 SVA断言 可以使用SVA在设计或者能够创建时序断言。断言的例化跟其他设计块的例化相似,而且在整个仿真过程中都是有效的。仿真器会跟踪哪些断言被激活,这样就可以在此基础上收集功能覆盖率的数据。 4.8.1 立即断言 测试平台的过程代码可以检查待测设计的信号值和测试平台的信号值,并且在存在问题时采取相应的行动。 bus.cb.request <= 1; repeat (2) @bus.cb; if(bus.cb.grant != 2'b01) $display("Error, grant!=1"); AI生成项目 systemverilog 1 2 3 断言比if语句更加紧凑,设计期望断言表达式为真,否则输出后面的错误。 bus.cb.request <= 1; repeat (2) @bus.cb; a1: assert(bus.cb.grant == 2'b01); AI生成项目 systemverilog 1 2 3 如果产生了一个grant信号,那么测试继续执行,如果信号不符合期望值,仿真将给出断言失败信息。断言是声明性代码,他的执行过程和过程性代码有很大差异。使用几行断言,可以验证复杂的时序关系,等价的过程代码可能远比这些断言要复杂和冗长。 //一个断言有可选的then和else分句。 a1:assert(bus.cb.grant == 2'b01) else $error("Grant not asserted"); AI生成项目 systemverilog 1 2 3 SV有四个输出消息的函数:$info,$warning,$error和$fatal。这些函数仅允许在断言内部使用,不允许在过程代码中使用。 4.8.2 并行断言 可以理解并行断言是一个连续运行的模块,它为整个仿真过程检查信号的值。需要在断言内指定一个采样时钟。 //并行断言,检查request在非复位状态下没有X/Z信号 interface arb_if(input bit clk); logic [1:0] grant, request; logic rst; property request_2state; @(posedge clk) disable iff (rst); $isunknown(request) == 0; endproperty assert_request_2state:assert property (request_2state); endinterface AI生成项目 systemverilog 1 2 3 4 5 6 7 8 9 10 11 断言还有许多其他的用法。例如,可以在接口中使用断言,这样接口不仅可以传送信号值也可以检查协议的正确性。 4.9 ref端口的方向 SV引入了一种新的端口方向ref,ref是对变量的引用,它的值是该变量最后一次赋的值。如果将一个变量连接到多个ref端口,就可能产生竞争,因为多个模块的端口都可能更新同一个变量。 4.10 仿真的结束 仿真在程序块中的最后一个initial块结束是结束。其实,在最后一个initial块完成时,它隐式地调用$exit以标志程序的结束。当所有的程序块都退出了,$finish函数的隐性调用也就结束了。也可以在需要的时候直接调用$finish来结束仿真。 但是,程序并没有完全结束。模块或者程序块可以定义一个或者多个finial块来执行仿真器退出前的代码。此块可以放置清理任务,在finial块中不能调度事件,或者有任何时延信息。
最新发布
07-22
以下是《SystemVerilog验证测试平台编写指南》第4 **“连接设计和测试平台”** 的知识要点总结,并以表格形式呈现核心内容。 --- ## ✅ 第4知识要点总结表格 | 知识点 | 内容 | |--------|------| | **测试平台与DUT通信方式** | DUT和测试平台通常作为独立模块在顶层模块中实例化,并通过信号连接进行通信。需注意信号方向(输入/输出)。 | | **接口(interface)的作用** | 接口是SystemVerilog中用于封装信号组的关键结构,用于将DUT与测试平台之间的通信信号集中管理,简化连接并提高可维护性。 | | **接口的定义与使用** | 使用`interface`关键字定义接口,接口中可以包含信号、modport、时钟块等。接口在顶层模块中实例化,并传递给DUT和Test模块。 | | **接口与端口连接** | 接口可以与传统端口进行点对点连接,例如:`.grant(arbif.grant)`,这种方式适用于DUT使用端口定义而Test使用接口的情况。 | | **modport的作用** | modport用于定义接口中信号的方向(输入/输出),使得不同模块(如DUT和Test)对接口信号的使用更加清晰和明确。 | | **时钟块(clocking block)** | 时钟块用于定义同步信号的驱动和采样时序,使得测试平台可以以更简洁的方式处理时序相关的操作。 | | **默认时钟块** | 每个接口可以定义一个默认的时钟块,用于指定该接口中同步操作所使用的时钟。 | | **logic与wire在接口中的使用** | 接口中信号建议使用`logic`类型,因其支持过程赋值;`wire`类型需要使用连续赋值语句驱动,且在时钟块中不推荐使用。 | | **接口中的双向信号处理** | 使用`inout`关键字定义双向信号,并在测试平台中通过三态赋值(如`<= 'z`)来控制驱动与释放。 | | **程序块(program)与模块交互** | 程序块用于编写测试逻辑,不能包含层次结构模块,但可以调用模块中的任务/函数来改变模块信号。 | | **时钟发生器的实现** | 建议将时钟发生器定义在模块中,使用阻塞赋值生成时钟边沿,避免在程序块中实现时钟逻辑。 | | **隐式端口连接(.*)** | 使用`.*`可以自动连接模块实例的端口到同名信号,简化连接代码,但要求端口名与信号名一致。 | | **编译单元作用域($unit)** | SystemVerilog引入了编译单元作用域,用于定义全局变量(如parameter),但其可见性依赖于工具实现,可能不可移植。 | | **断言(Assertion)** | 可在接口或模块中使用SVA断言验证协议和信号行为,包括立即断言和并行断言。断言可提高验证效率并减少测试代码量。 | | **ref端口方向** | `ref`端口用于引用变量的当前值,适用于函数/任务中高效传递大数据结构,但可能引起竞争。 | | **仿真结束机制** | 当所有`program`中的`initial`块执行完毕时,仿真结束。可以使用`$finish`强制结束,`final`块用于执行清理操作。 | --- ## 📌 示例代码解析 ### 示例1:接口定义与使用 ```systemverilog // 定义接口 interface arb_if(input bit clk); logic [1:0] grant, request; logic rst; // 时钟块 clocking cb @(posedge clk); output request; input grant; endclocking // modport定义 modport DUT(input request, rst, clk, output grant); modport TEST(clocking cb, output rst); endinterface ``` > **解释**:该接口定义了DUT与Test之间通信所需的信号,并通过`modport`指定了方向,`clocking block`定义了同步时序驱动和采样。 --- ### 示例2:测试平台中使用接口驱动信号 ```systemverilog program test(arb_if.TEST arbif); initial begin arbif.cb.request <= 0; @arbif.cb; // 等待时钟上升沿 arbif.cb.request <= 2'b01; repeat(2) @arbif.cb; if (arbif.cb.grant != 2'b01) $display("Error: grant not as expected"); end endprogram ``` > **解释**:该测试平台通过接口的时钟块驱动`request`信号,并在时钟边沿采样`grant`信号,验证DUT是否响应正确。 --- ### 示例3:使用.*隐式端口连接 ```systemverilog module top; bit clk; always #5 clk = ~clk; arb_if arbif(clk); arb al(arbif); test t1(arbif); endmodule ``` > **解释**:顶层模块中使用接口实例`arbif`连接DUT和Test模块,简化了信号声明和连接流程。 --- ## ❓
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值