← SystemC

SystemC · 2026年8月30日

SystemC 入门(一)

1. 学习目标与背景

  • 为什么学:SystemC 是做系统级 / 架构级建模(硬件模块 + 软件行为同仿真)的主流底座,能在 RTL 之前做架构探索、性能建模、虚拟原型。
  • 本次想搞懂:SystemC 到底是什么、四要素是什么、Channel 有哪些且怎么用、仿真内核怎么跑、最小可运行例子长什么样。

2. 核心概念

  • SystemC:一套 C++ 类库 + 仿真内核,用于系统级 / 架构级建模;不是用来写可综合 RTL 的,而是做 TLM(事务级)、周期近似 / 周期精确模型。
    • 补充「可综合 RTL」:RTL = Register Transfer Level(Verilog/VHDL 描述寄存器间的数据搬运/组合逻辑);可综合=能被综合工具(Design Compiler/Vivado 等)转成真实门级网表/电路。SystemC 常规用途在其之上的抽象层,不会被综合成电路。确实有「可综合 SystemC 子集」+ HLS(Catapult/Stratus 把受限 C++/SystemC 提上来成 RTL),但那是另一条 HLS 流程,非 SystemC 常规用途。
  • sc_module:层次化建模基本单元(宏 SC_MODULE),可层层嵌套,等于 RTL 的「模块 / entity」。
  • port(sc_in / sc_out / sc_inout):模块对外的通信端点,相当于「引脚」;不直接接线,必须 bound 到 channel 才通。端口绑定的是 channel 暴露的接口(sc_signal_in_if / sc_fifo_in_if 等)。
  • channel = 模块间通信 / 同步原语的统称,分两类:
    • primitive channel(从 sc_prim_channel 派生):sc_signal、sc_buffer、sc_fifo、sc_mutex、sc_semaphore、sc_clock、sc_event_queue。
    • hierarchical channel(从 sc_channel 派生):把若干原语/子模块组合成自定义复杂通道的基类(你继承 sc_channel 造自己的类)。
    • 关键层级:sc_signal<T> 是 primitive channel 的一种;sc_channel 不是直接 new 出来当 signal 用的类,而是你继承它造自定义复合通道的基类,日常建模几乎不直接碰。
  • process(SC_METHOD / SC_THREAD / SC_CTHREAD):模块内部真正「跑行为」的东西。
  • sc_clock / sc_time:提供时钟事件与时间单位(SC_NS 等),驱动仿真推进。

3. 知识要点

3.1 核心抽象

模块装进程;模块靠端口(port)连到信号(signal / channel)通信;时钟 / 时间驱动整个仿真。

top (SC_MODULE)
 ├─ M1 (SC_MODULE)  ── sc_out ─┐
 │     └─ 进程 SC_METHOD        ├── sc_signal/channel ──┐
 └─ M2 (SC_MODULE)  ── sc_in ──┘                        │
       └─ 进程 SC_THREAD  ───────────────────────────────┘

3.2 三种进程的区别

  • SC_METHOD:无状态、瞬时执行;由敏感表触发,跑完即返回;不能阻塞、不能 wait();适合组合逻辑 / 纯函数式更新。所谓「状态」必须存在模块成员变量 / signal 里(进程本身不保留执行现场,局部变量返回后即失)。
  • SC_THREAD:有独立栈、可挂起;能 wait(),用于建模带时序的并发行为;最常用。局部变量跨 wait() 保留,像「能暂停续跑的协程」。
  • SC_CTHREAD:时钟边沿触发的 thread(老式写法),新代码基本不用,有状态。
  • 敏感表差异:三种进程都可用 sensitive 指定初始唤醒事件——METHOD 只有静态敏感(sensitive << a << b),被事件触发就跑完返回、不能内部 wait();THREAD 支持静态 + 动态敏感(函数体内 wait(event) / wait(sensitive_list) 等后续事件);CTHREAD 特化为对时钟边沿敏感。另有 dont_initialize() 可阻止进程在 t=0 自动跑一次。

