路科验证的个人空间 https://blog.eetop.cn/1561828 [收藏] [复制] [分享] [RSS]

空间首页 动态 记录 日志 相册 主题 分享 留言板 个人资料

日志

SV与UVM接口应用篇之六:开辟后台C服务线程

已有 1363 次阅读| 2019-11-14 17:58 |个人分类:验证系统思想|系统分类:芯片设计

​在我们使用多数DPI的场景中,SV调用C一侧的函数多数情况下会立即或者在有限的时间内返回,而这对于SV一侧是可以“忍受”的。例如SV调用C算法模型函数,只要能够在一定时间内返回运算结果,我们可以允许SV等待C的函数线程调用结束再返回。然而,在个别的情况下,我们会需要在后台开辟C线程,让它作为服务程序做阻塞服务,例如通过socket接收数据,只不过阻塞的C函数调用对于SV而言,那就是一场噩梦。为了说明这种阻塞的情况,我们可以对C函数加以简化:


void slowReturn(void *arg) {

  int t = *(int *)arg;

  printf("slowReturn started sleep %d seconds ", t);

  sleep(t);

  printf("slowReturn finished sleep %d seconds ", t);

}



这个函数如果被SV通过DPI导入,并且加以调用的话,就变成了以下的形式。


`timescale 1ns/1ps

module swbox;

  import "DPI-C" context task slowReturn(inout int t);

  initial begin: thread1

    int t = 10;

    $display("thread1 called slowReturn() at time %t", $time);

    slowReturn(t);

    $display("thread1 finished slowReturn() at time %t", $time);

    #1ns;

    $display("thread1 exited at time %0t", $time);

  end

endmodule


这段代码的输出结果也很有意思。从仿真结果我们可以看到,我们在调用C函数的时候给传递了需要等待的参数,即函数需要在10秒以后才能够返回,然而仿真时间并没有向前运行!这就需要澄清两个不同的时间度量,即仿真时间和物理时间(现实时间)。C函数的调用和运行,从我们仿真执行的感受来看,它确实经过了10秒钟的物理时间,然而作为C函数的进入和退出来看,它并不能够对仿真带来额外的延迟。这也说明了,仿真是为了模拟硬件时间,而软件世界中的执行状态不会对仿真带来任何影响。


仿真运行结果:

thread1 called slowReturn() attime 0

slowReturn started sleep10 seconds 

slowReturn finished sleep10 seconds 

thread1 finished slowReturn() attime 0

thread1 exited at time 1

Simulation complete, time is 1 ns.


我们为了模拟C函数的阻塞行为,例如长时间的等待socket传入的数据,可以将10秒钟的参数再次放大到100或者更大,那么可想而知,C函数的阻塞延迟将会严重影响仿真效率(而不是仿真时间),更糟糕的是,如果阻塞一直持续下去,那么SV将无法继续执行接下来的程序。


聪慧如我的你一定想到了简单的办法,对吗?就像SV中开辟其它线程一样,使用fork-join_none,让C函数在后台去执行,不就可以了吗?于是,程序可以做以下简单的修改:


module swbox;

  import "DPI-C" context task slowReturn(inout int t);

  initial begin: thread1

    int t = 10;

    $display("thread1 called slowReturn() at time %t", $time);

   fork

      slowReturn(t);

   join_none

    $display("thread1 finished slowReturn() at time %t", $time);

    #1ns;

    $display("thread1 exited at time %0t", $time);

  end

endmodule


但似乎仿真结果并没有证明这种做法是对的,仿真器这时非常纠结——它要在我们开辟这个线程的0时刻内,必须执行完线程slowReturn(),才能够进入接下来的仿真时间。也就是说,无论在什么时间开辟的C线程,都需要在当时的仿真时间中执行完毕,然而C线程还会阻塞甚至死锁仿真进程。在这里可以看到,fork并发线程的管理方式对于C线程而言,是行不通的。


仿真运行结果:

thread1 called slowReturn() at time 0

thread1 finished slowReturn() at time 0

slowReturn started sleep 10 seconds 

slowReturn finished sleep 10 seconds 

thread1 exited at time 1

Simulation complete, time is 1 ns.


这一限制即是说,SV开辟的C线程,必须在开辟的0时刻内完成,仿真时间才可以继续执行,而这一限制使得我们不能让内置阻塞函数调用的C线程安静地在后台服务,同时不去妨碍仿真的执行。我们不得不像神农尝百草一样寻求其它的方法,例如开辟进程的C函数fork()也无法满足我们的需求,而在最后我们发现Linux多线程库pthread有能力开辟子线程。于是我们使用了pthread库按照以下的流程来开辟并且管理子线程。



我们另外声明一个非阻塞的函数quickReturn(),要求它在1秒内(或者0时刻)即返回,这保证了SV一侧thread1不会受到C的阻塞。同时,我们通过pthread_create()函数开辟新的线程slowReturn(),并使之分离在后台运行。slowReturn()函数会在10秒(甚至更久的物理时间)后返回,但这丝毫不会影响swbox::thread1线程继续执行。


`timescale 1ns/1ps

