系统级设计描述语言

语言架构

SystemC本质上是在C++的基础上添加的硬件扩展库和仿真核。

1
2
3
4
5
6
7
graph LR
A[C++语言标准]-->B[SystemC核心]
A-->C[数据类型]
B-->D[基本通道]
C-->D
D-->E[方法学库]
D-->F[层次Layered库]
  • 方法学库
    • Master Lib
    • Slave Lib
  • 层次(Layered)库
    • Verification Lib
    • Static Data Flow
  • 基本通道
    • 信号
    • 互斥
    • 信号量
    • FIFO
  • SystemC 核心
    • 模块(Module)
    • 端口(Port)
    • 进程(Process)
    • 接口(Interface)
    • 通道(Channel)
    • 事件(Event)
    • 基于事件的仿真核
  • 数据类型
    • 4值逻辑数据类型
    • 4值逻辑向量类型
    • 比特和比特向量
    • 任意精度整数型
    • 定点数据类型
    • C++用户自定义数据类型
  • C++语言标准

描述层次

SystemC 不仅仅是一种新的硬件描述语言,更是一种系统描述语言

  • 寄存器传输级(RTL)
  • 时钟周期精确级
  • 带时间信息的编程级(PVT)
  • 编程级(PV)
  • 算法级
  • 高层
  • 底层

基本语法

  • 缺省时间单位为 ns,缺省时间分辨率为 1ps
  • 模块用 SC_MODULE(module_name){...} 来声明
  • 一个模块实际上是一个类,拥有构造函数和析构函数
  • 最顶层的函数是 sc_main

全局函数

  • sc_version()
  • sc_copyright()
  • T sc_abs(const T& val)
  • T sc_max(const T& a, const T& b)
  • sc_start():开始运行仿真核
  • sc_top():停止运行

示例:与非门

upload successful

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include "systemc.h"

// 声明一个模块
SC_MODULE(nand2)
{
sc_in<bool> A, B; // 输入端口
sc_out<bool> F; // 输出端口

void do_nand() // 与非门
{
F = !(A & B);
}

SC_CTOR(nand2) // 模块的构造函数
{
SC_METHOD(do_nand);
sensitive << A << B;
}
};

SC_MODULE(tb)
{
sc_out<bool> a, b;
sc_in<bool> f;
sc_in_clk clk;

void gen_input()
{
auto setAB = [&](int a, int b) {
wait();
this->a = a;
this->b = b;
};

setAB(0, 0);
setAB(0, 1);
setAB(1, 0);
setAB(1, 1);
setAB(0, 0);
setAB(0, 0);
}

void display_variable()
{
cout << "a=" << a << ", b=" << b << ", f=" << f << endl;
}

SC_CTOR(tb)
{
SC_CTHREAD(gen_input, clk.pos());
SC_METHOD(display_variable);
sensitive << f << a << b;
dont_initialize();
}
};

// 主函数入口
int sc_main(int argc, char *argv[])
{
sc_signal<bool> a, b, f;
sc_clock clk("Clk", 20, SC_NS);

// 模块实例的初始化
nand2 N2("Nand2");
N2.A(a);
N2.B(b);
N2.F(f);

tb tb1("tb");
tb1.clk(clk);
tb1.a(a);
tb1.b(b);
tb1.f(f);

// 连接模块的通道的初始化
sc_trace_file *tf = sc_create_vcd_trace_file("Nand2");
sc_trace(tf, N2.A, "A");
sc_trace(tf, N2.B, "B");
sc_trace(tf, N2.F, "F");

// 设置缺省时间单位
sc_start();

sc_close_vcd_trace_file(tf); // 别忘了关闭
return 0;
}

仿真过程

仿真过程是基于事件的,时间只前进,不后退。前进的尺度与仿真时间分辨率和时间单位有关。

执行过程分三个阶段:

  1. 目标描述

    • 模块实例和连接模块的通道的初始化
    • 设置缺省时间单位和仿真分辨率
    • sc_clock 的初始化
    • sc_time 类型数据的初始化
  2. 初始化

    • 整个 SystemC 仿真的执行过程由 SystemC 调度器控制,初始化是其执行的第一步

    • SystemC 核心语言库定义了三种进程:

      • SC_METHOD
      • SC_THREAD
      • SC_CTHREAD

      在初始化阶段,缺省情况下每一个进程都被执行一次,THREAD 进程被执行到第一个 wait() 语句

    • 通过 don't_initialize() 函数可以关闭对进程的初始化

    • 在初始化阶段,进程的初始化顺序是不确定的;但不同次执行中进程的初始化顺序是确定的。因此用不同编译器可能产生不同的运行结果。

  3. 仿真

    • 从第一次遇到 sc_start() 开始到预先设定的仿真时间结束或者遇到 sc_stop()

    • 预先设定的仿真时间由 sc_start() 确定,如:

      1
      2
      3
      4
      5
      SC_MODULE(Example) {
      /* ... */
      sc_start(500);
      /* ... */
      }

      如果缺省的时间单位为 ns 且代码中没有使用 sc_stop(),则仿真进行 500ns

    • 如果 sc_start() 的参数为空,则仿真进行到遇到 sc_stop()

