SystemC 学习之 SystemC 基本语法(二)

1、SystemC 头文件

为了顺利进行编译和链接,任何 systemc 设计都必须包括合适的 systemc 库定义的头文件,systemc 和辛苦包括两个最基本和重要的名字空间,一个是 sc_core,一个是 sc_dt。

sc_core 时 SystemC 基本的内核空间,sc_dt 则定义了 SystemC 的最基本数据类型。#include <systemc> 只将 sc_core 和 sc_dt 包括到目标设计中,而 #include <systemc.h> 则包含了除 sc_core 和 sc_dt 外的其他仿真中所需要的名字,也包含了一些 c++ 的命名空间,比如 namespace std 等。

但是在大型设计中,常常希望只包括所需要的名字空间,而不是所有的名字空间以加快仿真编译速度,减少名字的冲突,比如可以写成下面这样

#include <systemc>
using sc_core::sc_module;
using sc_core::sc_signal;
using sc_core::SC_NS;

2、模块

模块是 SystemC 设计中最基本的单位,是完成一个特定功能的基本单元,一个模块可以包含一些其他的 SystemC 基本元素如端口,内部信号,内部数据,进程,构造函数和析构函数等。这些元素共同定义模块所表达的功能

在 SystemC 中使用 SC_MODULE(module_name) 来声明一个模块

SC_MODULE(Driver) {
};

SC_MODULE 是 SystemC 库中定义的一个宏,展开如下

#define SC_MODULE(user_module_name)                                           \
    struct user_module_name : ::sc_core::sc_module

那么 SC_MODULE(Driver) 也就可以展开如下,因为 struct 默认为公有继承,所以加上 public

struct Driver : public ::sc_core::sc_module {
};

但是一般在 c++ 中使用 class 表示一个类,所以也可以将 struct 换成 class

模块的基本组成有构造函数和析构函数,构造函数完成创建和初始化一个模块的最初工作,在模块的创建时就被执行。析构函数在模块结束的时候被自动调用。

C++ 中的构造函数用来创建模块内部数据结构,并进行初始化,在 SystemC 中,除了完成 C++ 所要求的基本功能外,构造函数还用于初始化进程的类型并创建进程的敏感表,类 Driver 的构造函数定义如下

SC_CTOR(Driver) {
}

同样的 SC_CTOR 也是一个宏,展开如下

#define SC_CTOR(user_module_name)                                             \
    typedef user_module_name SC_CURRENT_USER_MODULE;                          \
    user_module_name( ::sc_core::sc_module_name )

那么展开后的构造函数就如下所示

typedef Driver SC_CURRENT_USER_MODULE;
Driver( ::sc_core::sc_module_name ) {
}

另外我们发现在 sc_module.h 头文件中还有一个 SC_HAS_PROCESS 的宏定义

#define SC_HAS_PROCESS(user_module_name)                                      \
    typedef user_module_name SC_CURRENT_USER_MODULE

那么我们就可以将构造函数写成下面这样 

SC_HAS_PROCESS(Driver);
Driver(::sc_core::sc_module_name) {
}

这里我们可能会想 SC_HAS_PROCESS 是不是有一些多此一举了,实际上我们会在后面的进程中使用到 SC_CURRENT_USER_MODULE 这个宏,所以要在这里进行宏定义

任何 SystemC 模块必须继承 sc_module(sc_module_name),sc_module_name 是一个类,作为模块名的包容器,并负责建立一个设计中所有例化的模块的层次关系,这里可以简单把 sc_module_name 理解为常量字符串

SystemC 的析构函数和 C++ 定义析构函数一样,没有定义专门的宏来处理析构函数

3、端口和信号

芯片通过管脚与电路板上其他芯片或者元件通信,管脚有输入、输出和双向管脚,在 SystemC 中,模块通过端口与其他模块进程通信,一般情况下也只能使用端口进行通信。端口也分为输入、输出和双向端口。一个模块的端口通过信号与其它模块的端口相连,或者父子模块的端口可以直接相连,端口与信号的连接,在 SystemC 中称作绑定

端口的数据类型可以是常见 C++ 数据类型,比如 int、double、float、bool、char 等,也可以是 SystemC 专有数据类型(后边介绍),还可以是用户自定义结构体,还可以支持抽象端口(后边介绍)

SystemC 端口的定义方法如下

输入端口

sc_in<类型> 名称;

输出端口

sc_out<类型> 名称;

双向端口

sc_inout<类型> 名称;

一个输出端口可以和多个输入端口相连接

端口和信号的绑定

SystemC 中信号有两种定义方式,一种是 sc_signal<类型>,还有一种是 sc_buffer<类型>,sc_buffer 和 sc_signal 都是用来进行求值更新的操作,sc_signal 和 sc_buffer 不同的是 sc_buffer 不管 write 写的数据与原数据是否相同,都会进行数据更新,而 sc_signal 首先会检查新数据是否与原数据相同,如果不同才进行更新操作

sc_signal 更新时的逻辑

template< class T, sc_writer_policy POL >
void sc_signal_t<T,POL>::update()
{
    policy_type::update();
    if( !( m_new_val == m_cur_val ) ) {
        do_update();
    }
}

sc_buffer 更新时的逻辑

template< typename T, sc_writer_policy POL >
inline void sc_buffer<T,POL>::update()
{
    base_type::policy_type::update();
    base_type::do_update();
}

设置输出端口的初始值

sc_out<bool> valid;
valid.initialize(true);

在上面提到过 SystemC 端口的类型可以是 c++ 基本类型,SystemC 基本类型,也可以是结构体成员,但是我们这里要注意的是,如果将结构体成员用在端口的成员时,我们需要多做以下几件事,不然编译是会报错的

1. 重载 sc_trace

这个会在波形跟踪中提到

2. 重载 == 和 << 运算符

不然定义 sc_signal<struct> 会出现编译错误,一个完整的结构体如下所示

struct Color {
    sc_uint<8> r;
    sc_uint<8> g;
    sc_uint<8> b;
    sc_uint<8> a;

    inline bool operator==(const Color& color) const {
        return (color.r == r) && (color.g == g) && (color.b == b) && (color.a == a);
    }

    friend ostream& operator<<(ostream& out, const Color& color) {
        out << color.r << " " << color.g << " " << color.b << " " << color.a;
        return out;
    }
};

端口和信号的多驱动处理

在实际电路中多驱动是很常见的,但是在 SystemC 建模中很少使用,这里只简单介绍一下多驱动处理。

多驱动适用于输入值取决于多个模块的输出,比如下面这种情况

 下面简单给出一个多驱动代码实现的例子

class A : public sc_module {
public:
    SC_HAS_PROCESS(A);
    A(sc_module_name ins_name) : sc_module(ins_name) {
        SC_METHOD(Write_1);
        sensitive << clk.pos();
        dont_initialize();

        vec_.push_back(sc_lv<1>(SC_LOGIC_0));
        vec_.push_back(sc_lv<1>(SC_LOGIC_1));
        vec_.push_back(sc_lv<1>(SC_LOGIC_Z));
        vec_.push_back(sc_lv<1>(SC_LOGIC_X));
    }
    ~A() {

    }

    void Write_1() {
        std::cout << "1: " << sc_time_stamp() << " " << vec_[index_1_] << std::endl;
        val.write(vec_[index_1_]);
        index_1_++;
        if (index_1_ == 4) {
            index_1_ = 0;
        }
    }

public:
    sc_in_clk clk;
    sc_out_rv<1> val;

private:
    int index_1_{};
    std::vector<sc_lv<1>> vec_{};
};

class C : public sc_module {
public:
    SC_HAS_PROCESS(C);
    C(sc_module_name ins_name) : sc_module(ins_name) {
        SC_METHOD(Write_2);
        sensitive << clk.pos();
        dont_initialize();

        vec_.push_back(sc_lv<1>(SC_LOGIC_0));
        vec_.push_back(sc_lv<1>(SC_LOGIC_1));
        vec_.push_back(sc_lv<1>(SC_LOGIC_Z));
        vec_.push_back(sc_lv<1>(SC_LOGIC_X));
    }

    void Write_2() {
        std::cout << "2: " << sc_time_stamp() << " " << vec_[index_2_ / 4] << std::endl;
        val.write(vec_[index_2_ / 4]);
        index_2_++;
        if (index_2_ == 19) {
            index_2_ = 0;
        }
    }

public:
    sc_in_clk clk;
    sc_out_rv<1> val;

private:
    int index_2_{};
    std::vector<sc_lv<1>> vec_{};
};

class B : public sc_module {
public:
    SC_HAS_PROCESS(B);
    B(sc_module_name ins_name) : sc_module(ins_name) {
        SC_METHOD(Receive);
        sensitive << clk.pos();
        dont_initialize();
    }
    ~B() {

    }

    void Receive() {
        std::cout << "B: " << sc_time_stamp() << " : " << val.read() << std::endl;
    }

public:
    sc_in_clk clk;
    sc_in_rv<1> val;
};

SystemC 中引入了解析逻辑向量信号来解决多驱动的问题,可以使用下面的方法定义解析型端口

  • sc_in_rv<n> x

  • sc_out_rv<n> y

  • sc_inout_rv<n> z

这里 n 是任意正值,需要注意的是解析型的端口比普通的端口需要更多的仿真时间,如果不是必须最好不要使用

4、SystemC 时钟和时间模型

时钟是同步数字逻辑中必不可少的基本元素,在 systemc 中,时钟被作为一个特殊的对象处理,它就是 sc_clock 类,构造函数如下

sc_clock();
explicit sc_clock( const char* name_ );
sc_clock(const char* name_, const sc_time& period_, double duty_cycle_ = 0.5,
         const sc_time& start_time_ = SC_ZERO_TIME, bool posedge_first_ = true );
sc_clock(const char* name_, double period_v_, sc_time_unit period_tu_,
         double duty_cycle_ = 0.5 );
sc_clock(const char* name_, double period_v_, sc_time_unit period_tu_,
         double duty_cycle_, double start_time_v_, sc_time_unit start_time_tu_,
         bool posedge_first_ = true );
// for backward compatibility with 1.0
 sc_clock(const char* name_,
          double period_,            // in default time units
          double duty_cycle_ = 0.5,
          double start_time_ = 0.0,  // in default time units
          bool posedge_first_ = true );

其中 name_ 是时钟的名字

period 和 period_v 是时钟的周期

period_tu_ 是时钟的时间单位

duty_cycle_ 是占空比

start_time_v_ 和 start_time_ 是时钟初始第一逻辑值的持续时间

posedge_first_ 是第一个逻辑值是高电平还是低电平,如果为true,则时钟初始化为false,并在开始时间更改为true。也就是说如果为 true,那么初始化第一逻辑为 false,然后在初始化第一逻辑值的持续时间之后更改为 true。

 下面是常见的时钟定义方式

sc_clock clk("clk", 20, SC_NS);

比如时钟周期是 20 NS,占空比是 0.5,那么就是高低电平各 10 NS,如果占空比为 0.9 那么就表示有 18 NS 是高电平,2 NS 是低电平

Systemc 定义的时间单位如下表

时间单位

中文

时间长度/s

SC_SEC

1

SC_MS

毫秒

10 ^ -3

SC_US

微秒

10 ^ -6

SC_NS

纳秒

10 ^ -9

SC_PS

皮秒

10 ^ -12

SC_FS

飞秒

10 ^ -15

时间分辨率是仿真系统能够处理时间的最小精度,比时间分辨率更精细的时间将被四舍五入到时间分辨率所定义的精度,假设系统的时间分辨率为 10ps,则下面的语句

wait(33.667, SC_NS) 实际上等于 wait(33.67, SC_NS)

SystemC 缺省的时间分辨率为 1ps,同时提供了 sc_set_time_resolution(double, sc_time_uint) 函数允许修改系统的时间分辨率

SystemC 对时间分辨率的设置有以下要求

  • 时间分辨率必须是 10 的幂

  • 时间分辨率只能在仿真开始之前设置

  • 时间分辨率只能设置一次

  • 时间分辨率必须在任何非零的 sc_time 声明之前设置

SystemC 缺省的时间单位是 SC_NS,同时允许通过 sc_set_default_time_unit(double, sc_time_uint) 来设置缺省的时间单位,但是最好的是我们在设置的时候就加上时间单位,这样可以更加清晰

SystemC 对缺省的时间单位的设置有以下的要求

  • 缺省时间单位必须是 10 的幂

  • 缺省时间单位必须大于时间分辨率

  • 缺省时间单位只能设置一次

  • 缺省时间单位只能在仿真开始之前设置

比如设置缺省时间单位为 100 ps

sc_set_default_time_uint(100, SC_PS);

那么下面的一个 clk 时钟周期就是 10 * 100ps = 1 NS

sc_clock clk("clk", 10);

5、基本数据类型

SystemC 除了支持所有的 c++ 类型,同时还定义了一些专有数据类型

类型

说明

sc_bit

2 值单比特数据类型

sc_logic

4 值单比特数据类型

sc_int

1-64 比特有符号整型数据类型

sc_uint

1-64 比特无符号整型数据类型

sc_bigint

任意宽度有符号整型数据类型

sc_biguint

任意宽度无符号整型数据类型

sc_bv

任意宽度的 2 值比特向量数据类型

sc_lv

任意宽度的 4 值比特向量数据类型

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

'0' ------ 逻辑低电平

'1' ------ 逻辑高电平

'Z' ------ 高阻态

'X' ------ 不定值

在 SystemC 中,这四个值使用 '0'、'1'、'Z'、'X' 或者 SC_LOGIC_0、SC_LOGIC_1、SC_LOGIC_Z、SC_LOGIC_X 表示,sc_bit 只有 '1' 和 '0' 两种值,sc_logic 比 sc_bit 多两种值,仿真速度也要慢上一些

sc_int 和 sc_uint 支持 1-64 比特的数字,虽然大于 64 bit 也不会出现编译错误,但是可能会产生不确定的运行结果。

下表给出一些 sc_int 和 sc_uint 支持的操作类型,和 C++ 一样的就不再说明

