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 用的类,而是你继承它造自定义复合通道的基类,日常建模几乎不直接碰。
- primitive channel(从
- 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 仿真内核与时间模型
- 时间不自动流逝:靠
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」。 - 同一
sc_time内分「求值(evaluate)→ 更新(update)」两步,且可能多轮(delta cycle)直到信号稳定:进程里a.write(x),别的进程这一拍读到的可能还是旧值,新值等到 update 阶段才生效。 - 跨模块 / 跨进程可见性(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 的写对同轮读不可见,对未来时刻可见。
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 |
基类 | 继承造自定义通道 | 复杂复合通道 |