SystemC 调度器

sc_start() 函数激活调度器,第一个工作是对进程的初始化。

调度器控制:

  • 仿真时序
  • 进程的执行顺序
  • 处理仿真过程中的事件
  • 更新信号的值

SystemC 调度器也是基于 Delta 周期的,一个 Delta 周期包括求值和更新两个阶段。

SystemC 模块

模块是最基本的单位,包含一些其他元素如:端口、内部信号、内部数据、子模块、进程、构造函数和析构函数等。这些元素共同定义模块所表达的功能。

使用关键字 SC_MODULE 来声明一个模块,也可以用 C++ 的类来定义模块。

模块的端口

模块间的端口使数据能够在模块间通过,模块之间通过信号将端口连接起来。

端口分为三种类型:

  • in
  • out
  • inout

如果需要将某一端口的数据赋给模块自身的其他信号,那么该端口就应该是 inout 类型。

你也可以指定端口的数据类型,允许的数据类型包括C++基本数据类型如 bool、int、short、char 等或者是 SystemC 专有数据类型如 sc_int、sc_unit、sc_logic 等或用户定义的任何数据类型

下面是定义端口的示例:

1
2
3
sc_in<packet> pkt_in; // 一个输入端口
sc_int<sc_logic> a[32]; // 端口向量(如计算机的数据和地址总线)
sc_signal<sc_logic> abus[16]; // 信号向量

抽象端口

SystemC 为了支持交易级建模,还支持抽象端口,示例:

1
2
3
4
5
6
class direct_if : public virtual sc_interface
{
public:
virtual bool direct_read(int* data, unsigned int address) = 0;
virutal bool direct_write(int* data, unsigned int address) = 0;
}
1
sc_port<direct_if> arbiter_port; // 定义类一个抽象端口

端口的读写

1
2
3
4
5
6
7
8
// 定义输入端口
sc_in<bool> data_in;

// 可以进行下面的读操作
if (data_in == TRUE) { ... }
if (data_in.read() == 1) { ... }
bool flag = data_in;
bool flag = data_in.read();
1
2
3
4
5
6
7
8
9
// 定义输出端口
sc_out<int> data_out;

// 可以进行下面的写操作
data_out.write(10);
data_out.write(data_in.read());

// 非法的
data_out = data_in; // 直接赋值是不行的,因为两者类型不同。

端口和信号的多驱动处理

0 1 Z X
0 0 X 0 X
1 X 1 1 X
Z 0 1 Z X
X X X X X

普通的信号是不允许多驱动的。SystemC 中引入了解析逻辑向量信号(Resolved LogicalVector signal)来解决多驱动的问题。可以使用下面的方法定义解析型端口:

1
2
3
4
sc_in_rv<n> x; // n 比特宽的解析逻辑向量型输入端口
sc_out_rv<n> y;
sc_inout_rv<n> z;
sc_signal_rv_<n> x; // 宽度为 n 比特的解析型向量信号

信号和变量

  • 信号不能用 in、out 或 inout 来声明,信号的传输方向取决于连接部分的端口状态。

  • 信号常常被用来连接模块和用于进程间通信,变量则用于进程和模块的本地存储。

  • 变量仿真的赋值是立刻发生的,没有 delta 延时;而信号和端口的值刷新要经过一个 delta 延时。

  • 信号应常被综合为逻辑块间的连线;变量常被综合为逻辑块,可以是组合或者时序逻辑。

信号和端口的关联

  • 关联(Association)基本等于连接(Connect),也成为了绑定(Bind)-

  • 关联分为位置关联和名字关联。

    1
    2
    sc_signal<bool> a, b, f;
    sc_clock clk("Clk", 20, SC_NS);
    • 位置关联:按照端口定义的顺序一一对应
      适合少量端口的模块,但在大量端口的模块中非常危险,因为可能不经意间修改了端口顺序

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      nand2 N2("Nand2");
      N2.A(a);
      N2.B(b);
      N2.F(f);

      tb tb1("tb");
      tb1.clk(clk);
      tb1.a(a);
      tb1.b(b);
      tb1.f(f);
    • 名字关联:按照名字一一对应
      对于大的 SystemC 项目,一般建议统一使用名字关联

      1
      2
      3
      4
      5
      nand2 N2("Nand2");
      N2(a, b, f);

      tb tb1("tb");
      tb1(clk, a, b, f);

构造函数

