所展示的要点是
为什么要看模块和进程?原因在于SystemC的目的是要处理硬件和软件,并允许大型系统建模。
进程是与其他进程同时运行的小块代码。几乎所有开发的高级系统级设计(SLD)工具都使用了一个流程网络的基础模型。SystemC提供进程来支持构建独立(并行/并行)代码段的网络。
SLD需要处理大型设计。为了应付这种情况,通常使用层次结构。在SystemC中通过使用模块实现层次结构,该模块可以使用端口链接到其他模块。模块允许一个设计单独工作。模块可能包含进程和其他模块的实例。
该设计由一个用四个与非门(NAND)实现的异或(EXOR)门组成。此外,重要的是要注意,这不是一个典型的设计风格 - 但它是好的,简单的理解。设计看起来像这样
第一步是模拟NAND门。与非门是一个组合电路; 其输出纯粹是输入值的函数。它没有记忆,也不需要时钟。正因为如此,该模型可以使用最简单的一种SystemC进程,SC_METHOD。
SC_METHOD只是C ++函数。因此,SystemC类库必须使它们像进程一样工作。尤其是
这是NAND门的代码,在一个文件nand.h中
#include“systemc.h” SC_MODULE(nand2)//声明nand2 sc_module { sc_inA,B; //输入信号端口 sc_outF; //输出信号端口 void do_nand2()//一个C ++函数 { F.write(!(A.read()&& B.read())); } SC_CTOR(nand2)// nand2的构造函数 { SC_METHOD(do_nand2); //用内核注册do_nand2函数作 进程 sensitive << A << B ; //敏感表 } };
SystemC中的层次结构是使用类sc_module创建的。可以直接使用sc_module,也可以使用宏SC_MODULE “隐藏” 。上面的例子SC_MODULE创建一个名为nand2的sc_module类对象。
接下来是声明的输入和输出端口。通常,使用类sc_port声明一个端口。例如,使用sc_signal的输入端口将被声明
sc_port ,1> A,B;
但正如你所看到的,这是很多打字。为了方便起见,也可以创建和使用专门的端口。sc_in是sc_signal类的专用端口的示例。
这些端口可以是任何C ++或SystemC类型的,例如使用bool(一种内置的C ++类型)。
接下来,声明工作函数。输入和输出(专用)端口包括方法read()和write()以允许读写端口。读取A和B,NAND功能计算,并使用write()方法将结果写入F.
请注意,在不使用read()和write()方法的情况下,您经常可以离开,因为=运算符和类型转换运算符已被重载。所以你可以写
F =!(A && B);
但是使用read()和write()是一个好习惯,因为它可以帮助C ++编译器消除表达式的歧义。
在函数do_nand2()被写入之后,有一个sc_module来实例nand2的构造函数。SystemC使用宏SC_CTOR提供了一个简单的方法。构造函数执行以下操作
也可以在这里初始化任何需要初始化的东西 - 例如,类数据成员可以被初始化。
在上面的例子中,构造函数声明do_nand2是一个SC_METHOD,并且说端口A和B上的任何事件都必须使内核运行该函数(从而为F计算一个新的值)。
“异或”门由“与非”门的四个副本(或实例)构成。这是通过使用EXOR门构造函数来连接NAND门实例来实现的。这是EXOR门的代码
#include“systemc.h” #include“nand2.h” SC_MODULE(EXOR2) { //模块exor2被创建 sc_inA,B;//端口被声明 sc_outF; //允许重复使用名称A,B 和F,因为这是层次结构的不同层 nand2 n1,n2,n3,n4; //声明nand2的四个实例 sc_signalS1,S2,S3; //sc_signal是一个原始通道,一个带有模板参数的类,该参数指定了信号可以容纳的数据类型 SC_CTOR(exor2):n1(“N1”),n2(“N2”),n3(“N3”),n4(“N4”)//初始化列表 { n1.A(A);//构造函数内 端口连接 n1.B(B); n1.F(S1); N2.A(A); n2.B(S1); n2.F(S2); N3.A(S1); N3.B(B); n3.F(S3); n4.A(S2); n4.B(S3); n4.F(F); } };
开始看起来非常类似于NAND门,但是请注意,它包含文件nand2.h。这允许访问包含NAND门的模块。
模块exor2被创建,端口被声明。请注意,允许重复使用名称A,B 和F,因为这是层次结构的不同层。
原图显示了一些连接NAND门的“线”。这些通过声明创建 sc_signal信号S1, S2和S3。 sc_signal是一个带有模板参数的类,该参数指定了信号可以容纳的数据类型 - 在本例中为bool。sc_signal是一个原始通道的例子,是SystemC类库中的一个内置通道。它在VHDL中表现得像一个信号。
EXOR门的构造函数比NAND门更复杂,因为它必须有四个nand2实例。在端口声明之后,声明nand2的四个实例:n1,n2,n3和n4。必须给每个实例一个标签。通过在exor2的构造函数中使用初始化列表,将四个标签“N1”,“N2”,“N3”和“N4”传递给nand2实例的构造函数。
最后,端口连接起来。端口连接在所示的构造函数内完成。
为了测试设计,有一个刺激发生器(stimulus generator)。这是另一个模块(module),与上面非常相似。唯一重要的一点是它使用了一个线程(SC_THREAD),一种可以暂停的进程。这里是stim.h的代码
#include“systemc.h” SC_MODULE(stim) { sc_outA,B; // 输出端口!! sc_inClk; //定义一个时钟输入信号。(事实上有typedef sc_insc_in_clk; )时钟是sc_clock类的特殊对象 void StimGen() { A.write(false); B.write(false); wait();//线程进程使用wait()挂起,当敏感表中有事件发生,线程进程被重新激活运行到遇到新的wait()语句再重新挂起。 A.write(false);//在一次仿真中,线程进程一旦退出,将不能再次进入。 B.write(true); wait(); A.write(true); B.write(false); wait(); A.write(true); B.write(true); wait(); sc_stop(); //仿真停止 } SC_CTOR(stim) { SC_THREAD(StimGen); //内核注册StimGen()函数作 线程 sensitive << Clk.pos() ; //为线程声明 敏感表。 //时钟Clk的每一个上升沿,StimGen()函数被激活一次,当执行到wait()函数,则重新被挂起,直到下一个Clk的上升沿到来,StimGen()重新被激活, //并开始接着执行wait()后面接下来的代码,这里wait()的作用是将进程挂起,等待下一次被激活. } };
请注意sc_stop()的最终调用,它会使仿真停止。监视器代码看起来非常相似,并且被省略了 - 它在一个文件mon.h中。
下面这是最高层 --- 它位于包含上述所有子模块的main.cpp文件中
#include“systemc.h” #include“stim.h” #include“exor2.h” #include“mon.h” //包含模块的头文件 int sc_main(int argc,char * argv []) { sc_signalASig,BSig,FSig; //声明 连线的顶层信号以及 使用sc_clock创建的时钟(sc_clock声明语句时钟初始逻辑值持续时间为0时将在时间0触发一个上升沿) sc_clock TestClk(“TestClock”,10,SC_NS,0.5);//sc_clock有5个重载的构造函数.(时钟名,时钟周期,时间单位,占空比,时钟初始逻辑值持续,时间,首个逻辑值) //每个模块被实例化和连接 stim Stim1(“Stimulus”);//刺激发生器(相当于信号电源?) 模块实例化 Stim1.A(ASIG); Stim1.B(BSIG); Stim1.Clk(TestClk); exor2 DUT(“exor2”);//异或门 模块实例化 DUT.A(ASIG); DUT.B(BSIG); DUT.F(FSig); mon Monitor1(“Monitor”);//监视器 模块实例化 Monitor1.A(ASIG); Monitor1.B(BSIG); Monitor1.F(FSig); Monitor1.Clk(TestClk); sc_start(); //调用sc_start()开始仿真 并永久运行(或者直到它遇到在刺激模块中对sc_stop()的调用) return 0; }
包含模块的头文件,声明连线的顶层信号以及使用sc_clock创建的时钟; 然后每个模块被实例化和连接。
之后调用sc_start()开始仿真,并永久运行(或者直到它遇到在刺激模块中对sc_stop()的调用)。
这里是这个例子的输出
时间 A B F 0 s 0 0 1 10 ns 0 0 0 20 ns 0 1 1 30 ns 1 0 1 40 ns 1 1 0
如果你仔细看看,你会发现一些非常奇怪的东西 - 第一行0时表示F是1(真),而A和B是0 - 不是一个非常令人信服的EXOR门!到10纳秒后,一切才都是正常的。在时间0是怎么回事呢??
SystemC库包含一个仿真内核。这决定了哪些进程(软件线程)运行。在0时刻,所有的SC_METHOD和SC_THREAD将以未定义的顺序运行,直到它们挂起。然后,SC_CTHREAD将在时钟边沿触发时运行。
上面的问题是由于环境的组合
为了证明这种情况,可以修改sc_clock声明语句来延迟第一个时钟边沿,如下所示
sc_clock TestClk(“TestClock”,10,SC_NS,0.5,1,SC_NS);
最后的1,SC_NS实参 指定在第一个时钟沿 出现之前的1 ns延迟。现在时间已经过去了,所以F会被更新。下面这是相应的输出
时间 A B F 1 ns 0 0 0 11 ns 0 0 0 21 ns 0 1 1 31 ns 1 0 1 41 ns 1 1 0
现在你可以看到F总是正确的。
结束对模块和过程的快速浏览。你已经看到了理解SystemC模拟内核的并发特性以及sc_signal原始通道的行为的重要性。
您还看到了顶层模块中实例化较低层模块的一些基本示例,以及如何使用sc_main。