4、SystemC模块设计:SC_MODULE定义、SC_CTOR构造函数、进程(SC_THREAD/SC_METHOD)与敏感列表
好,咱们今天聊聊SystemC模块设计的核心。说白了,就是怎么用代码把硬件模块“画”出来,再让它动起来。
我刚开始接触SystemC时,总觉得它跟C++差不多,写起来很顺手。但真正上手做项目才发现,硬件描述和软件编程完全是两码事。你想想看,软件是顺序执行的,而硬件是并行跑的。这个思维转换,是入门第一道坎。
SC_MODULE:模块的骨架
SC_MODULE是SystemC里最基本的构建单元。你可以把它想象成一个芯片的“黑盒子”——有输入、有输出、内部有逻辑。
定义方式很简单,用宏SC_MODULE(模块名)来声明一个类,继承自sc_module。我个人习惯在模块名后面加个“_mod”后缀,方便区分。
SC_MODULE(counter_mod) {
// 端口声明
sc_in<bool> clk;
sc_in<bool> rst_n;
sc_out<int> count;
// 内部变量
int internal_count;
// 构造函数
SC_CTOR(counter_mod);
};
嗯,这里要注意:SC_MODULE本质上就是一个C++类,但它被SystemC库赋予了特殊的“硬件语义”。你可以在里面放任何C++代码,但只有用SystemC提供的机制(端口、进程、事件等)才能描述硬件行为。
SC_CTOR:模块的“出生证明”
SC_CTOR是构造函数,负责初始化模块。它干三件事:
- 注册进程(SC_THREAD或SC_METHOD)
- 绑定端口
- 初始化内部变量
我曾经犯过一个低级错误——在SC_CTOR里写了复杂的计算逻辑。结果仿真时模块初始化就卡死了。后来才明白,构造函数只做“登记”工作,真正的逻辑要放到进程里。
SC_CTOR(counter_mod) {
internal_count = 0;
SC_METHOD(comb_logic);
sensitive << clk.pos() << rst_n;
SC_THREAD(seq_logic);
sensitive << clk.pos();
dont_initialize();
}
看到没?SC_CTOR里只做两件事:初始化变量、注册进程。其他什么都别干。
进程:SC_THREAD vs SC_METHOD
这是SystemC最核心的概念。进程就是硬件里的“always块”或“process”。SystemC提供了两种进程类型,各有各的脾气。
| 特性 | SC_THREAD | SC_METHOD |
|---|---|---|
| 执行方式 | 独立线程,可挂起/恢复 | 函数调用,执行完就结束 |
| 状态保持 | 自动保持(局部变量存活) | 不保持(每次重新执行) |
| 适用场景 | 时序逻辑、状态机 | 组合逻辑、简单计算 |
| 敏感列表 | 静态敏感列表 | 静态/动态敏感列表 |
| wait()使用 | 可以 | 不可以 |
为什么会有两种?说白了,SC_THREAD适合描述“有记忆”的电路,比如计数器、状态机。SC_METHOD适合描述“纯组合”逻辑,比如加法器、译码器。
我建议新手先掌握SC_THREAD,因为它更接近RTL思维。等你熟练了,再用SC_METHOD优化仿真性能。
敏感列表:进程的“闹钟”
敏感列表告诉进程:什么时候该干活。没有敏感列表,进程就像没有闹钟的人——要么一直睡,要么一直醒。
SystemC支持多种敏感方式:
sensitive << signal:信号任何变化都触发sensitive << signal.pos():上升沿触发sensitive << signal.neg():下降沿触发sensitive << event:自定义事件触发
我曾经在项目里遇到一个bug:一个SC_METHOD的敏感列表漏了一个信号,结果仿真结果跟RTL对不上。查了三天才找到原因。从那以后,我写敏感列表都会对照RTL的always块,一个一个信号核对。
核心原则:敏感列表里的信号,必须是进程里读到的所有输入信号。少一个,仿真就错;多一个,仿真就慢。
实战:一个简单的计数器
咱们写个完整的计数器模块,把上面这些概念串起来。
#include <systemc.h>
SC_MODULE(counter_mod) {
sc_in<bool> clk;
sc_in<bool> rst_n;
sc_out<int> count;
int internal_count;
SC_CTOR(counter_mod) {
internal_count = 0;
SC_THREAD(count_process);
sensitive << clk.pos() << rst_n;
dont_initialize();
}
void count_process() {
while(true) {
wait(); // 等待敏感事件
if(!rst_n.read()) {
internal_count = 0;
} else {
internal_count++;
}
count.write(internal_count);
}
}
};
这个例子虽然简单,但包含了所有核心要素:模块定义、端口声明、构造函数、进程注册、敏感列表、进程实现。
小技巧:SC_THREAD里一定要有wait(),否则会死循环。SC_METHOD里绝对不能有wait(),否则编译报错。这个区别,我刚开始时经常搞混。
避坑指南
我踩过的坑,列出来给你参考:
- 端口读写顺序:先读输入,再算逻辑,最后写输出。顺序乱了,仿真结果就乱了。
- 敏感列表遗漏:对照RTL的always块,一个信号一个信号核对。
- SC_THREAD死循环:检查wait()是否在正确位置。
- SC_METHOD状态丢失:需要用成员变量保存状态,不能依赖局部变量。
警告:千万不要在SC_CTOR里调用wait()!构造函数不是进程,不能挂起。我见过有人这么写,结果仿真器直接崩溃。
知识体系图
下面这张图,把本章的核心逻辑串起来了。你看一眼,就能明白各个概念之间的关系。
这张图把本章的知识点串成了流水线:先定义模块骨架(SC_MODULE),再通过构造函数(SC_CTOR)注册进程,进程分为SC_THREAD和SC_METHOD两种,最后由敏感列表驱动执行。
嗯,到这里,SystemC模块设计的基础就讲完了。你把这些概念吃透,后面写复杂模块就会顺手很多。
一句话总结:SC_MODULE是壳,SC_CTOR是注册处,进程是干活的人,敏感列表是闹钟。四者配合,才能描述出正确的硬件行为。
公众号:蓝海资料掘金营,微信deep3321