
1. 联合仿真到底在解决什么问题1.1 两个工具各自的能力边界gem5这个模拟器做体系结构研究的人应该不陌生。它最擅长的是处理器微架构级别的模拟流水线怎么排、分支预测器用哪种、缓存一致性协议怎么维护这些细节在gem5里都有非常成熟的模型。但换成SoC级别的验证场景事情就变了。你关心的可能不止CPU核还有总线拓扑、DMA引擎、硬件加速器、中断控制器、DDR控制器这一大堆东西。如果全都用gem5去建模工作量会非常可怕而且gem5对这些组件的建模深度也远不如它对待CPU核心那么细致。SystemC恰好补上这块短板。它本质上是一个基于C的建模库加仿真内核系统级工程师常用它做TLM事务级建模仿真。SystemC里的模块、接口、socket这套东西天生就是为SoC互连和外设建模设计的。拿它模拟一个AXI总线、一个带FIFO的DMA或者一块简单的寄存器设备成本很低速度也快。换句话说gem5擅长“微观”的处理器行为SystemC擅长“宏观”的系统集成两者正好互补。1.2 联合仿真的典型架构把gem5和SystemC放在同一个仿真环境里运行思路其实很简单gem5负责模拟CPU和缓存这些核心部件SystemC负责模拟总线、外设和硬件加速器。两者之间通过TLM 2.0协议通信。SystemC里的主设备模块比如一个DMA控制器想访问内存时会走TLM的socket发起事务而gem5里的CPU发出的访存请求也可以通过桥接组件转换到SystemC这一侧来。我在实际中接触过的一个典型场景是这样的芯片团队在做一颗RISC-V核的SoC验证处理器核的微架构还在迭代还没到流片阶段但软件团队已经需要跑固件了。他们就把gem5作为处理器模型的载体把片内总线、外设体系全部用SystemC搭起来两套仿真器在同一个进程中协同运行。软件看到的是一个完整的SoC虚拟平台可以对寄存器做读写、发起DMA传输、响应中断而处理器的行为又足够真实和最终硬件的行为能对上。如果你之前只接触过gem5的SE模式或FS模式可能会对“联合”这个词有点陌生。其实联合的含义就是gem5不再唱独角戏而是把自己的外部端口暴露出来跟SystemC里的其他组件对接。1.3 “从零搭建”到底要搭什么搭建这样一个联合仿真环境核心工作可以分为四块第一准备操作系统依赖和第三方库这一步决定了你后面编译会不会摔跟头第二把系统C库和gem5源码各自编译好确保单测通过第三编写或者借鉴官方的TLM桥接Demo把gem5的端口和SystemC的socket连起来第四跑通一个最小Demo验证数据通路和时间推进都正常。这篇文章会完整走一遍上面的流程同时把我在实际操作中踩过的坑写出来。对于那些准备把gem5用于SoC虚拟平台验证的同学这份记录应该能帮你省下不少排查时间。2. 环境准备依赖装齐少踩一半坑2.1 操作系统与编译器要求搭建联合仿真环境强烈建议使用LinuxUbuntu 22.04或者Debian这类发行版最好。这套工具链里的SCons、gcc、Python脚本在Linux下的兼容性最省心。硬件方面四核CPU加8GB内存是起步配置因为gem5编译一次可能要几十分钟跑仿真又得吃不少内存。如果你需要同时开多个编译任务建议内存加到16GB。编译器版本方面gcc 11以上比较稳妥。SystemC 2.3.3对高版本gcc的兼容性略有些小脾气编译的时候需要显式加上C14或C17标准。Python版本建议3.8以上gem5构建系统和运行时都会用到Python。2.2 gem5的系统依赖安装gem5的构建依赖可以在Ubuntu上一条命令装齐sudo apt update sudo apt install build-essential python3 python3-dev git scons swig zlib1g-dev libgoogle-perftools-dev这里面每一项都有用处。build-essential提供gcc和makepython3-dev让gem5能编译Python扩展模块scons是gem5的构建工具swig用来生成Python绑定zlib和perftools分别是解压和性能分析工具链的支撑。装好之后可以用scons --version检查一下STM32CubeMX的SCons版本过老会导致gem5的构建脚本报错。2.3 SystemC库的编译安装SystemC库从Accellera官网下载即可我用的版本是2.3.3。下载后解压、配置、编译wget https://www.accellera.org/downloads/standards/systemc/systemc-2.3.3.tar.gz tar -xzf systemc-2.3.3.tar.gz cd systemc-2.3.3 mkdir build cd build ../configure --prefix/opt/systemc-2.3.3 --enable-pthreads make -j$(nproc) sudo make install需要注意的地方有两个。一是--enable-pthreads一定要加否则SystemC的多线程特性不可用联合仿真中多个时钟域调度时会出问题。二是高版本gcc编译SystemC头文件时会有警告配置阶段给CXXFLAGS加上-stdc17../configure --prefix/opt/systemc-2.3.3 --enable-pthreads CXXFLAGS-stdc17装完后把路径记好后面编译联合仿真工具需要引用/opt/systemc-2.3.3/include和/opt/systemc-2.3.3/lib-linux64这两个目录。注意不要用系统的包管理器安装SystemC发行版自带的版本往往过旧而且头文件路径和库路径不一定符合后续编译环境的预期。自己编译一次最多花几分钟一劳永逸。3. 获取源码与编译先把两个核心组件跑起来3.1 获取gem5源码gem5的官方源码托管在Google的服务器上直接克隆主干即可git clone https://gem5.googlesource.com/public/gem5 cd gem5建议切到一个稳定的release分支上我常用的是v21.2.1.0git checkout v21.2.1.0版本号的选择有讲究。太老的版本对SystemC联合仿真的支持不够成熟太新的版本还在快速迭代相关文档和坑的数量都对新手不友好。v21.2这个系列的代码结构和官方Demo都比较完整适合作为学习起点。3.2 编译纯gem5做自检先不要急着搞联合把纯gem5编译一遍。这一步的核心目的是确认工具链本身没问题免得后面联合环境出问题时分不清是gem5的问题还是SystemC的问题。scons build/ARM/gem5.opt -j$(nproc)ARM表示目标架构你可以换成X86或RISCV。opt是优化编译带调试信息的版本日常开发和调试用它就够了。首次编译会比较久在我的机器上大概需要三十分钟到一小时。编译完可以跑一个最简单的东西验证一下。echo int main() { return 0; } hello.c gcc hello.c -o hello ./build/ARM/gem5.opt configs/example/se.py -c ./hello如果最后能看到gem5的输出文件m5out/stats.txt生成说明gem5本身已经可以正常工作。3.3 编译带SystemC支持的gem5真正做联合仿真时有两条路可以走。一条路是直接用scons构建时可执行文件让gem5自带SystemC入口支持。这个能力在gem5源码树里已经内置了构建时给它指定SystemC库的路径scons build/ARM/gem5.opt --with-systemc/opt/systemc-2.3.3 -j$(nproc)这样构建出来的gem5.opt在启动时会把SystemC的仿真内核也拉起来你可以在里面注册自己的SystemC模块。不过这条路径对自定义程度不高的情况比较合适如果要在顶层高度定制SystemC环境我更推荐下面这种方式。更通用的做法是参考gem5源码目录下util/tlm里的官方联合仿真示例。这个目录里的代码把gem5封装成一个SystemC模块同时提供了端口适配层。你需要照它的结构写出自己的sc_main.cc然后让构建系统把它一起编译进去。4. 联合仿真的核心配置与示例代码4.1 理解端口桥接的基本套路联合仿真中最关键也最容易搞混的是gem5端口与SystemC socket的对应关系。gem5的内存系统里有经典的masterPort和slavePort概念masterPort是主动发起访存的那一侧slavePort是接收访存请求的那一侧。SystemC的TLM 2.0里对应概念是initiator_socket主动发起事务和target_socket接收事务。把两套体系对接时适配器要么把gem5的masterPort伪装成一个SystemC的target方向要么反过来。为什么会有这种“看似反向”的对接因为桥接层做的事情本质上是协议转换而不是简单的端口同名映射。一个事务从gem5的CPU侧出发先经过masterPort再由适配器转换成TLM的generic_payload通过initiator或者target的socket语义递交给SystemC模块。不同版本的gem5对适配器类的命名不同有的叫Gem5SlavePort有的叫Gem5MasterPort但核心逻辑都是一样的解析gem5的Packet构造TLM的事务对象在b_transport或者nb_transport里完成数据的搬运。4.2 编写SystemC顶层模块下面给你看一个最小结构的顶层文件这个结构参考了gem5官方示例的通用写法实际使用中类名可能不同但骨架是一致的// sc_main.cpp #include systemc #include tlm.h #include gem5_systemc.h class SimpleMemory : public sc_module { public: tlm_utils::simple_target_socketSimpleMemory target_socket; explicit SimpleMemory(sc_module_name name, uint64_t base, uint64_t size) : sc_module(name), data(base), size(size) { target_socket.register_b_transport(this, SimpleMemory::b_transport); } void b_transport(tlm::tlm_generic_payload trans, sc_time delay) { uint64_t addr trans.get_address() - base; if (addr size) { trans.set_response_status(tlm::TLM_ADDRESS_ERROR_RESPONSE); return; } unsigned char* ptr trans.get_data_ptr(); if (trans.is_read()) { for (unsigned int i 0; i trans.get_data_length(); i) { ptr[i] data[addr i]; } } else if (trans.is_write()) { for (unsigned int i 0; i trans.get_data_length(); i) { data[addr i] ptr[i]; } } trans.set_response_status(tlm::TLM_OK_RESPONSE); delay sc_time(10, SC_NS); } private: std::vectorunsigned char data; uint64_t base, size; }; int sc_main(int argc, char* argv[]) { // 创建gem5侧仿真对象传入原来的命令行参数 Gem5SystemC* gem5 new Gem5SystemC(gem5_system); SimpleMemory* mem new SimpleMemory(memory, 0x80000000, 0x10000); // 绑定双方 gem5-master_socket.bind(mem-target_socket); sc_start(); return 0; }创建一个继承自sc_module的顶层类里面实例化gem5系统对象和你自己的SystemC外设然后在构造过程中把所有socket绑定好。sc_main是SystemC仿真的统一入口运行时会自动调用。bind这一行做的事就是把两个模块的接口对接起来后续的TLM事务都会沿着这条连接流动。写这个文件的时候需要注意顶层模块里不要再自己调用sc_time那些时间设置函数时钟和同步逻辑交给gem5和SystemC的适配层去处理。初期最简单可靠的做法是直接调用sc_start()不带参数让它跑到所有事件都完成为止。4.3 gem5侧实例化与参数传递Gem5SystemC这个封装对象内部逻辑相当于一个外部壳壳里的gem5子程序通过解析命令行参数来构建内部的CPU、缓存、内存控制器等模块。也就是说CPU型号、缓存大小、内存延迟这些参数的设置方式和纯gem5环境一样都是通过命令行传参区别只是这次仿真的顶层是SystemC不是gem5自己的Python脚本。在参数传递方面标准的做法是把原始命令行透传给Gem5SystemC的构造函数。我在实际中的做法是保留一份原始的argc和argv传给gem5同时将联合仿真专用的参数比如量子长度单独放进SystemC侧。4.4 参考官方示例配置构建脚本构建联合仿真可执行文件我踩过不少坑。直接拿g把sc_main.cpp和libgem5的静态库链接在一起会发现扯出了一堆额外的动态库依赖比如protobuf、hdf5等光补齐链接参数就够折腾一阵。我的建议是别自己造轮子直接参考gem5源码里util/tlm目录下的构建脚本。官方维护的构建脚本会帮忙处理所有依赖和平台差异。如果你的目标是自定义SystemC设备就复制那个目录在它的工程文件之上加入自己的模块文件然后把SystemC库路径指到/opt/systemc-2.3.3。打开这个目录下的SConstruct文件核心有这么几项配置指定SystemC头文件和库路径指定gem5构建输出目录把目标可执行文件和源代码依赖关系列清楚改完以后直接在util/tlm目录下执行scons -j$(nproc)等它编译完成你就能得到一个自己的联合仿真可执行文件。5. 实测运行与结果验证5.1 最小可运行Demo的设计为了验证环境搭得对不对我建议第一遍先用一个最简单的DemoSystemC侧放一个SimpleMemory模块gem5侧放一个带缓存的最小CPU模型CPU发出一小段访存操作读出一段数据再把这数据写回去。如果两边都能正常工作CPU最后能从内存里拿到正确数值就说明数据通路已经打通。具体用到gem5的哪条配置不用太纠结。只要记住一点刚开始不要上太复杂的架构不要开多个CPU核不要用太深的多级缓存把复杂度降到最低。我这里实际用的参数大致是这个级别./build/ARM/gem5.opt \ --num-cpus1 \ --cpu-typetiming \ --caches \ --l1i-size32kB \ --l1d-size32kB \ configs/example/se.py \ -c ./hello这里有个细节容易被忽略在联合仿真模式下命令行参数里仍然要加入configs/example/se.py因为gem5内部需要Python配置脚本来实例化SimObjects。但SIM_OBJECT的创建和消息传递已经通过适配层被转发到SystemC端了。5.2 运行日志和统计信息怎么读运行联合仿真后gem5依然会像纯gem5一样生成m5out/目录。里面最重要的输出是stats.txt这是所有仿真统计数据的汇总CPU周期数、指令数、缓存命中率、分支预测准确率等。SystemC侧如果有自研模块一般也会通过printf或者sc_trace输出日志。第一次跑通的时候重点看两个地方一是程序退出码是否为0二是stats.txt里simSeconds和simTicks的值是否合理。我遇到过的情况是仿真没报错但simSeconds显示只有几纳秒明显是SystemC时间没被正确推进通常意味着TLM事务没有经过延时累加。5.3 功能正确性的三条验证手段建议采用以下三条验证路径确保联合环境是对的而不是“看起来对”。第一和纯gem5结果做对照。同一个测试程序分别跑纯gem5和联合仿真对比最终写出的数据文件内容应该完全一致性能数值可以有差别。第二在SimpleMemory里加统计变量记录读写次数CPU访问特定地址后检查统计值是否符合预期。第三用SystemC的波形追踪功能把TLM事务的读写事件记录下来看看事务顺序是否和代码逻辑一致。功能验证通过了你才有底气去扩展更复杂的外设模型否则后面排查问题时会陷入两套仿真环境的交叉故障中难以脱身。6. 常见问题与排查技巧实录6.1 sc_main链接错误的排查联合仿真编译时最常见的报错是链接阶段出现undefined reference to sc_main或者undefined reference to sc_start。这种问题通常不是代码逻辑错了而是链接顺序不对。SystemC库在链接命令中必须位于你的目标文件之后因为静态库的解析顺序是从前到后的前面如果有未解析符号而库放在前面链接器不会回头再去找。我的建议是写Makefile时把SystemC的库放在最后一项或者使用--no-as-needed参数。另外还要确保sc_main函数没有被声明为static否则它不会被导出为全局符号SystemC主函数就找不到入口了。6.2 时间同步与时钟域配置的坑联合仿真中最让人头疼的问题往往是SystemC的sc_time和gem5的Tick之间对应不上。默认情况下gem5里的1个Tick对应1皮秒而SystemC里的时钟单位通常设置成纳秒甚至微秒。如果SystemC侧某个模块在b_transport里返回的延时是100ns而gem5侧的仿真只推进了100个Tick双方对“时间过去多少”的理解就会完全错位。解决的办法是用适配层统一管理同步粒度。一般做法是设置一个quantum参数表示每次同步时gem5可以向SystemC推进多长时间。我在项目中常用的值是10us到100us之间。太大会导致时序精度下降太小会频繁同步从而拖慢性能。这个参数没有绝对正确值取决于你的系统对时间精度的要求。6.3 TLM事务不回包的问题如果你发现仿真跑着跑着卡住了CPU一直等待某个访存请求完成多半是SystemC侧某个模块没有正确调用事务的完成响应。TLM 2.0里target模块在b_transport中处理完数据后必须为tlm_generic_payload设置一个响应状态比如TLM_OK_RESPONSE否则gem5侧会一直等下去。我第二次搭环境时就在这个坑里蹲了半下午最后发现是SimpleMemory里忘了判断地址范围导致对越界访问返回了错误响应状态gem5一看响应不对就不再往下推进了。调试这类问题建议在适配器中加打印函数把每次TLM事务的地址、读写标志、响应状态都打出来对照CPU的程序流程就知道问题出在哪个模块身上了。6.4 联合仿真性能偏低的改善思路联合仿真的性能天然比纯gem5慢因为每个事务都要经历两套仿真内核的调度和同步。如果慢到不可接受优先不要怀疑机器性能而要看同步频率。把quantum从1us放大到100us可能仿真速度直接翻倍前提是系统对时序的容忍度允许这样调整。另外编译时选用gem5.opt而不是gem5.debug调试完功能后换成生产模式。SystemC库编译时如果用了通用配置性能和优化编译相比也会有一点差距。如果你对SystemC侧模块的性能要求很高可以考虑把SystemC库用Release模式重新编译一遍会不会那点编译时间换来仿真结束提前半小时相当划算。最后还要检查一个点如果你在SystemC模块里用了大量sc_event动态分配每次事务都new一个对象再delete那这个开销很快会成为瓶颈。改成重用对象池性能会明显改善。这个优化在做高流量DMA建模时非常重要纯功能仿真阶段还不明显。我个人在实际操作里最喜欢的一个小习惯是每次改动联合环境配置后都先用同样的最小测试程序回归一遍功能正确性再去做性能调优。环境是地基地基不稳就别急着盖高楼。这套gem5加SystemC的联合仿真环境搭通之后后面就可以往里面加自己关心的设备模型了。比如接一个DMA控制器、挂一串寄存器组甚至把自研的RISC-V核的TLM模型塞进去做全系统验证。每一步都不难难的只是迈出搭建这第一步的耐心。