3.3 仿真内核与时间模型

  1. 时间不自动流逝:靠 sc_start(时长) 推进;没事件的时间点会被跳过。仿真由 sc_main(入口,类似 main)显式 sc_start() 拉着走——在 sc_main 里例化模块、绑定端口、调 sc_start()。sc_start() 可多次调用(先 sc_start() / sc_start(SC_ZERO_TIME) 初始化,再分段跑)。内核事件驱动:即使 sc_start(100,SC_NS),中途无排定事件就会停到「下一个有事件的时间点或 100ns」。
  2. 同一 sc_time 内分「求值(evaluate)→ 更新(update)」两步,且可能多轮(delta cycle)直到信号稳定:进程里 a.write(x),别的进程这一拍读到的可能还是旧值,新值等到 update 阶段才生效。
  3. 跨模块 / 跨进程可见性(delta cycle 的核心):
    • 在同一个 delta cycle 的 evaluate 阶段,A 读 signal X 拿到的是旧值;它看不到 B 在同一轮 evaluate 里写的 X——因为 B 的写也要等到本轮 update 才生效。
    • 到了下一个 delta cycle(或下一个时钟周期),A 就能读到 B 的更新值了。delta cycle(零时间)≠ 时钟周期:一个时钟周期内部往往由多个 delta cycle 组成。组合逻辑环要多个 delta cycle 才稳定。
    • 一句话:同 delta cycle 的写对同轮读不可见,对未来时刻可见。
  4. sc_clock 提供时钟事件,sc_time / SC_NS 描述时间。sc_time 是类型(带单位的时间量),SC_NS 是单位常量;构造:sc_time(10, SC_NS),算术运算单位自动换算(1*SC_US + 500*SC_NS 得 1.5µs)。

3.4 sc_clock & sc_time 用法

// sc_clock:产生时钟的 primitive channel
sc_clock clk("clk", 10, SC_NS);                 // 周期 10ns,50% 占空比(默认)
// 完整签名:sc_clock(name, period, duty, start_time, start_value, posedge_first)
sc_clock clk2("clk2", sc_time(10,SC_NS), 0.5, sc_time(0,SC_NS), false, true);

// sc_time:带单位的时间值
sc_time t1(10, SC_NS);
sc_time t2 = t1 + sc_time(5, SC_NS);            // 支持加减/比较,单位自动换算
sc_start(sc_time(100, SC_NS));                 // 推进仿真 100ns

4. 关键公式 / 代码 / 结构

4.1 环境配置与构建(macOS,已实测工具链)

安装与版本:通过 brew install systemc 安装(stable 3.0.2,Accellera)。本机默认未预装,需先执行该命令;装后路径以 brew --prefix systemc 实际输出为准(本机为 /opt/homebrew/opt/systemc)。编译器需 C++17(Apple clang / g++ 均可)。

# 1) 安装(若未装):brew install systemc
# 2) 编译 + 运行(单行命令,避免多行 \ 续行在终端粘贴时失效)
clang++ and_gate.cpp -std=c++17 -I/opt/homebrew/opt/systemc/include -L/opt/homebrew/opt/systemc/lib -Wl,-rpath,/opt/homebrew/opt/systemc/lib -lsystemc -o and_gate && ./and_gate

# 说明:macOS 运行时找的是 DYLD_LIBRARY_PATH(非 Linux 的 LD_LIBRARY_PATH);
# 上面用 -Wl,-rpath 把 dylib 路径写死进可执行文件,运行时无需设环境变量。
# 若不用 rpath,则需在运行前:export DYLD_LIBRARY_PATH=/opt/homebrew/opt/systemc/lib:$DYLD_LIBRARY_PATH

4.2 最小例子:二输入 AND 门

#include <systemc.h>
#include <iostream>
using namespace std;

SC_MODULE(and_gate) {
    sc_in<bool>  a, b;
    sc_out<bool> y;

    void do_and() {                 // SC_METHOD:组合逻辑,瞬时
        y.write(a.read() & b.read());
    }

    SC_CTOR(and_gate) {             // 构造函数宏
        SC_METHOD(do_and);
        sensitive << a << b;        // a 或 b 变化即触发
    }
};

int sc_main(int argc, char* argv[]) {
    sc_signal<bool> sig_a, sig_b, sig_y;
    and_gate g("g");
    g.a(sig_a); g.b(sig_b); g.y(sig_y);   // 端口 bound 到信号

    sc_start(SC_ZERO_TIME);               // 初始化(t=0 初始化阶段)
    sig_a.write(true); sig_b.write(true);
    sc_start(1, SC_NS);
    cout << "a=1 b=1 -> y=" << sig_y.read() << endl;  // 输出 1
    return 0;
}

要点:sc_main 是入口(类似 main),内部 sc_start() 启动内核;端口与信号用 () 运算符绑定。先把上面环境配好,再把本例存为 and_gate.cpp,用同一行编译命令即可运行,输出 a=1 b=1 -> y=1。

4.3 常用 Channel 代码清单

