在我们使用多数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的交互