在我们的自定义 CPU 上运行毁灭战士并走红
毁灭战士 # 毁灭战士 毁灭战士是由 id Software 制作的一款视频游戏,于 1993 年发布。它是游戏史上的一次革命,席卷全球,并定义了现代第一人称射击游戏。它的流行催生了“毁灭战士可以在任何地方运行”这一说法。为了证明这一点,毁灭战士几乎被移植到了所有东西上,从微控制器到烤面包机,甚至是细菌。两周前,我们成功地在一台我们从头开始构建的 CPU 上运行了(爬行)毁灭战士。老实说,我仍然不敢相信。我们究竟做了什么呢?好的,我们在逻辑门级别设计了一个自定义 CPU,将其连接到外设,调整了毁灭战士源代码以在我们的机器上运行,并将其部署到 FPGA 上以实时运行。在运行毁灭战士之前,我们仅运行过一些自己编写的简单程序,比如乒乓球和曼德布罗特集合。现在我们可以运行完整的发布游戏,但到达这里的过程相当崎岖。 要求 从我们在这篇文章中的流水线设计开始,我们的目标是更复杂的程序。乒乓球很好,但它是从 70 年代的。我们想跃入 90 年代。然而,我们面临两个重大问题:内存和速度。较大的程序需要更大的内存,而我们的设计只能利用 FPGA 的 BRAM,容量不到一兆字节。毁灭战士的基本共享软件(doom1.wad)有 14 兆字节。这甚至不包括运行程序所需的内存,仅仅是存储它。第二个障碍是速度。虽然毁灭战士对于现代 PC 是轻而易举,但对于我们的 CPU 却是相当艰难。我们需要更快。因此,Liam 和我决定分别解决每一个问题。Liam 正在为乱序处理打下基础,这将使得并行性和流水线完成技巧大大增强。我则负责内存集成。虽然这听起来很简单:只需连接一个额外的内存芯片,但现实要复杂得多。 内存集成 在我们最初的 CPU 设计中,内存非常清晰。FPGA BRAM 具有 1 个周期延迟,并且与之接口非常简单。这种一致性意味着我们的流水线处理器无需停顿以满足内存需求,因为一致的延迟直接被纳入我们的流水线中。此外,BRAM 是粒状的,我们可以逐字读取和编辑内存。另一方面,DDR3 内存速度慢,延迟可变,并且宽总线宽度。这使得内存操作变得更加复杂和不可预测。速度也慢得多。如果每个内存操作都发送到 DDR3 内存,CPU 将会变得缓慢如蜗牛。这就是缓存的作用。程序并不总是使用所有内存,因此缓存将活动内存区域存储在 BRAM 中以提高访问速度。一个优化良好的缓存几乎可以消除由于 DDR3 带来的额外延迟。 设计 这个版本的 CPU 使用相对标准的 5 阶段流水线:指令获取、解码、寄存器读取、执行、写回。此外,这种设计将内存操作抽象成统一接口,以简化核心流水线阶段。 核心流水线 指令获取阶段跟踪程序指针,并通过指令缓存(ICache)从内存中获取正确的指令。与之前的设计不同,它内部处理重定向和停顿。随着 DDR 内存的加入,重定向变得更加复杂。之前,内存的延迟是一个周期,因此可以每个周期获取一条指令,并在重定向请求到达时切换内存请求地址。然而,在新设计中,如果获取阶段收到重定向请求,内存可能已经有请求在飞行中。我们现在需要跟踪来自内存的下一个响应是否无效,然后请求正确的地址。在解决这个问题后,获取阶段工作得很好。解码阶段很简单。它接受 32 位指令,将其分解为组成部分,并将其保存到一个指令束中,该指令束被发送到流水线的前面。现在,这一阶段也有一些独特的问题,但我会把这留到后面的部分。 读取阶段经过显著修改。我们之前的寄存器文件的一个问题是它使用组合读取,减慢了我们的 CPU 速度。这个版本的读取采用流水线读取,这增加了一个周期的延迟,但缩短了临界路径延迟。另一个变化是读取后写入(RAW)危害处理。RAW 危害是指在待处理写入完成之前,从寄存器中读取,导致获取到错误的数据。之前的读取阶段内部跟踪通过的最后几个寄存器写入。这在逻辑上是高效的,但脆弱且不总是有效。更严格的测试显示,特殊情况使它无法检测到危害。新版本依赖于由周围核心计算并输入到该阶段的寄存器使用图(RUM)。RUM 是一个 32 位图,跟踪寄存器是否正在使用。这自然可以扩展到可变的流水线长度,而无需任何魔术数字。使用 RUM,读取阶段
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