SystemC 介绍

105 阅读14分钟

What’s SystemC?

SystemC 是一种用于系统设计(system design)与建模(modeling )的语言,本质上是一个 C++ 库

它通过内建的simulation kernel提供并发执行的“假象”,从而支持并行行为的建模。

当与 SystemC Verification Library (事务级建模(TLM)、随机化测试、约束生成、事务追踪(transaction recording)等)结合使用时,SystemC 能提供许多对系统设计和建模至关重要的特性,这些特性在其他语言((如 Verilog、VHDL))中要么缺失,要么分散存在。(优点)

SystemC 的语言架构

底层:C++ 与 STL

  • C++ :SystemC 是建立在 C++ 上的库(library),它用 C++ 类、模板、运算符重载等机制来实现硬件建模的抽象。
  • STL(Standard Template Library) :可以使用 C++ 的标准容器(如 vectormap)和算法辅助构建复杂模型。

中间层:SystemC 核心功能模块

这一层是 SystemC 的核心功能区,基于 C++ 实现了硬件建模所需的各种机制:

模块区域功能说明
Simulation KernelSystemC 的模拟内核,调度所有线程、事件和时间,负责仿真流程的管理。
Threads & Methods硬件行为的建模单元,类似于 HDL 中的 always 块。使用 SC_THREAD, SC_METHOD 来定义进程。
Events, Sensitivity & Notifications事件触发机制(比如 wait(event)),实现进程对某些信号变化的响应。
Modules & Hierarchy支持模块化和层级建模(使用 SC_MODULE),组织系统结构。
Channels & Interfaces建模通信机制,例如 sc_fifo, sc_signal, sc_mutex 等;通过 sc_interface 定义接口。
Data TypesSystemC 提供特定硬件建模数据类型,如: - sc_logic、sc_bv(布尔/位向量) - sc_int, sc_uint(有符号/无符号整型) - sc_fixed, sc_ufixed(定点数)

上层:预定义通道与用户扩展

  • Predefined Primitive Channels:SystemC 内置的通道类型,如:

    • sc_signal:用于建模信号线
    • sc_fifo:建模先进先出队列
    • sc_mutex:互斥量,支持多线程同步
  • User Libraries:用户可以在其上层构建自己的库或接口。

  • SystemC Verification Library(SCV) :提供高级验证支持,如随机化、事务记录等。

  • Other IP:可以集成其他现有模块,比如第三方协议模型、内存模型等。

SystemC 开发环境通常包括以下几个组成部分

  • 支持 SystemC 的运行平台
  • 与 SystemC 兼容的 C++ 编译器
  • SystemC 库(需下载并针对所用 C++ 编译器进行编译)www.accellera.org/downloads/s…
  • 用于编译和构建项目的命令行编译指令或 Makefile(或其他等效构建脚本)

SystemC的主要用途

  1. 开发硬件参考模型(Develop reference model for hardware) 用 SystemC 构建硬件模块的高层行为模型,为后续 RTL 或实际实现提供参考。
  2. 探索新算法或硬件架构(Explore new algorithm or hardware architecture) 在 RTL 实现之前,使用 SystemC 快速验证新算法或架构设计的可行性与性能。
  3. 测试平台开发,整合多个 IP (Testbench development, integrate multiple IPs) SystemC 可用于搭建复杂的测试平台,将多个模块(IP 核)集成测试,提高验证效率。
  4. 推动验证工作前移(Verification work can be shift-left) 在设计早期阶段(例如在 RTL 之前),即进行功能验证,减少后期修改成本。
  5. 软件团队提前开发(Software team can use this to shift-left the dev job) 软件开发人员可以基于 SystemC 模型开展软件开发与调试,而无需等待硬件完成。
  6. 构建高级测试平台(High-level testbench) 相比传统 RTL 测试平台,SystemC 支持更高抽象级别的验证环境,更接近系统级验证。

这张图是一个 高层次测试平台(High-level testbench) 的结构示意图,展示了如何在系统级别对被测设计(DUT)进行功能验证。

DUT(Design Under Test)中间的灰色方块:

  • DUT 是被测试的设计模块,比如一个通信接口、总线转换器、协议桥接器等。
  • 它有多个输入输出端口,支持不同的接口协议,如 AXI、USB、HT、PCI-X 等。