操作类型

操作符

说明

串联

(,)

(a, b) 将 a 和 b 串联起来构成更大的数字,比如 sc_uint<4> a = 0b1100,sc_uint<4> b = 0b0011;那么将 a、b 串联起来的结果是 sc_uint<8> c = (a, b) = 0b11000011

范围选择

range(right, left)

sc_uint<32> a = 0x12131415,那么我们要选择 a 的低 7 位,那么就可以这么写 a.range(7, 0)

位减操作

and_reduce()

a.and_reduce() 返回的是 a 的所有位相与后得到的 bool 型整数

nand_reduce()

a.nand_reduce() 返回的是 a 的所有位相与后取反得到的 bool 型整数

or_reduce()

a.or_reduce() 返回的是 a 的所有位相或后得到的 bool 型整数

nor_reduce()

a.nor_reduce() 返回的是 a 的所有位相或后取反得到的 bool 型整数

xor_reduce()

a.xor_reduce() 返回的是 a 的所有位相异或后得到的 bool 型整数

xnor_reduce()

a.xnor_reduce() 返回的是 a 的所有位相异或后取反得到的 bool 型整数

在实际设计中,有时候需要宽度大于 64 bit 的整型数据类型,引入了 sc_bigint 和 sc_biguint,宽度可以从 1 到任意值,相比 sc_uint 仿真速度要慢很多,如果不是必要,就不要使用。

在 sc_constants.h 中,用户可以根据自己的需要定义 SC_MAX_NBITS 以约束 sc_bigint 和 sc_biguint 的最大值,这样可以提供 2-3 倍的仿真速度增加,SystemC 推荐的 SC_MAX_NBITS 值应该为 BITS_PER_DIGIT 的整数倍,BITS_PER_DIGIT 在 sc_nbdefs.h 定义,它的值为 30。

 6、delta Δ延迟

Δ 延迟是硬件描述语言中一个非常重要的概念,它是模块的端口与模块内数据的重要区别之一,在硬件描述语言的仿真软件将一个时间点分为一个或者多个 Δ ,在 Δ 延迟之前完成赋值和值的读取,而 Δ 之后才对被赋值的信号和端口的值进行更新,请看下面的一段代码

SC_MODULE(Writer) {
    SC_CTOR (Writer) {
        SC_METHOD(Write);
        sensitive << clk.pos();
        dont_initialize();
    }

    void Write() {
        val++;
        out_val.write(val);
        std::cout << "write " << sc_time_stamp() << " " << val << std::endl;
    }

    sc_in_clk clk;
    sc_out<int> out_val;
    int val{};
};

SC_MODULE(Reader) {
    SC_CTOR (Reader) {
        SC_METHOD(Read);
        sensitive << clk.pos();
        dont_initialize();
    }

    void Read() {
        std::cout << "read " << sc_time_stamp() << " " << in_val.read() << std::endl;
    }

    sc_in_clk clk;
    sc_in<int> in_val;
};

Writer 每隔一个时钟向 out_val 写入一个数字,Reader 每隔一个时钟从 in_val 读取数字,那么所谓的 Δ 就是在第 1 个时钟 Writer 写入 1,但是 Reader 要在第 2 个时钟才能读到这个 1,也就是说赋值和拿到值相差了一个时钟

通过 Δ 延迟,仿真软件实现了赋值和更新的分离,保证了仿真结果与硬件一致

7、仿真控制

开始仿真:sc_start()

sc_start() 函数控制所有始终的产生并在适当的时刻激活 SystemC 调度器,SystemC 调度器控制整个仿真过程中的调度工作,包括激活进程,产生 delta 延迟,计算和更新变量和信号的值,一般来讲,sc_start() 只在 sc_main() 中调用

sc_start 函数原型为

void sc_start();
void sc_start(const sc_time& duration);
void sc_start(double duration, sc_time_unit unit);
void sc_start(int duration, sc_time_unit unit);

其中 sc_start() 没有参数,表示仿真一直进行直到遇到 sc_stop 函数,其它的 sc_start() 函数如果没有遇到 sc_stop 函数,那么会在时间执行完之后退出

结束仿真:sc_stop()

sc_stop 函数停止仿真并将控制权交给 sc_main。我们可以通过 void sc_set_stop_mode(sc_stop_mode mode) 来控制 sc_stop 的停止模式,sc_stop_mode 有 SC_STOP_IMMEDIATE 个 SC_STOP_FINISH_DELTA 两个值,其中

SC_STOP_IMMEDIATE:立即停止当前进程和其他任何进程,不再执行更新阶段
SC_STOP_FINISH_DELTA:在返回到 sc_main 之前将当前 delta 周期能够执行的进程都执行并且完成当前 delta 周期的更新阶段

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值