
第一次在 gem5 里需要挂一个自定义外设模型的时候我几乎没有犹豫就选了 SystemC。原因很现实手头 IP 厂商交付的虚拟原型就是 SystemC/TLM 形式的而 gem5 原生的设备模型又实在不适合做深度二次开发。可真正动手时才发现gem5 与 SystemC 联合仿真环境搭建这件事比想象中琐碎得多版本怎么配、库先编哪个、时间单位怎么对齐、sc_main 怎么组织每一步都有老教程和新版本之间的“兼容性陷阱”。这篇文章是系列的第一章目标只有一个从零开始把 gem5 与 SystemC 联合仿真环境完整搭起来同时把每一步背后的原因讲清楚。适合刚开始接触 gem5、又需要在仿真环境里集成 SystemC TLM 模型的体系结构研究者、SoC 验证工程师和做软硬件协同仿真的同学。1. 先搞清楚一个问题为什么非要 gem5 与 SystemC 联合仿真1.1 单独使用时的能力边界gem5 在体系结构模拟领域几乎是事实标准。它支持多种指令集架构能跑全系统模式可以直接引导 Linux 内核内存层次结构可以细到 Ruby 模型、缓存一致性协议逐字段配置。CPU 侧从简单的 Atomic 到详细的 O3 流水线都有现成模型。这些都是 SystemC 生态不擅长的事情——SystemC 虽然也能做处理器建模但绝大多数团队并不会用 SystemC 从零去写一个乱序执行 CPU 核成本和验证难度都太高。SystemC 的强项在 SoC 级集成。TLM 2.0 是半导体行业虚拟原型和 IP 验证的标准接口总线互联、DDR 控制器、DMA、网卡、中断控制器这类外设模型IP 厂商通常直接提供 SystemC 版本。如果你在做一个 SoC 项目需要验证自己写的一段驱动在“真实 CPU 自定义外设模型”环境里能不能跑系统级互联的事务有没有问题就绕不开 SystemC 模型。问题在于gem5 原生的外设模型虽然数量不少但它的设备模型框架是为“够用”设计的不是为“扩展验证”设计的。想在里面加一个自定义的 NoC 模型、改动一条总线的时序行为、挂一个来自厂商的 TLM 外设会遇到大量框架层面的阻力。1.2 联合仿真真正解决的场景把两者放进同一个进程里协同运行就能把各自主场都发挥出来gem5 负责处理器核、缓存、内存系统的模拟SystemC 负责互联和外设模型。软件栈跑在 gem5 的 CPU 模型上访问外设时通过桥接层落进 SystemC 的 TLM 事务里再由 SystemC 侧的设备模型完成行为模拟。我见过几个典型场景特别适合这种组合驱动开发与验证驱动代码需要真实指令集环境但硬件设备只有 SystemC 虚拟原型联合仿真让驱动可以在全系统环境中跑起来还能打断点观察设备寄存器行为。自定义 NoC 或总线建模gem5 自带的总线模型偏理想想要评估一套自定义片上网络的延迟和仲裁策略时把 SystemC TLM 网络模型塞进 gem5 的主存通路里比改 gem5 本身容易得多。架构探索中的软硬件接口验证CPU 侧用 gem5 做性能分析外设侧用 SystemC 做功能验证两边在同一个时间轴上跑便于统计和调试。1.3 不适合联合仿真的情况也得泼盆冷水并不是所有时候都该上联合仿真。如果只是跑 SPEC CPU 基准测试评估 CPU 微架构单独用 gem5 就足够了加 SystemC 只会拖慢速度、增加配置成本。如果目标是验证 RTL 代码那就别来凑热闹直接走 Verilog/VHDL 仿真器或者 UVM 环境。联合仿真的价值出现在“架构级 CPU 模型”和“事务级外设模型”需要共存的那一刻少了这个前提复杂度完全白花。2. 版本选型一套能顺利编译的组合比想象中更重要2.1 推荐版本组合与理由早期做 gem5 与 SystemC 联合仿真需要从独立的 gem5-systemc 仓库拉代码再配合外部安装的 Accellera SystemC 2.3.x 库使用配置路径繁琐且容易和 gem5 版本脱节。后来 gem5 主线把 SystemC 内核直接挪进了源码树路径在src/systemc构建产物中也多出libsc_gem5_control.so。整体流程比老方案顺畅很多。我建议直接用 gem5 主线或较新的稳定 release 分支比如 v23.0、v24.0 这一档。搭配 Ubuntu 20.04 或 22.04gcc 9 或 11Python 3.8 以上SCons 3.x 或 4.x。守着太老的 gem5 版本很可能绕回独立的 gem5-systemc 时代教程对不上报错也对不上。学这个内容本来就有一定门槛没必要再跟旧工具链搏斗。2.2 Ubuntu 下的依赖安装先把基础依赖装齐。以 Ubuntu 22.04 为例核心包是这些sudo apt update sudo apt install -y build-essential git scons python3-dev \ zlib1g-dev libboost-dev swig ninja-build pkg-config \ libprotobuf-dev protobuf-compiler libgoogle-perftools-dev其中libprotobuf-dev和protobuf-compiler是 gem5 生成 protobuf trace 时需要用的libgoogle-perftools-dev提供 tcmalloc 等性能分析库swig用于生成 Python 绑定。这些都不是联合仿真独有的依赖但缺了任何一个后面编译时可能冒出莫名其妙的错误。我的建议是一步到位全部装上省得后续排查。2.3 源码确认检查你的 gem5 是否支持联合仿真拿到源码后先确认版本里确实带了 SystemC 集成。直接看目录git clone https://github.com/gem5/gem5.git cd gem5 git checkout v23.0 ls src/systemc如果src/systemc目录存在说明这套代码内置了 SystemC 内核可以往下走。如果不存在说明版本太老建议直接切到新版。新版 gem5 构建系统里也已经有对应的构建目标后面编译时直接指定即可。3. 联合仿真的底层机制两个仿真内核如何在一个进程里协同3.1 SCAbsorb时间同步的核心思路gem5 和 SystemC 各自都有独立的离散事件仿真内核。gem5 以 tick 为逻辑时间单位维护自己的全局事件队列SystemC 以sc_time管理进程和事件。两者放进同一个进程后关键问题就是时间怎么同步。这里最核心的机制叫 SCAbsorb思路非常直白SystemC 事件队列里如果没有待处理事件说明这段时间 SystemC 侧处于“空闲”状态gem5 就可以一口气往前大步推进把这段空闲时间“吸收”掉而不是每隔几个 tick 就切来切去。等下一次 SystemC 侧需要被唤醒时再由桥接层通知 gem5 暂停。这个过程可以类比成开车遇到连续绿灯一路没车时不需要每个路口都一脚刹车踩油门直接冲过去等红灯出现时再停。3.2 tick 与 sc_time 的换算逻辑时间同步的前提是单位换算。gem5 的 tick 本身是一个不带物理单位的逻辑计数器SystemC 则要求显式使用SC_FS、SC_PS、SC_NS这类物理时间单位。联合仿真桥接层需要约定一套换算关系常见做法是默认 1 tick 对应 1 ps。这个换算关系到后期调 TLM 事务延时的时候特别重要。假设 SystemC 侧一个外设模型用delay sc_time(500, SC_NS)表示一次读事务的延时按 1 tick 1 ps 的换算它对应 gem5 侧就是 500,000 ticks。反过来看只要 gem5 配置里的时钟频率不变这个比例关系就能保持稳定。实际项目里如果发现 SystemC 侧时间行为跟预估差很多先检查映射关系是否对得上。3.3 sc_start() 如何驱动整个仿真联合仿真的组织方式是让 SystemC 的sc_main成为程序入口gem5 被包装成一个特殊的 SystemC 模块挂进去。sc_main里调用sc_start()后SystemC 内核开始调度同时也会驱动包裹在内部的 gem5 实例让两个事件队列交替执行。用文字示意的话整体结构是这样sc_main() └─ sc_start() ├─ SystemC 事件队列TLM 外设、总线互联模型 └─ Gem5Module封装 gem5 仿真核心 └─ gem5 内部事件队列CPU、Cache、Memory这里需要理解一个关键点虽然sc_main是入口gem5 侧的初始化流程并没有被省略。gem5 仍然会解析 Python 配置脚本、构造 SimObject、创建事件队列只是这一整套启动流程发生在了sc_start()的调用链内部。你在系统里看到的进程是sc_main而不是gem5.opt但从日志和初始化顺序看gem5 的启动逻辑一条不落。4. 编译与链接从 libgem5_opt.so 到你的第一个 sc_main4.1 构建顺序与产物清单联合仿真不能直接使用命令行下的gem5.opt因为那是一个独立可执行程序不会给外部留出嵌入接口。我们需要的是 gem5 的共享库形态libgem5_opt.so让 SystemC 仿真程序在进程里直接链接并调用。推荐构建顺序是两条 scons 命令scons build/ARM/libgem5_opt.so -j$(nproc) scons build/ARM/libsc_gem5_control.so -j$(nproc)这里ARM是目标 ISA也可以换成X86或RISCV。第一条命令生成 gem5 主库第二条生成 SystemC 控制器库。实际构建时第二条命令会依赖第一条的产物所以顺序不要颠倒。构建完成后build/ARM下会出现几个关键文件产物作用libgem5_opt.sogem5 主库包含 CPU、内存系统、Python 嵌入等全部核心模块libsc_gem5_control.soSystemC 控制器负责两套内核之间的桥接与调度gem5.opt独立可执行程序常规模式使用联合仿真场景用不到build/ARM/...下的其他静态库构建中间产物通常不需要直接操作4.2 最小 sc_main 示例gem5 源码的util/systemc/sc_main目录下自带了一个最小的联合仿真程序我的建议是直接用它做第一个验证目标别从零手写。以 gem5 v23.0 附近版本为参考代码结构大致长这样#include systemc // gem5 在 SystemC 中的封装模块头文件 // 不同版本路径略有差异以 util/systemc/sc_main 示例为准 #include gem5/systemc/sc_module.hh int sc_main(int argc, char *argv[]) { // 创建 gem5 子系统实例参数是实例名 Gem5SystemC::Module gem5_module(gem5); // 启动统一仿真gem5 会在这个循环里被逐步调度 sc_core::sc_start(); return 0; }这个程序做了一件最重要的事把 gem5 实例作为 SystemC 层次结构中的一个模块创建出来然后让 SystemC 内核开始运行。像处理器核、缓存这些具体配置并不写在这个 C 文件里而是通过 gem5 的 Python 脚本在初始化阶段动态构建。这个设计意味着同一个sc_main可以配合不同的配置脚本运行灵活性比把一切写死在 C 里高很多。4.3 编译链接命令与库顺序util/systemc/sc_main目录如果自带 Makefile直接执行make就能得到可执行文件。有些版本需要手动编译命令是典型的 g 链接场景g -stdc17 -I$GEM5/src -I$GEM5/build/ARM \ sc_main.cc -o sc_main \ $GEM5/build/ARM/libsc_gem5_control.so \ $GEM5/build/ARM/libgem5_opt.so \ -lpthread -lprotobuf我需要特别提示库的顺序。g 链接时是从左到右解析符号的右边的库如果依赖左边的库链接器会在处理到右边的库时发现符号已经满足但反过来会报 undefined reference。所以libsc_gem5_control.so必须放在libgem5_opt.so前面因为前者依赖后者。很多人第一次编译联合仿真程序就在这一步卡住报错一堆未定义符号其实不是库缺了就是顺序反了。5. 跑一个最小联合仿真场景并验证链路是否真的通了5.1 先跑通自带的 sc_main 示例编译完成后直接运行./sc_main看到 gem5 的启动横幅、版本信息以及 SystemC 内核的相关日志说明桥接链路已经打通。这一步之所以重要是因为它把环境问题从业务逻辑中完全剥离出来——如果这个最小示例都跑不起来后面挂任何 TLM 模型都是白搭。先确认这层地基是稳的再往里面加业务模块。如果sc_main没有任何输出直接退出常见原因是没传 gem5 配置脚本。联合仿真不是sc_main一个可执行文件就能独立完成整个仿真的它仍然需要一个 Python 配置脚本描述系统拓扑。运行时可以通过参数指定比如./sc_main $GEM5/configs/example/se.py --help这条命令至少能验证Python 配置脚本解析链路正常gem5 初始化流程能被成功触发。如果这里输出了se.py的选项说明说明 gem5 的 Python 嵌入和 SystemC 桥接都工作正常。5.2 从 SE 到 FS联合仿真验证怎么选跑完最小示例后可以用真实负载再验证一层./sc_main $GEM5/configs/example/se.py \ -c $GEM5/tests/test-progs/hello/bin/x86/linux/helloSE 模式适合快速验证“联合仿真环境本身是通的”但要注意一个核心局限SE 模式是系统调用模拟CPU 直接截获应用程序的 syscall 并在宿主机侧处理外设模型不会参与其中。也就是说SE 模式下即使挂了 SystemC 外设外设也不会被访问到。真正能发挥联合仿真价值的是 FS 全系统模式也就是让 gem5 引导一个完整 Linux 内核驱动代码在真实的内核设备访问路径上运行访问落到 SystemC TLM 外设模型里。FS 模式相比 SE 模式多两个必要元素内核镜像和磁盘镜像。你可以在 gem5 官方资源页找到预构建的镜像然后通过参数传入./sc_main $GEM5/configs/example/fs.py \ --kernel/path/to/vmlinux \ --disk-image/path/to/disk.img如果运行时提示找不到镜像优先检查路径是否写对、镜像权限是否可读。这里很容易踩到一个误区把fs.py路径当成是给 SystemC 用的配置其实它是给 gem5 侧用的 Python 配置脚本传参逻辑和直接运行gem5.opt configs/example/fs.py一样。5.3 如何判断“联合仿真真的生效了”很多人跑完sc_main看到 Linux 启动就以为联合仿真没问题但这里有个陷阱如果 SystemC 侧没有挂任何自定义模型本质只是“一个系统里跑了个 gem5 全系统仿真”SystemC 内核确实在运行但没有实际参与业务。要验证链路真通最直接的办法是在 SystemC 侧加一个带回调的 TLM 模块在初始化或事务处理时打印日志SC_MODULE(probe_dev) { tlm_utils::simple_target_socketprobe_dev socket; SC_CTOR(probe_dev) { socket.register_b_transport(this, probe_dev::b_transport); } void b_transport(tlm::tlm_generic_payload trans, sc_core::sc_time delay) { std::cout TLM transaction received at sc_core::sc_time_stamp() std::endl; } };如果在运行 FS 场景时控制台输出了这类来自 SystemC 模块的日志那就说明 gem5 侧发起的设备访问确实通过桥接层进入了 SystemC 仿真域联合仿真的数据通路是真正打通的。否则哪怕仿真能跑也只是一具空壳框架。6. 搭建过程中最容易踩的坑和完整排查思路6.1 链接阶段 undefined reference 的完整排查链路这是我见过最多人卡住的一步。现象是编译时一堆未定义符号比如gem5::...之类的名字错误信息能排几十行。很多人第一反应是“库没编好”于是反复重新编译结果问题依旧。排查链路应该是这样走先看第一条报错到底来自哪个符号再用nm去对应库里搜nm -C build/ARM/libgem5_opt.so | grep 符号名如果符号确实在库里面却仍然报 undefined reference那基本可以断定是链接顺序问题。把libsc_gem5_control.so和libgem5_opt.so都放到需要链接它们的对象文件之后顺序调整为从左到右依赖。也就是先写sc_main.o再写libsc_gem5_control.so最后写libgem5_opt.so。这个顺序我实际验证过多次改动一下立刻就过了。还有一种隐藏情况同一个符号同时存在于多个库中报错变成了 multiple definition。这说明你可能在系统的某个路径里还有另一个版本的 SystemC 或 gem5 库g 的-L搜索路径把两者都带进来了。解决方法是检查LD_LIBRARY_PATH和-L参数确保只包含当前构建目录。6.2 仿真启动后卡住或直接退出的几种原因卡住不动的场景也很典型。sc_start()是无参数调用时SystemC 会一直运行到所有进程结束或显式sc_stop()。如果 gem5 侧没有设置仿真结束条件仿真就会无限运行。这种“卡住”不是 bug而是缺少终止条件。排查方法很简单故意用限时启动比如把代码里的sc_start()改成sc_start(1, SC_SEC)看程序是否在 1 秒后正常返回。能返回说明是终止条件的问题不是死锁。直接退出则通常是 gem5 的 Python 配置阶段就失败了。注意看启动日志里有没有 Python traceback比如找不到m5模块、configs路径错误或某个 SimObject 参数不合法。这些错误在gem5.opt下也会出现不能归咎于 SystemC 集成。还有一类退出是因为M5_PATH或资源路径没有配置好FS 模式下找不到内核镜像或 bootloader初始化过程直接放弃。建议一上来就把资源路径设置成绝对路径不要用相对路径省去大量无谓的方向性误导。6.3 版本混用带来的“隐形地雷”版本混用是这类环境搭建中最难察觉的坑。我之前见过一个项目按照一篇老博客设置SYSTEMC_HOME在系统里额外装了一套 Accellera SystemC 2.3.3结果 gem5 内置 SystemC 控制器在运行时行为变得非常怪异报错信息又完全跟 SystemC 库本身无关。排查到最后才发现是链接器优先搜到了系统路径下的外部 SystemC 库两套 SystemC 内核同时存在于一个进程里调度逻辑直接打架。这类问题最有效的规避方法就是不要装外部 SystemC 库。gem5 源码树自带的src/systemc就是基于 Accellera 参考实现改出来的构建和链接时只使用 gem5 源码目录和build/ARM目录下的产物。检查LD_LIBRARY_PATH确认没有把其他 SystemC 安装路径混进去。如果你确实需要用到外部 SystemC 跑别的项目那就靠环境变量或者显式指定绝对路径来区分绝不默认混用。6.4 关于“照着教程做却总差一步”的调试心态最后想分享一句实在话这类联合仿真环境的搭建最耗时间的往往不是环境本身而是“以为环境有问题但其实只是参数传错”的反复试错。我的习惯是一个报错出现后先在当前版本源码的util/systemc目录、SConstruct文件以及官方文档里搜索关键词而不是急着打开搜索引擎找老帖子。版本之间的接口差异真的很大一篇两年前的文章很可能已经失效照着改只会越走越偏。我个人在实际操作中的体会是先跑通自带的官方示例再往里面加自己的 SystemC 模型是一个稳得多的路径。官方示例是经过 CI 验证的能跑通说明你手边的工具链配置基本正确跑不通问题大概率在环境跑通了再挂自己的模型问题就局限于业务代码了。第二章里我会接着讲如何把你自己的 TLM 外设模型真正挂到 gem5 的 IO 通路上并把 SE、FS 两种模式下的接入点差异一起理清。