module swbox;

  bit clk;

  import "DPI-C" context task quickReturn(inout int t);

  import "DPI-C" context task slowReturn(inout int t);

  initial begin: thread1

    int t = 10;

    $display("thread1 called quickReturn() at time %0t", $time);

    quickReturn(t);

    $display("thread1 finished quickReturn() at time %0t", $time);

    #1ns;

    $display("thread1 exited at time %0t", $time);

  end

  initial forever #10ns clk = !clk;

endmodule

SV swbox.sv


#include "stdio.h"

#include "pthread.h"


void slowReturn(void *arg) {

  int t = *(int *)arg;

  printf("slowReturn started sleep %d seconds ", t);

  sleep(t);

  printf("slowReturn finished sleep %d seconds ", t);

}


void quickReturn(int *arg) {

  pthread_t tSR;

  int t = *(int *)arg;

  printf("quickReturn() got arg t = %d ", t);

  printf("quickReturn() started ");

  printf("creating thread: slowReturn() ");

  pthread_create(&tSR, NULL, (void *)&slowReturn, (void *)&t);

  printf("created thread: slowReturn() ");

  if (pthread_detach(tSR)) {

    printf("thread tSR is not detached ... ");

  }

  printf("quickReturn() started sleep 1s ");

  sleep(1);

  printf("quickReturn() finished sleep 1s ");

  printf("quickReturn() finished ");

}

C swcall.c


仿真运行结果:

thread1 called quickReturn() at time 0

quickReturn() got arg t = 10 

quickReturn() started 

creating thread: slowReturn() 

created thread: slowReturn() 

quickReturn() started sleep 1s

slowReturn started sleep 10 seconds 

quickReturn() finished sleep 1s

quickReturn() finished 

thread1 finished quickReturn() at time 0

thread1 exited at time 1

slowReturn finished sleep 10 seconds 


从示例中可以看到,我们新实现了一个C函数quickReturn(),它的作用就是用来开辟(pthread_create())线程slowReturn并且剥离它(pthread_detach())。在剥离以后,quickReturn()的使命就完成了,它可以立即返回。我们在这里要求它等待1秒钟以后返回。带quickReturn()返回以后,swbox::thread1线程继续执行接下来的代码,同时也包括其他仿真中的过程块也不会再受到C一侧没有完成的slowReturn()线程的影响。从仿真结果可以看到,slowReturn()经过了物理时间10秒钟以后会在后台结束并且回收其资源。


quickReturn()在创建线程slowReturn()时传入了参数(void *)&t,即quickReturn::t的指针,而在进入slowReturn()后,slowReturn先通过拷贝的方式获取参数值 int t = *(int *)arg,而不是做指针类型的转换 int *t = (int *)arg。这一细微的差别背后需要注意的是,做数值拷贝要更加可靠,这是因为quickReturn()在结束以后其资源也将被回收,因此slowReturn()获取的参数指针void* arg不再有效,所以我们应该先拷贝传入指针所指向地址的数值,而不是继续利用该指针参数。为了使得开辟的线程结束以后其资源能够被自动回收,建议使用pthread_detach()。