使用 SC_CTOR 标识,构造函数的名字必须与模块的名字相同,用于初始化进程的类型并创建进程的敏感表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SC_MODULE(Tb)
{
sc_out<bool> data_source;
bool value;

void GenInput()
{
data_source.write(value);
value = !value;
}

SC_CTOR(Tb)
{
SC_METHOD(GenInput);
sensitive_pos << clk;

data_source.initialize(true); // OK
value = TRUE; // OK
data_source.write(TRUE); // Wrong!
data_source = TRUE; // Wrong!
}
};

时钟模型

在 SystemC 中,时钟被作为一个特殊的对象处理,它就是 sc_clock 类。

时钟端口作为一个特殊的端口,如:

1
2
sc_in_clk clk1;
sc_in<bool> clk1; // 两种方式等价

在 SystemC2.0.1 中,sc_clock 一共有 6 个重载的构造函数,如:

1
2
sc_clock(sc_module_name name, const sc_time& period, double duty_cycle = 0.5,
const sc_time& start_time = SC_ZERO_TIME, bool posedge_first = true);

定义实例:

1
sc_clock clk1("clk1", 20, 0.5 5, true);
upload successful
upload successful
1
sc_clock clk2("clk2", 20, 0.5, 0, true);
upload successful
upload successful

另一种定义时钟的办法:

1
2
3
4
5
6
7
8
9
sc_signal<bool> clock;
sc_initialize();
for (int i = 0; i < 1000; i++)
{
clock = 1;
sc_cycle(5);
clock = 0;
sc_cycle(5);
}

采用这种方法初始化时钟的好处是可以同时插入对其他信号的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
clock = 1;
sc_cycle(2.5);
rst = 1;
sc_cycle(2.5);

clock = 0;
sc_cycle(5);

clock = 1;
sc_cycle(2.5);
rst = 0;
sc_cycle(2.5);

clock = 0;
sc_cycle(5);
......

等价于:

1
2
sc_clock clk("main clock", 10, 0.5);
sc_start(10000);

时间单位

时间单位 英文 时间长度(秒)
SC_SEC Second 1
SC_MS Millisecond 10-3
SC_US Microsecond 10-6
SC_NS Nanosecond 10-9
SC_PS Picosecond 10-12
SC_FS Femtosecond 10-15

时间模型

SystemC 采用基于整数的时间模型,系统时间采用一个 64 位无符号整数来表示。

时间分辨率是仿真系统能够处理的时间的最小精度,比时间分辨率更精细的时间将被四舍五入。

假设系统的时间分辨率为 10ps,则 wait(33.667, SC_NS) 实际上等效于 wait(33.67, SC_NS)


SystemC 缺省时间分辨率为 1ps,同时提供了 sc_set_time_resolution(double, sc_time_unit) 函数来修改系统的时间分辨率。如下面的代码将系统的时间分辨率设置为 10ps:

1
sc_set_time_resolution(10, SC_PS);

SystemC 缺省时间单位是 SC_NS,同时允许通过 sc_set_default_time_unit(double, sc_time_unit) 来修改缺省的时间单位。如下面的代码将时间单位设置为 100ps:

1
sc_set_default_time_unit(100, SC_PS);

SystemC 对时间分辨率和时间单位的设置有以下的要求:

  • 必须是 10 的幂
  • 只能在仿真开始之前设置
  • 只能设置一次
  • 时间单位必须大于等于时间分辨率
  • 时间分辨率必须在任何的非零的 sc_time 声明之前设置

在时间单位设置为 100ps 的情况下,下面的 clk1 的周期为 100*100ps=1ns

1
sc_clock clk1("clk1", 10);

数据类型

基本数据类型

类型名 类型说明
sc_bit 2 值比特数据类型
sc_logic 4 值比特数据类型
sc_int 1 到 64 比特有符号整型数据类型
sc_uint 1 到 64 比特无符号整型数据类型
sc_bigint 任意宽度的有符号整型数据类型
sc_biguint 任意宽度的无符号整型数据类型
sc_bv 任意宽度的 2 值比特向量数据类型
sc_lv 任意宽度的 4 值比特向量数据类型
sc_fixed 模板类有符号定点数据类型
sc_ufixed 模板类无符号定点数据类型
sc_fix 非模板类有符号定点数据类型
sc_ufix 非模板类无符号定点数据类型

四值逻辑 sc_logic

数字系统中最常见的四个逻辑为:

表示 描述
0 SC_LOGIC_0 逻辑低电平
1 SC_LOGIC_1 逻辑高电平
Z SC_LOGIC_Z 高阻态
X SC_LOGIC_X 不定值

可以对 sc_bit / sc_logic 类型进行赋值。

在进行代数操作时 sc_bit 可与 C++ 的 bool 类型混合使用,但推荐的做法是多使用 bool 型。

