首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
gem5与SystemC联合仿真环境搭建:从零到跑通全流程指南
📅 2026/9/28 19:16:58
✍️ 爱科研究院
👁 阅读 3,247
写这篇文章之前先聊两句很多人第一次听说gem5和SystemC联合仿真第一反应是“这是不是有点重复了”——gem5本身不就是仿真器吗SystemC也是建模语言为什么还要把两个拼在一起用这个疑问很正常。我当初也是被“Unit与Cycle级协同仿真”“异构SoC验证”这些词绕晕了真正把环境搭起来跑通之后才理解了这对组合的价值。这篇文章我就把从零搭建gem5与SystemC联合仿真环境的完整过程记录下来给想做体系结构仿真、想验证自定义硬件模块、或者单纯想弄清楚两个模拟器之间到底怎么通信的同学一个踩过坑的参考路径。1. 为什么要把gem5和SystemC绑在一起——联合仿真到底解决什么问题先说清楚一个前提gem5和SystemC并不是替代关系它们各自擅长的事情完全不同。gem5擅长的是处理器行为建模它对CPU流水线、Cache层次、内存控制器、NoC这些东西的时序建模非常细跑操作系统、跑基准测试都很成熟RISC-V、ARM、x86都有完整支持。但gem5有一个明显的短板它内部跑的是编译后的二进制硬件外设和系统级互连的验证功能很弱你很难把一个用Verilog写的DMA控制器或者一个自定义的IP直接挂到gem5的总线上做联合验证。SystemC恰恰补上了这一块。SystemC本质上是一个基于C的离散事件仿真框架它擅长做系统级建模ESL特别是SoC层面的事务级建模TLM。硬件工程师可以用SystemC把一块新IP的行为描述出来几十行代码就能搭出一个可仿真的网卡模型而且这个模型随后可以逐步细化成RTL。SystemC单独跑很容易但问题是它没有CPU模型你不可能在SystemC里跑一个Linux内核或者跑一个benchmark程序。联合仿真解决的核心痛点就是“我既要一个真实的CPU在跑程序又要让这个程序能访问到我自己搭的SystemC外设模块”。典型场景比如你在设计一个自定义的加速器想评估它在跑某个算法时的性能同时也想看它在真实程序上下文里的交互行为。纯用SystemCCPU部分没法仿纯用gem5你的加速器逻辑没法挂进去。两个绑在一起gem5负责跑程序、出访存行为SystemC负责响应这些访问、模拟外设时序这是目前在学术研究和SoC预验证阶段比较常用的一条路径。那这套方案适合谁我的判断是适合两类人。一类是做体系结构研究的研究生需要评估新硬件模块对系统性能的影响另一类是IC设计公司做SoC架构探索和软硬件协同验证的工程师需要在RTL冻结前先确认系统方案的可行性。至于纯软件背景、没接触过SystemC的同学门槛会高一些但只要能沉住气把环境搭起来后面的收获非常大。一句话总结联合仿真的运行方式写一个SystemC的仿真主程序sc_main在里面用SC_MODULE挂接gem5提供的外设接口然后编译出一个可执行文件这个可执行文件同时包含SystemC仿真内核和gem5模拟器核心。跑起来的时候SystemC内核负责调度事件gem5的核心作为SystemC里一个特殊的模块来执行访存请求会通过一个桥接层转发到SystemC侧由你的模型来响应。理解了这个运行关系后面所有的编译选项、目录结构、链接错误都好办了。2. 环境准备里的版本陷阱选错SystemC版本基本白干搭建联合仿真环境第一步不是装软件而是确定版本组合。我在这里踩过一个大坑SystemC版本选得太新导致gem5源码里的SystemC子模块编译不过。方向不对后面全白费这一步值得多花时间。2.1 gem5端需要什么样的源码分支现在的gem5官方仓库维护在项目主分支上但联合仿真的代码经过多次重构和常规的gem5代码构建路径不一样。你得确认你的gem5源码里存在src/systemc这个目录且里面有完整的SCModuleWrapper实现。早期的一些教学版本这套代码是放在扩展目录里需要手动拷进去的现在的主流版本已经合入了主分支。如果卡在某个老版本的教程上建议直接拉最新的release分支。另外一个关键点gem5的联合仿真代码是构建成sc_gem5这个目标的不是常规的gem5.opt和gem5.debug。在构建阶段scons会先编译SystemC的内核源码gem5代码库里自带了一份SystemC参考实现的镜像再编译连接层最后把sc_main链接进可执行文件。官方仓库里带的SystemC版本非常保守是基于2017年左右的Accellera SystemC实现维护的它和最新的SystemC 3.0会有API层面的冲突。所以我在这一步踩的坑就是别自作聪明地去装一个最新版SystemC然后用系统的库去替换最好用gem5源码配套的那份除非你已经很熟悉SystemC内部结构。2.2 依赖库清单与编译工具链从零开始的话一个“干净”的Ubuntu环境需要准备这些构建工具g建议gcc版本不低于8低版本编译C17的gem5会报各种模板错误、scons版本建议3.0老版本对Python 3支持不好、Python 3开发头文件。第三方库Boost库这是gem5常规构建也必须有的主要给Python嵌入和网络模拟等模块用。可选组件Doxygen如果之后想看源码文档、Graphviz输出拓扑图用但搭环境的阶段用不到不用提前装。这里有个容易忽略的点不要用太老的gcc。很多老教程默认gcc 7甚至gcc 5那些老版本能跑是能跑但编译新版gem5时你会遇到一堆现代C特性的编译错误比如结构化绑定、折叠表达式之类的。最后你会花大量时间在修补编译错误而不是理解联合仿真的逻辑上。我建议直接装gcc-9以上的版本一次到位。2.3 环境变量的作用和坑SystemC发挥作用的路径机制非常“复古”——它依赖SYSTEMC_HOME这种环境变量来找头文件和库。联合仿真构建的时候gem5的构建脚本会去读这个路径。如果你的SystemC头文件被安装在系统的/usr/include/systemc目录里而环境变量没设置编译器就找不到systemc.h。我的做法是保留gem5源码目录下systemc子目录的索引式配置然后在自己编译的SystemC库安装好后设置export SYSTEMC_HOME/opt/systemc export SYSTEMC_INCLUDE$SYSTEMC_HOME/include export SYSTEMC_LIBRARY$SYSTEMC_HOME/lib-linux64 export LD_LIBRARY_PATH$LD_LIBRARY_PATH:$SYSTEMC_LIBRARY:/path/to/gem5/build/ARM注意lib-linux64这个目录名Accellera的SystemC在64位Linux下编译默认会生成这个目录名。有的系统是lib-linux看清楚你机器上实际生成的是哪个。这个环境变量建议写进~/.bashrc因为每次跑联合仿真可执行文件时都要用它来加载动态库。3. 先啃下SystemC编译这块硬骨头版本与源码细节可能有人会问既然gem5自带SystemC内核镜像为什么还要单独编译SystemC这里有两套路径可以走我在实践里都试过各有取舍。3.1 方案A使用gem5源码内置的SystemC代码gem5仓库的src/systemc目录下不仅实现了与SystemC库兼容的接口还包含了一个轻量级的SystemC内核实现类似Accellera参考实现的精简版。如果你想快速跑通完全可以不单独编译SystemC库直接用gem5自带的这套。构建sc_gem5目标时scons会自动编译这些内嵌代码。优点是版本匹配无冲突少踩很多版本的坑缺点是这个内置实现只覆盖了SystemC最核心的离散事件仿真语义比如SC_MODULE、SC_METHOD、sc_signal、sc_time这些。如果你后续想用到SystemC的TLM 2.0收发模型比如simple_initiator_socket这些接口这套内置代码是远远不够的。联合仿真链路中最常见的实际需求恰恰是挂一个TLM模块所以这个方案在很长远的进阶层面上是有天花板的。3.2 方案B单独编译Accellera标准SystemC库这才是真正的“从零”也为你后面写自己的SystemC模型打下基础。我推荐用这个方案虽然前期多花半小时但它给你留了完整的可能性。从Accellera官网拉SystemC 2.3.3或2.3.4源码包解压之后进入根目录安装流程是很经典的configure-make-install三步tar -xzf systemc-2.3.4.tar.gz cd systemc-2.3.4 ./configure --prefix/opt/systemc --with-arch-suffixno make -j$(nproc) sudo make install这里有个非常重要的参数--with-arch-suffixno。加上它之后编译出的库目录是lib而不是lib-linux64这样你在Makefile里写-L$(SYSTEMC_LIB)的时候路径会比较统一。不加前缀的话后续链接时你还要根据架构写lib-linux64或者lib-linux多一道隐患。安装完成之后验证方式很简单# 进入安装好的include目录 ls /opt/systemc/include/systemc # 应该可以看到 systemc.h、sc_config.h 等核心头文件看到systemc.h就说明头文件装好了lib目录下应该能看到libsystemc.so有了库文件后面的链接环节才有着落。3.3 一个容易被忽略的点Static Library还是Shared Library在默认配置下SystemC编译出来的是静态库libsystemc.a和动态库libsystemc.so。联合仿真阶段我建议用动态库。原因很简单如果你编译成静态库每次gem5的scons构建链接时都要把整个SystemC内核嵌进可执行文件第一次链接会非常慢而动态库在运行时加载编译链接速度快而且你修了自己SystemC模块后只需要重新编译模块不用重新链接整个可执行文件迭代效率高很多。不过要注意动态库方式下运行时的库依赖路径必须正确指到libsystemc.so所在位置否则会报error while loading shared libraries: libsystemc.so。这也是我前面特意强调LD_LIBRARY_PATH的原因。4. 编译接入层与sc_main真正把两个模拟器缝合起来的关键环节当SystemC库就绪之后进入联合仿真的重头戏构建gem5这边的sc_gem5目标并且把你自己写的SystemC模块和sc_main链接到一起。4.1 gem5的构建命令和常规构建有什么区别常规构建gem5时命令是python $(which scons) build/ARM/gem5.opt -j8联合仿真时你要构建的目标不是gem5.opt而是sc_gem5.opt。完整命令是python $(which scons) build/ARM/sc_gem5.opt -j8注意build/ARM目录会被复用所以如果之前已经构建过一个gem5.opt不需要清空目录scons会增量编译只把SystemC接入层相关的源文件新加进来。这个过程我第一次跑的时候大概多了十分钟左右取决于机器性能。值得留意的是gem5联合仿真在构建时对Python的依赖关系比较复杂要求Python 3.6以上。如果你机器上Python版本过老编译到gem5的sim/init.cc这个文件时会很痛苦会报一屏的PyImport_*相关错误。我当时的解决方法是直接用Python 3.8一样能正常构建出sc_gem5。4.2 熟悉接入层目录结构SCModuleWrapper与端口注册gem5的联合仿真代码位于src/systemc目录下核心文件是sc_module_wrapper.hh/cc和sc_port.hh/cc。运行机理可以这样理解SystemC需要一个顶层模块gem5在这个顶层模块里以SCModuleWrapper的形式注册自己。当SystemC调度器启动后轮到gem5模块的事件时它会调用gem5的事件队列EventQueue由gem5执行若干条指令的模拟然后返回控制权给SystemC调度器。对应到写代码的层面你需要做这些事在自己的SC_MODULE里声明一个sc_gem5对象。gem5的接入层通常会提供一个工厂方法让你通过配置实例化gem5的某个CPU模型。定义gem5侧的内存端口和SystemC侧的Socket之间的桥接。这个过程要手动声明一个继承自gem5::SystemC::SCModuleWrapper的类或者使用现成的Gem5ModuleWrapper。在模块初始化阶段调用sc_start进入交互仿真。这一块最容易绕晕的是方向SC_module是SystemC侧的主控代码它会实例化gem5对象而不是反过来。搞清楚这个从属关系写代码的时候思路就顺了。4.3 自己写sc_main的最小模板下面给出一个可以跑到“CPU发出访存请求SystemC模块回复”的最小代码骨架方便你理解整个连接方式这个模板我简化过只保留了核心流程实际使用时需要根据自己的外设模型扩展#include systemc.h #include gem5_module_wrapper.hh class MemoryPeripheral : public sc_module { public: sc_inunsigned long addr; sc_outunsigned long data; int latency_count 0; SC_HAS_PROCESS(MemoryPeripheral); MemoryPeripheral(sc_module_name name) : sc_module(name) { // 注册响应函数每个时钟周期检查是否有访问请求 SC_METHOD(handle_request); sensitive addr; } void handle_request() { // 在这里实现你的外设逻辑比如返回某个寄存器值 latency_count; unsigned long a addr.read(); data.write(a * 2); // 模拟一个返回逻辑 } }; int sc_main(int argc, char* argv[]) { sc_clock clk(clock, 1, SC_NS); sc_signalunsigned long addr_sig; sc_signalunsigned long data_sig; // 实例化SystemC侧的外设模型 MemoryPeripheral mem(mem_peripheral); mem.addr(addr_sig); mem.data(data_sig); // 实例化gem5侧的主处理模块 Gem5ModuleWrapper gem5_host(gem5_host, clk); gem5_host.attach_port(addr_sig, data_sig); sc_start(1000, SC_NS); // 跑1微秒的仿真时间 return 0; }这个模板不是一个能直接跑的成品但它把两个重要的接口点展示清楚了SystemC模块定义和sc_main入口。你后续要做的就是把MemoryPeripheral替换成自己的真实外设模型引入TLM socket来传递更复杂的事务。4.4 链接阶段最容易报错的文件config到协议的层层依赖编译链接sc_gem5时最常见的报错集中在无法解析sc_time、sc_event以及sc_interface这些符号上。它们本质上是同一个问题链接器找不到SystemC库或者编译器传入了不相容的头文件版本。check链接阶段的命令行使用的是libsystemc.so还是libsystemc.a。如果是静态库那么-lsystemc必须是最后一个库参数否则在链接时会出现未定义符号的怪错。gem5的scons配置里默认使用动态库方式所以不用过多纠结这个顺序。另外一个容易踩的坑是如果同时装了多个版本的SystemC编译器可能默认找/usr/include/systemc下的老版本头文件但链接器找的是/opt/systemc下的新版本库。两者混用API定义不一致编译可能勉强通过但链接时会报大量duplicate definition或incomplete type错误。根治方法是环境变量统一指到一个地方然后把系统路径里多余的SystemC头文件清理干净。5. 用最小案例验证环境跑通一次完整的联合仿真链路环境搭建做的对不对光看编译通过不算数。真正有意义的是你能跑到CPU真正发出访存请求SystemC侧模型真实响应并且仿真时间正确推进。这一步我建议用一个极简的案例来验证不要一上来就挂复杂的DMA模型那样出了问题都不知道从哪查。5.1 从gem5标准配置里挑一个最简CPU模型验证阶段选什么CPU模型很关键。我建议用atomic CPU搭配简单的single-core配置不要选带Cache的detailed模型因为Cache会引入大量虚假的路径事件增加调试难度。最简单的方式是利用gem5的configs/deprecated/example/fs.py或se.py启动脚本但需要做一点修改把内存端口指向SystemC模块而不是标准内存控制器。在启动脚本里你会看到类似这样的代码root.cpu[0].createInterruptController() root.cpu[0].connectBus(root.membus) root.membus.master system.peripheral_slave # 这里替换为自己的SystemC侧端口做这个修改的原理是gem5里CPU发出的所有访存请求都会经membus路由你只要把路由的目的端换成SystemC模块的socket就完成了对外设模型的接入。这一步之后跑一个简单的打印程序如果终端能看到SystemC侧打印的“received request”日志链路就已经通了。5.2 用Printf Epoch验证时序推进SystemC和gem5的时间推进粒度不同SystemC是基于离散事件的逻辑时间gem5则有自己的事件周期。联合仿真时两边需要互相“让步”。一个很实用的验证方法是在你的SystemC模块里写一个SC_METHOD每隔100个时钟周期打印当前的sc_time_stamp()同时用gem5侧的setDebugFlag开启访存的printf跟踪两边的日志要对得上。下面是我验证时用的一个打印思路// 在SC_MODULE的构造函数里注册一个每周期触发的进程 SC_METHOD(probe_clock); sensitive clk.pos(); void probe_clock() { if (counter % 100 0) { cout sc_time_stamp() gem5 request count: gem5_host.get_request_count() endl; } counter; }跑完之后观察日志里sc_time_stamp()是否在推进。如果时间一直卡在0不动大概率是时钟事件没有正确触发回到构建模块的敏感列表上查问题。5.3 第一次运行报错的常见清单第一次跑通联合仿真大概率会遇到以下几类报错这里列一个排查表按出现频率排序报错特征根因解决方向libsystemc.so: cannot open shared object fileLD_LIBRARY_PATH没有包含SystemC库目录检查环境变量确保指向真实存在的so文件generic gem5 fatal error: port already connectedCPU端口已经连到某个默认内存端口又被SystemC模块重复连接修改启动脚本用SystemC端口替换默认内存端口而不是追加Segmentation fault immediately after sc_startsc_main里gem5模块实例化顺序不对gem5核心尚未初始化就开始仿先启动gem5核心模块再调用sc_startSimulated time stuck时钟源接错SystemC侧的事件没在正确边沿触发检查敏感列表用的是clk.pos()还是默认事件undefined reference to sc_mainsc_main没有被链接进可执行文件检查scons构建配置里是否正确包含你的源文件这些坑我几乎都踩过一轮尤其是前两个排查起来非常费时建议直接对着表自查能节约大量时间。6. 再往前走一步让联合仿真支持真实工作负载环境跑通“最小案例”之后很多人会问那我能不能在联合仿真的gem5里跑Linux系统能不能跑Benchmark程序这里必须坦诚说明一个实际情况可以但要做好性能很慢的思想准备并且启动流程要比裸gem5冗长得多。6.1 让CPU执行真实指令流时的联合仿真性能SystemC的事件驱动内核和gem5的事件驱动内核在每一轮交互时都有协商成本。尤其在CPUs处于活跃状态比如跑循环密集的Benchmark时gem5每推进几个CPU周期就要和SystemC侧交接一次这种循环调度模式效率远低于gem5单独运行。我实测过同一个测试程序纯gem5模拟只需要几分钟跑完的挂在SystemC联合仿真下可能要跑半小时以上。所以验证阶段尽量用短小的程序比如printf、内存读写、简单数组运算。真正评估复杂加速器性能时建议用采样式仿真的思路不要全时间跑完只跑到关键区间。6.2 事件协商机制的正确理解跨时钟域的含义联合仿真的“时间推进策略”是有讲究的。SystemC维护自己的sc_timegem5维护自己的Tick两者通过一个固定的换算比例关联。默认情况下gem5的1 Tick对应SystemC的1皮秒根据配置可能不同。如果SystemC侧外设的时钟周期是10纳秒而gem5侧CPU主频是2GHz周期0.5纳秒那么在10纳秒的SystemC时钟周期内gem5已经推进了20个CPU周期。这种不同步是非常正常的因为两侧建模精度不同。对我写外设模型时的启发是不要假设“SystemC一个时钟周期里gem5的访存请求数量是固定的”你要把外设模型设计成“任意时刻都可能收到gen请求”的状态机而不是固定时序的状态机。这也是SystemC行为级建模和RTL时序建模之间的本质差异。6.3 扩展自定义外设模型时要注意的API边界最终跑通后你想再接一个更复杂的外设比如一个带状态寄存器的网络控制器或者自定义加速器。这里要提醒你一个容易翻车的点gem5的接入层目前对外设的访存接口支持是有边界的不是所有在gem5内部可以使用的端口类型都能在SystemC侧直接暴露。目前稳定可用的做法是在SystemC侧定义一个sc_portsc_fifo_out_ifgem5_packet_t之类的TLM端口或者直接自定义一个继承sc_interface的端口类。让这个端口实现对gem5的recvTiming回调函数。在gem5侧编写一个ExternalSlave对象负责把gem5内部请求转发到这个SystemC端口。理论上一通道理实际操作时你会遇到不少类型转换和指针生命周期的问题。我的建议是仿照gem5源码里自带的dumpmem示例去改写而不是凭空想一套新接口。“站在示例之上改”比“从零设计接口”稳妥十倍。7. 调试联合仿真环境的三个实用技巧既然环境已经搭起来了最后分享一些我在反复折腾过程中沉淀下来的调试技巧这些经验在官方文档里基本找不到系统性的说明。第一个技巧用--debug-flagsMemoryAccess定位访存方向。联合仿真中一旦SystemC模块没收到预期的请求你无法确定是CPU没发请求还是请求在桥接层被吞了。这种时候直接在gem5启动命令行加--debug-flagsMemoryAccess --debug-filetrace.txt把所有访存事件导出然后对照系统C模块的日志时间戳就能判断问题出在哪个环节。我的经验是80%的“系统C侧没收到请求”问题最终出在启动脚本里端口没连对查trace比反复改SystemC模型端口的敏感列表效率高得多。第二个技巧善用gdb挂到sc_main入口。联合仿真的错误往往不是逻辑特别深而是在两个事件队列交界处出现的野指针。这时候在sc_main断点单步走过sc_start()直到首条访存请求出现。用于GDB确认SystemC侧的模块是否被初始化、指针是否为空往往几个回合就能定位出问题。定期检查调试符号SystemC库如果是release编译很多内经变量的值根本看不到建议再编译一个--enable-debug版本的SystemC库备用排查问题时替换用。第三个技巧打印两个时间源。在联合仿真调试时最让我困惑的时刻是分不清日志里的时间到底是gem5的Tick还是SystemC的ns。建议写一个全局的打印宏把两种时间同时打出来#define TRACE_SYNC(msg) do { \ std::cout [SYSTEMC sc_time_stamp() ] \ [GEM5 Ticks gem5_host.current_eventqueue_time() ] \ msg std::endl; \ } while(0)这个宏帮我省了无数核对日志对齐的时间。你在自己的开发环境里只要把gem5_host替换成你的模块实例名就行。说到底联合仿真环境的搭建并不神秘核心就是两个事件队列的桥接。熟悉了这套编译、连接、验证的流程之后你就能把注意力完全放在自己真正关心的外设模型和系统架构上而不再被环境折腾。这也正是我写这篇长文的初衷帮你把环境这件事一口气跑通把时间花在真正该研究的问题上。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/9/28 19:16:58
信创动环监控技术穿透:从协议适配到智能闭环的全栈重构
2026/9/28 19:16:57
从对话到操控:用OpenClaw+TaoToken打造产线指挥官Shell骨架
2026/9/28 19:11:57
工业物联网感知系统全链路实战:从传感器选型到RESTful API设计
2026/9/28 20:07:01
AI虚拟细胞:多组学整合→临床转化
2026/9/28 20:07:01
【BMS 系列教程】电池 SOH 特征工程实战:IC 曲线 + 物理特征,R² 从 0.49 提升到 0.90
2026/9/28 20:07:01
基于springboot + vue企业后台管理系统(源码+数据库+文档)
2026/9/28 20:07:01
论文选题怎么定?26年实测5款辅助工具清单
2026/9/28 20:07:01
代码即画笔:用 Markdown 优雅地“写”出一张时序图
2026/9/28 20:02:01
173、MLIR的Reproducibility(可重现性)保证
2026/9/28 0:04:25
新手从零搭建网站促销活动策划避坑指南:3个方案费用全拆解
2026/9/28 0:04:25
网站被黑挂马?3步图解步骤搞定软件介绍下载网站建设安全
2026/9/28 0:04:25
国内可以做的国外兼职网站进阶技巧
2026/9/28 2:37:38
网站建设的英语怎么说?别只背单词,看完这套安全完整流程才敢上线
2026/9/28 5:00:42
新手入门看这篇:建设网站加盟避坑指南与SEO实操
2026/9/28 8:17:28
论文AIGC疑似度是什么意思?想查论文AI率有哪些免费工具?