本节包含一个完整的简单设计,以演示SystemC中模块和进程的使用。

为了简单起见,它是非常低的水平 - 不是你通常在系统级设计语言期望的编码风格!

所展示的要点是

  • 创建层次结构
  • 所述sc_signal原始信道
  • (专门)端口
  • 进程(SC_METHOD,SC_THREAD,SC_CTHREAD)
  • 一个简单的测试台(test bench)

SystemC背景

为什么要看模块和进程?原因在于SystemC的目的是要处理硬件和软件,并允许大型系统建模。

进程是与其他进程同时运行的小块代码。几乎所有开发的高级系统级设计(SLD)工具都使用了一个流程网络的基础模型。SystemC提供进程来支持构建独立(并行/并行)代码段的网络。

SLD需要处理大型设计。为了应付这种情况,通常使用层次结构。在SystemC中通过使用模块实现层次结构,该模块可以使用端口链接到其他模块。模块允许一个设计单独工作。模块可能包含进程和其他模块的实例。

示例设计

该设计由一个用四个与非门(NAND)实现的异或(EXOR)门组成。此外,重要的是要注意,这不是一个典型的设计风格 - 但它是好的,简单的理解。设计看起来像这样


第一步是模拟NAND门。与非门是一个组合电路; 其输出纯粹是输入值的函数。它没有记忆,也不需要时钟。正因为如此,该模型可以使用最简单的一种SystemC进程,SC_METHOD。

SC_METHOD只是C ++函数。因此,SystemC类库必须使它们像进程一样工作。尤其是

  • SystemC类库包含一个模拟内核 - 一段代码,用于模拟时间的流逝,并调用函数来计算输ru发生变化时的输出。
  • 该函数必须声明为SC_METHOD,并对其输入敏感。

这是NAND门的代码,在一个文件nand.h中

#include“systemc.h”
SC_MODULE(nand2)//声明nand2 sc_module
{
  sc_in <bool> A,B; //输入信号端口
  sc_out <bool> F; //输出信号端口

  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 <sc_signal_in_if <bool>,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_in <bool> A,B;//端口被声明
  sc_out <bool> F;	//允许重复使用名称A,B 和F,因为这是层次结构的不同层

  nand2 n1,n2,n3,n4;	//声明nand2的四个实例

  sc_signal <bool> S1,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实例的构造函数。

最后,端口连接起来。端口连接在所示的构造函数内完成。

试验台(Test Bench)

为了测试设计,有一个刺激发生器(stimulus generator)。这是另一个模块(module),与上面非常相似。唯一重要的一点是它使用了一个线程(SC_THREAD),一种可以暂停的进程。这里是stim.h的代码

#include“systemc.h”
SC_MODULE(stim)
{
  sc_out <bool> A,B;	//输出端口!!
  sc_in <bool> Clk;	//定义一个时钟输入信号。(事实上有typedef sc_in<bool> sc_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_signal <bool> ASig,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是怎么回事呢??

仿真(Simulation)

SystemC库包含一个仿真内核。这决定了哪些进程(软件线程)运行。在0时刻,所有的SC_METHOD和SC_THREAD将以未定义的顺序运行,直到它们挂起。然后,SC_CTHREAD将在时钟边沿触发时运行。

上面的问题是由于环境的组合

  • sc_clock声明语句在时间0 引起一个上升沿,(有时钟边沿触发)因此监视器进程和刺激发生进程都将开始运行(以未定义的顺序,它不知道谁先运行)
  • C ++中的变量并不总是有一个定义的初始值(除非它们被声明为静态的)。所以F所持有的数据值恰巧是从1(真)开始
  • do_nand2 SC_METHOD 在时间0 运行,并且计划使F更新,但F是一个信号,它不能立即更新,因此当监视进程运行时,值1仍然存在!

为了证明这种情况,可以修改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。