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()!构造函数不是进程,不能挂起。我见过有人这么写,结果仿真器直接崩溃。

知识体系图

下面这张图,把本章的核心逻辑串起来了。你看一眼,就能明白各个概念之间的关系。

SystemC模块设计核心知识体系 SC_MODULE SC_CTOR 进程 SC_THREAD SC_METHOD 敏感列表 可挂起/恢复 有wait() 适合时序逻辑 函数调用 无wait() 适合组合逻辑 信号变化触发 边沿触发 事件触发 核心:模块定义 → 构造函数注册 → 进程实现 → 敏感列表驱动

这张图把本章的知识点串成了流水线:先定义模块骨架(SC_MODULE),再通过构造函数(SC_CTOR)注册进程,进程分为SC_THREAD和SC_METHOD两种,最后由敏感列表驱动执行。

嗯,到这里,SystemC模块设计的基础就讲完了。你把这些概念吃透,后面写复杂模块就会顺手很多。

一句话总结:SC_MODULE是壳,SC_CTOR是注册处,进程是干活的人,敏感列表是闹钟。四者配合,才能描述出正确的硬件行为。

公众号:蓝海资料掘金营,微信deep3321