前言
做 SystemC 仿真、模块间通信时,新手最容易踩坑的就是三种核心交互方式:sc_signal、sc_buffer、sc_event。
很多人疑惑:
- 为什么
sc_signal跨模块传信号永远晚一拍? - 换成
sc_buffer明明值立即更新,为什么线程wait还是有延迟? - 想要完全零延迟握手、跨模块唤醒,该用什么?
本文从底层调度机制、读写行为、事件触发、模块互联方式、适用场景五个维度,彻底讲透三者区别,同时给出工程选型标准,告别仿真时序错乱、握手延迟问题。
一、底层核心基础:SystemC Delta 周期机制
先铺垫最关键的底层逻辑,所有差异都源于它:SystemC 仿真采用两阶段调度:
- 评估阶段:所有 SC_THREAD/SC_METHOD 并发执行,执行赋值、逻辑运算;
- 更新阶段 :统一刷新
sc_signal暂存值、触发信号变化事件。
物理时间不变时,Delta 周期是仿真最小时序单位,也是 "晚一拍" 的根本来源。
二、sc_signal<T>:标准硬件信号(带时序延迟)
1. 核心行为
- 执行
sig.write(val)时,不会立即更新当前值,仅把新值存入内部缓存; - 必须等到当前 Delta 周期结束、进入更新阶段才刷新信号真实值;
- 同 Delta 周期内,写完立刻读,读到的还是旧值;
- 信号变化事件
default_event()/posedge_event()/negedge_event(),仅在更新阶段才触发。
2. 特点
- 严格遵循硬件时序,天然模拟寄存器、时序逻辑、时钟同步信号;
- 支持
sc_trace抓取波形,可在 GTKWave 查看时序; - 遵守单写者规则 :同一 Delta 周期仅允许一个进程写,开启
DEBUG_SYSTEMC会自动报冲突错误; - 跨模块端口互联友好:可直接搭配
sc_in/sc_out端口连线。
3. 致命短板
跨模块、跨线程通信必然晚 1 个 Delta 周期,做即时握手、立即响应场景完全不适用。
4. 适用场景
- 时钟、复位等时序同步信号;
- 模拟寄存器、流水线级间时序传递;
- 需要严格贴合真实硬件一拍一节奏的设计。
5. 实践
假设有以下这个场景,模块A有一个输出作为请求,模块B有从模块A接受请求,当收到请求后模块B会返回一个响应,模块A撤销请求。
cpp
#include <systemc.h>
using namespace sc_core;
// ==============================================
// 模块 B:接收请求 → 回传响应
// ==============================================
SC_MODULE(ModuleB) {
sc_in<bool> req; // 来自 A 的请求
sc_out<bool> ack; // 发往 A 的响应
SC_CTOR(ModuleB) {
SC_THREAD(handle_req);
sensitive << req;
}
void handle_req() {
while (true) {
// 等待 req 上升沿(收到请求)
while(!req.read()){
wait(10,SC_NS);
}
cout << sc_time_stamp() << " ModuleB:收到请求,回复 ack" << endl;
// 回复响应
ack.write(true);
wait(10, SC_NS); // 模拟处理延迟
// 等待 A 撤销请求
while(req.read()){
wait(10,SC_NS);
}
cout << sc_time_stamp() << " ModuleB:请求已撤销,拉低 ack" << endl;
ack.write(false);
}
}
};
// ==============================================
// 模块 A:发送请求 → 等待响应 → 撤销请求
// ==============================================
SC_MODULE(ModuleA) {
sc_out<bool> req;
sc_in<bool> ack;
SC_CTOR(ModuleA) {
req.initialize(false);
SC_THREAD(send_request);
}
void send_request() {
// 发送请求
wait(10, SC_NS);
cout << sc_time_stamp() << " ModuleA:发送请求 req=1" << endl;
req.write(true);
// 等待响应
while(!ack.read()){
wait(10,SC_NS);
}
cout << sc_time_stamp() << " ModuleA:收到响应,撤销请求 req=0" << endl;
// 撤销请求
req.write(false);
}
};
// ==============================================
// 顶层:连接信号
// ==============================================
SC_MODULE(Top) {
sc_signal<bool> req_sig;
sc_signal<bool> ack_sig;
ModuleA* a;
ModuleB* b;
SC_CTOR(Top) {
a = new ModuleA("ModuleA");
b = new ModuleB("ModuleB");
a->req(req_sig);
a->ack(ack_sig);
b->req(req_sig);
b->ack(ack_sig);
}
};
// ==============================================
// 主函数
// ==============================================
int sc_main(int, char**) {
Top top("top");
sc_start(200, SC_NS);
return 0;
}
运行结果:
SystemC 3.0.2-Accellera --- Mar 27 2026 21:51:17
Copyright (c) 1996-2025 by all Contributors,
ALL RIGHTS RESERVED
10 ns ModuleA:发送请求 req=1
20 ns ModuleB:收到请求,回复 ack
30 ns ModuleA:收到响应,撤销请求 req=0
40 ns ModuleB:请求已撤销,拉低 ack
虽然A在10ns的时候发起的请求,但是B在20ns才真正的感知到,相当于数据晚了一拍。
上面的握手行为修改为sc_signal的event:
cpp
#include <systemc.h>
using namespace sc_core;
// ==============================================
// 模块 B:接收请求 → 回传响应
// ==============================================
SC_MODULE(ModuleB) {
sc_in<bool> req; // 来自 A 的请求
sc_out<bool> ack; // 发往 A 的响应
SC_CTOR(ModuleB) {
SC_THREAD(handle_req);
sensitive << req;
}
void handle_req() {
while (true) {
// 等待 req 上升沿(收到请求)
wait(req.posedge_event());
cout << sc_time_stamp() << " ModuleB:收到请求 "<< req.read() <<",回复 ack" << endl;
// 回复响应
ack.write(true);
// 等待 A 撤销请求
wait(req.negedge_event());
cout << sc_time_stamp() << " ModuleB:请求已撤销,拉低 ack" << endl;
ack.write(false);
}
}
};
// ==============================================
// 模块 A:发送请求 → 等待响应 → 撤销请求
// ==============================================
SC_MODULE(ModuleA) {
sc_out<bool> req;
sc_in<bool> ack;
SC_CTOR(ModuleA) {
req.initialize(false);
SC_THREAD(send_request);
}
void send_request() {
// 发送请求
wait(10, SC_NS);
cout << sc_time_stamp() << " ModuleA:发送请求 req=1" << endl;
req.write(true);
// 等待响应
wait(ack.posedge_event());
cout << sc_time_stamp() << " ModuleA:收到响应"<< ack <<",撤销请求 req=0" << endl;
// 撤销请求
req.write(false);
}
};
// ==============================================
// 顶层:连接信号
// ==============================================
SC_MODULE(Top) {
sc_signal<bool> req_sig;
sc_signal<bool> ack_sig;
ModuleA* a;
ModuleB* b;
SC_CTOR(Top) {
a = new ModuleA("ModuleA");
b = new ModuleB("ModuleB");
a->req(req_sig);
a->ack(ack_sig);
b->req(req_sig);
b->ack(ack_sig);
}
};
// ==============================================
// 主函数
// ==============================================
int sc_main(int, char**) {
Top top("top");
sc_start(200, SC_NS);
return 0;
}
SystemC 3.0.2-Accellera --- Mar 27 2026 21:51:17
Copyright (c) 1996-2025 by all Contributors,
ALL RIGHTS RESERVED
10 ns ModuleA:发送请求 req=1
10 ns ModuleB:收到请求 1,回复 ack
10 ns ModuleA:收到响应1,撤销请求 req=0
10 ns ModuleB:请求已撤销,拉低 ack
可以发现所有的事件都是在10ns的时候完成的,req(A)->ack(B)->撤销req(A)->撤销ack(B)
其实这个结论是个悖论,因为中间只是单纯的握手,没有延时,时间当然不会推进。但是其实仿真器底层已经执行过多次评估更新的过程了。
三、sc_buffer<T>:无延迟数值通道(值即时、事件仍延迟)
1. 核心行为
- 执行
buf.write(val),立即刷新内部真实值,当前评估阶段马上生效; - 同进程内写后立刻读,能马上拿到新值,无 Delta 延迟;
- 关键点:值是即时的,但信号变化事件依旧要等到更新阶段触发;
- 线程
wait(buf.default_event()),依然会晚一个 Delta 周期唤醒。
2. 特点
- 接口和
sc_signal完全兼容,可无缝替换; - 支持
sc_trace抓波形,不影响仿真调试; - 同样遵守单写者规则,多进程同周期写会触发竞态;
- 解决了数值读取延迟 ,但线程事件唤醒延迟依旧存在。
3. 常见误区
很多人以为换了sc_buffer就能彻底零延迟,实际上只能解决同进程即时读值 ,解决不了跨线程即时唤醒。
4. 适用场景
- 组合逻辑直通连线、模块间纯数值实时传递;
- 握手信号(req/grant)需要即时读值,但不要求线程立刻唤醒;
- 不想改架构,仅消除数值读取一拍延迟的场景。
四、sc_event:纯事件同步(真正零延迟唤醒)
1. 核心行为
sc_event不存储任何数值 ,只做线程唤醒同步;- 调用
evt.notify()支持立即通知,无需等待 Delta 周期、无需进入更新阶段; - 只要触发通知,等待该事件的
SC_THREAD立刻被唤醒,无任何仿真延迟; - 无数值属性,不能单独传递数据,只能搭配
sc_buffer/ 普通变量存业务值。
2. 特点
- 真正零 Delta 延迟,是 SystemC 最快的跨线程同步方式;
- 不能用 sc_in/sc_out 端口互联,没有端口接口;
- 无法
sc_trace抓取波形,只能靠日志打印调试; - 支持一对多广播:一个事件通知,多个线程同时唤醒。
3. 模块间传递方式
sc_event不能像信号一样端口连线,工程标准做法:
- 在顶层模块定义 sc_event;
- 通过构造函数引用 / 指针传递给各个子模块;
- 子模块保存事件引用,实现跨模块事件同步。
4. 适用场景
- 要求跨模块、跨线程立即唤醒的握手逻辑;
- 调度器、状态机即时跳转、异步中断通知;
- 彻底消除 Delta 延迟,实现仿真层面即时交互。
五、三者核心对比汇总表
表格
| 特性 | sc_signal | sc_buffer | sc_event |
|---|---|---|---|
| 数值更新时机 | 更新阶段,晚 1Delta | 评估阶段,立即更新 | 不保存数值 |
| 同进程写后读 | 读到旧值 | 读到新值 | 无读值概念 |
| 事件唤醒延迟 | 晚 1Delta | 仍晚 1Delta | 无延迟,立即唤醒 |
| 模块端口互联 | 支持 sc_in/sc_out | 支持 sc_in/sc_out | 不支持端口,只能传引用 |
| 波形 Dump | 支持 sc_trace | 支持 sc_trace | 不支持波形抓取 |
| 单写者规则 | 遵守 | 遵守 | 无数值,无写冲突 |
| 核心定位 | 时序硬件信号 | 即时数值通道 | 纯异步同步事件 |
六、工程选型最佳实践
- 要模拟硬件时序、寄存器一拍延迟 → 直接用
sc_signal - 只要数值即时传递,不需要线程立刻唤醒 → 换
sc_buffer - 既要即时读值,又要跨模块零延迟唤醒 →
sc_buffer 存数值 + sc_event 做事件通知组合使用 - 异步中断、即时握手、调度器跳转 → 优先用
sc_event - 追求代码规范、可综合风格:时序用
sc_signal,组合用sc_buffer,异步同步用sc_event,各司其职不混用。
七、总结
sc_signal:带时序延迟,贴合真实硬件,适合时序逻辑;sc_buffer:值即时、事件不即时,解决数值一拍延迟,解决不了线程唤醒延迟;sc_event:无数值、零延迟唤醒,是跨模块即时握手的终极方案;
看懂三者底层 Delta 周期的差异,就不会再被 "晚一拍、仿真时序错乱" 困扰,写 SystemC 模块互联、调度器、握手逻辑时,选型再也不纠结。