• IEEE Standard for SystemVerilog Chapter9. Processes


    9.1 General

    本条款描述了以下内容:
            --结构化程序(initial程序、always程序、final程序)
            --块语句(begin-end顺序块,fork-join并行块)
            --时序控制(delays, events, waits, intra-assignment)
            --进程线程和进程控制

    9.2 Structured procedures

            SystemVerilog中的所有结构化过程都在以下构造之一中指定:
            --initial程序,用关键字initial表示(见9.2.1)
            --always过程,用关键字表示:
                    • always (see 9.2.2.1)
                    • always_comb (see 9.2.2.2)
                    • always_latch (see 9.2.2.3)
                    • always_ff (see 9.2.2.4)
            --final 程序,用关键字final表示(见9.2.3)
            — Task
            — Function
            这些结构化过程的语法如语法9-1所示。

    1. initial_construct ::= initial statement_or_null // from A.6.2
    2. always_construct ::= always_keyword statement
    3. always_keyword ::= always | always_comb | always_latch | always_ff
    4. final_construct ::= final function_statement
    5. function_declaration ::= function [ lifetime ] function_body_declaration // from A.2.6
    6. task_declaration ::= task [ lifetime ] task_body_declaration // from A.2.7

                            Syntax 9-1—Syntax for structured procedures (excerpt from Annex A)
            initial和always过程在模拟开始时启用。initial程序应仅执行一次,其活动应在声明完成后停止。相反,always程序应重复执行,并且只有在模拟终止时,其活动才应停止。
            initial程序和always程序之间不应存在任何隐含的执行命令。initial程序不需要在always程序之前安排和执行。模块中可以定义的initial和always程序的数量不受限制。有关变量初始化相对于过程执行的顺序,请参见6.8。
            final过程在模拟时间结束时启用,并且仅执行一次。
            Tasks和functions是从其他过程中的一个或多个位置启用的过程。第13条描述了任务和功能。

            除了这些结构化过程外,SystemVerilog还包含其他过程上下文,如coverage point expressions (19.5), assertion sequence match items (16.10, 16.11), and action blocks (16.14).
    SystemVerilog在一个过程中具有以下类型的控制流:

            — Selection, loops, and jumps (see Clause 12)

            — Subroutine calls (see Clause 13)

            — Sequential and parallel blocks (see 9.3)

            — Timing control (see 9.4)

            — Process control (see 9.5 through 9.7)

    9.2.1 Initial procedures

            initial程序应仅执行一次,其活动应在声明完成后停止。以下示例说明了在模拟开始时初始化变量的initial过程的使用。

    1. initial begin
    2. a = 0; // initialize a
    3. for (int index = 0; index < size; index++)
    4. memory[index] = 0; // initialize memory word
    5. end

          initial过程的另一个典型用途是指定波形描述,该波形描述执行一次以向被模拟的电路的主要部分提供激励。

    1. initial begin
    2. inputs = 'b000000; // initialize at time zero
    3. #10 inputs = 'b011001; // first pattern
    4. #10 inputs = 'b011011; // second pattern
    5. #10 inputs = 'b011000; // third pattern
    6. #10 inputs = 'b001000; // last pattern
    7. end

    9.2.2 Always procedures

            一共有4中形式的always过程,always, always_comb, always_latch, and always_ff. 所有形式的always程序在整个模拟过程中不断重复。

    9.2.2.1 General purpose always procedure

            always关键字表示通用的  always过程,可用于表示重复行为,如时钟振荡器。该结构还可以与适当的时序控制一起使用,以表示组合、锁存和时序硬件行为。通用程序由于其循环性质,只有在与某种形式的时序控制结合使用时才有用。如果 always过程无法控制模拟时间的前进,则会创建模拟死锁条件。
            例如,以下代码创建了一个零延迟无限循环:

    always areg = ~areg;

            为前面的代码提供时序控制创建了一个潜在有用的描述,如下所示:

    always #half_period areg = ~areg;
    9.2.2.2 Combinational logic always_comb procedure

            SystemVerilog为组合逻辑行为建模提供了一个特殊的always_comb过程。例如:

    1. always_comb
    2. a = b & c;
    3. always_comb
    4. d <= #1ns b & c;

            always_comb过程提供了不同于通用always过程的功能,如下所示:
                    --有一个推断灵敏度列表,其中包括9.2.2.2.1中定义的表达式。
                    --任何其他过程都不得写入赋值左侧的变量。 然而,只要变量的最长静态前缀不重叠,就允许对变量的独立元素进行多次赋值(见11.5.3)。例如,一个未封装的结构或数组可以有一个由always_comb过程分配的位,另一个由另一个always_comb过程连续分配的位等。有关更多详细信息,请参阅6.5。
                    --在所有initial和always程序启动后,程序在时间零点自动触发一次,以使程序的输出与输入一致。软件工具应该执行额外的检查,以警告always_comb过程中的行为是否不代表组合逻辑,例如是否可以推断锁存行为。

    9.2.2.2.1 Implicit always_comb sensitivities

            always_comb的隐式敏感度列表包括在块内或在块内调用的任何函数内读取的每个变量或选择表达式的最长静态前缀的展开,但以下情况除外:
            a) 在块内或在块内调用的任何函数内声明的变量的任何扩展
            b) 也写在块内或在块内调用的任何函数内的任何表达式
            有关最长静态前缀的定义,请参见11.5.3。层次函数调用和包中的函数调用被分析为正常函数,对类范围解析运算符引用的静态方法函数的调用也是如此(见8.23)。对类对象的引用和类对象的方法调用不会向always_comb的敏感度列表中添加任何内容,除了传递给这些方法调用的参数表达式的任何贡献。
            在always_comb中允许任务调用,但任务的内容不会向敏感度列表中添加任何内容
            注意——不消耗时间的任务可以用void函数代替,以便对内容进行灵敏度分析。过程中的立即断言(见16.3)或过程中调用的任何函数中使用的表达式会导致always_comb的隐式敏感度列表,就好像该表达式被用作if语句的条件一样
            断言操作块中使用的表达式对always_comb的隐式敏感度列表没有贡献。在以下示例中,每当b、c或e发生变化时,always_comb就会触发。

    1. always_comb
    2. begin
    3. a = b & c;
    4. A1:assert (a != e) else if (!disable_error) $error("failed");
    5. end
    9.2.2.2.2 always_comb compared to always @*

    SystemVerilog always_comb程序与always@*(见9.4.2.2)的不同之处在于:
            --always_comb在时间零点自动执行一次,而always@*则等待,直到推断的灵敏度列表中的信号发生变化
            --always_comb对函数内容的更改敏感,而always@*只对函数参数的更改敏感
            --always_comb过程中赋值左侧的变量,包括被调用函数内容中的变量,不应由任何其他进程写入,而@*始终允许多个进程写入同一变量
            --always_comb中的语句不应包括 阻塞时序 或 event 控制或fork-join 语句
            --always_comb对过程中和过程中调用的函数内容中的立即断言中的表达式敏感,而always@*仅对过程中的立即声明中的表达式灵敏

    9.2.2.3 Latched logic always_latch procedure

            SystemVerilog还提供了一个特殊的always_lach过程,用于对锁存逻辑行为进行建模。
    例如:

    1. always_latch
    2. if(ck) q <= d;
    9.2.2.4 Sequential logic always_ff procedure

            always_ff过程可用于对可综合的时序逻辑行为进行建模。例如:

    1. always_ff @(posedge clock iff reset == 0 or posedge reset) begin
    2. r1 <= reset ? 0 : r2 + 1;
    3. ...
    4. end

            always_ff过程施加了一个限制,即它包含一个且仅包含一个事件控制,而不包含阻塞时序控制。always_ff过程中赋值左侧的变量,包括来自被调用函数内容的变量在内的任何其他进程都不应写入。如果always_ff过程中的行为不代表时序逻辑,软件工具应执行额外的检查以发出警告。

    9.2.3 Final procedures

            final过程就像一个initial过程,定义了一个过程语句块,只是它发生在模拟时间结束时并且执行时没有延迟。final过程通常用于显示有关模拟的统计信息。final过程中唯一允许的语句是函数声明中允许的语句,因此它们在单个模拟周期内执行。
            与initial过程不同,final过程不是作为一个单独的过程执行的;相反,它在零时间内执行,作为来自单个进程的一系列函数调用。所有final程序应按任意顺序执行。在执行完所有final程序后,不得执行任何剩余的预定事件。当模拟由于对$finish的显式或隐式调用而结束时,将执行final过程。

    1. final
    2. begin
    3. $display("Number of cycles executed %d",$time/period);
    4. $display("Final PC = %h",PC);
    5. end

            在final程序中执行$finish、tf_dofinish()或vpi_control(vpiFinish,…)将导致模拟立即结束。final过程在模拟中只能触发一次。在指示模拟结束的PLI回调之前,应执行final程序。SystemVerilog  final过程以任意但具有确定性的顺序执行。这是可能的,因为final程序仅限于函数所允许的合法声明集。
            注意——SystemVerilog没有指定执行 final过程的顺序,但实现应该定义在运行之间保留顺序的规则。这有助于保持输出日志文件的稳定,因为最终过程主要用于显示统计信息。

    9.3 Block statements

            块语句是一种将语句分组在一起的方法,以便它们在语法上像单个语句一样工作
            有两种类型的块,如下所示:
                    --顺序块,也称为begin-end
                    --平行块,也称为fork - join 块
            顺序块应由关键字begin和end分隔。顺序块中的程序语句应按给定顺序依次执行
            并行块应由关键字fork和join、join_any或join_none分隔。并行块中的程序语句应同时执行

    9.3.1 Sequential blocks

    顺序块应具有以下特性:
            --语句应按顺序执行,一个接一个
            --每个语句的延迟值应相对于前一语句执行的模拟时间进行处理
            --控制应在最后一条语句执行后从块中传出
    语法9-2给出了顺序块的形式语法。

    1. seq_block ::= // from A.6.3
    2. begin [ : block_identifier ] { block_item_declaration } { statement_or_null }
    3. end [ : block_identifier ]
    4. block_item_declaration ::= // from A.2.8
    5. { attribute_instance } data_declaration
    6. | { attribute_instance } local_parameter_declaration ;
    7. | { attribute_instance } parameter_declaration ;
    8. | { attribute_instance } let_declaration

                                    Syntax 9-2—Syntax for sequential block (excerpt from Annex A)

    示例1:顺序块使以下两个赋值具有确定性结果:

    1. begin
    2. areg = breg;
    3. creg = areg; // creg stores the value of breg
    4. end

            执行第一个赋值,并且在控制传递到第二个赋值之前更新areg。示例2:事件控制(见9.4.2)可以在顺序块中使用,以在时间上分离两个赋值:

    1. begin
    2. areg = breg;
    3. @(posedge clock) creg = areg; // assignment delayed until
    4. end // posedge on clock

    示例3:以下示例显示了如何使用顺序块和延迟控制的组合来指定时序波形:

    1. parameter d = 50; // d declared as a parameter and
    2. logic [7:0] r; // r declared as an 8-bit variable
    3. begin // a waveform controlled by sequential delays
    4. #d r = 'h35;
    5. #d r = 'hE2;
    6. #d r = 'h00;
    7. #d r = 'hF7;
    8. end

    9.3.2 Parallel blocks

            fork-join并行块构造允许从其每个并行语句创建并发进程。
    并行块应具有以下特性:
            --语句应同时执行
            --每个语句的延迟值应与进入块的模拟时间相关
            --延迟控制可用于为赋值提供时间顺序
            --控件应在最后一次基于join关键字类型执行有序语句时从块中传出
            --在函数调用中的使用受到限制(请参见13.4)。

    语法9-3给出了并行块的形式语法。

    1. par_block ::= // from A.6.3
    2. fork [ : block_identifier ] { block_item_declaration } { statement_or_null }
    3. join_keyword [ : block_identifier ]
    4. join_keyword ::= join | join_any | join_none
    5. block_item_declaration ::= // from A.2.8
    6. { attribute_instance } data_declaration
    7. | { attribute_instance } local_parameter_declaration ;
    8. | { attribute_instance } parameter_declaration ;
    9. | { attribute_instance } let_declaration

                            Syntax 9-3—Syntax for parallel block (excerpt from Annex A)

            可以指定一个或多个语句;每个语句都应作为一个并发过程执行。fork-join块中的时序控件不必按时间顺序排列。以下示例通过使用并行块而不是顺序块对9.3.1的示例3中所示的波形描述进行编码。对于两种实现方式,在变量上产生的波形完全相同。

    1. fork
    2. #50 r = 'h35;
    3. #100 r = 'hE2;
    4. #150 r = 'h00;
    5. #200 r = 'hF7;
    6. join

            SystemVerilog提供了三个选项,用于指定父(forking)进程何时恢复执行,总结见表9-1。
                                                    Table 9-1—fork-join control options

    选项描述
    join父进程将阻塞,直到该派生的所有进程完成为止。
    join_none父进程继续与派生的所有进程同时执行。派生进程直到父线程执行阻塞语句或终止后才开始执行。
    join_any父进程将阻塞,直到该派生的任何一个进程完成为止。

            当定义fork-join块时,将整个fork封装在begin-end块中会导致整个块作为单个进程执行,每个语句都按顺序执行

    1. fork
    2. begin
    3. statement1; // one process with 2 statements
    4. statement2;
    5. end
    6. join

            在下面的示例中,两个进程被forked。第一个等待20ns,第二个等待命名事件eventA被触发。由于指定了join关键字,父进程应阻止,直到两个进程完成,即,直到20 ns过去并且触发了事件A。

    1. fork
    2. begin
    3. $display( "First Block\n" );
    4. # 20ns;
    5. end
    6. begin
    7. $display( "Second Block\n" );
    8. @eventA;
    9. end
    10. join

            fork-join块上下文中的return语句是非法的,会导致编译错误
            例如:

    1. task wait_20;
    2. fork
    3. # 20;
    4. return ; // Illegal: cannot return; task lives in another process
    5. join_none
    6. endtask

            每当执行进入其范围时,在派生任何进程之前,应将fork-join块的block_item_declaration中声明的变量初始化为其初始化值表达式。在fork-join_any或fork-join_none块中,引用通过引用传递的形式参数是非法的,而不是在fork的block_item_declaration中声明的变量的初始化值表达式中。这些变量在循环构造生成的过程中非常有用,以存储每次迭代的唯一数据。
    例如:

    1. initial
    2. for( int j = 1; j <= 3; ++j )
    3. fork
    4. automatic int k = j; // local copy, k, for each value of j
    5. #k $write( "%0d", k );
    6. begin
    7. automatic int m = j; // the value of m is undetermined
    8. ...
    9. end
    10. join_none

    前面的示例生成输出123。

    9.3.3 Statement block start and finish times

            顺序块和并行块都有开始和结束时间的概念。对于顺序块,开始时间是执行第一条语句时,结束时间是执行最后一条语句时。对于并行块,所有语句的开始时间相同,结束时间由所使用的join构造类型控制(见9.3.2,表9-1)。
            顺序块和并行块可以相互嵌入,从而使复杂的控制结构能够容易地表达并具有高度的结构。
    当块相互嵌入时,块开始和结束的时间很重要。直到达到块的完成时间,也就是说,直到块完全完成执行,才应继续执行块后面的语句。
            示例1:以下示例显示了9.3.2中示例中的语句,这些语句以相反的顺序编写,但仍产生相同的波形。

    1. fork
    2. #200 r = 'hF7;
    3. #150 r = 'h00;
    4. #100 r = 'hE2;
    5. #50 r = 'h35;
    6. join

            示例2:当在发生两个单独的事件(称为事件的连接)之后进行赋值时,fork-join块可能很有用。

    1. begin
    2. fork
    3. @Aevent;
    4. @Bevent;
    5. join
    6. areg = breg;
    7. end

            这两个事件可以以任何顺序发生(甚至在同一模拟时间发生),一旦两个事件都发生,fork-join块就会完成,并进行赋值。相反,如果fork-join块是begin-end块,并且Bevent发生在Aevent之前,那么该块将等待下一个Bevent。
            示例3:此示例显示了两个顺序块,每个块将在其控制事件发生时执行。因为事件控件在fork-join块中,所以它们是并行执行的,因此顺序块也可以并行执行

    1. fork
    2. @enable_a
    3. begin
    4. #ta wa = 0;
    5. #ta wa = 1;
    6. #ta wa = 0;
    7. end
    8. @enable_b
    9. begin
    10. #tb wb = 1;
    11. #tb wb = 0;
    12. #tb wb = 1;
    13. end
    14. join

    9.3.4 Block names

            顺序块和并行块都可以通过在关键字begin或fork后面添加:name_of_block来命名。命名块会创建一个新的层次结构范围。块的命名有以下目的:
            --它允许使用块名称分层引用局部变量、参数和命名事件
            --它允许在诸如disable语句之类的语句中引用块(见9.6.2)。
            只有当未命名块直接包含块项声明(如变量声明或类型声明)时,它才会创建新的层次结构作用域。
            此层次结构作用域未命名,其中声明的项不能按层次结构引用(请参见6.21)。所有变量应为静态变量;也就是说,所有变量都存在一个唯一的位置,离开或进入块不应影响存储在块中的值
            块名称提供了在任何模拟时间唯一识别所有变量的方法。匹配的块名称可以在块end、join、join_any或join_none关键字后面指定,前面加一个冒号。当存在嵌套块时,这可以帮助记录哪个end或fork、join_any或join_none与哪个begin或fork相关联。
            不需要块末尾的名称。如果末尾的名称与开头的块名称不同,则应为错误。

    1. begin: blockB // block name after the begin or fork
    2. ...
    3. end: blockB

            类似地,匹配的块名称可以在以下块结束关键字后面指定,前面加一个冒号:

    1. — endchecker (see 17.2)
    2. — endclass (see 8.3)
    3. — endclocking (see 14.3)
    4. — endconfig (see 33.4)
    5. — endfunction (see 13.4)
    6. — endgroup (see 19.2)
    7. — endinterface (see 25.3)
    8. — endmodule (see 23.2.1)
    9. — endpackage (see 26.2)
    10. — endprimitive (see 29.3)
    11. — endprogram (see 24.3)
    12. — endproperty (see 16.2)
    13. — endsequence (see 16.8)
    14. — endtask (see 13.3)

            匹配的块名称也可以跟随在生成块末尾的关键字末尾(参见27.3)。不需要块末尾的名称。
    如果末尾的名称与开头的块名称不同,则应为错误。

    9.3.5 Statement labels

            标签可以在任何过程语句(可以出现在begin-end块内的任何非声明语句)之前指定,如C中所示。语句标签用于标识单个语句。标签名称在语句之前指定,后面跟一个冒号

    labelA: statement

            begin-end或fork-join块被认为是一个语句,并且可以在块之前有一个语句标签。在begin或fork关键字之前指定语句标签相当于在关键字之后指定块名,并且可以在块end、join、join_any或join_none关键字之后指定匹配的块名。例如:

    1. labelB: fork // label before the begin or fork
    2. ...
    3. join_none : labelB

            在begin或fork之前同时有标签和在begin或fork之后有块名是违法的。标签不能出现在end、join、join_any或join_none之前,因为这些关键字不构成语句

            foreach循环或具有声明为for_initialization一部分的变量的for循环上的语句标签,命名为由循环创建的隐式块。对于其他类型的语句,语句标签会在语句周围创建一个命名的begin-end,并创建一个新的层次结构范围。
            还可以在生成begin-end块之前指定标签(参见27.3)。
            标签也可以在并发断言之前指定(见16.5)。
            可以使用disable语句禁用带有标签的语句。禁用语句的行为应与禁用命名块的行为相同
    关于禁用语句和过程控制,请参见9.6.2。

    9.4 Procedural timing controls

            SystemVerilog有两种类型的显式定时控制,用于控制程序语句何时可以发生。
            第一种类型是延迟控制,其中表达式指定从最初遇到语句到实际执行语句之间的持续时间。延迟表达式可以是电路状态的动态函数,也可以是在时间上分隔语句执行的简单数字。当指定激励波形描述时,延迟控制是一个重要特征。如9.4.1和9.4.5所述。
            第二种定时控制是事件表达式,它允许语句执行延迟,直到某个模拟事件发生在与该过程同时执行的过程中。模拟事件可以是网络或变量的值变化(隐式事件),也可以是从其他过程触发的显式命名事件的发生(显式事件)。通常情况下,事件控制是时钟信号的上升沿或下降沿。9.4.2至9.4.5中讨论了事件控制。
            到目前为止遇到的过程语句都是在不提前模拟时间的情况下执行的。模拟时间可以通过以下三种方法之一提前:
            --由符号#引入的延迟控制
            --由符号@引入的事件控件
            --wait语句,其操作方式类似于事件控制和while循环的组合
            9.4.1至9.4.5中讨论了三种程序时序控制方法。语法9-4显示了过程语句中定时控制的语法。

    1. procedural_timing_control_statement ::= // from A.6.5
    2. procedural_timing_control statement_or_null
    3. delay_or_event_control ::=
    4. delay_control
    5. | event_control
    6. | repeat ( expression ) event_control
    7. delay_control ::=
    8. # delay_value
    9. | # ( mintypmax_expression )
    10. event_control ::=
    11. @ hierarchical_event_identifier
    12. | @ ( event_expression )
    13. | @*
    14. | @ (*)
    15. | @ ps_or_hierarchical_sequence_identifier
    16. event_expression[31] ::=
    17. [ edge_identifier ] expression [ iff expression ]
    18. | sequence_instance [ iff expression ]
    19. | event_expression or event_expression
    20. | event_expression , event_expression
    21. | ( event_expression )
    22. procedural_timing_control ::=
    23. delay_control
    24. | event_control
    25. | cycle_delay
    26. ...
    27. wait_statement ::=
    28. wait ( expression ) statement_or_null
    29. | wait fork ;
    30. | wait_order ( hierarchical_identifier { , hierarchical_identifier } ) action_block
    31. edge_identifier ::= posedge | negedge | edge // from A.7.4

    [31]当使用位置绑定将包含逗号分隔的事件表达式的事件表达式作为实际参数传递时,需要使用括号。
                    Syntax 9-4—Delay and event control syntax (excerpt from Annex A)
            如Clause 28所述,门延迟和线延迟也会提前模拟时间。

    9.4.1 Delay control

            延迟控制之后的过程语句在执行时应相对于延迟控制之前的过程语句延迟规定的延迟。如果延迟表达式评估为未知或高阻抗值,则应将其解释为零延迟。如果延迟表达式的计算结果为负值,则应将其解释为与时间变量大小相同的二进制无符号整数。延迟表达式中允许指定参数。SDF注释可以覆盖它们,在这种情况下,表达式将被重新计算
            示例1:以下示例将任务的执行延迟10个时间单位:

    #10 rega = regb;

    示例2:接下来的三个示例提供了一个数字符号(#)后面的表达式。赋值的执行将延迟表达式值指定的模拟时间量。

    1. #d rega = regb; // d is defined as a parameter
    2. #((d+e)/2) rega = regb; // delay is average of d and e
    3. #regr regr = regr + 1; // delay is the value in regr

    9.4.2 Event control

            过程语句的执行可以与网络或变量上的值更改或声明事件的发生同步。网络和变量上的值更改可以用作触发语句执行的事件。这被称为检测隐式事件。该事件也可以基于变化的方向,即朝向值1(posedge)或朝向值0(negedge)。上升沿和下降沿事件的行为如表9-2所示,可描述如下:
            --应在从1到x、z或0以及从x或z到0的过渡过程中检测到下降沿
            --在从0到x、z或1以及从x或z到1的转换过程中,应检测到上升沿


            除了posedge和negedge之外,第三个边沿事件edge表示向1或0的变化。更准确地说,边沿事件的行为可以描述为:
            --当检测到下降沿或上升沿时,应检测到边沿
            表达式值的任何变化都应检测到隐式事件。边沿事件只能在表达式的LSB上检测到。表达式的任何操作数中的值发生变化而不改变表达式的结果,不应被检测为事件
            以下示例显示了边缘控制语句的图示:

    1. @r rega = regb; // controlled by any value change in the reg r
    2. @(posedge clock) rega = regb; // controlled by posedge on clock
    3. forever @(negedge clock) rega = regb; // controlled by negedge on clock
    4. forever @(edge clock) rega = regb; // controlled by edge on clock

            如果表达式表示clocking的input 或output(见第14条),则event控制运算符使用同步值,即时钟事件采样的值。该表达式还可以表示将由时钟事件触发的clocking块名称(没有边沿限定符)。与event控件一起使用的变量可以是任何一种整形数据类型(见6.11.1)或字符串。变量可以是简单变量,也可以是ref参数(通过引用传递的变量);它可以是数组、关联数组或上述类型的对象(类实例)的成员。
           event表达式应返回奇异值。聚合类型可以在表达式中使用,前提是表达式缩减为奇异值。对象成员或聚合元素可以是任何类型,只要表达式的结果是奇异值即可。如果事件表达式是对简单对象句柄或chandle变量的引用,则当对该变量的写入不等于其先前值时,将创建事件。
            只要返回值的类型是奇异的,并且方法被定义为函数而不是任务,则event控制表达式中允许对象的非虚拟方法以及聚合类型的内置方法或系统函数
            更改对象数据成员的值、聚合元素或方法或函数引用的动态大小数组的大小,将导致重新评估事件表达式。在更改值或大小时,即使方法或函数未引用成员,实现也可能导致重新评估event表达式。

    1. real AOR[]; // dynamic array of reals
    2. byte stream[$]; // queue of bytes
    3. initial wait(AOR.size() > 0) ....; // waits for array to be allocated
    4. initial wait($bits(stream) > 60)...; // waits for total number of bits
    5. // in stream greater than 60
    6. Packet p = new; // Packet 1 -- Packet is defined in 8.2
    7. Packet q = new; // Packet 2
    8. initial fork
    9. @(p.status); // Wait for status in Packet 1 to change
    10. @p; // Wait for a change to handle p
    11. # 10 p = q; // triggers @p.
    12. // @(p.status) now waits for status in Packet 2 to change,
    13. // if not already different from Packet 1
    14. join
    9.4.2.1 Event OR operator

            任何数量的事件的逻辑OR都可以表示,这样,任何一个事件的发生都会触发随后的过程语句的执行。关键字或逗号字符(,)用作事件逻辑or运算符。它们的组合可以用于同一事件表达式中。逗号分隔的敏感度列表应与or分隔的敏感度列表同义。
            接下来的两个示例分别显示了两个和三个事件的逻辑或:

    1. @(trig or enable) rega = regb; // controlled by trig or enable
    2. @(posedge clk_a or posedge clk_b or trig) rega = regb;

            以下示例显示了逗号(,)作为事件逻辑or运算符的使用:

    1. always @(a, b, c, d, e)
    2. always @(posedge clk, negedge rstn)
    3. always @(a or b, c, d or e)
    9.4.2.2 Implicit event_expression list

            event事件控件的不完整事件表达式列表是寄存器传输级别(RTL)模拟中常见的错误来源。隐式事件表达式@*是一种方便的简写,它通过将procedure_timing_control_statement的语句(可以是语句组)读取的所有网络和变量添加到事件表达式中来消除这些问题。
            注——当在always过程的开头用作敏感度列表时,always_comb过程(见9.2.2.2)比使用@*隐式事件表达式列表更可取。关于always_comb和@*的比较,请参见9.2.2.2.2。
            语句中出现的所有网络标识符和变量标识符都将自动添加到事件event表达式中,但以下情况除外:
            --仅出现在等待或事件表达式中的标识符
            --仅在赋值左侧的variable_lvalue中显示为hierarial_variable_identifier的标识符

            出现在赋值右侧、子程序调用中、case和条件表达式中、作为赋值左侧的索引变量或case项表达式中的变量的网和变量都应包含在这些规则中

    1. Example 1:
    2. always @(*) // equivalent to @(a or b or c or d or f)
    3. y = (a & b) | (c & d) | myfunction(f);
    4. Example 2:
    5. always @* begin // equivalent to @(a or b or c or d or tmp1 or tmp2)
    6. tmp1 = a & b;
    7. tmp2 = c & d;
    8. y = tmp1 | tmp2;
    9. end
    10. Example 3:
    11. always @* begin // equivalent to @(b)
    12. @(i) kid = b; // i is not added to @*
    13. end
    14. Example 4:
    15. always @* begin // equivalent to @(a or b or c or d)
    16. x = a ^ b;
    17. @* // equivalent to @(c or d)
    18. x = c ^ d;
    19. end
    20. Example 5:
    21. always @* begin // same as @(a or en)
    22. y = 8'hff;
    23. y[a] = !en;
    24. end
    25. Example 6:
    26. always @* begin // same as @(state or go or ws)
    27. next = 4'b0;
    28. case (1'b1)
    29. state[IDLE]: if (go) next[READ] = 1'b1;
    30. else next[IDLE] = 1'b1;
    31. state[READ]: next[DLY ] = 1'b1;
    32. state[DLY ]: if (!ws) next[DONE] = 1'b1;
    33. else next[READ] = 1'b1;
    34. state[DONE]: next[IDLE] = 1'b1;
    35. endcase
    36. end
    9.4.2.3 Conditional event controls

            @event控件可以有一个iff限定符

    1. module latch (output logic [31:0] y, input [31:0] a, input enable);
    2. always @(a iff enable == 1)
    3. y <= a; //latch is in transparent mode
    4. endmodule

            只有当iff之后的表达式为true(如12.4中所定义)时,事件表达式才会触发,在这种情况下,当enable等于1时。此类型的表达式在a更改时求值,而不是在enable更改时求值。此外,在该类型的类似事件表达式中,iff的优先级高于or。使用括号可以使这一点更加清楚。

    9.4.2.4 Sequence events

            序列实例可以在事件表达式中使用,以根据序列的成功匹配来控制过程语句的执行。这允许命名序列的终点(见16.7)触发其他进程中的多个操作。语法16-3和语法16-5描述了用于声明命名序列和序列实例的语法。序列实例可以直接用在事件event表达式中,如语法9-4所示。
            当在事件event表达式中指定序列实例时,执行事件控制的进程将被阻止,直到指定的序列到达其终点。进程在检测到终点的观察区域之后恢复执行
    使用序列作为事件控件的示例如下:

    1. sequence abc;
    2. @(posedge clk) a ##1 b ##1 c;
    3. endsequence
    4. program test;
    5. initial begin
    6. @ abc $display( "Saw a-b-c" );
    7. L1 : ...
    8. end
    9. endprogram

            在前面的示例中,当命名序列abc到达其终点时,程序块测试中的初始过程被取消阻止,然后显示字符串“Saw-a-b-c”,并使用标记为L1的语句继续执行。在这种情况下,序列的末尾充当取消阻止事件的触发器。
            事件控件中使用的序列被实例化(就像通过断言属性语句一样);事件控件用于同步到序列的末尾,而与序列的开始时间无关。这些序列的自变量应是静态的;用作序列自变量的自动变量将导致错误

    9.4.3 Level-sensitive event control

            过程语句的执行也可以延迟,直到某个条件变为真。这是使用wait语句实现的,wait语句是一种特殊形式的事件控制。wait语句的性质是点平级别敏感的,而基本事件控制(由@字符指定)是边沿敏感的
          wait语句应评估条件;如果不成立(如12.4所定义),则等待声明之后的程序声明应保持阻塞状态,直到该条件成立后再继续。wait语句具有语法9-5中给出的形式。

    1. wait_statement ::= // from A.6.5
    2. wait ( expression ) statement_or_null
    3. | wait fork ;
    4. | wait_order ( hierarchical_identifier { , hierarchical_identifier } ) action_block

                    Syntax 9-5—Syntax for wait statement (excerpt from Annex A)
            以下示例显示了使用wait语句来实现点平级别敏感的事件控制:

    1. begin
    2. wait (!enable) #10 a = b;
    3. #10 c = d;
    4. end

            如果当块被输入时enable的值是1,wait语句将延迟对下一个语句(#10a=b;)的求值,直到enable的值变为0。如果在输入begin-end块时enable已经为0,则在延迟10之后评估赋值“a=b;”,并且不会发生额外的延迟。
            另请参见关于过程控制的9.6。

    9.4.4 Level-sensitive sequence controls

            过程代码的执行可以延迟,直到序列终止状态为true。这是通过使用点平级别敏感的wait语句和内置方法来实现的,该方法返回命名序列的当前结束状态:triggered。如果给定序列在特定时间点(在当前时间步长中)达到其终点(见16.7),则触发序列方法评估为true(1’b1),否则评估为false(1’b0)。序列的触发状态在观察区域期间设置,并在剩余的时间步长中持续(即,直到模拟时间推进)。

    1. For example:
    2. sequence abc;
    3. @(posedge clk) a ##1 b ##1 c;
    4. endsequence
    5. sequence de;
    6. @(negedge clk) d ##[2:5] e;
    7. endsequence
    8. program check;
    9. initial begin
    10. wait( abc.triggered || de.triggered );
    11. if( abc.triggered )
    12. $display( "abc succeeded" );
    13. if( de.triggered )
    14. $display( "de succeeded" );
    15. L2 : ...
    16. end
    17. endprogram

            在前面的例子中,程序检查中的initial过程等待序列abc或序列de的终点。当任一条件计算为true时,wait语句将取消阻止进程,显示导致进程取消阻止的序列,然后继续执行标记为L2的语句。序列方法的定义见16.9.11和16.13.6。

    9.4.5 Intra-assignment timing controls

            前面描述的延迟和事件event控制构造在语句之前并延迟其执行。相反,赋值内延迟和事件event控制包含在赋值语句中,并以不同的方式修改活动流。本款描述了可用于赋值内延迟的赋值内定时控制和重复定时控制的目的。
          赋值内延迟或事件控制将延迟向左侧分配新值,但是右边的表达式应该在延迟之前而不是在延迟之后进行评估。语法9-6中给出了内部分配延迟和事件控制的语法。

    1. blocking_assignment ::= // from A.6.2
    2. variable_lvalue = delay_or_event_control expression
    3. | ...
    4. nonblocking_assignment ::=
    5. variable_lvalue <= [ delay_or_event_control ] expression

            Syntax 9-6—Syntax for intra-assignment delay and event control (excerpt from Annex A)

            delay_or_event_control语法如9.4中的语法9-4所示。
            赋值内延迟和事件控制可以应用于阻塞赋值和非阻塞赋值。重复事件控制应规定指定事件发生次数的赋值内延迟。如果重复计数次数或包含重复计数的带符号变量在求值时小于或等于0,则赋值将如同没有重复构造一样发生

    1. For example:
    2. repeat (3) @ (event_expression)
    3. // will execute event_expression three times
    4. repeat (-3) @ (event_expression)
    5. // will not execute event_expression.
    6. repeat (a) @ (event_expression)
    7. // if a is assigned -3, it will execute the event_expression if a is
    8. // declared as an unsigned variable, but not if a is signed

            当事件必须与时钟信号的计数同步时,这种构造是方便的。表9-3通过显示可以在不使用分配内定时的情况下实现相同定时效果的代码来说明赋值内定时控制的原理。

            下面三个示例使用了fork-join构造。在关键字fork和join之间的语句并行执行。这一构造在9.3.2章节详细描述。
            以下示例显示了可以通过使用分配内定时控制来防止的竞争条件:

    1. fork
    2. #5 a = b;
    3. #5 b = a;
    4. join

            此示例中的代码在同一模拟时间对a和b的值进行采样和设置,从而创建竞争条件。下一个示例中使用的定时控制的内部赋值形式防止了这种竞争条件。

    1. fork // data swap
    2. a = #5 b;
    3. b = #5 a;
    4. join

           赋值内定时控制起作用,因为赋值内延迟导致在延迟之前评估a和b的值,并且导致在延迟之后进行赋值。内部赋值等待事件也是有效的。在以下示例中,当遇到赋值语句时,将计算右侧表达式,但赋值会延迟到时钟信号的上升沿:

    1. fork // data shift
    2. a = @(posedge clk) b;
    3. b = @(posedge clk) c;
    4. join

            以下是作为非阻塞赋值的分配内延迟的重复事件控制的示例:

    a <= repeat(5) @(posedge clk) data;

            图9-1说明了这种重复事件控制所产生的活动。

            在本例中,当遇到赋值时,将评估data的值。在出现五次posedge clk之后,将为a分配data的值。以下是作为程序赋值的分配内延迟的重复事件控制的示例:

    a = repeat(num) @(clk) data;
    

            在本例中,当遇到赋值时,将评估data的值。在clk的转换次数等于num的值之后,a被分配data的值。以下是重复事件控件的示例,该控件的表达式包含指定事件发生次数和计数事件的操作:

    a <= repeat(a+b) @(posedge phi1 or negedge phi2) data;

            在本例中,当遇到赋值时,将评估data的值。在phi1的上升沿和phi2的下降沿之和等于a和b之和之后,a被赋予data值。即使phi1上升沿和phi2下降沿发生在同一模拟时间,也将分别检测和计数。如果phi1和phi2指的是相同的信号,那么前面的赋值可以简化为:

    a <= repeat(a+b) @(edge phi1) data;

    9.5 Process execution threads

    SystemVerilog为以下内容创建一个执行线程:
            --每个initial程序
            --每个final 程序
            --每个always, always_comb, always_latch, and always_ff过程
            --fork-join(或join_any或join_none)语句组中的每个并行语句
            --每个动态过程(dynamic process)
            每个连续赋值也可以被视为其自己的线程(见10.3)。

    9.6 Process control

            SystemVerilog提供了允许一个进程终止或等待其他进程完成的构造。wait-fork构造等待进程的完成。disable构造停止命名块或任务内所有活动的执行,而不考虑父子关系(子进程可以终止父进程的执行,或者一个进程可以终止不相关进程的执行)。disable fork构造停止进程的执行,但要考虑父子关系
            进程控制语句的语法形式如语法9-7所示。

    1. wait_statement ::= // from A.6.5
    2. wait ( expression ) statement_or_null
    3. | wait fork ;
    4. | wait_order ( hierarchical_identifier { , hierarchical_identifier } ) action_block
    5. disable_statement ::=
    6. disable hierarchical_task_identifier ;
    7. | disable hierarchical_block_identifier ;
    8. | disable fork ;

                    Syntax 9-7—Syntax for process control statements (excerpt from Annex A)

    9.6.1 Wait fork statement

            wait-fork语句阻塞流程执行流,直到所有直接的子流程(由当前流程创建的流程,不包括其子流程)都完成了执行
            wait-fork的语法如下:

    wait fork ; // from A.6.5

            指定wait-fork会导致调用进程阻塞,直到它的所有直接子进程都完成为止
            当没有任何进一步的活动时,仿真会自动终止。仿真也会在所有程序块完成执行时自动终止(即,它们到达执行块的末尾),而不管任何子进程的状态如何(见24.7)。wait-fork语句允许程序块在退出之前等待其所有并发线程的完成
            在下面的示例中,在调用任务do_test之前,将派生出两个直接的子进程(child1和child2)。在任务do_test中,将派生出另外三个子进程(child3、child4和child5)和两个子进程(descendant1和descendant2)。接下来,函数do_sequence将派生出另外两个直接的子进程(child6和child7)。wait-fork语句阻塞任务do_test的执行流,直到所有七个直接子进程都完成,然后返回到调用方。wait-fork语句并不直接依赖于child5派生的子进程。

    1. initial begin : test
    2. fork
    3. child1();
    4. child2();
    5. join_none
    6. do_test();
    7. end : test
    8. task do_test();
    9. fork
    10. child3();
    11. child4();
    12. fork : child5 // nested fork-join_none is a child process
    13. descendant1();
    14. descendant2();
    15. join_none
    16. join_none
    17. do_sequence();
    18. wait fork; // block until child1 ... child7 complete
    19. endtask
    20. function void do_sequence();
    21. fork
    22. child6();
    23. child7();
    24. join_none
    25. endfunction

    9.6.2 Disable statement

            disable语句提供了终止与并发活动流程相关联的活动的能力,同时保持过程描述的结构化性质。disable语句提供了一种机制,用于在任务执行所有语句之前终止任务、中断循环语句或跳过语句以继续循环语句的另一次迭代。它可用于处理异常情况,如硬件中断和全局复位disable语句也可用于终止标记语句的执行,包括延迟断言(见16.4)或过程并发断言(见16.14.6)。
            disable语句应终止任务或命名块的活动。应在块之后的语句或任务启用语句之后恢复执行。
    命名区块或任务内启用的所有活动也应终止。如果任务启用语句是嵌套的(即,一个任务启用另一个任务,而该任务又启用了另一个),则禁用链内的任务将禁用链上向下的所有任务。如果一个任务被启用多次,那么禁用该任务将禁用该任务的所有激活

            如果任务被禁用,则不会指定任务可以启动的以下活动的结果:
            --输出和输入参数的结果
            --计划但未执行的非阻塞赋值
            --程序性连续赋值(赋值和force语句)
            disable语句可以在块和任务中使用,以禁用包含disable语句的特定块或任务。disable语句可用于禁用函数中的命名块,但不能用于禁用函数。如果函数中的disable语句禁用了调用该函数的块或任务,则行为是未定义的。对于任务的所有并发执行,禁用automatic任务或automatic任务内的块的过程与常规任务一样
            示例1:此示例说明了一个块是如何禁用自身的。

    1. begin : block_name
    2. rega = regb;
    3. disable block_name;
    4. regc = rega; // this assignment will never execute
    5. end

            示例2:此示例显示了在命名块中使用的disable语句,其方式类似于前向goto。disable语句之后执行的下一个语句是命名块之后的语句。

    1. begin : block_name
    2. ...
    3. ...
    4. if (a == 0)
    5. disable block_name;
    6. ...
    7. end // end of named block
    8. // continue with code following named block
    9. ...

            示例3:此示例说明使用disable构造终止不包含disable语句的命名块的执行。如果该块当前正在执行,则会导致控件立即跳转到该块之后的语句。如果该块是循环体,则其作用类似于continue(参见12.8)。如果该块当前未执行,则禁用无效。

    1. module m (...);
    2. always
    3. begin : always1
    4. ...
    5. t1: task1( ); // task call
    6. ...
    7. end
    8. ...
    9. always
    10. begin
    11. ...
    12. disable m.always1; // exit always1, which will exit task1,
    13. // if it was currently executing
    14. end
    15. endmodule

            示例4:此示例显示了用作任务早期返回的disable语句。SystemVerilog也有来自任务的返回,这将终止执行返回的过程的执行(见12.8)。但是,使用disable语句禁用自身的任务并不是return语句的简写。如果对任务应用了disable,则该任务的所有当前活动执行都将被禁用。

    1. task proc_a;
    2. begin
    3. ...
    4. ...
    5. if (a == 0)
    6. disable proc_a; // return if true
    7. ...
    8. ...
    9. end
    10. endtask

            示例5:此示例显示disable语句的使用方式与continue和break这两个语句的使用方法等效(请参见12.8)。该示例说明了允许命名块执行的控制代码,直到循环计数器达到n次迭代,或者直到变量a被设置为b的值。命名块outer_block包含执行到a==b的代码,此时禁用outer_block;语句终止该块的执行。命名块inner_block包含为for循环的每次迭代执行的代码。每次此代码执行disable inner_block;语句,inner_block块终止,执行传递到for循环的下一次迭代。对于inner_block块的每次迭代,如果(a!=0),将执行一组语句。另一组语句执行if(a!=b)。

    1. begin : outer_block
    2. for (i = 0; i < n; i = i+1) begin : inner_block
    3. @clk
    4. if (a == 0) // "continue" loop
    5. disable inner_block ;
    6. ... // statements
    7. ... // statements
    8. @clk
    9. if (a == b) // "break" from loop
    10. disable outer_block;
    11. ... // statements
    12. ... // statements
    13. end
    14. end

            注意——类似C的break和continue语句(见12.8)可能是对前面示例进行编码的一种更直观的方式。
            示例6:此示例显示了disable语句,该语句用于在重置事件发生时同时禁用一系列定时控件和任务命名的操作。该示例显示了一个fork-join块,其中有一个命名的顺序块(event_expr)和一个等待事件重置发生的disable语句。顺序块和等待重置并行执行。event_expr块等待事件ev1的一次出现和事件trig的三次出现。当这四个事件发生后,再加上d个时间单位的延迟,任务操作就会执行。当事件重置发生时,无论顺序块中的事件如何,fork-join块都会终止,包括任务操作。

    1. fork
    2. begin : event_expr
    3. @ev1;
    4. repeat (3) @trig;
    5. #d action (areg, breg);
    6. end
    7. @reset disable event_expr;
    8. join

            示例7:下一个示例是可再触发单稳态的行为描述。命名事件 retrig重新启动单稳态时间段。
    如果 retrig在250个时间单位内继续发生,那么q将保持在1。

    1. always begin : monostable
    2. #250 q = 0;
    3. end
    4. always @retrig begin
    5. disable monostable;
    6. q = 1;
    7. end

    9.6.3 Disable fork statement

            disable fork语句终止调用进程的所有活动子进程(子进程)
            disable fork的语法如下:

    disable fork ; // from A.6.5

            disable fork语句终止调用进程的所有子程序以及进程的子程序的子程序。换句话说,如果任何子进程都有自己的子进程,disable-fork语句也会终止它们
            在以下示例中,任务get_first生成等待特定设备(1、7或13)的任务的三个版本。任务wait_device等待特定设备准备就绪,然后返回设备的地址。当第一个设备可用时,get_first任务将恢复执行,并继续终止未完成的wait_device进程。

    1. task get_first( output int adr );
    2. fork
    3. wait_device( 1, adr );
    4. wait_device( 7, adr );
    5. wait_device( 13, adr );
    6. join_any
    7. disable fork;
    8. endtask

            disable构造在应用于进程正在执行的命名块或语句时终止进程。disable fork语句与disable的不同之处在于disable fork考虑进程的动态父子关系,而disable使用被禁用块的静态语法信息。因此,disable将结束执行特定块的所有进程,无论这些进程是否由调用线程fork,而disable fork将仅结束由调用线程派生的进程

    9.7 Fine-grain process control

            进程是一个内置类,允许一个进程在启动后访问和控制另一个进程。用户可以声明process类型的变量,并安全地将它们传递给任务或将它们合并到其他对象中。流程类的原型如下:

    1. class process;
    2. typedef enum { FINISHED, RUNNING, WAITING, SUSPENDED, KILLED } state;
    3. static function process self();
    4. function state status();
    5. function void kill();
    6. task await();
    7. function void suspend();
    8. function void resume();
    9. function void srandom( int seed );
    10. function string get_randstate();
    11. function void set_randstate( string state );
    12. endclass

            进程类型的对象是在派生进程时在内部创建的。用户不能创建process类型的对象;尝试调用new不应创建新进程,而应导致错误。
            无法扩展进程类。试图扩展它将导致编译错误。
            类型进程的对象是唯一的;一旦底层进程终止并且对对象的所有引用都被丢弃,它们就可以重用。
            self()函数返回当前进程的句柄,即进行调用的进程的句柄。函数的作用是返回进程状态,如状态枚举所定义:
            --FINISHED表示进程正常终止。
            --RUNNING表示进程当前正在运行(不在阻塞语句中)。
            --WAITING表示进程正在阻塞语句中等待。
            --SUSPENDED表示进程已停止,等待恢复。
            --KILLED表示进程被强制终止(通过终止或禁用)。
            kill()函数终止给定的进程及其所有子进程,即被终止的进程使用fork语句派生的进程。如果要终止的进程没有在其他条件下被阻止等待,例如事件、等待表达式或延迟,则该进程应在当前时间步长中的某个未指定时间终止。
            await()任务允许一个进程等待另一个进程的完成。在当前进程上调用此任务是错误的,即进程不能等待自己的完成。
            函数suspend()允许一个进程挂起自己或另一个进程的执行。如果要挂起的进程没有在其他条件下被阻止等待,例如事件、等待表达式或延迟,则该进程应在当前时间步长中的某个未指定时间挂起。在同一(挂起的)进程上多次调用此方法没有任何效果。
            resume()函数会重新启动以前挂起的进程。对在另一个条件下被阻止时挂起的进程调用resume将使该进程对事件表达式重新敏感,或等待等待条件变为true或等待延迟到期。
            如果等待条件现在为真或发生了原始延迟,则进程被安排到活动或反应区域,以在当前时间步长中继续执行。对挂起自身的进程调用resume会导致该进程在调用挂起后的语句处继续执行。
    方法kill()、await()、suspend()和resume()应限于由初始过程、始终过程或其中一个过程的fork块创建的过程。
            以下示例启动任意数量的进程,由任务参数N指定。接下来,任务等待最后一个进程开始执行,然后等待第一个进程终止。此时,父进程将强制终止所有尚未完成的fork进程。

    1. task automatic do_n_way( int N );
    2. process job[] = new [N];
    3. foreach (job[j])
    4. fork
    5. automatic int k = j;
    6. begin job[k] = process::self(); ... ; end
    7. join_none
    8. foreach (job[j]) // wait for all processes to start
    9. wait( job[j] != null );
    10. job[1].await(); // wait for first process to finish
    11. foreach (job[j]) begin
    12. if ( job[j].status != process::FINISHED )
    13. job[j].kill();
    14. end
    15. endtask

  • 相关阅读:
    物联网之点灯app按键事件绑定,远程开灯
    老师的经典口头禅,这一句最扎心
    FFmpeg内存对齐
    Docker-compose
    MySQL精髓:如何使用ALL一次找到最大值
    Linux系统网卡配置详细教程!
    Spring Cache使用Redis自定义缓存key
    Qt QObject Cannot create children for a parent that is in a different thread
    PUBG官方:聊聊外挂、封号、误封、白名单等问题
    forwardRef, useImperativeHandle实现父组件调用子组件方法(个人笔记,非教程)
  • 原文地址:https://blog.csdn.net/qq_33300585/article/details/133847549