左侧(输入侧):

  • p1p3p4:是模拟外部模块或设备的 发起者(Initiator)/ 主设备
  • 它们通过不同的协议接口向 DUT 发送输入数据。

这些模块在高层次 testbench 中可以是用 SystemC/C++ 实现的行为模型(如 CPU、DMA 控制器等),用于生成测试激励。


右侧(输出侧):

  • p2p5:是 DUT 的输出连接对象,通常模拟响应器(Responder)/ 从设备。
  • 它们接收 DUT 的处理结果,进行数据消费、记录或校验。

同样,这些模块也是高层模型,用于响应 DUT 的输出或进一步仿真交互行为。

  1. Arch model<-> DUT(仿真模型与设计单元的交互) 可以将高层次的软件模型(如 Arch model)与待验证的设计单元(DUT)进行联调,构建混合验证平台。

这张图是典型的 验证环境架构图,常见于使用 SystemVerilog UVM(Universal Verification Methodology)或 SystemC 构建的 测试平台(testbench) 中,用于验证一个设计单元(DUT,Design Under Test)。参考:verificationguide.com/uvm/uvm-sco…

总体结构

  • 图中黄色的方块是 DUT(被测设计模块)。
  • 左边是 Active Agent(主动代理),负责驱动 DUT,发起 stimulus(测试激励)。
  • 右边是 Passive Agent(被动代理),只监听 DUT 的响应,不驱动。
  • 上方是 Scoreboard,负责结果检查:把 DUT 输出和参考模型输出进行比较。

🧩 各部分功能详解

🟦 Active Agent

  • Sequencer(序列器) :生成一系列测试激励(sequence item),比如数据包、操作命令等。
  • Driver(驱动器) :接收 sequencer 发来的数据,通过接口发送给 DUT。
  • Monitor(监视器) :观察 DUT 的输入(或中间状态),提取数据送给 scoreboard 分析。

🟪 Passive Agent

  • Monitor(监视器) :只负责监听 DUT 的输出接口,将数据发到 scoreboard。

🟨 DUT(Design Under Test)

  • 被验证的硬件设计模块。

🟩 Interface(接口)

  • 模拟 DUT 与 testbench 的信号连接,实际传输信号(如 clk、rst、data 等)。

🟥 Scoreboard(记分板)

  • Reference Model(参考模型) :用 SystemC 或高层代码实现的期望功能行为模型。
  • Compare Logic(比较逻辑) :把 DUT 的实际输出与参考模型的输出进行比对,检查是否一致。
  • 作用是 验证 DUT 的功能是否正确

Components

  1. 模块化与层级结构(Modules and Hierarchy) 用于构建系统的结构化模型,通过嵌套模块支持自顶向下的设计方法。
  2. 进程机制:线程与方法(Threads and Methods) 描述硬件行为的基本单元,SC_THREADSC_METHOD 提供事件驱动或循环驱动的执行模型。
  3. 事件、敏感性与通知机制(Events, Sensitivity, and Notification) 实现进程之间的同步与通信,通过事件触发和敏感列表响应信号变化。
  4. SystemC 专用数据类型(SystemC Data Types) 提供适用于硬件建模的精确定义类型,如逻辑值、定点数、位向量和有/无符号整数等。
  5. 端口 、接口与通道机制(Ports, Interfaces, and Channels) 支持模块间的数据传输和交互建模,是模块之间连接和通信的关键构件。

Modules and Hierarchy

SC_MODULE(...) 定义的模块,是 SystemC 建模的基本单元,相当于硬件里的一个“组件”或“子系统”。可以包含三种东西:

  • 状态(State) :模块内部的数据,比如寄存器值、变量等(通过成员变量定义)
  • 行为(Behavior) :模块的功能逻辑,比如一个进程如何处理输入信号、产生输出(用 SC_METHODSC_THREAD 定义)
  • 结构(Structure) :模块的组成,比如它内部包含哪些端口、通道、子模块(你可以像拼积木一样把多个模块连接成系统)

可以把一个模块当成“父模块”,里面包含多个“子模块”,再把子模块之间用端口和通道连起来。这样可以像设计真实硬件那样,按层级来组织设计。

