书接上回:
sv标准解读第三章-设计和验证的building block
第9章 进程
9.1 综述
1.介绍结构化过程语句,包括initial、always、final
2.介绍块语句,包括begin-end顺序块、fork join并行块
3.timing控制,包括delay、events、waits、intra-assignment
4.过程控制
9.2 结构化过程语句
结构化过程语句包括以下内容:
---initial
---always[包括always/always_comb/always_latch/always_ff]
---finial
---Task
---function
Initial过程:在simulation开始时就会启动,且只执行一次;
Always过程:重复执行直到simulation结束。
Initial和always之间的执行顺序是不固定的。
Final过程发生在simulation就是且只执行一次。
Task/function可以被其他过程一次或者多次调用。
9.2.1 initial过程
Initial的特点:只执行一次,语句结束则initial结束,在simulation开始时启动。
作用:(1)用来初始化变量
(2)提供波形激励和进展
9.2.2 always过程
特点:在整个simulation阶段不断重复执行
9.2.2.1 always
可以利用always重复的特性制作一个时钟振荡器。
另外还可以结合其他定时控制来实现一些硬件行为。但是always要能控制simulation time,否则容易产生死锁,如下:
只要加上一个定时控制就能解决这个问题,如下:
9.2.2.2 always_comb
特点:是专门为组合逻辑服务的,如下:
Always_comb和always的区别是:
- 存在敏感列表,详见9.2.2.2.1
- 左侧被赋值的变量不能被多个进程写,但是如果是多bit变量,那么每一bit可以由不同进程写
- Simulation time=0时会自动触发一次
9.2.2.2.1 always_comb的敏感列表
包括:
- 在该block内部声明的所有变量;
- 在该block里调用的function里声明的所有变量;
- 在该block内部被写的所有表达式;
- 在该block里调用的函数里被写的所有表达式;
通过::调用的hierarchical function或者package function和普通的function一样解析。
调用类对象或者类对象里的方法不是always_comb的敏感列表,但是如果调用的对象方法需要传参,那么这个参数会被列入敏感列表。
如果调用task,task里面的内容不会被列入敏感列表。
注意:如果一个task不消耗仿真时间且等于一个void function,那么它里面的内容是会被列入敏感列表的。
如果调用了立即断言里的表达式,该表达式被用在if语句的条件里,那么会被列入敏感列表,如果是断言里的action block里的表达式,那么不会被列入敏感列表。举例:
上面的例子中,敏感因素包括:b/c/e
9.2.2.2.2 always_comb和always @*的区别
(1)always_comb总是会在仿真0时刻执行一次,但是always@*只有当敏感列表里信号发生改变了才会执行;
(2)always_comb对function里面的内容也敏感,但是always@*只对function的参数敏感;
(3)always_comb里赋值的左边不能被多个进程赋值,但是always@*允许多个进程对相同的变量赋值;
(4)always_comb里面不允许出现有阻塞时间、事件控制或者fork-join的语句
9.2.2.3 always_latch
作用:模拟锁存器的逻辑行为。举例:
Always_latch和always_comb的构造是相同的,只不过前者为锁存器服务,后者为组合逻辑服务。当Always_latch里的代码不符合锁存器逻辑时,工具会给出相应的warn。
9.2.2.4 always_ff
作用:用于模拟顺序逻辑行为。举例:
Always_ff有一个使用限制:只能有一个事件控制且不允许有阻塞时间的控制。赋值左侧不能被其他进程written。
9.2.3 final 过程
Final和initial类似,只不过final发生在仿真结束之时,且final执行没有delay。一般final用于display仿真的统计信息。
Final里唯一允许存在的语句就是function声明里允许存在的语句。但是和initial不同的是,final不是作为独立的进程而执行,相反,它可以被很多进程以函数调用的方式执行,并且执行顺序是任意的。
当调用$finish系统函数时,final过程就会被调用。
Final里面可以调用$finiash/tf_dofinish()/vpi_control(vpiFinish…),在一个simulation里finial只能被触发一次。
9.3 block语句
有两类block语句:
- 串行,被begin-end包裹;
- 并行,被fork-join包裹
9.3.1串行block
串行block有以下特点:
- 串行执行语句;
- 延迟语句的作用是相对于前一条语句而言的;
- 最后一条语句执行完会跳出该block
举例:
上面两条语句依次执行。
举例2:
可以出现时间控制语句
9.3.2 并行block
有以下特点:
- 同时执行;
- 添加的延迟语句是相对于进入该block的时刻而言的;
- 延迟语句可以用于控制赋值的时间顺序;
- 根据fork join类型的不同,最后一条语句执行完就会跳出该block
有三种fork join语句:
- fork join:所有并行语句都执行完才会跳出该block;
- fork join_any:任意一条并行语句执行完就会跳出该block;
- fork join_none:不需要任何语句执行完就会跳出该block;
举例:
上面这个例子只有一个进程,该进程有两条串行执行的语句
举例2:
上面这个例子有两个进程,两个进程并行执行
举例3:
Fork join里面不允许出现return,会报编译错误
如果在fork join里面出现了声明语句,且该声明语句是在进程启动之前,那么声明的变量都会在进程启动之前完成初始化;特殊地,对于fork-join_any/fork-join_none块,如果声明语句出现在进程启动之后,且引用了进程之外的变量,那么这样的语句是非法的。举例:
上面会打印出123
9.3.3 block语句的开始时间和结束时间
串行block:开始时间是第一条语句执行的时间;结束时间是最后一条语句执行的时间;
并行block:所有语句的开始时间是一样的,结束时间取决于fork-join的类型
举例:
上面这个实现的是:赋值语句要在两个事件都发生后才会执行,且两个事件是独立的。
举例:
上面这个例子中,有两个并行进程,一个进程需要先等enable_a事件触发才会执行后面的begin end,另一个进程需要等enable_b事件触发才能执行后面的begin end
9.3.4 block名称
给begin end块和fork join块添加名字的方式是begin:name或者fork:name。添加名字的作用是:
- 通过名字可以创建一个新的层次结构;
- 通过名字可以引用局部成员;
- 名字可以在其他语句中调用,如disable语句;
如果一个block没有名字,那么它只有在里面有变量声明或者类型声明时才会创建一个新的层次结构。因为没有名字,所以里面定义的变量就不能通过层次引用了。
Block里面的变量是static的。
举例:
Begin/fork后面的名字和end后面的名字要保持一致。如果不一致会报错。
End后面的名字可有可无。
9.3.5 语句标签
在任何过程性语句前面是可以加上标签的,格式如下:
因此,对于begin end和fork join,在关键字begin/fork前面加标签和后面加标签作用是一样的,例如:
但是前面加标签和后面加标签是不允许同时存在的,且不允许标签加在end/join/join_any/join_none前面。
标签页同样可以加在foreach/for循环关键字前面;也可以加在generate begin end block前面,也可以加在并发断言的签名前面。
所有加了标签的语句都可以通过disable语句disable掉,相当于是把整个block给disable掉。
9.4 过程定时控制
Sv存在两种控制过程语句合适发生的定时控制:第一种就是delay控制,控制的是从开始遇见语句到真正执行该语句之间的时间,这种delay语句既可以是一个表示电路状态的动态函数,也可以就是一个简单的数字;第二种就是事件表达式,事件表达式可以是某个变量或者net的变化,也可以是其他过程触发的事件。
那么存在下面三种定时控制语句:
- delay控制语句,需要加上关键符号#
- 事件控制,需要加上关键符号@
- Wait控制,相当于是事件控制和while循环的结合
9.4.1 delay控制
Delay控制语句的作用就是将其后面的过程语句推迟指定的时间执行。如果延迟控制语句计算出来是x/z态,那么就是零延迟;如果计算出来是负值,那么会转化为大小相同的补码无符号整数。延迟控制语句里可以有参数。延迟控制语句可以被SDF覆盖。
例:下面的例子将赋值语句推迟了指定数量的时间单位
9.4.2 事件控制
事件控制语句的作用就是将其后面的过程语句的执行受到net/var变化或者事件发生的控制。变化有两个方向:posedge / negedge:
Posedge:0跳变成x/z/1、x跳变成1、z跳变成1
Negedge:1跳变成x/z/0、x跳变成0、z跳变成0
总结成一个表就是:
另外,edge相当于posedge+negedge
如果使用了edge/posedge/negedge关键字,那么观察的就是后面表达式LSB的变化;如果不加这三个关键字,那么观察的就是后面表达式所有bit位的变化。举例:
如果事件控制表达式是一个clocking block的input/inout,那么会采用它的同步值,也就是时钟事件采样的值;事件控制表达式也可以直接调用整个clocking block;事件控制表达式还可以是任意一种整型数据或者字符串。
整个事件表达式返回奇异值。
举例:
9.4.2.1 事件的or操作符
Or表示事件触发表达式任何一个触发就会执行后面的过程语句,使用关键字or或者逗号,表示。举例:
9.4.2.2 隐式事件表达式列表
@*的写法会将过程语句里的所有net/var标识符添加到敏感列表中,以下除外:
- 只出现在wait或者事件表达式中的标识符;
- 只在赋值语句左侧出现的hierarchical标识符
举例:
9.4.2.3 有条件的事件控制
@事件控制允许有iff条件控制。举例:
上面这个例子中,只有当enable为1且a发生变化时,对y的非阻塞赋值才会发生。Iff的优先级高于or
9.4.2.4 sequence事件
事件表达式里可以出现sequence。只有sequence执行到end point的observed域时,进程才会恢复执行。举例:
上面这个例子中,只有当abc sequence执行到end point后,才会打印出“Saw a-b-c”,因此,sequence执行的end point是触发后面语句执行的trigger。
9.4.3 电平敏感事件控制
由wait控制完成,对电平敏感。
Wait等待的条件如果不为真,那么会一直处于阻塞状态。
语法如下:
举例:
如果进入该block时enable=1,那么会一直阻塞直到enable=0才会执行#10 a=b;语句。
9.4.4 电平敏感的sequence控制
当wait语句里有sequence控制语句时,那么wait会阻塞到sequence为真为止。举例:
上面的例子中,check里的initial块里的wait语句将会一直等到abd/de sequence的end point,当sequence为真时,就会解除阻塞继续往下执行后面的语句。
9.4.5 intra-assignment定时控制
intra-assignment delay的作用是对左边的新值赋值进行推迟,但是右边表达式的更新是在延迟之前。语法如下:
intra-assignment delay可以写在阻塞赋值或者非阻塞赋值中。还可以用repeat约束事件执行的次数,如果次数是有符号数且<=0,那么不会执行repeat结构。举例:
下表展示了使用intra-assignment delay的作用:
举例:
上面这个例子会产生竞争冒险,为了解决这个问题,可以使用intra-assignment delay:
之所以能解决竞争冒险问题,是因为intra-assignment delay会在delay之前先把a/b的值保存下来,delay之后再进行复制。同时,intra-assignment delay也可以用事件,例如:
还可以使用repeat语句:
下图展示了上面这个例子的效果:
举例:
注意,上面这个例子中,如果phi1和phi2在同一时刻都发生了,那么会分别计数。
9.5 进程执行线程
Sv能够产生线程的过程性语句有:
- initial过程
- final过程
- always/always_comb/always_latch/always_ff过程
- fork-join/fork-join_any/fork_join_none过程
- 每一个动态过程
9.6 进程控制
Sv提供了wait其他进程和disable其他进程的构造,语法如下:
9.6.1 wait fork语句
Wait fork的作用是阻塞当前进程直到所有的直接子进程(直接子进程:由当前进程创建的进程,不包括这些进程的后代进程)完成后才会继续执行后面的语句。举例:
上面这个例子中,在调用任务do_test前有两个直接子进程:child1和child2,在do_test任务中,有三个直接子进程:child3/child4/child5和两个后代子进程:descendant1和descendant2,而在do_sequence中,有两个直接子进程:child6和child7。Wait fork语句会阻塞do_test任务的执行直到所有的的7个直接子进程完成,但是不会依赖于两个后代子进程的执行。
9.6.2 disable语句
Disable语句的作用是终止在它之前其他语句的执行,在处理硬件中断和全局重置等异常情况非常有用。Disable还可以用于终止label语句、deferred assertion、并发断言、命名的task、命名的block。
如果task被disable,下面这些结果是不确定的:
- output/inout参数的结果;
- scheduled但是没有执行的非阻塞赋值
- 过程连续赋值,包括assign和force
Disable不能用于disable function。
举例1:使用disable来disable自己
举例2:下面disable的作用相当于goto
例3:下面这个例子使用disable来disable另一个命名block,该block内部没有disable语句,如果disable发挥作用时该block正在运行,那么就会使代码跳转到该block后面的代码,如果该block是个循环体,那么disable的作用就相当于continue,如果该block没有运行,那么disable将不起任何作用。
例4:下面这个例子用disable来提前结束task
例5:下面这个例子分别展示了disable用于continue或者break的作用
例6:下面这个例子展示了disable用于reset
9.6.3 disable fork语句
Disable fork语句会终止掉所有该process的子process,语法:
例:下面这个例子只要device1/7/13任意一个被wait到了,那么就会结束该task并返回输出adr。
Disable和disable fork的区别:disable fork会动态考虑进程之间的父子关系,但是disable则不会。
9.7 内置进程类
内置进程类如下:
用户不能创建process类型的对象;尝试调用new不会创建一个新进程,而是会导致一个错误。不能扩展process类。尝试扩展它将导致编译错误。process类型的对象是唯一的;一旦底层进程终止并且所有对对象的引用都被丢弃,它们就可以被重用。
Self()函数返回当前process的句柄
Status()函数的返回结果有以下几种:
- finished:表示进程正常结束
- running:表示进程正在running(不在阻塞语句里)
- waiting:表示进程正在等待一个阻塞语句
- suspended:表示进程停止了正在等待恢复
- killed:表示进程被强制终止(通过kill或者disable)
kill()函数会终止所有进程包括它的子进程。
Await()任务运行一个进程等待另一个进程完成,但是不能等待自己进程完成。
Suspend()函数运行进程挂起自己进程或者另一个进程。
Resume()函数使得先前挂起的进程重新启动。
举例: