SystemC入门笔记

首发地址个人博客

变量说明

数据类型

SystemC为C++的一个库,因此C++的特性在SystemC中均可以使用,数据类型同理,除了C++中的数据类型外,SystemC也有一些自己的数据类型,如下所示:

  • 二值变量:sc_bitsc_bv<n>(n为宽度)分别为二值(0、1)变量和任意位宽二值向量。
  • 四值变量:sc_logicsc_lv<n>(n为宽度)分别为四值(0、1、x、z)变量和任意位宽四值向量
  • int型变量:sc_int<n>sc_uint<n>(n为宽度)分别为有符号和无符号的不超过64位宽的整型变量
  • bigint变量:sc_bigint<n>sc_bituing(n为宽度)分别为有符号和无符号任意位宽的整型变量

信号

信号使用sc_signal<type>声明,一般用于连接端口和进程通信(功能进程之间连接信号)

端口

SystemC中端口类型主要有sc_in<type>sc_out<type>sc_inout<type>,type中为端口的类型,可以使用C++自带的一些类型,也可以使用SystemC中的数据类型。

sc_out<sc_int<WIDTH * 2> > vec_o;

例如上面为一个输出端口例子,该输出端口名称为vec_o,类型为SystemC的数据类型sc_int<W>

sc_in<sc_int<WIDTH> > vec1[VEC_WIDTH];

可以声明端口数组,如上所示,声明了一个宽度为VEC_WIDTH的端口数组,每个端口的类型是sc_int<W>

模块设计——矩阵-向量乘法器

设计一个矩阵-向量乘法器用于熟悉语法,需要注意的是若要使用SystemC特性,需要使用#include "systemc.h"

系统结构

structure.png

该系统用于实现矩阵-向量乘法的行为级建模,包括以下几个部分:

  • 乘法器:实现矩阵-向量乘法功能,由多个向量-向量乘法器构成
  • 测试平台:激励生成器,用于产生指定尺寸的矩阵和向量以及时钟复位等控制信号

子模块设计

每个子模块在SystemC使用一个类描述,这个类使用宏SC_MODULE(<module name>)声明,这里的子模块是向量-向量乘法器,这一部分代码如下所示:

SC_MODULE(vector_mul) {

    sc_in<bool> clk,rst_n;
    sc_in<sc_int<WIDTH> > vec1[VEC_WIDTH],vec2[VEC_WIDTH];
    sc_out<sc_int<WIDTH * 2> > vec_o;

    void compute_vector_mul(void) {
        int temp = 0;
        if (rst_n.read() == false) {
            vec_o.write(0);
            return;
        }
        for (int i = 0; i < VEC_WIDTH; ++i) {
            temp = temp + vec1[i].read() * vec2[i].read();
        }
        vec_o.write(temp);
    };

    SC_CTOR(vector_mul) {
        SC_METHOD(compute_vector_mul);
        sensitive_pos << clk;
        sensitive_neg << rst_n;
    };
};

一个子模块包括三个部分:端口定义、功能描述与构造函数。端口定义使用上述的端口定义,如下所示,该乘法器定义了两个类型为bool的控制端口:clkrst_n。分别是时钟和复位端口;还定义了两个输入端口数组,分别是vec1vec2,数组宽度为VEC_WIDTH(宏定义),类型为有符号整数类型sc_int<WIDTH>;此外还定义了一个输出端口vec_o,类型为指定位宽的整数类型sc_int<WIDTH * 2>。定义端口后,该部分框图如下所示:

sc_in<bool> clk,rst_n;
sc_in<sc_int<WIDTH> > vec1[VEC_WIDTH],vec2[VEC_WIDTH];
sc_out<sc_int<WIDTH * 2> > vec_o;
port.png

功能描述使用一个无输入参数无输出参数的方法描述,建议使用.read()读取输入端口的数据并使用.write()向输出端口写入数据。一个模块可以有多个功能描述,这里的功能描述功能类似于Verilog中的always块。SystemC中的赋值基本都是阻塞的,可以在这一函数中使用任意的C++特性和库等。定义了这一方法后,该部分框图如下所示:

void compute_vector_mul(void) {
    int temp = 0;
    if (rst_n.read() == false) {
        vec_o.write(0);
        return;
    }
    for (int i = 0; i < VEC_WIDTH; ++i) {
        temp = temp + vec1[i].read() * vec2[i].read();
    }
    vec_o.write(temp);
};
func.png

最后一个部分是构造函数,这一部分是为了注册功能,描述连接关系和敏感列表等。无参数化的构造函数使用SC_CTOR(<module name>)定义,这里的module name必须与这个类的名称一致。一个类可能有很多方法,只有如下所示的方式定义为METHOD(或THREAD等)的方法才作为模块的一个功能,定义后需要立刻定义敏感列表,只有敏感列表中的变量发生改变时,功能才运行:sensitive <<表示事件(电平)敏感,一般用于组合逻辑建模;sensitive_pos <<sensitive_neg <<分别为正跳变敏感和负跳变敏感,一般用于时序逻辑建模。完成以上所有部分的定义后,完成对一个子模块的构建。

METHOD是一种阻塞式的功能进程,当这个进程被敏感列表触发之后,获取仿真控制权开始运行,直到运行完成,将控制权返回SystemC仿真内核。使用METHOD注册的功能函数不能含有无限循环,这会导致仿真卡死在这个任务中,控制权无法返回仿真内核。

SC_CTOR(vector_mul) {
    SC_METHOD(compute_vector_mul);  // 注册为METHOD
    sensitive_pos << clk;           // clk为正跳变敏感信号
    sensitive_neg << rst_n;         // rst_n为负跳变敏感信号
};
final.png

顶层模块

顶层模块用于实现子模块的连接,代码实现如下所示。除了声明端口以外,还需要在构造函数中定义连接关系。连接关系的定义分为三个部分:模块指针声明、模块实例化和端口连接。

SC_MODULE(matrix_vector_mul) {

    sc_in<bool> clk,rst_n;
    sc_in<sc_int<WIDTH> > matrix[VEC_NUM][VEC_WIDTH];
    sc_in<sc_int<WIDTH> > vector_in[VEC_WIDTH];
    sc_out<sc_int<WIDTH * 2> > vector_out[VEC_NUM];

    vector_mul *pe[VEC_NUM];

    SC_CTOR(matrix_vector_mul) {
        std::ostringstream pe_name;
        for (int i = 0; i < VEC_NUM; ++i) {
            pe_name << "pe" << i;
            pe[i] = new vector_mul(pe_name.str().c_str());
            pe[i]->clk(clk);
            pe[i]->rst_n(rst_n);
            for (int j = 0; j < VEC_WIDTH; ++j) {
                pe[i]->vec1[j](matrix[i][j]);
                pe[i]->vec2[j](vector_in[j]);
            }
            pe[i]->vec_o(vector_out[i]);
            pe_name.str("");
        }
    };
};

模块指针声明如下所示,这里声明了VEC_NUM个子模块指针作为顶层模块的成员变量。

vector_mul *pe[VEC_NUM];

模块实例化如下所示,若使用SC_CTOR宏定义构造函数的子模块,需要使用一个唯一的字符串作为实例名。由于C++没有格式化字符串的功能,因此使用std::ostringstream生成唯一不重复的实例名,再用c_str()转为构造函数指定的类型。

pe[i] = new vector_mul(pe_name.str().c_str());

信号连接额部分使用如下所示的方式完成:<point>-><port name>(<signal name>)。需要注意的是若声明的是端口数组,则需要将每个数组中的每个端口拆分出来依次连接。

pe[i]->clk(clk);                    // clk端口与clk信号连接
pe[i]->rst_n(rst_n);                // rst_n端口与rst_n信号连接
for (int j = 0; j < VEC_WIDTH; ++j) {
    pe[i]->vec1[j](matrix[i][j]);   // vec1[j]端口与matrix[i][j]信号连接
    pe[i]->vec2[j](vector_in[j]);   // vec2[j]端口与vector_in[j]信号连接
}
pe[i]->vec_o(vector_out[i]);        // vec_o端口与vector_out[i]信号连接