sc_bit 只有 0 和 1 两个值。

sc_logic 数据类型比 sc_bit 多两个值 X 和 Z,它所支持的运算与 sc_bit 一样,如下:

  • 位操作
    • &
    • |
    • 异或 ^
    • 取反 ~ (没有取反赋值)
  • 赋值操作
    • 与赋值 &=
    • 或赋值 |=
    • 异或赋值 ^=
    • 直接赋值 =
  • 逻辑操作
    • 等于 ==
    • 不等于 !=

任意宽度整型 sc_int

SystemC 中引入了 sc_int<W>sc_uint<W> 来实现 1 到 64 比特中任意宽度的整型数据类型,W<=64。以及 sc_bigintsc_biguint 来实现任意宽度的整型操作。

1
2
sc_int<34> a; // 34 位有符号整数型
sc_uint<60> b; // 60 位无符号整型

除最基本操作外,还支持以下操作:

操作 语法 说明
串联 (, ) (a,b) 将 a 和 b 串联起来构造更大的数
范围选择 range(left, right) a,range(x, y) 选择了 a 的右数第 y+1 到第 x+1 位。Y 可以是 0
位选择 [x] a[x] 选择了 a 的右数第 x+1 位
自动增加 ++
自动减少 --
位减操作 如下表

位减操作:

语法 说明
a.and_reduce() 返回 a 的所有位相与后得到的 bool 型数
a.nand_reduce() 返回 a 的所有位相与后取反得到的 bool 型数
a.or_reduce() 返回 a 的所有位相或后得到的 bool 型数
a.nor_reduce() 返回 a 的所有位相或后取反得到的 bool 型数
a.xor_reduce() 返回 a 的所有位相异或后得到的 bool 型数
1
2
3
4
5
sc_int<4> x, y;
sc_int<8> z;
z = (x, y); // 串联
x = z.range(7, 4); // z的高4位
bool temp = x.or_reduce(); // 所有位异或
1
2
3
4
sc_biguint<128> b1;
sc_biguint<64> b2;
sc_biguint<152> b3;
b3 = b1 * b2; // 结果有192位,只有低152位被赋值给b3

当一个无符号整数 sc_uint<M> 被赋值给有符号整数 sc_int<N> 时,uint 首先被扩展为 64 位(高位直接填零),然后从低位开始取 N 位赋值给 sc_int。

当 sc_int 被赋值给 sc_uint 时,系统首先将它按符号(负数高位填 1,正数填 0)扩展为 64 位,然后从低位开始取 M 位赋值给 sc_uint。

用户自定义类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define MAX_LENGTH 1504
struct packet
{
sc_uint<16> length;
char[MAX_LENGTH] info;
sc_int<32> fcs;

inline bool operator == (const packet& rhs) const
{
return rhs.info == info
&& rhs.length == length
&& rhs.fcs == fcs;
}
};

定点数据类型

四种基本定点数据类型

  • sc_fixed
  • sc_ufixed
  • sc_fix
  • sc_ufix

sc_fixedsc_ufixed 的参数是静态的,在程序中设定后不能再修改,而 sc_fixsc_ufix 的参数是非静态的,其字长和整数部分长度可以是变量

定点数据类型的定义方法如下:

1
2
3
4
sc_fixed<wl, iwl, q_mode, o_mode, n_bits> x;
sc_ufixed<wl, iwl, q_mode, o_mode, n_bits> y;
sc_fix x(list of option);
sc_ufix y(list of option);
  • wl:字长,总比特数,必须大于 0
  • iwl:整数部分字长,可以是正数、负数,也可以大于总字长
  • q_mode:量化模式。超出精度时根据量化模式对尾数进行取舍
  • o_mode:溢出模式。超出范围时根据溢出模式对数据进行处理
  • n_bits:饱和比特的尾数。仅用于具有饱和行为的溢出模式下饱和比特的位数

量化模式

量化模式 含义
SC_RND 向正无穷舍入
SC_RND_ZERO 向 0 舍入
SC_RND_MIN_INF 向负无穷舍入
SC_RND_INF 向无穷舍入
SC_RND_CONV 收敛舍入
SC_TRN 删除舍入
SC_TRN_ZERO 向 0 删除舍入

定点数据类型的量化示例:

1
2
3
4
sc_fixed<4, 2> x;
sc_fixed<3, 2, SC_RND_ZERO, SC_SAT> y;
x = 1.25;
y = x; // 这里发生了量化

x 的值:二进制为 01.01,十进制为 1.25

y 的值:二进制为 01.0,十进制为 1.0,q=0.5,x=2.5q


饱和模式

溢出模式 意义
SC_SAT 饱和为最大最小值
SC_SAT_ZERO 饱和为 0
SC_SAT_SYM 对称饱和
SC_WRAP 循环饱和
SC_WRAP_SM 符号幅度循环饱和