一个模块的主体(MODULE BODY)通常由以下组成部分构成:

  • 端口 (Ports) :用于模块之间的通信连接
  • 成员通道实例(Member channel instances) :如信号、FIFO、互斥锁等,用于实现通信和同步
  • 成员数据(Member data instances) :模块内部使用的变量或数据结构
  • 子模块实例(Member module instances) :嵌套的子设计,实现层次化建模
  • 构造函数(Constructor) :用于初始化模块,注册进程、建立连接等
  • 析构函数 Destructor :用于资源清理(通常可选)
  • 仿真进程函数(Simulation processes) :如 SC_METHODSC_THREAD,定义模块的具体行为
  • 其他成员函数(Other methods) :可用于模块内部辅助逻辑或对外提供接口

(port、channel和interface的关系,找个例子,our cmodel)

SC_MODULE 类构造函数(Constructor):SC_CTOR

在 SystemC 中,每个模块的构造函数(通过 SC_CTOR 宏定义)负责完成以下几项关键工作:

  • 初始化或创建子模块:比如分配子模块的实例,用于构建更复杂的层次结构

  • 连接子模块之间的接口:将端口和信号正确连接起来,形成完整的模块结构

  • 注册进程到 SystemC 内核:比如通过 SC_THREADSC_METHOD 把你的行为函数注册进去,以便仿真时调度执行

  • 设置静态敏感列表:定义哪些信号的变化会触发某个进程执行(通过 sensitive <<

  • 其他自定义初始化操作:例如变量赋初值、打印信息、参数设置等

仿真的基本执行单元:仿真进程(Simulation processes)

在 SystemC 中,仿真进程是定义在 SC_MODULE 中的成员函数,由 SystemC 仿真内核的调度器自动调用。

所有仿真进程都必须注册到仿真内核中,只有仿真内核会负责调用这些进程,用户代码不能直接调用它们。

Threads and Methods

注册进程:SC_THREAD

这一步是为了让线程函数能被 SystemC 仿真内核调度。需要在模块类的构造函数里(通常用 SC_CTOR 宏)把线程注册进去。

其实,SystemC 里的“仿真进程”就是你在 sc_module 类里写的普通成员函数。SystemC 仿真内核会统一调度所有已注册的仿真进程。只要用 SC_THREAD(或 SC_METHOD)把它注册了,SystemC 的内核就会在仿真开始时自动安排它的执行。

SC_METHOD —— 常用于组合逻辑 (内部是否可以用event,notify)

特点:
  • 不可阻塞(不能使用 wait()
  • 执行一次后立即返回
  • 执行周期短,性能高
  • 支持动态敏感(通过 next_trigger()
常用场景:
  • 建模组合逻辑:电路中无时钟驱动、仅根据输入变化立即计算输出的逻辑
  • 建模同步逻辑中的敏感边沿行为(需配合 clk.pos()
示例(组合逻辑):
SC_METHOD(comb_logic);
sensitive << a << b << sel;

void comb_logic() {
    if (sel)
        out = a;
    else
        out = b;
}

SC_THREAD —— 常用于时序逻辑

特点:
  • 可阻塞(可使用 wait()
  • 可以包含无限循环(如 while(true))
  • 支持更复杂的状态机控制、顺序执行等
  • 支持动态敏感(通过 wait()
常用场景:
  • 建模时序逻辑:需要按时钟节拍执行的行为
  • 多周期操作(如 DMA、协议状态机)
  • 需要等待事件/信号的复杂进程
示例(时序逻辑):
SC_THREAD(seq_logic);
sensitive << clk.pos();

void seq_logic() {
    while (true) {
        wait();  // 等待时钟上升沿
        q = d;   // 寄存器行为
    }
}

Events, Sensitivity, and Notification

在 SystemC 中,事件通过 sc_eventsc_event_queue 类实现。事件的触发是通过事件类的成员函数 notify() 完成的。

当事件发生且某个进程(如 SC_METHODSC_THREAD)对该事件具有敏感性时,SystemC 的仿真内核会自动调用对应的进程函数。

SystemC 提供两种类型的敏感性机制:

  • 静态敏感性(Static Sensitivity) :在 elaboration 阶段(通常是在模块构造函数中)通过 sensitive 命令将某个 SC_METHODSC_THREAD 注册到特定事件上。

  • 动态敏感性(Dynamic Sensitivity) :允许在仿真运行过程中动态改变进程对事件的敏感性。

    • SC_METHOD 通过 next_trigger(arg) 实现动态敏感性。
    • SC_THREAD 通过 wait(arg) 实现动态敏感性。

这种灵活的事件驱动机制使得 SystemC 能够高效、准确地模拟硬件系统中的并发行为。

注意:静态敏感性(Static Sensitivity)和动态敏感性(Dynamic Sensitivity)是 SystemC 中定义进程触发条件的两种方式,它们在建模场景、灵活性和执行时机上有明显区别。

Data type

SystemC 支持所有 C++ 原生数据类型。同时,它还提供了丰富的硬件专用数据类型,以及在硬件数据类型之间或硬件与软件数据类型之间进行转换的必要方法。

对于非二进制的硬件类型,SystemC 提供了支持四值逻辑(0、1、X、Z)的数据类型,如 sc_logic。这些逻辑状态的表示方式如下:

  • 逻辑 0SC_LOGIC_0Log_0'0'
  • 逻辑 1SC_LOGIC_1Log_1'1'
  • 高阻态SC_LOGIC_ZLog_Z'Z''z'
  • 不确定态SC_LOGIC_XLog_X'X''x'

通过这些扩展逻辑状态,SystemC 能够更准确地建模数字系统中的硬件行为。

Ports, Interfaces, and Channels

模块之间以及模拟进程(如 SC_METHODSC_THREAD)之间的通信,是通过ports, interfaces, and channels的不同组合实现的。

模拟进程之间的协调通常通过event机制完成,用于同步和调度。

Simulation Kernel

仿真流程主要分为四个阶段。

阶段一:结构展开(Elaboration)

在这个阶段,模型的各个组件会被实例化,并通过端口和信号相互连接,搭建出完整的系统结构,准备进入仿真。

此阶段还会完成仿真进程的注册(比如 SC_THREAD、SC_METHOD)。

该阶段结束后,调用 sc_start(),正式启动仿真内核,进入下一个“初始化阶段”。

阶段二:初始化(Initialization)

在这个阶段,SystemC 仿真内核会扫描并识别所有已注册的仿真进程,并将它们分配到“可运行进程集合”或“等待进程集合”中。如下图所示。

默认情况下,大多数仿真进程会被放入“可运行”集合,准备在仿真开始时立即执行。

如果某些进程在构造函数中显式调用了 dont_initialize(),它们就不会在初始化阶段触发,而是被放入“等待”集合中,直到将来某个事件触发它们。

阶段三:仿真(Simulation)

这个阶段通常可以看作是一个状态机:它负责调度进程执行,同时推进仿真的时间。

在每一个仿真周期内,内核会依次执行所有处于“可运行”状态的进程。每个进程会一直运行,直到遇到 wait(时间)wait(事件),或执行 return 为止。

进程的执行顺序在 SystemC 标准中是未定义的,也就是说,不同的仿真器实现可以选择不同的调度顺序。

不过,为了确保结果具有可重复性,一个特定的仿真器在每次运行相同测试时,必须使用相同的调度顺序,以保证仿真结果一致。

阶段四:仿真结束与清理(Post-processing / Cleanup)

仿真结束后,进入最后的清理阶段,直到出现以下三种情况之一:

  • 所有进程都挂起,即“可运行进程集合”为空,没有代码可继续执行

  • 某个进程调用了 sc_stop() ,主动终止仿真

  • 仿真时间达到上限,内部 64 位时间变量耗尽(例如时间精度非常高的情况下)

DV通常通过以下方式退出 testbench:

  • 测试激励逻辑在判断“功能已完成”后主动调用 sc_stop() 结束仿真

  • 所有模块都空闲了一定周期(如空转 500 cycles)后退出

  • 为避免程序“卡死”或无限等待,可以设置最大仿真周期数(例如 10⁷ cycles)。一旦仿真时间超出这个限制,就自动停止仿真并提示“超时”。

一个假设的例子

t1, t2, t3 non-zero

Reference

learnsystemc.com/

SystemC – From the ground up