1) sc_signal<T> —— 最常用的值通道(primitive channel)

SC_MODULE(ff) {
    sc_in<bool>  clk;
    sc_in<bool>  d;
    sc_out<bool> q;
    void run() { q.write(d.read()); }
    SC_CTOR(ff) { SC_METHOD(run); sensitive << clk.pos(); }
};

2) sc_buffer<T> —— 每个写都算数的「信号」(同 delta cycle 每次写都触发值变更事件)

SC_MODULE(buf_demo) {
    sc_buffer<int> buf;
    void writer() {
        buf.write(1);   // 触发一次事件:值 1
        buf.write(2);   // 再触发一次事件:值 2(同 delta cycle)
        // 对照 sc_signal:只触发一次(最终到 2)
    }
    SC_CTOR(buf_demo) { SC_METHOD(writer); /* sensitive 绑某事件(示意略) */ }
};

3) sc_fifo<T> —— FIFO 通道(生产者-消费者,阻塞读写)

SC_MODULE(producer) {
    sc_fifo_out<int> out;
    void run() { for (int i=1;i<=3;i++){ out.write(i); wait(10,SC_NS);} }
    SC_CTOR(producer) { SC_THREAD(run); }
};
SC_MODULE(consumer) {
    sc_fifo_in<int> in;
    void run() { while(true){ int v=in.read(); cout<<"got "<<v<<endl; wait(10,SC_NS);} }
    SC_CTOR(consumer) { SC_THREAD(run); }
};
// 顶层:sc_fifo<int> f(4); p.out(f); c.in(f);   // 容量 4

4) sc_mutex —— 互斥锁(保护多 THREAD 共享资源)

SC_MODULE(worker) {
    sc_port<sc_mutex_if> mtx;          // 绑到顶层 sc_mutex
    int id;
    void run() {
        while(true){ mtx->lock(); cout<<"worker "<<id<<" 临界区"<<endl;
                     wait(5,SC_NS); mtx->unlock(); wait(5,SC_NS); }
    }
    SC_CTOR(worker) : id(0) { SC_THREAD(run); }
};
// 顶层:sc_mutex mtx("mtx");  w.mtx(mtx);

5) sc_semaphore —— 计数信号量(许可池 / 限流)

SC_MODULE(limiter) {
    sc_port<sc_semaphore_if> sem;
    void run() {
        while(true){ sem->wait(); cout<<"拿到许可,干活"<<endl;
                     wait(10,SC_NS); sem->post(); }
    }
    SC_CTOR(limiter) { SC_THREAD(run); }
};
// 顶层:sc_semaphore sem("sem", 3);  // 初始 3 个许可  limiter.sem(sem);

6) sc_clock —— 产生时钟的 primitive channel(绑到模块时钟端口)

SC_MODULE(dut) { sc_in<bool> clk; /* ... */ SC_CTOR(dut){ SC_METHOD(tick); sensitive<<clk.pos(); } };
// 顶层:
sc_clock clk("clk", 10, SC_NS);    // 周期 10ns
dut d("d"); d.clk(clk);             // 时钟 channel 绑到端口

7) sc_event_queue —— 定时事件队列(延迟 / 超时通知)

sc_event_queue eq;
eq.notify(5, SC_NS);               // 5ns 后触发一次事件
// 另一进程 sensitive << eq 或 wait(eq) 即可被唤醒

8) sc_channel —— 基类:自定义「层次通道」

class my_bus : public sc_channel {       // 自造带互斥保护的总线通道
    sc_mutex m; int data = 0;
public:
    my_bus(sc_module_name nm) : sc_channel(nm) {}
    void put(int v) { m.lock(); data=v; m.unlock(); }
    int  get()      { m.lock(); int v=data; m.unlock(); return v; }
};
// 顶层:my_bus bus("bus"); 模块用 sc_port<my_bus_if> 暴露 put/get 后绑定 bus

Channel 速查表:

Channel 类型 关键用法 典型场景
sc_signal<T> primitive read()/write() 值传递、组合/时序逻辑
sc_buffer<T> primitive 每次写都触发事件 需「每个写都通知」
sc_fifo<T> primitive sc_fifo_in/out,阻塞读写 生产者-消费者数据流
sc_mutex primitive lock()/unlock() THREAD 共享资源互斥
sc_semaphore primitive wait()/post() 许可池 / 限流
sc_clock primitive 绑 sc_in<bool> 时钟驱动
sc_event_queue primitive notify(delay) 延迟 / 超时事件
sc_channel 基类 继承造自定义通道 复杂复合通道