定点数据类型饱和示例:

1
2
3
4
sc_fixed<4, 4> x;
sc_fixed<3, 3, SC_TRN, SC_SAT> y;
x = 5;
y = x; // 这里发生饱和,因为 y 的范围是 -4~3,而 x 为 5

x 的值:二进制为 0101,十进制为 5

y 的值:二进制为 011,十进制为 3,q=1

SystemC 的进程

在 SystemC 中,进程是程序在并发环境中的执行过程,也是一个基本执行单位,具有动态性、并发性、独立性、异步性和结构性五大特征。

基本进程有三种:

  • SC_METHOD
  • SC_THREAD
  • SC_CTHREAD
  • SC_SLAVE (在 Master/Slave 库中定义的第四种进程类型)

进程不是层次化的,不能包含或直接调用其它进程,但可以调用非进程的函数和方法。

进程通常会有一个敏感表,当在敏感表中的信号上有事件发生时,进程就会被激活。信号上的事件是指信号的值的变化,如时钟的上升沿就是时钟信号从 0 变为 1。当信号上的事件发生,所有对该事件敏感的进程对会被激活。

进程的敏感表在模块的构造函数内设定。

方法进程 SC_METHOD

方法进程 SC_METHOD 是唯一的可以综合的寄存器传输级(RTL)进程。

特点是当敏感表上有事件发生,它就会被调用,调用后应该立刻返回。只有该类进程返回后仿真系统的事件才有可能前进,因此该类进程中不能使用 wait() 这样的语句。

示例:全加器

upload successful

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <systemc.h>
SC_MODULE(FullAAdder)
{
sc_in<sc_bit> A, B, Ci;
sc_out<sc_bit> S, Co;

void do_ad()
{
S = A.read() ^ B.read() ^ Ci.read();
Co = (A.read() & B.read())
| (B.read() & Ci.read())
| (A.read() & Ci.read());
}

SC_CTOR(FullAdder)
{
SC_METHOD(do_add);
sensitive << A << B << Ci;
}
}

线程进程 SC_THREAD

特点是它能够被挂起和重新激活。线程进程使用 wait() 挂起,当敏感表中有事件发生,线程进程被重新激活运行到遇到新的 wait() 语句再重新挂起。

线程进程不是寄存器传输级进程,一个方便的用途就是用来描述验证平台的输入激励和输出获取。

显示全加器的输入和输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SC_MODULE(Monitor)
{
sc_in<sc_bit> m_a, m_b, m_cin, m_sum, m_cout;

void prc_monitor()
{
while (true)
{
cout << m_a.read() << m_b.read() << m_cin.read() << ",";
cout << m_sum.read() << m_cout.read() << endl;
wait();
}
}

SC_CTOR(Monitor)
{
SC_THREAD(prc_monitor);
sensitive << m_a << m_b << m_cin << m_sum << m_cout;
}
}

当敏感表中的信号至少有一个值发生变化时,prc_monitor 就会被激活显示这时全加器的输入和输出结果。

钟控线程进程 SC_CTHREAD

SC_CTHREAD 继承于 SC_THREAD,只能在时钟的上升沿或者下降沿被触发或者激活,这种行为更加接近实际硬件的行为。

SC_CTHREAD 的敏感表与其他类型的线程不同,它必须在指定线程名字的同时指定时钟和它的边沿。

产生全加器的激励

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SC_MODULE(Driver)
{
sc_in_clk clk;
sc_out<sc_bit> d_a, d_b, d_cin;
sc_uint<3> pattern;

void prc_driver()
{
pattern = 0;
while (true)
{
d_a.write((sc_bit)pattern[0]);
d_b.write((sc_bit)pattern[1]);
d_cin.write((sc_bit)pattern[2]);
wait();
pattern++;
}
}

SC_CTOR(Driver)
{
SC_CTHREAD(prc_driver, clk.pos());
}
}

有限状态机

通常意义上的有限状态机要明确定义系统的状态,一般要使用 case 语句来实现状态转移。

upload successful

隐式有限状态机

指编程中不现实定义状态机的状态,而是通过程序中的 wait() 语句和 wait() 语句中间的赋值语句来完成对状态机的描述。

钟控线程进程最适合来描述隐式有限状态机。

挂起

wiat_until()

将进程挂起直到指定的表达式的值为真,只能用于线程进程和钟控线程进程。

1
wait_until(data.delayed() == true);

该语句中的 delayed() 是必须的!

参数必须是 bool 型,如:

1
2
wait_until(clock.delayed()== true
&& reset.delayed() == false);