从这个简单的抽象模型中我们可以在以后的工作中,由quickReturn()承担非阻塞的开辟线程任务,而由slowReturn()承担阻塞任务在后台保持持续服务。在slowReturn()持续服务的过程中,还可能需要将服务过程中的结果汇报给SV一侧,这就需要考虑如何返回运算结果。需要注意的是,由于quickReturn()此时已经结束,那么slowReturn()无法再使用quickReturn()的资源作为中转,再继续返回给swbox::thread1,简单而言,就是swbox::thread1无法通过参数的形式再去获取slowReturn()的返回值(由于quickReturn()已经结束),这里我们建议使用SV DPI-C export的形式,由C来调用SV的函数,继而利用DPI完成返回值的传递。



`timescale 1ns/1ps

module swbox;

  bit clk;

  int retval;

  import "DPI-C" context task quickReturn(inout int t);

  import "DPI-C" context task slowReturn(inout int t);

  export "DPI-C" function qrReturn;

  function void qrReturn(input int v);

    retval = v;

  endfunction

  initial begin: thread1

    int t = 10;

    $display("thread1 called quickReturn() at time %0t", $time);

    quickReturn(t);

    $display("thread1 finished quickReturn() at time %0t", $time);

    wait(retval > 1);

    $display("thread1 got slowReturn() return value %0d", retval);

    $display("thread1 exited time %0t", $time);

    $finish();

  end

  initial forever #10ns clk = !clk;

endmodule

SV swbox.sv


#include "stdio.h"

#include "pthread.h"

#include "svdpi.h"

extern void qrReturn(int v);

void slowReturn(void *arg) {

  int t = *(int *)arg;

  printf("slowReturn started sleep %d seconds ", t);

  sleep(t);

  printf("slowReturn finished sleep %d seconds ", t);

  svSetScope(svGetScopeFromName("swbox"));

  qrReturn(t);

}

void quickReturn(int *arg) {

  ...

}

C swcall.c


仿真运行结果:

quickReturn() got arg t = 10 

quickReturn() started 

creating thread: slowReturn() 

created thread: slowReturn() 

quickReturn() started sleep 1s

slowReturn started sleep 10 seconds 

quickReturn() finished sleep 1s

quickReturn() finished 

thread1 finished quickReturn() at time 0

slowReturn finished sleep 10 seconds 

thread1 got slowReturn()return value 10

thread1 exited time 1918917250

$finish at simulation time        1918917250

Simulation complete, time is 1918917250 ns.


从更新后的示例可以看到,C一侧隔离的线程slowReturn()由于无法直接返回值给SV,而需要间接通过DPI-C export函数qrReturn()来完成,但是在调用的时候,由于还需要通过svdpi.h的函数svSetScope()来指明qrReturn()在SV哪个域(scope)中定义。这一额外的步骤是由于slowReturn()线程并不是直接由SV创建的,无法直接获取SV的域,也无法确定qrReturn()来自于哪个域,因此还需要显式指定该域。


通过这种方式,我们可以使得SV可以开辟在后台保持响应的C线程而不影响仿真的执行,再利用DPI-C export函数由C一侧来返回数值,使得SV和C能够保持通信。这个原型将会让SV调用C的方式变得有更多的可能,我们也将在稍后的方法学创新中,利用这一手段来提高仿真性能。


SV及UVM接口应用篇之一:DPI接口和C测试(上)

SV及UVM接口应用篇之二:DPI接口和C测试(下)

SV及UVM接口应用篇之三:SystemC与UVM的TLM通信

SV及UVM接口应用篇之四:Matlab及Simulink模型与UVM的混合仿真

SV及UVM接口应用篇之五(终):脚本语言与UVM的交互




点赞

评论 (0 个评论)

facelist

您需要登录后才可以评论 登录 | 注册

  • 关注TA
  • 加好友
  • 联系TA
  • 0

    周排名
  • 0

    月排名
  • 0

    总排名
  • 0

    关注
  • 253

    粉丝
  • 25

    好友
  • 33

    获赞
  • 45

    评论
  • 访问数
关闭

站长推荐 上一条 /2 下一条

小黑屋| 关于我们| 联系我们| 在线咨询| 隐私声明| EETOP 创芯网
( 京ICP备:10050787号 京公网安备:11010502037710 )

GMT+8, 2024-4-19 21:16 , Processed in 0.033671 second(s), 19 queries , Gzip On, Redis On.

eetop公众号 创芯大讲堂 创芯人才网
返回顶部