一、概述
对于复杂的片上系统,在进行RTL编码前,需进行深入的系统级仿真,以确认设计的体系结构是否恰当、总线是否能满足吞吐量和实现性要求以及存储器是否浪费,所进行的这些仿真要求在芯片的仿真模型上运行大量的软件,以覆盖所需的功能1。
SystemC的特征
SystemC是C++的标准库
为什么要用SystemC取代C++进行系统级建模?
原始的C++模型程序必须手工转换为Verilog/VHDL,而SystemC可以用于描述不同的抽象级别(系统级、寄存器传输级等)
SystemC官网
SystemC的开发流程
环境安装
cd /home/user/dir
./configure -prefix=INSTALL_DIR #用-prefix=指定要安装到的目录路径,此路径必须已经存在;
make
make install
使用流程
- SystemC源代码可以使用任何标准C++编译环境进行编译,生成可执行文件
- 可以生成VCD格式的波形文件,可以用simvision/dve等工具查看波形
- 使用综合工具(如Cynthesizer)可以将SystemC的RTL级描述综合为Verilog代码,也可以使用体系结构综合工具(如ESLFlex)总和为包括软件和硬件的片上系统。
SystemC的建模精度
周期精确模型
仿真速度很慢,什么功能都可以模拟,但一般适合做硬件RTL验证。
非定时模型
仿真速度很快,但是包含信息较少,适合定义设计规范,也可以结合先进的综合工作用来做软件开发。
松散定时模型
松散定时模型具有时间解耦特性,能极大地提高仿真速度,这是因为每个进程都可以运行一个时间片之后切换到下一个进程,从而模型的一部分可以在当前仿真时间之前进行。
既可以做软软件开发和软件性能评估,也可以做体系结构分析和硬件验证
近似定时模型
通过非阻塞传送接口支持,主要用于体系结构探索和性能分析。
在近似定时模型中,一个事务被分为多个相位,由不同的时间点进行分割,一个非阻塞传送中存在多个定时点,不同传送相位可以分别用时间标注。在基础协议中,基本的时间点包括请求的开始和结束、应答的开始和结束。特定的协议可以包括更多的时间点,但可能导致与通用净核不兼容。
每个进程根据SystemC调度器的时间进度执行,使用两种延迟目标延迟和发起延迟(即目标接收延迟),延迟一般使用wait(delay)和notify(delay)实现。
相关资源
VCML
用SystemC描述的SoC组件模型库。
vcml GitHub
or1kmvp
是基于OpenRISC1000的多核虚拟平台,能够运行Linux的交互界面。
or1kmvp GitHub
NVDLA
Nvidia用VCML构建的深度学习加速器
vcml-nvdla GitHub
NVDLA Quick Start
Paranut
使用SystemC搭建的risc-v架构的SoC系统,没有使用TLM。
Paranut GitHub
二、语法
模块定义
SC_MODULE
SystemC库种定义的一个宏,用来定义一个新的C++结构体,类似硬件模块
SC_MODULE(sram8x256)
{
.....
}
SC_CTOR
构造函数,除了完成C++种所要求的基本功能外,构造函数还用于初始化进程的类型并创建进程的敏感表。
SC_CTOR(sram8x256)
{
......
}
端口和信号
专有数据类型
sc_int/sc_uint/sc_bigint/sc_biguint/
sc_bit:2值单bit数据类型
sc_logic:4值单bit数据类型
用户自定义数据结构类型
typedef struct _frame{
......
} frane;
抽象端口
class direct_if
:public virtual sc_interface
{
.....
}
sc_in/sc_out/sc_inout/sc_signal
端口类型,端口使用的数据类型可以时C++的数据类型,也可以是SystemC专用数据类型。
sc_in_clk clk; //端口定义,特殊情况,时钟定义
sc_in<sc_uint<8>> wr_data; //端口定义
sc_signal<sc_logic> addr[16]; // 信号定义
read()/write()
通过read()和write()函数对信号读取和赋值
addr_o.write(addr_i.read()); //将addr_i的值赋给addr_o的值
敏感表
sensitive
sensitive为SC_METHOD和SC_THREAD进程设置敏感表
SC_METHOD(main);
sensitive<<clk_i.pos()<<rst_i.pos();
静态进程创建和调用
SystemC中进程是一个基本执行单位,被调用来仿真目标系统的行为。
在SystemC中,进程不是层次化的,一个进程中不能包含或者直接调用其他进程,但进程可以调用非进程的函数和方法。
SC_METHOD
使用该进程调用方法,当敏感表有事件发生,该进程就会被调用。只有该类进程返回后仿真系统的事件才有可能前进,因此该类进程中不能使用wait()这样的语句。
SC_METHOD进程的敏感表在模块的构造函数内设定。
SC_THREAD
线程进程,该调用方式下线程能够被挂起和重新激活。线程进程使用wait()挂起,当敏感表中有时间发生,线程进程被重新激活运行到遇到新的wait()语句再重新挂起。当该进程一旦推出,将不能再次进入。
可以用来描述testbench的输入激励和输出捕获
SC_CTHREAD
钟控线程进程,继承于线程进程,只能再时钟的posedge或negaedge被触发或者激活,更接近于实际硬件的行为。
时序控制
wait()
用于SC_THREAD和SC_CTHREAD。用于将进程挂起等待下一个事件发生重新激活被挂起的进程。
wait(); //等待敏感表中有事件发生
wait(const sc_event& ); // 等待事件发生
wait(100, SC_NS); // 进程将被挂起100ns后激活
wait(100, SC_NS, e1); // 如果100ns内有事件e1发生,或者时间超过了100ns,进程将被激活
三、常用用法
仿真流程
sc_main()
sc_main是将设计中所有模块连接在一起的顶层函数,并引入时钟和波形跟踪。
int sc_main(int argc, char * argv[])
{
......
}
sc_start()
控制所有时钟的产生并在适当的时刻激活SystemC调度器。调度器控制整个仿真过程中的调度工作,包括激活进程,产生延迟、计算和更新变量和信号的值。
sc_start()只在sc_main()中调用。
sc_start(); // 没有参数,表示仿真一直进行直到遇到sc_stop()函数。
sc_start(1000); // 仿真持续1000个时间单位停止或者期间遇到sc_stop()停止
波形跟踪
SystemC可以将仿真结果保存为VCD格式
- 只有在整个仿真期间都存在的信号和变量才能被追踪,及模块中的信号和数据成员能被跟踪,函数的本地变量只有在被调用是才存在,所以不能跟踪
- 任何类型的信号和变量都可以被跟踪
int sc_main(int, char **)
{
sc_in<int> datain;
sc_trace_file * my_trace_file;
my_trace_file = sc_create_vcd_trace_file("Wave"); // 产生一个Wave.vcd文件
......
sc_trace(my_trace_file, datain, "DataIn"); // 跟踪信号datain,datain在波形文件中被保存为DataIn
......
sc_close_vcd_trace_file(my_trace_file); // 关闭打开的波形文件
......
return 0;
}
信息打印
SystemC定义了几种打印等级,INFO和WARNING可直接打开,ERROR类型由SystemC仿真器抛出异常,用户定义的异常处理代码去处理,对于FATAL,则停止仿真。
SC_REPORT_INFO( msg_type, msg);
SC_REPORT_WARNING( msg_type, msg);
SC_REPORT_ERROR( msg_type, msg);
SC_REPORT_FATAL( msg_type, msg);
sc_asserrt( expr ); // 打印的等级严重程度为FATAL
set_actions
修改打印等级
时钟相位关系
sc_set_time_resolution()
sc_set_default_time_unit()
四、事务处理级建模(TLM)
事务指两个时间点内发生的不可分割的活动。可以是一次总线读或写事务
接口、端口、通道、模块间的关系
为了支持TLM,systemc中定义了接口(interface)、端口(port)和通道(channel),三者和模块的关系如图1所示。
接口
接口是C++抽象类,即该类中只定义一组抽象方法,但不定义具体实现,这些方法都以纯虚函数的方式实现。
sc_interface是所有接口类的父类。
端口
端口总是与一定的接口类型相关联,端口只能连接到实现了该类接口的通道上。模块中的进程可以通过端口使用通道提供的方法,且只能通过端口访问通道,
通道
1个通道可以继承一个或多个接口,接口定义的抽象方法在通道中实现,但通道自己却不能调用。通道分为基本通道和分层通道。
例如两个模块source和sink通过FIFO通信,可以定义一个FIFO接口,并分为读接口和写接口,代码如下:
class fifo_write_if:public sc_interface{
bool fifo_write(T data)=0;
...
}
class fifo_read_if:public sc_interface{
bool fifo_read(T& data)=0;
...
}
SC_MODULE(Source) { //source模块中调用FIFO
sc_port<fifo<int>> fifo_write_port;
.....
fifo_write_port->fifo_write(data);
......
}
SC_MODULE(Top) {
fifo<int, DEPTH> fifo1;
Source Source1;
SC_CTOR(Top) {
Source1.fifo_write_port(fifo1);
......
}
}
基本通道
基本通道不包含任何进程,也不对外展现任何可见结构,也不能调用其他基本通道。
- sc_fifo
- sc_signal
- sc_signal_rv
- sc_buffer
- sc_sephmore
- sc_mutex
sc_mutex
互斥通道
sc_mutex protect;
......
protect.lock();
......
protect.unlock();
sc_fifo < T >
已实现好的FIFO通道,T指存储的数据类型。
sc_fifo<packet> fifo2(4);
sc_semaphore
信号量,限制同时使用某共享资源的进程的数量。
分层通道
分层通道本身是一个模块,实现了一个或多个接口,当然也可以调用包括基本通道在内的其他通道。相比基本通道,分层通道包含进程,可以直接操作其他通道。
分层通道能够建模复杂的硬件模块,例如片上总线(AMBA、OPB总线)。当通道的结构层次很明显,或者通道中包含进程时,一般会使用分层通道
动态进程创建和调用
动态进程创建允许在同一个函数方法中自动船舰和分配多个进程,用于临时断言检查、处理临时性的并发事件。
- sc_spawn(…)
- sc_spawn_options
- SC_FORK
- SC_JOIN
- sc_process_handle::wait()
- sc_bind
- sc_ref
- sc_cref
五、TLM2.0
TLM2.0是专门为建模存储器映射的片上总线而设计的SystemC模型库。
事务对象是一个C++类,TLM2.0预定义了一个通用净核类和相应的基础协议,以进一步保障不同提供商提供的模型互联互通。
核心接口
事务发起者和目标模块必须遵守的接口标准,包含四种:阻塞、非阻塞传送接口、DMI和调试传送接口
阻塞传送接口
支持松散时间模型。发起者通过调用一个函数就可以完成一个非阻塞事务处理。
class ...
{
public:
virtual void b_transport(TRANS& trans, sc_core::sc_time& t)=0
}
b_transport(TRANS& trans, sc_core::sc_time& t)是欸阻塞事务处理接口的方法。trans是事务,t是双向时间参数。当发起者通过b_transport访问目标时,t为事务处理发起的时间;当目标返回时,t为事务处理结束时间。
非阻塞传送接口
非阻塞传送接口用于支持建模近似时间模型,该模型用于描述发起者和目标之间的事务处理过程的多相位的细节,每个相位都有一个明确的定时点。非阻塞接口有
tlm_phase是非阻塞传送接口模板类的缺省相位类型。tlm_phase的值有5个:UNINITIALIZED_PHASE=0, BEGIN_REQ=1, EDN_REQ, BEGIN_RESP, END_RESP。
事务定义
可以使用C++类来表示事务
class bus_payload // 定义
{
public:
unsigned int address;
......
};
bus_payload bp; // 例化