wait()

  • wait():等待敏感表中有事件发生

  • wait(const sc_event&):等待事件发生

    1
    2
    sc_event e1;
    wait(e1);
  • wait(sc_event_or_list&):等待事件之一发生

    1
    2
    sc_event e1, e2, e3;
    wait(e1 | e2 | e3);
  • wait(sc_event_and_list&):等待事件全部发生

next_trigger()

只能用于 SC_METHOD 类进程。

参数与 wait() 的参数相同,只是分别用于不同类型的进程。

next_trigger() 调用后进程立即返回。

watching()

只能用于 SC_CTHREAD 进程。

SC_CTHREAD 进程中通常有一个死循环,但有时候需要初始化一些变量和信号,或者当某些条件满足的时候能够让进程从循环中跳出来。使用 watching 结构可以跳出循环。

watching 结构会不停地监视某一个条件,一旦该条件发生,则 SC_CTHREAD 进程就会跳出循环从进程的开始处重新执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
SC_MODULE(Driver)
{
sc_in_clk clk;
sc_in<bool> rst;
sc_out<sc_bit> d_a, d_b, d_cin;
sc_uint<3> pattern;

void prc_driver()
{
if (rst.read() == true)
pattern = 0;
while (true)
{
d_a.write((sc_bit)pattern[0]);
d_b.write((sc_bit)pattern[1]);
d_cin.write((sc_bit)pattern[2]);
wait();
pattern++;
}
}

SC_CTOR(Driver)
{
SC_CTHREAD(prc_driver, clk.pos());
// 当 rst 为高,进程将会跳出循环重新执行
watching(rst.delayed() == true);
}
}

局部 watching()

在前面的 watching 中,当 watching 的条件成立的时候,整个进程就会重新开始运行,但有的时候会只需要局部代码重新运行,这时候就需要使用局部 watching。

dont_initialize()

dont_initialize()是希望进程在仿真的0时不被执行,sensitive是敏感表。

仿真与波形跟踪

支持以下三种标准的波形格式:

  • VCD (Value Change Dump)
  • WIF (Waveform Intermediate Format)
  • ISDB (Integrated Signal Data Base) (可能会被淘汰)

只有在整个仿真期间都存在的信号和变量才能被跟踪,这与多数仿真器是一样的。它能够保证模块中的所有信号和数据成员都被跟踪。函数的本地变量只有在函数被调用期间才存在,所以不能跟踪。

任何类型的信号和变量包括标量、数组和其它聚合类型(如结构 struct 类型)都能被跟踪。

不同格式的波形文件可以在同一次仿真过程中同时产生,任何一个信号和变量都可以在不同格式的波形文件中不限制次数的被跟踪。

创建和关闭波形跟踪文件

以创建 vcd 波形文件为例。下面代码生成 Wave.vcd 文件:

1
2
sc_trace_file* my_trace_file;
my_trace_file = sc_create_vcd_trace_file("Wave");

sc_main() 函数调用 return 之前必须关闭波形文件:

1
sc_close_vcd_trace_file(my_trace_file);

跟踪标量型变量和信号

创建了波形跟踪文件后,还必须告诉 SystemC 调度器到底要跟踪那些信号和变量以及被跟踪的信号和变量在波形文件中保存的名字:

1
2
3
4
sc_in<int>datain;
sc_out<int> dataout;
sc_trace(my_trace_file, datain, "DataIn");
sc_trace(my_trace_file, dataout, "DataOut");

trace() 函数只能在所有的信号和模块已经例化、波形跟踪文件已经产生后才能调用。

跟踪聚合型变量和信号

sc_trace() 函数只能跟踪标量类型的信号和变量,为了跟踪聚合类型的变量和信号,你需要重载 sc_trace 函数。所谓聚合类型可以是数组、向量和结构等。

假设我们在设计中定义了下面的结构:

1
2
3
4
5
6
struct packet
{
BYTE source_address;
BYTE destination_address;
WORD payload;
};

为了跟踪 packet,我们要重载 sc_trace()函数。

1
2
3
4
5
6
7
void sc_trace(sc_trace_file *tf, cont packet& v, const sc_string& name)
{
int i;
sc_trace(tf, v.source_address, name + ".src_addr");
sc_trace(tf, v.destination_address, name + ".dst_addr");
sc_trace(tf, v.payload, name + ".payload");
}

行为建模

系统抽象的三个关键元素:

  • 行为:算法(运算、控制……)
  • 通信:各个算法模块之间的数据交互,控制配合
  • 时序:行为和通信在时间域上的协调

在 SystemC 中,模块是行为的主要载体,通道是通信的主要载体,时序隐含在模块和通道的描述中。

  • 行为和通信分开
  • 支持接口方法调用

upload successful

端口

端口连接模块内的进程(行为)和通道(通信)。

基本的 SystemC 端口类型:

  • sc_in<T>
  • sc_out<T>
  • sc_inout<T>

为了满足行为建模的需要,允许用户自定义端口类型:

1
sc_port<InterfaceType, ChannelNumber = 1>

一些端口定义的示例:

1
2
3
sc_port<ram_if> ram_port1; // 连接到一个RAM上
sc_port<ram_if, N> ram_portN; // 可以连接到N个RAM上
sc_port<ram_if, 0> ram_port0; // 不限制所连接的RAM数量

可以通过 ram_port0.size() 得到实际连接到 ram_port0 的通道 RAM 的数量。


端口必须与特定的通道接口相连,或者同父模块的端口相连。一个模块的端口连接到零个、一个或者多个通道,或者零个、一个或者多个父模块的端口,但必须至少连接到1个通道或者父模块的端口上。

sc_port<IF,N> 是所有端口的基类,它是一个模板类。IF 是接口类型,N 是所连接的同一类型的通道数目,也就是接口数,它的缺省值是 1

示例:RAM读写端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
SC_MODULE(Master)
{
sc_in_clk clk;
sc_port<ram_if<int>> ram_port; // 端口实例
int data;
unsigned int address;

void main_action()
{
wait();
int i = 0;
address = 0;
while (i++ < 100)
{
if ((status = ram_port->write(address, data)))
{ /* ... */ }
else
cout << "RAM write fail" <<endl;

if ((status = ram_port->read(address, data)))
{ /* ... */ }
else
cout << "RAM read fail" << endl;

wait();
address++;
}
}

SC_CTOR()
{
SC_CTHREAD(main_action, clk.pos());
}
}

通道

在SystemC中,接口本身只是定义了一组通信方法,而不具体负责这些方法如何实现。通道才是这些接口方法的实现者。

通道可以实现一个或者多个接口,也连接一个或者多个模块。

SystemC中通道分为两种:基本通道和分层通道

  • 基本通道不包含任何进程,也不对外展现出任何可见结构,它们也不能够直接的或者间接的调用其它基本通道。
  • 分层通道本身是一个模块,可以包含进程、子模块,也可以包含和调用其它通道。
1
2
3
4
5
// 端口与通道的关联
source1.write_port(fifo1);
source1.clk(clk);
sink1.read_port(fifo1);
sink1.clk(clk);

基本通道

基本通道不包含任何进程,也不对外展现出任何可见结构,它们也不能够直接的或者间接的调用其它基本通道。

SystemC2.01中定义了若干基本通道类型,它们是:

  • sc_signal<T>
  • sc_signal_rv<N>
  • sc_mutex
  • sc_fifo<T>
  • sc_semaphore
  • sc_buffer<T>
sc_signal
  • sc_signal<T> 是最基本的通道,它用于连接模块的基本端口 sc_in<T>sc_out<T>sc_inout<T>
  • 最多只有一个 sc_out<T> 或者 sc_inout<T> 可以连接到 sc_signal<T>,否则就会产生典型的多驱动情况。
  • 可以有多个 sc_in<T> 同时连接到 sc_signal<T>
  • sc_signal<T> 继承于基本通道类,并实现了 sc_signal_inout_if<T> 接口。sc_signal_inout_if<T> 接口的最重要成员函数 read()write()
sc_signal_rt

sc_signal_rv<T> 是所谓“解析的”信号通道,与 sc_signal<T> 的不同之处是它允许同时有多个端口连接到其上并进行写操作。

sc_buffer

sc_buffer<T> 继承于 sc_signal<T>,并重载了 write()update()函数。

sc_buffer<T> 不管 write() 写的数据是否与原数据相同,都要求进行数据更新;而 sc_signal<T> 首先要检查新数据是否与原数据相同,如果不同才进行更新。

sc_fifo

upload successful

  • sc_fifo<T> 是SystemC核心语言库中已经实现了的 FIFO 通道
  • write(&T) 代表写 FIFO 的方法。
  • read()是读 FIFO 的方法,它返回队头单元的数据。
  • num_free() 用于查询FIFO还有多少空单元。
  • num_available() 查询FIFO还有多少个数据可以读。
  • Size 代表FIFO的总单元数,对于 sc_fifoSize 的默认值为 16
1
2
sc_fifo<int> fifo1; // 默认深度为16
sc_fifo<packet> fifo2(64);
示例:信源和信宿模块通过FIFO通信
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include "systemc.h"

// 信源模块
class source : public sc_module
{
public:
sc_in_clk clk;
sc_port<sc_fifo_out_if<char>> write_port;

SC_HAS_PROCESS(source);

source(sc_module_name name) : sc_module(name)
{
SC_CTHREAD(main, clk.neg());
}

void main()
{
int i = 0;
const char str[] = "For any problems, feel free to contact the author via Email: chenxiee@mails.tsinghua.edu.cn";
wait();
while (true)
{
if (rand() & 1)
{
if (str[i])
{
write_port->write(str[i++]);
wait();
}
}
}
}
};

