快速 SystemC 之旅(一)
- 一、前言背景
- 二、实验环境
- 1. 安装步骤
- 2. 验证安装
- 三、RTL 级硬件描述
- 1. 初看模块
- 2. 二输入与非门
一、前言背景
因项目需求,近期开始开展电子系统级设计(ESL)进行事务级建模(TLM),方案上选择了 NVIDIA 的开源架构 NVDLA 。
NVDLA 基于 GreenSocs 的 QBOX 框架,也是一种 QEMU 和 SystemC 联合仿真的解决方案。为学习该开源框架,为深入理解该开源架构,笔者开始系统学习 QEMU 与 SystemC。本文为学习 SystemC 过程中记录的笔记,一方面便于日后查阅,另一方面也希望对想要学习相关知识的朋友一些帮助。
实验环境基于 NVDLA 框架适配版本:SystemC 2.3.0,使用 C++11 标准编译。
二、实验环境
本节介绍 SystemC 2.3.0 的安装过程。SystemC 本质上是一个基于 C++ 的建模扩展库。
1. 安装步骤
使用以下命令完成 SystemC 2.3.0 的下载、编译与安装:
sudo wget -O systemc-2.3.0a.tar.gz http://www.accellera.org/images/downloads/standards/systemc/systemc-2.3.0a.tar.gz
tar -xzvf systemc-2.3.0a.tar.gz
cd systemc-2.3.0a
sudo mkdir -p /usr/local/systemc-2.3.0/
mkdir objdir
cd objdir
../configure --prefix=/usr/local/systemc-2.3.0
make
sudo make install
2. 验证安装
安装完成后,通过编写一个简单的 HelloWorld
项目验证环境配置是否成功。
项目结构
.
├── CMakeLists.txt
└── main.cpp
CMakeLists.txt
:
cmake_minimum_required(VERSION 3.12)# === 项目信息 ===
set(CMAKE_PROJECT_NAME demo_systemc)project(${CMAKE_PROJECT_NAME} VERSION 2025.06.13 LANGUAGES C CXX
)# === 构建类型 ===
if(NOT CMAKE_BUILD_TYPE)set(CMAKE_BUILD_TYPE Debug)
endif()# === 编译配置 ===
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")# === 可执行目标 ===
add_executable(${CMAKE_PROJECT_NAME})# === 打印信息 ===
message(STATUS "[Build type] : ${CMAKE_BUILD_TYPE}")
message(STATUS "[C++ Standard] : C++${CMAKE_CXX_STANDARD}")
string(TOUPPER ${CMAKE_BUILD_TYPE} BUILD_TYPE_UPPER)
set(_CXXFLAGS "CMAKE_CXX_FLAGS_${BUILD_TYPE_UPPER}")
message(STATUS "[Compiler Flags] : ${${_CXXFLAGS}}")# === SYSTEMC库 ===
set(SYSTEM_HOME /usr/local/systemc-2.3.0)
set(SYSTEMC_INCLUDE_DIR ${SYSTEM_HOME}/include)
set(SYSTEMC_LIBRARY ${SYSTEM_HOME}/lib-linux64/libsystemc.a)# === 源文件 ===
file(GLOB SOURCES CONFIGURE_DEPENDS${PROJECT_SOURCE_DIR}/*.cpp
)
target_sources(${CMAKE_PROJECT_NAME} PRIVATE${SOURCES}
)# === 头文件目录 ===
target_include_directories(${CMAKE_PROJECT_NAME}PRIVATE${SYSTEMC_INCLUDE_DIR}
)# === 宏定义 ===
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE# Add user defined symbols
)# === 链接库 ===
target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE# Add user defined library search paths
)
target_link_libraries(${CMAKE_PROJECT_NAME}PRIVATE${SYSTEMC_LIBRARY}
)
main.cpp
:
#include <systemc.h>SC_MODULE(Hello)
{SC_CTOR(Hello){SC_METHOD(run);}void run(){std::cout << "Hello SystemC" << std::endl;}
};int sc_main(int, char **)
{Hello hello("hello");sc_start();return 0;
}
编译与运行
执行以下命令完成编译与运行:
cmake . -B build/
cmake --build build/
./build/demo_systemc
运行结果如下,表明 SystemC 配置成功:
SystemC 2.3.0-ASI --- May 20 2025 10:22:36Copyright (c) 1996-2012 by all Contributors,ALL RIGHTS RESERVEDHello SystemC
三、RTL 级硬件描述
SystemC 目前能够描述包括门级在内所有更高抽象级别的数字逻辑电路和软件,在后续内容中,我们也主要讨论基于 SystemC 的 RTL 级以上的硬件描述。但笔者认为 RTL 级建模是一个非常适合作为入门的 Demo,因此本节将用 RTL 级的实例帮助大家快速了解 SystemC 的基本语法与建模风格。
1. 初看模块
回顾我们之前编写的 HelloWorld
示例,这段简洁的代码已经体现出 SystemC 作为硬件描述语言的基本特征:其核心建模单元是模块(SC_MODULE
)。
SC_MODULE
是 SystemC 中的基本构建块,用于表示一个硬件组件。其本质是一个宏定义,展开后为继承自 sc_core::sc_module
的类(C++ 中,struct
的默认访问类型是 public
,class
的默认访问类型是 private
):
#define SC_MODULE(user_module_name) \struct user_module_name : ::sc_core::sc_module
模块的构造函数通常通过 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 语句也可以通过单独的 SC_HAS_PROCESS
宏显式声明:
#define SC_HAS_PROCESS(user_module_name) \typedef user_module_name SC_CURRENT_USER_MODULE
SC_HAS_PROCESS
用于注册 SystemC 的进程(如 SC_METHOD
、SC_THREAD
、SC_CTHREAD
)到仿真内核。
例如,SC_METHOD(func)
用于将 func()
注册为一个敏感事件触发的仿真进程。
SystemC 提供了多个宏用于简化模块定义过程。根据模块的构造方式和进程类型,选择合适的宏可使代码更简洁:
- 如果模块不包含仿真进程,无需使用
SC_CTOR
或SC_HAS_PROCESS
。 - 如果模块构造函数仅包含
sc_module_name
,使用SC_CTOR
即可。 - 如果模块构造函数包含额外参数,应使用
SC_HAS_PROCESS
并自行定义构造函数。
以下是一个带额外参数和仿真进程的模块示例:
SC_MODULE(module) {const int i;SC_HAS_PROCESS(module);module(sc_module_name name, int i) : i(i) {SC_METHOD(func);}void func() {cout << name() << ", addithonal input argument" << endl;}
};
这里我们仅做简要介绍,意在带大家快速上手,后续章节我们将进一步介绍 SystemC 中模块、进程与敏感列表等关键概念。
2. 二输入与非门
以一个典型的门级电路——二输入与非门(NAND2)为例,展示 SystemC 在 RTL 级硬件建模中的基本用法。这可以更好的类比于其他的硬件描述语言,有助于大家快速建立认知。为简洁起见,一些语法细节将于后续章节展开讨论。
Nand2.h
:
#ifndef NADN2_H
#define NADN2_H#include <systemc.h>SC_MODULE(Nand2)
{sc_in<bool> input_a; sc_in<bool> input_b;sc_out<bool> output_x; SC_CTOR(Nand2){SC_METHOD(do_nand);sensitive << input_a << input_b;}
private:void do_nand(){output_x.write(!(input_a.read() && input_b.read()));}
}; /* Nand2 */#endif /* NADN2_H */
该模块实现了一个二输入与非门:
input_a
和input_b
是bool
型输入端口。output_x
是bool
型输出端口。- 成员函数
do_nand()
作为一个SC_METHOD
进程被注册,其敏感列表包含input_a
和input_b
,即在任一输入变化时触发该进程完成对输出信号output_x
的求值。
为验证功能正确性,我们编写一个测试模块(Testbench):
tb/TB_Nand2.h
:
#ifndef TB_NADN2_H
#define TB_NADN2_H#include <systemc.h>SC_MODULE(TB_Nand2)
{sc_out<bool> output_a;sc_out<bool> output_b;sc_in<bool> input_x;sc_in_clk clk;SC_CTOR(TB_Nand2){SC_CTHREAD(gen_input, clk.pos());SC_METHOD(display_variable);sensitive << input_x << output_a << output_b;dont_initialize();}private:void gen_input(){wait();output_a.write(0); output_b.write(0);wait();output_a.write(0); output_b.write(1);wait();output_a.write(1); output_b.write(0);wait();output_a.write(1); output_b.write(1);wait(100);}void display_variable(){cout << "A = " << output_a.read() << " "<< "B = " << output_b.read() << " "<< "X = " << input_x.read() << endl;}}; /* tb_nand2 */#endif /* TB_NADN2_H */
该测试模块中包含:
- 一个
SC_CTHREAD
进程gen_input()
依时钟clk.pos()
生成所有输入组合。 - 一个
SC_METHOD
进程display_output()
用于输出当前输入与输出状态。 dont_initialize()
意味着不要在仿真零时刻对SC_METHOD
类型进程display_output()
初始化。如果不使用该语句,运行结果将会出现A = 0 B = 0 X = 0
,这将不符合预期。
有了验证程序,我们就可以在顶层模块 Top
中将 Nand2
和 TB_Nand2
进行例化,并连接各个信号,以验证设计的正确性。
Top.h
:
#ifndef TOP_H
#define TOP_H#include <systemc.h>
#include "Nand2.h"
#include "tb/TB_Nand2.h"SC_MODULE(Top)
{sc_signal<bool> sig_a, sig_b, sig_x;sc_in_clk clk;Nand2 na2;TB_Nand2 tb_na2;SC_CTOR(Top) : na2("NAND2"),tb_na2("TB_NAND2"){na2.input_a(sig_a);na2.input_b(sig_b);na2.output_x(sig_x);tb_na2.output_a(sig_a);tb_na2.output_b(sig_b);tb_na2.input_x(sig_x);tb_na2.clk(clk);}
}; /* Top */#endif /* TOP_H */
最后我们例化顶层模块 Top
,并添加波形跟踪:
main.cpp
:
#include "Top.h"int sc_main(int, char **)
{Top top("TOP");sc_clock clk("CLK", 20, SC_NS);top.clk(clk);sc_trace_file *tf = sc_create_vcd_trace_file("Nand2");tf->set_time_unit(1, SC_NS);sc_trace(tf, top.sig_a, "A");sc_trace(tf, top.sig_b, "B");sc_trace(tf, top.sig_x, "X");sc_trace(tf, top.clk, "CLK");sc_start(200, SC_NS);sc_close_vcd_trace_file(tf);return 0;
}
运行结果:
$ ./build/demo_systemc SystemC 2.3.0-ASI --- May 20 2025 10:22:36Copyright (c) 1996-2012 by all Contributors,ALL RIGHTS RESERVEDNote: VCD trace timescale unit is set by user to 1.000000e-09 sec.
A = 0 B = 0 X = 1
A = 0 B = 1 X = 1
A = 1 B = 0 X = 1
A = 1 B = 1 X = 1
A = 1 B = 1 X = 0
输出验证了一个 NAND 门的真值功能。
生成的波形文件 Nand2.vcd
可通过以下命令转换为 Modelsim 支持的格式:
vcd2wlf Nand2.vcd Nand2.wlf
然后在 Modelsim 中加载 Nand2.wlf
文件,即可查看波形:
Ref:
[1] 李挥, 陈曦. SystemC电子系统级设计[M]. 北京: 科学出版社, 2010.
[2] LearnSystemC.com. Learn SystemC[EB/OL]. [2025-06-13]. https://www.learnsystemc.com/