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

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注