// 信宿模块
class sink : public sc_module
{
public:
sc_in_clk clk;
sc_port<sc_fifo_in_if<char>> read_port;

SC_HAS_PROCESS(sink);

sink(sc_module_name name)
{
SC_CTHREAD(main, clk.neg());
}
void main()
{
char c;
while (true)
{
if (rand() & 1)
{
read_port->read(c);
cout << c;
}
wait();
}
}
};

// Top模块
#define PERIOD 20
class Top : public sc_module
{
public:
sc_clock clk;
sc_fifo<char> fifo1;
source source1;
sink sink1;

Top(sc_module_name name, int size)
: sc_module(name), fifo1("Fifo1", size), source1("source1"),
sink1("sink1"), clk("Clk", PERIOD, SC_NS)
{
source1.write_port(fifo1);
source1.clk(clk);
sink1.read_port(fifo1);
sink1.clk(clk);
}
};

int sc_main(int, char **)
{
unsigned size = 16;
Top top("Top", size);

cout << "Testbench started, the simulation result is:" << endl;
sc_start(100000, SC_NS);
cout << "\n"
<< endl;

return 0;
}

从本例看模块、接口、端口、通道之间的关系:

  • 接口是一个C++抽象类,它定义了一组抽象方法,但不定义这些方法的具体实现。
  • 通道实现一个或者多个接口。也就是说,通道必须继承一个或者多个接口,这些接口中定义的抽象方法必须在通道中实现。
  • 端口总是与一定的接口类型相关联的,端口只能连接到实现了该类接口的通道上。
  • 通过端口,模块中的进程可以连接到通道并使用通道提供的方法
sc_semaphore

通常翻译为信号量。

信号量代表可用资源实体的数量,所以可以认为信号量就是一个资源计数器,它限制的是同时使用某共享资源(也称为临界资源)的进程的数量。信号量计数的值代表的就是当前仍然可用的共享资源的数量。

sc_semaphore 实现的是 sc_semaphore_if 接口。

其中,wait() 方法获得一个信号量,其作用效果是获得一份资源的使用权,使信号量计数减一,如下面的实现代码。

sc_mutex

具有锁定和非锁定两种状态。当互斥(器)已经由另外的进程锁定,这时申请互斥的进程就会被阻塞,直到锁定互斥的进程将互斥解锁。

直接通道调用

在同一模块内,各个进程之间也需要通信。它们可以通过共享变量、握手信号、模块内通道等方式通信。如果它们之间的通信是通过模块内通道,则此时需要进行直接通道调用。

使用 sc_mutex 的直接通道调用的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sc_in_clk clk;
sc_out<int> data;
sc_mutex protect;

void writer1()
{
wait();
while (true)
{
protect.lock();
data.write(rand());
protect.unlock();
wait();
}
}

另一种形式:不通过端口直接调用通道实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ram<int>* ram0; // 通道实例的指针

void main_action()
{
...;
while (address <= end_address)
{
ram0->write(address, data); // 不通过端口直接调用通道实现
wait();
}
...;
}

SC_CTOR(PortLess)
{
SC_CTHREAD(main_action, clk.pos());
ram0 = new ram<int>("RAM", 0, 255);
}

分层通道

分层通道具有可见结构,可以包含进程,可以直接调用其它通道,它是一个实现了一个或者多个接口的模块。

常见的分层通道有两种:

  • 一是在一个通道中直接例化并使用其它通道,被例化的通道可以是分层通道,也可以是基本通道。这种类型的通道比较常见,被称为一般分层通道
  • 二是一个通道利用端口进行间接通道调用,调用穿越了一个以上的通道。被穿过的通道似乎被“合成”到了一起,这种通道是一种特殊的分层通道,称作合成通道(compositechannel)。合成通道间接的体现了通道的层次性。
一般分层通道

凡是例化了其它通道的通道都可以归入一般分层通道之列。如下例:

upload successful

合成通道

一个模块可以利用端口穿越了一个以上的通道进行间接通道调用。被穿过的通道似乎被“合成“到了一起。

我们假定设计好了一个接口类 GetFIFO_if,它的一个成员函数 getWriteFIFO() 返回的是一个 FIFO 通道的指针,如返回上节中定义的 tlm_fifo 的指针 tlm_fifo,再假设又我们实现了一个 GetFIFO 通道,data 是一个初始化了的 char 型数据。那么在 Source 模块中就可以通过下面的方式来写 FIFO:

1
read_port->getWriteFIFO()->write(data);

交易级建模

SoC设计中的通信体系结构的抽象层次图:

屏蔽的细节
L3 消息层 资源共享,时序
L2 交易层 时钟,协议
L1 传输层 连线,寄存器
L0 寄存器传输层 门,门/连线延时