十二 · 2022年06月01日

仿真那点事事儿——0+还是0-

✎ 编 者 按 

    在仿真里,信号的驱动究竟是在时钟沿之前还是在时钟沿之后?

》关于仿真中信号驱动那点事儿

    记得在SystemVerilog中,对于仿真时信号的驱动绿皮书里有这么两个建议:

  1. 时钟信号驱动赋值采用=。   
  2. 其他信号的驱动赋值采用<=形式。

    乍一看有点儿像设计里阻塞赋值与非阻塞赋值的差异。当然仿真里是没有这个概念的。具体底层仿真器的原理也不在我个人的研究范围内(不求甚解)。无论是SpinalHDL还是cocotb,其均是基于协程的思路来进行的仿真。那么,这里面在仿真信号赋值时,今天来一探究竟。

》SpinalHDL中的仿真信号驱动

    我们以下面这个简单的example来进行测试:

case class dut()extends Component {
  val io=new Bundle{
    val dataIn=in Bool()
    val dataOut=out Bool()
  }
  noIoPrefix()
  io.dataOut:=RegNext(~io.dataIn,True)
}

    这里dataOut我们使其复位时处于高电平。

    所采用的仿真代码为:

3540e810849758f8d3b4698963f7e70e.jpg

    我们来看下其仿真波形:

786df89ed4e10666ca02d9073b5a85ae.jpg

    这里从波形上看,当复位信号被释放的同时,dataOut随机被翻转。不妨一同来看下背后的实现机制。

    首先,我们来看下forkStimulus里面的实现逻辑:

def forkStimulus(period: Long) : Unit = {
      cd.config.clockEdge match {
        case RISING  => fallingEdge()
        case FALLING => risingEdge()
      }
      if(cd.hasResetSignal) cd.deassertReset()
      if(cd.hasSoftResetSignal) cd.deassertSoftReset()
      if(cd.hasClockEnableSignal) cd.deassertClockEnable()
      fork(doStimulus(period))
    }

    这里,首先对时钟信号和复位信号进行初始化,结合我们时钟的配置,这里时钟信号首先被设置为低电平,复位信号被释放掉,随后拉起一个doStimulus的协程,再看下doStimulus的实现:

75c229532a66024fe2b95e028151635a.png

    这里我们采用的是同步复位,我们来看其处理流程。首先复位信号被拉起,此时clk为低电平,随后clk被重复拉起释放了16个时钟周期退出for循环。此时clk为低电平。紧接着复位信号被释放,进入DoClock函数。DoClock函数实现为:

fc14c467bc1ebe3816ebba6f653698a3.jpg

    这里是简单的时钟信号驱动函数,按照指定周期产生时钟信号。

    而再看waitSampling()函数的实现:

70357cba6cc7a58f5b14c3de171cfad8.jpg

    若时钟是上升沿有效,那么该函数推出的条件是已经采样到时钟信号从低电平到高电平变化完成之后才推出。那么我们在随后对dataIn的赋值其实是发生在clk上升沿到来后的0+时间才发生的。

    从这里代码分析我们可以看出,在SpinalHDL的仿真里:

  1. 复位信号会持续16个时钟周期,并在下一个时钟周期上升沿到来之前信号将复位信号释放(在下一个时钟沿0-时刻到来)。也正因如此,从仿真波形上看,dataOut和reset似乎同时发生了改变。
  2. waitSampling()之后对信号的操作是发生时钟有效沿跳变之后才进行的,即0+时刻。
  3. 在SpinalHDL中没有=与<=两种赋值,无论是时钟还是普通信号,都采用类似阻塞赋值的意思。

》cocotb中的仿真信号驱动

    cocotb仿真的实现机制和SpinalHDL原理无差。在cocotb中,对于信号的赋值,也和SystemVerilog提供了两种类似的方式:

  • sig.value=new_value
  • sig.setimmediatevalue(new_val)

    前者有点儿类似非阻塞赋值,而后者类似阻塞赋值。

    这里我们以类似的dut来进行测试:

module dut(
    input clk,
    input reset,
    input dataIn,
    output reg dataOut
);
 always_ff @(posedge clk ) begin
    if(reset)
        dataOut<=1'b1;
    else begin
        dataOut<=~dataIn;
    end
 end   
endmodule

    我们对复位信号的驱动采用两种不同的形式来进行对比测试:

    首先采用“非阻塞”赋值的方式:

41a15f87b307ad421a7ca99102413c1e.jpg

    其仿真波形如下图所示:

01e70827746afbe05a28ba5bcb847038.jpg

   可以看到,从波形上来看,dataOut的变化比reset拉低完了一个时钟周期,也就意味着reset信号的拉低是发生在时钟跳变沿产生之后的0+时刻,在该时钟沿采样时dataOut仍被处于复位状态。

    再来看下采用“阻塞”赋值的方式:

d9750feccc0eb5b14eace2847f51770f.jpg

    其仿真波形如下:

ebed39261037c4954912048ec5a1fd98.jpg

    而在这里,从波形上来看,dataOut在reset跳变的同一时钟沿发生了变化,那么也就意味着在时钟沿到来的0-时刻,reset发生了变化,故而dataOut在该时钟沿也发生了跳变。

》写在最后

    关于SystemVerilog/Verilog中关于仿真时隙的划分还是蛮详细的,自己基本也是看一遍忘一遍,背后的实现机制非个人所擅长,只是照本宣科使用,感兴趣的小伙伴可以自行研究。

作者:玉骐
原文链接:Spinal FPGA
微信公众号:
 title=

推荐阅读

更多SpinalHDL技术干货请关注Spinal FPGA专栏。
推荐阅读
关注数
1581
内容数
133
用SpinalHDL提升生产力
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息