测试平台设计

平台组件

这里实现的组件仅有激励生成器,该模块与上述模块没有太大的差别,该模块用于生成复位信号和数据信号。

SC_MODULE(driver)
{
    sc_in <bool> clk;
    sc_out<bool> rst_n;
    sc_out<sc_int<WIDTH> > mat[VEC_NUM][VEC_WIDTH];
    sc_out<sc_int<WIDTH> > vec[VEC_WIDTH];

    void generate_input(void) {
        for (int i = 0; i < VEC_WIDTH; ++i) {
            for (int j = 0; j < VEC_NUM; ++j) {
                mat[j][i].write(rand() % ((int)pow(2,WIDTH) - 1));
            }
            vec[i].write(rand() % ((int)pow(2,WIDTH) - 1));
        }
        while(1) {
            wait();
            for (int i = 0; i < VEC_WIDTH; ++i) {
                for (int j = 0; j < VEC_NUM; ++j) {
                    mat[j][i].write(rand() % ((int)pow(2,WIDTH) - 1));
                }
                vec[i].write(rand() % ((int)pow(2,WIDTH) - 1));
            }
        }
    };

    void generate_reset(void) {
        rst_n.write(1);
        wait(1,SC_NS);
        rst_n.write(0);
        wait(1,SC_NS);
        rst_n.write(1);
    };

    SC_CTOR(driver) {
        SC_THREAD(generate_input);
        sensitive_neg << clk;
        SC_THREAD(generate_reset);
    };
};

需要注意的是这里的功能描述和构造函数,如下所示。构造函数中功能成员generate_inputgenerate_reset均使用THREAD宏注册为功能,这一宏与METHOD的区别是这一种功能进程在仿真开始时运行,碰到wait()跳出,直到敏感列表中的信号再次触发这一进程,从上次跳出的wait()处继续运行,因此这种进程可以使用循环体包括wait()的无限循环。

SC_CTOR(driver) {
    SC_THREAD(generate_input);
    sensitive_neg << clk;
    SC_THREAD(generate_reset);
};

除了使用wait()阻塞运行外,还可以使用wait(<times>,SC_NS);将执行延迟指定的时钟周期,如rst_n信号的实现,使用多个wait(<times>,SC_NS)延迟执行。

void generate_reset(void) {
    rst_n.write(1); 
    wait(1,SC_NS);      // 延迟1ns
    rst_n.write(0);
    wait(1,SC_NS);      // 延迟1ns
    rst_n.write(1);
};

平台运行

最终的平台运行集成在main函数中,如下所示,分为以下几个步骤:信号声明,模块声明和端口连接,波形追踪和仿真运行。

#include "systemc.h"
int sc_main(int argc,char* argv[])
{
    sc_clock clk("clk",10,SC_NS);
    sc_signal<bool> rst_n;
    sc_signal<sc_int<WIDTH> > mat[VEC_NUM][VEC_WIDTH],vec[VEC_WIDTH];
    sc_signal<sc_int<WIDTH * 2> >vec_o[VEC_NUM];

    sc_trace_file *fp;                  // Create VCD file
    fp=sc_create_vcd_trace_file("wave");// open(fp), create wave.vcd file
    fp->set_time_unit(1, SC_NS);        // set tracing resolution to ns

    matrix_vector_mul dut("dut");
    dut.clk(clk);
    dut.rst_n(rst_n);
    for (int i = 0; i < VEC_NUM; ++i) {
        for (int j = 0; j < VEC_WIDTH; ++j) {
            dut.matrix[i][j](mat[i][j]);
        }
    }
    for (int i = 0; i < VEC_WIDTH; ++i) {
        dut.vector_in[i](vec[i]);
    }
    for (int i = 0; i < VEC_NUM; ++i) {
        dut.vector_out[i](vec_o[i]);
    }

    driver d("dri");
    d.clk(clk);
    d.rst_n(rst_n);
    for (int i = 0; i < VEC_WIDTH; ++i) {
        for (int j = 0; j < VEC_NUM; ++j) {
            d.mat[j][i](mat[j][i]);
        }
        d.vec[i](vec[i]);
    }

    sc_trace(fp,clk,"clk");
    sc_trace(fp,rst_n,"rst_n");
    for (int i = 0; i < VEC_NUM; ++i) {
        for (int j = 0; j < VEC_WIDTH; ++j) {
            std::ostringstream mat_name;
            mat_name << "matrix(" << i << "," << j << ")";
            sc_trace(fp,mat[i][j],mat_name.str());
            mat_name.str("");
        }
    }
    for (int i = 0; i < VEC_WIDTH; ++i) {
        std::ostringstream stream1;
        stream1 << "vec(" << i << ")";
        sc_trace(fp,vec[i],stream1.str());
        stream1.str("");
    }
    for (int i = 0; i < VEC_NUM; ++i) {
        std::ostringstream out_name;
        out_name << "dout(" << i << ")";
        sc_trace(fp,vec_o[i],out_name.str());
        out_name.str("");
    }

    sc_start(1000,SC_NS);

    sc_close_vcd_trace_file(fp);        // close(fp)
    return 0;
};

