1、SystemC 进程
在操作系统中,进程是程序在并发环境中的执行过程,它具有动态性、并发性、独立性、异步性和结构性五大特征。
使用 C++ 可以很容易的描述系统的顺序行为,但是要描述系统的并发行为是比较困难的,一般会使用多线程和多进程的方式。为了描述硬件系统的并发行为 SystemC 也引入了进程的概念,但是与操作系统的进程和线程相比,它更简单。
在 SystemC 中,进程是一个基本的执行单位,被用来仿真目标系统的行为,SystemC 的基本进程有三种
- SC_METHOD
- SC_THREAD
- SC_CTHREAD
进程的行为是多样的,有些进程的行为像函数一样被调用后立即返回,有些进程在仿真开始以后只运行一次一直运行到仿真结束,有些进程在运行过程中可能被挂起直到某一个条件满足。引起进程运行或者解除挂起的条件可能是时钟的边沿或者信号上值的变化,SystemC 引入了上述三个进程来描述不同进程的行为
在 SystemC 中,进程不是层次化的,一个进程不能包含或者直接调用其他进程,但是进程可以调用非进程的函数和方法
进程通常会有一个敏感列表,当敏感列表中的信号有事件发生时,进程会被激活,信号上的事件指的是信号的值变化,比如时钟从下降沿变到上升沿,或者某个端口的值被赋值。当信号上的事件发生,所有对该事件敏感的进程都会被激活,在 SystemC 中我们使用 sensitive 设置敏感列表,敏感列表的设置方式为
sensitive << 敏感信号1 << 敏感信号2 << 敏感信号 N;
但是一般建议是一个进程不要设置多个敏感信号
2、方法进程 SC_METHOD
在 SystemC 中,SC_METHOD 的特点是当敏感列表上有事件发生就会被调用,调用后立刻返回,只有该类进程返回后仿真系统的事件才有可能继续前进,因此不能使用 wait 这样的语句,如果方法进程内部有一个死循环,仿真时间将会停止
下面我们看一个 SC_METHOD 的例子
SC_MODULE(Test) {
SC_CTOR(Test) {
SC_METHOD(DoTest);
sensitive << clk;
}
void DoTest() {
std::cout << ++index << " Test" << std::endl;
}
public:
sc_in_clk clk;
int index{};
};
int sc_main(int argc, char* argv[]) {
sc_clock clk("clk", 20, SC_NS);
Test test("test");
test.clk(clk);
sc_start(200, SC_NS);
return 0;
}
我们期望可以打印 10 次 Test,但是通过运行,我们可以看到打印了 21 次 Test
这是为什么呢,首先第一个原因是时钟,在前面的文章提到过,假如一个时钟周期是 20 ns,那么会有 10 ns 的上升沿和 10 ms 的下降沿,那么我们可以将代码改成只在时钟上升沿触发
sensitive << clk.pos();
在有的教程中会使用 sensitive_pos << clk 这样的方式,但是会提示 sensitive_pos << clk 已经被 sensitive << clk.pos() 替代,所以在这里还是使用 sensitive << clk.pos()
当然能让在上升沿触发,也可以让进程在时钟下降沿触发,如果是时钟下降沿触发,那么可以将代码改成这样
sensitive << clk.neg();
但是无论是把时钟改成上升沿触发,还是下降沿触发,Test 还是会打印 11 次,和我们想要的 10 次还是不一样,这里是因为在默认的情况下,所有进程在仿真的 0 时刻会被执行一次,有些时候我们希望在仿真 0 时刻不被执行,此时可以使用 done_initialize(),那么创建进程的代码就可以变成下面这样了
SC_METHOD(DoTest);
sensitive << clk.pos();
dont_initialize();
3、线程进程 SC_THREAD
线程进程能够被挂起和重新激活,线程进程使用 wait 挂起,当敏感列表中有时间发生时,或者 wait 时间超时时,线程进程会被重新激活,在一次仿真中,线程进程一旦退出,将不能再重新进入
线程进程的一个方便用途是用来描述验证平遥的输入激励和输出获取
SC_MODULE(Test) {
SC_CTOR(Test) {
SC_THREAD(DoTest);
sensitive << clk.pos();
dont_initialize();
}
void DoTest() {
while (true) {
std::cout << ++index << " Test" << std::endl;
wait();
}
}
public:
sc_in_clk clk;
int index{};
};
int sc_main(int argc, char* argv[]) {
sc_clock clk("clk", 20, SC_NS);
Test test("test");
test.clk(clk);
sc_start(200, SC_NS);
return 0;
}
或者可以可以将代码改成下面这样
SC_MODULE(Test) {
SC_CTOR(Test) {
SC_THREAD(DoTest);
}
void DoTest() {
while (true) {
std::cout << ++index << " Test" << std::endl;
wait(20, SC_NS);
}
}
public:
int index{};
};
int sc_main(int argc, char* argv[]) {
Test test("test");
sc_start(200, SC_NS);
return 0;
}
表示每次挂起 20 ns 后再重新被激活
4、钟控线程进程 SC_CTHREAD
钟控线程进程是一种特殊的线程进程,它继承于线程进程,但只能在时钟的上升沿或者下降沿被触发或者激活,这种行为更加接近实际硬件的行为,引入钟控线程进程的目的是为了产生更好的行为综合
为了仿真硬件行为,钟控线程进程被约束只能采用 wait 和 wait(int n) 两种等待形式,这里 n 是等待的时钟周期数,同时,该进程还引入了专门的复位信号说明函数
void reset_signal_is(const sc_in<bool>&, bool);
void reset_signal_is(const sc_signal<bool>&, bool);
它们只能在钟控线程进程中使用
钟控线程进程最适合来描述隐式有限状态机,所谓隐式有限状态机是指编程中并不显示的定义状态机的状态,而是通过程序中的 wait 语句和 wait 语句中间的赋值语句来完成对状态机的描述,显示的有限状态机通常要明确定义系统的状态,使用 case 语句来实现状态转移,修改上面的例子,让每两个时钟上升沿才打印一次 Test
SC_MODULE(Test) {
SC_CTOR(Test) {
SC_CTHREAD(DoTest, clk.pos());
}
void DoTest() {
while (true) {
std::cout << ++index << " Test" << std::endl;
wait(2);
}
}
public:
int index{};
sc_in_clk clk;
};
int sc_main(int argc, char* argv[]) {
sc_clock clk("clk", 20, SC_NS);
Test test("test");
test.clk(clk);
sc_start(200, SC_NS);
return 0;
}
接下来可以看一个带复位的例子
SC_MODULE(Test) {
SC_CTOR(Test) {
SC_CTHREAD(DoTest, clk.pos());
// 只要当 val = true 就执行复位
reset_signal_is(val, true);
}
void DoTest() {
int index = 0;
std::cout << "cthread start" << std::endl;
while (true) {
std::cout << ++index << " Test" << std::endl;
wait(1);
}
}
public:
// int index{};
sc_in_clk clk;
sc_in<bool> val;
};
SC_MODULE(Driver) {
SC_CTOR(Driver) {
SC_METHOD(GenData);
sensitive << clk.pos();
dont_initialize();
}
void GenData() {
index_++;
if (index_ % 2 == 0) {
std::cout << "reset: " << index_ << std::endl;
valid = !valid;
val.write(valid);
}
}
public:
sc_in_clk clk;
sc_out<bool> val;
int index_{};
bool valid{};
};
int sc_main(int argc, char* argv[]) {
sc_clock clk("clk", 20, SC_NS);
sc_signal<bool> val;
Test test("test");
Driver driver("driver");
test.clk(clk);
test.val(val);
driver.clk(clk);
driver.val(val);
sc_start(200, SC_NS);
return 0;
}
wait 和 next_trigger
wait() 只能用于线程进程和钟控线程进程,其作用是将进程挂起等待下一个事件发生重新激活被挂起的进程,wait 有以下几种参数形式
-
wait() 等待敏感列表有事件发生
-
wait(const sc_event&) 等待特定的事件发生
-
wait(sc_event_or_list&) 等待指定的其中一个事件发生
-
wait(sc_event_and_list&) 等待指定的所有事件发生
-
wait(double v, sc_time_uint tu) 等待指定的时间
-
wait(double v, sc_time_uint tu, sc_event& e) 如果在指定的时间内有指定事件发生,那么进程被重新激活,否则在超时后被重新激活
-
wait(double v, sc_time_uint tu, sc_event_or_list& e) 如果在指定的时间内有指定的某一个事件发生,那么进程被重新激活,否则在超时后被重新激活
-
wait(double v, sc_time_uint tu, sc_event_and_list& e) 如果在指定的时间内有指定的全部事件发生,那么进程被重新激活,否则在超时后被重新激活
next_trigger 只能用于 SC_METHOD 进程,next_trigger 的参数与 wait 相同,区别是用于不同的进程,SC_METHOD 进程不能挂起,next_trigger 只是为 SC_METHOD 进程增加一个动态敏感事件而已
SC_METHOD 和 SC_CTHREAD 进程必须要有敏感列表,而 SC_THREAD 进程可以不需要
dont_initialize 只对 SC_THREAD 和 SC_METHOD 有效,而 CTHREAD 在仿真 0 时刻总是开始执行
5、动态创建进程
SC_METHOD、SC_THREAD、SC_CTHREAD 都是静态进程,从 SystemC 2.1 开始引入了动态进程。动态进程创建允许在同一个函数中自动创建和分配多个进程,用于临时断言检查、处理临时性的并发事件等
sc_spawn()
函数 sc_spawn 主要用于创建动态进程,函数原型如下
template <typename T>
inline sc_process_handle sc_spawn(T object, const char* name_p = 0,
const sc_spawn_options* opt_p = 0);
template <typename T>
inline sc_process_handle sc_spawn(typename T::result_type* r_p, T object,
const char* name_p = 0,
const sc_spawn_options* opt_p = 0);
参数 T object 是唯一必须指定的 sc_spawn 参数,它可以是一个函数指针,可以是由 sc_bind() 指定的模块的方法,也可以是一个函数对象
参数 T::result_type 是一个指向某一个存储区的指针,用于从动态创建的进程中返回值
参数 const char* name_p 是新创建的动态进程的名字
sc_spawn_options* opt_p 是创建进程所包含的一些属性信息,比如敏感列表
sc_spawn_options
类 sc_spawn_options 用于创建作为 sc_spawn 实参的一个对象,类 sc_spawn_options 包括很多方法用来修改 sc_spawn_options 的属性,这些属性在将 sc+spawn_options 传给 sc_spawn() 创建动态进程前进行修改才有效
sc_spawn_options 主要成员函数有以下这些
void dont_initialize();
void set_sensitivity(const sc_event* event);
void set_sensitivity(sc_port_base* port_base);
void set_sensitivity(sc_interface* interface_p);
void set_sensitivity(sc_export_base* export_base);
void set_sensitivity(sc_event_finder* event_finder);
void spawn_method();
void set_stack_size(int stack_size);
dont_initialize 表示进程在初始化阶段不运行
set_sensitivity 设置敏感列表
spawn_method 使得新创建的一个进程是方法进程,而缺省的情况下,新创建的一个进程是一个线程进程
set_stack_size 用于设置进程的栈大小
下面给出一个使用 sc_spawn 创建进程的例子
class Test : public sc_module {
public:
SC_HAS_PROCESS(Test);
Test(sc_module_name ins_name) : sc_module(ins_name) {
}
void CreateProcess() {
sc_spawn_options opts;
opts.dont_initialize();
opts.spawn_method();
opts.set_sensitivity(&clk.pos());
auto lmd = [&]() -> void {
std::cout << sc_time_stamp() << " Print" << std::endl;
};
// sc_core::sc_spawn(lmd, "sp1", &opts);
sc_core::sc_spawn(sc_bind(&Test::Print, this), "sp1", &opts);
}
void Print() {
std::cout << sc_time_stamp() << " Print" << std::endl;
}
public:
sc_in_clk clk;
};
int sc_main(int argc, char* argv[]) {
sc_clock clk("clk", 20, SC_NS);
Test test("test");
test.clk(clk);
test.CreateProcess();
sc_start(200, SC_NS);
return 0;
}
SC_FORK 和 SC_JOIN
SC_FORK 和 SC_JOIN 用于同步动态创建的多个线程进程,其基本语法是
SC_FORK
sc_spawn(),
sc_spawn(),
...
SC_JOIN
不同的动态进程用 "," 分开,SC_FORK 和 SC_JOIN 所同时创建的所有进程同时运行,当所有的进程退出时,SC_JOIN 后面的语句才得以执行