第一个步骤是声明信号和模块,这一步用于声明连接需要的信号,这里的时钟信号使用SystemC的方法生成。用于连接的信号需要声明成sc_signal<>类型。

sc_clock clk("clk",10,SC_NS);       // 时钟,周期为10ns
sc_signal<bool> rst_n;
sc_signal<sc_int<WIDTH> > mat[VEC_NUM][VEC_WIDTH],vec[VEC_WIDTH];
sc_signal<sc_int<WIDTH * 2> >vec_o[VEC_NUM];

第二步是声明模块并连接信号,这里的连接和声明与顶层模块中类似,只是这里直接声明对象而不是指针。

matrix_vector_mul dut("dut");
dut.clk(clk);
dut.rst_n(rst_n);
for (int i = 0; i < VEC_NUM; ++i) {
    for (int j = 0; j < VEC_WIDTH; ++j) {
        dut.matrix[i][j](mat[i][j]);
    }
}
for (int i = 0; i < VEC_WIDTH; ++i) {
    dut.vector_in[i](vec[i]);
}
for (int i = 0; i < VEC_NUM; ++i) {
    dut.vector_out[i](vec_o[i]);
}

第三步是实现波形跟踪与保存,首先定义一个sc_trace_file类型的指针,使用对应方法打开指定的文件类型和波形名称(fp=sc_create_vcd_trace_file("wave");保存vcd格式的波形)并进行配置。随后使用sc_trace(fp,signal,<signal name>);将需要观察的信号添加到波形跟踪中,其中<signal name>为波形文件中这一信号的名称,因此需要保证对于每一个信号该名称唯一。当仿真完成后,需要使用sc_close_vcd_trace_file(fp);关闭仿真文件。

sc_trace_file *fp;                  // Create VCD file
fp=sc_create_vcd_trace_file("wave");// open(fp), create wave.vcd file
fp->set_time_unit(1, SC_NS);        // set tracing resolution to ns
......
sc_trace(fp,clk,"clk");
sc_trace(fp,rst_n,"rst_n");
for (int i = 0; i < VEC_NUM; ++i) {
    for (int j = 0; j < VEC_WIDTH; ++j) {
        std::ostringstream mat_name;
        mat_name << "matrix(" << i << "," << j << ")";
        sc_trace(fp,mat[i][j],mat_name.str());
        mat_name.str("");
    }
}
for (int i = 0; i < VEC_WIDTH; ++i) {
    std::ostringstream stream1;
    stream1 << "vec(" << i << ")";
    sc_trace(fp,vec[i],stream1.str());
    stream1.str("");
}
for (int i = 0; i < VEC_NUM; ++i) {
    std::ostringstream out_name;
    out_name << "dout(" << i << ")";
    sc_trace(fp,vec_o[i],out_name.str());
    out_name.str("");
}
......
sc_close_vcd_trace_file(fp);        // close(fp)

第四个部分是启动仿真,使用sc_start()运行指定的时间长度。

sc_start(1000,SC_NS);   // 运行1000ns
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容