在MCU上复刻阿波罗导航计算机:AGC指令集仿真与嵌入式实现
发布时间:2026/10/15 5:25:09 锦皓数字建站

1. 项目缘起与整体设计思路Apollo-11 这个标题一摆出来懂行的人脑子里立刻会跳出两个画面一个是人类首次载人登月的壮举另一个是麻省理工仪器实验室那台只有 72KB ROM 的导航计算机。我这次要聊的就是围绕后者展开的一个复刻项目——用现代微控制器重新实现阿波罗导航计算机AGC的核心指令集与执行环境。说白了就是在一块巴掌大的开发板上跑通当年把三个人送上月球的那些代码。这个项目解决的核心问题很具体原始 AGC 硬件早已停产备件稀缺且价格离谱而现存的仿真器大多跑在桌面系统上体积大、依赖多没法塞进嵌入式场景做教学演示或硬件在环测试。我的目标是把 AGC 的 CPU 仿真、存储器模型和基本 I/O 通道压缩到一个资源受限的 MCU 上让它能独立运行不依赖操作系统上电就能跑。适合谁来参考嵌入式开发者、复古计算爱好者、航天史技术研究者以及想拿真实案例练手指令集仿真的人。哪怕你之前没碰过 AGC只要会写 C 语言、懂基本的数字电路概念跟着走也能把东西跑起来。整体设计上我做了几个关键取舍。第一指令集仿真采用解释执行而非 JIT 编译原因是 AGC 的指令格式特殊15 位字长、单累加器架构JIT 的收益在 MCU 上并不明显反而增加代码复杂度。第二存储器模型用分页加 Bank 切换的方式还原原始架构而不是简单映射成线性数组这样才能正确跑通那些依赖 Bank 寄存器切换的原始程序。第三I/O 通道用查表加回调的方式模拟把每个通道号映射到一个函数指针既保持了时序上的近似又方便后续扩展外设。这些选择背后的逻辑是一致的在资源约束下优先保证行为正确性性能可以妥协但时序语义不能丢。提示AGC 的 15 位字长是个容易踩坑的地方。很多仿真器为了省事用 16 位结果在溢出判断和移位指令上出现偏差跑简单测试看不出来一跑原始任务代码就露馅。2. 核心细节解析与实操要点2.1 AGC 指令集的关键特征AGC 的指令集跟现代 CPU 差别很大理解这几个特征才能把仿真写对。首先是字长15 位数据加 1 位奇偶校验实际有效数据 15 位。累加器 A 是 16 位多出来的那一位用于进位捕获。其次是寄存器结构除了 A 之外还有 L低位累加器、Q返回地址、Z程序计数器、EB 和 FBBank 寄存器。指令格式分两类一类是基本指令操作码加 12 位地址另一类是扩展指令通过操作码 000 触发再用后续字指定具体操作。实操中最容易出错的是地址解析。AGC 的地址不是简单的线性偏移而是受 EB、FB 两个 Bank 寄存器影响。EB 管的是“可擦写”区域FB 管的是“固定”区域。同一段代码在不同 Bank 配置下访问的物理地址完全不同。我在第一版仿真里忽略了 EB 的切换逻辑结果原始程序里的变量读写全乱套调试了整整一个周末才定位到问题。2.2 存储器模型的分页设计原始 AGC 的存储器分两类固定存储器Core Rope相当于 ROM和可擦写存储器相当于 RAM。固定存储器 36KB分成 8 个 Bank每个 Bank 1KB通过 FB 寄存器选择。可擦写存储器 2KB分成 8 个 Bank每个 Bank 256 字通过 EB 寄存器选择。这个分页结构必须在仿真里如实还原否则程序里的 Bank 切换指令就失去意义。我的做法是用两个二维数组分别模拟固定和可擦写存储器再维护 FB、EB 两个全局变量。每次取指或读写数据时先根据当前 Bank 寄存器算出物理索引再访问数组。这里有个细节固定存储器的 Bank 切换有奇偶校验位的特殊处理某些地址段在切换时会触发奇偶错原始程序依赖这个行为做自检。我在仿真里加了一个可配置的开关默认关闭奇偶校验需要跑自检程序时再打开。2.3 I/O 通道的模拟策略AGC 的 I/O 不是内存映射而是通过专门的通道号访问用 IN 和 OUT 指令操作。通道号是 9 位理论上 512 个通道实际用到的有几十个包括 DSKY 显示、键盘输入、雷达接口、发动机控制等。仿真时不可能每个通道都接真实外设我的策略是分层处理核心通道如 DSKY 相关的通道 10、11、163用回调函数实现完整逻辑次要通道用空操作加日志记录调试时能看到访问痕迹就行。通道回调的注册用一张函数指针表初始化时把每个通道号对应的处理函数填进去。这样做的好处是扩展方便想加新外设只需写个函数再注册不用改核心仿真循环。实测下来DSKY 的显示刷新逻辑占了整个 I/O 处理的大头因为原始程序刷新频率高回调函数里不能做耗时操作否则会拖慢仿真速度。注意通道访问的时序在原始硬件上是有严格要求的某些通道读写之间有最小间隔。仿真里如果完全不考虑时序跑简单程序没问题但跑复杂的交会对接程序时会出现状态不一致。我的建议是在回调里加一个简单的周期计数器对时序敏感的通道做访问间隔检查。3. 实操过程与核心环节实现3.1 开发环境搭建与工具链选择我选的是 ARM Cortex-M4 内核的开发板主频 168MHz192KB RAM1MB Flash。为什么选这个配置AGC 仿真本身对算力要求不高但需要足够的 RAM 存放存储器模型和运行栈。192KB 听起来富裕实际上固定存储器 36KB 加可擦写 2KB再加上仿真状态和栈空间用掉一半很正常。工具链用 GCC ARM 嵌入式版本构建系统用 Makefile不引入任何 RTOS裸机跑。代码结构分四层最底层是硬件抽象层封装 GPIO、定时器和串口往上是 AGC 核心仿真层包含 CPU、存储器和 I/O再往上是调试接口层提供断点、单步和寄存器查看最上层是应用层负责加载程序镜像和启动仿真。分层的好处是移植方便换一块板子只需改硬件抽象层核心仿真代码不动。3.2 CPU 仿真循环的实现细节CPU 仿真主循环的逻辑是取指、译码、执行、更新周期计数。取指阶段从固定存储器读取 15 位字根据当前 FB 和 Z 寄存器算出物理地址。译码阶段判断是基本指令还是扩展指令基本指令直接查操作码表扩展指令再读一个字确定具体操作。执行阶段根据指令类型调用对应的处理函数更新累加器、寄存器和存储器。周期计数用于模拟原始硬件的执行时间某些程序依赖周期数做延时。这里有个性能优化的点操作码表用函数指针数组实现避免冗长的 switch-case。实测下来函数指针调用的开销在 M4 上可以接受而且代码更清晰。另一个优化是地址计算把 Bank 切换的乘法运算提前算好存成查找表运行时直接查表省掉每次取指的乘法。这两个优化加起来仿真速度提升了大约 40%跑原始程序能到实时性的三分之一左右做演示足够了。3.3 程序镜像的加载与验证原始 AGC 程序以二进制镜像形式存在加载时需要解析成仿真器能识别的格式。我的做法是写一个 Python 脚本把镜像文件转换成 C 数组编译时直接链接进固件。这样做的好处是不依赖文件系统上电即可运行。转换脚本要处理奇偶校验位的剥离和 Bank 地址的重映射这两步都不能省。验证环节我分了三级第一级是单元测试针对每条指令写测试用例验证执行结果第二级是集成测试跑一段已知输出的原始代码片段比对结果第三级是端到端测试加载完整的任务程序观察 DSKY 输出是否与历史记录一致。第一级和第二级在 PC 上跑用同一套核心仿真代码只是硬件抽象层换成模拟实现。第三级在目标板上跑通过串口输出调试信息。验证级别运行环境测试内容通过标准单元测试PC单条指令执行寄存器与存储器状态正确集成测试PC代码片段输出与预期一致端到端测试目标板完整任务程序DSKY 输出匹配历史记录3.4 DSKY 交互的模拟实现DSKY 是宇航员与 AGC 交互的界面包含数字键盘、指示灯和数字显示屏。仿真里我用一个简单的状态机模拟键盘输入映射成通道 15 的读操作显示输出映射成通道 10 和 11 的写操作。数字显示屏用七段码表示指示灯用位标志表示。状态机每收到一次写操作就更新内部状态再通过串口把状态发到 PC 端显示。这里有个实操心得DSKY 的闪烁逻辑在原始程序里是通过反复写通道实现的仿真时如果每次写都刷新串口输出数据量会很大。我的做法是加一个变化检测只有状态真正改变时才发串口这样既保留了行为正确性又避免了串口拥塞。实测下来串口波特率 115200 足够用再低就会丢数据。4. 常见问题与排查技巧实录4.1 指令执行结果异常的排查思路跑仿真时最常遇到的问题是某条指令执行后寄存器值不对。排查顺序我总结成三步先看操作码译码是否正确再看地址计算是否命中正确的 Bank最后看执行逻辑是否处理了边界情况。译码错误通常是操作码表填错地址错误多半是 Bank 寄存器没更新边界情况常见于溢出和移位指令。举个例子有一次跑原始程序时发现累加器值总是差 1查了半天发现是进位标志的处理有问题。AGC 的加法指令在溢出时会翻转累加器的最高位我的实现里漏了这一步。加上之后问题解决。这个坑的教训是AGC 的算术指令跟现代 CPU 差别很大不能想当然地套用常见语义必须逐条对照原始文档。4.2 存储器访问越界的定位方法存储器越界在仿真里不会像现代系统那样直接崩溃而是静默地读写错误位置导致行为诡异。我的定位方法是在存储器访问函数里加边界检查越界时记录访问地址和当前指令地址通过串口输出。这样跑一遍就能拿到越界点的上下文再对照原始程序的 Bank 配置分析原因。常见原因是 FB 或 EB 寄存器在中断处理里被意外修改。AGC 的中断机制跟现代 CPU 不同中断向量固定在特定地址处理程序里如果没保存和恢复 Bank 寄存器返回后就乱套了。我的做法是在中断入口和出口自动保存恢复 Bank 寄存器虽然增加了一点开销但避免了大量隐蔽 bug。4.3 仿真速度不足的优化手段在 MCU 上跑仿真速度是绕不开的问题。原始 AGC 的主频大约 2MHz现代 MCU 跑仿真时每条指令要几十个周期实际速度只有原始的几十分之一。优化手段我试过几种一是把频繁访问的存储器区域缓存到快速 RAM二是把常用的地址计算提前算好三是减少函数调用层级把热点代码内联。实测下来最有效的优化是减少 Bank 切换的开销。原始程序里 Bank 切换很频繁每次切换都要重新计算地址基址。我把 Bank 基址预先算好存成数组切换时只更新一个索引变量取指时用基址加偏移的方式算地址。这个改动让仿真速度提升了将近一倍。另一个有效手段是把调试日志编译成条件编译发布版本里完全去掉日志输出速度又能提升 20% 左右。提示优化之前先用性能分析工具找到热点不要凭感觉优化。我一开始以为取指是瓶颈优化了半天发现真正的热点在 I/O 回调里的串口输出把串口改成缓冲发送后速度立刻上来了。4.4 常见问题速查表现象可能原因排查方法解决方案寄存器值差 1进位标志处理错误对照指令文档逐条检查补上溢出时的最高位翻转变量读写错乱Bank 寄存器未更新打印 FB/EB 值检查 Bank 切换指令实现程序跑飞中断未保存 Bank在中断入口加日志自动保存恢复 Bank 寄存器仿真速度慢I/O 回调耗时性能分析定位热点串口改缓冲发送DSKY 无输出通道号映射错误打印通道访问日志核对通道号与回调表5. 扩展方向与个人实操体会这个项目做完之后我发现可扩展的方向比预想的多。一个方向是加网络接口把仿真器变成远程可访问的教学平台学生通过浏览器就能操作 DSKY不用每人配一块开发板。另一个方向是加硬件在环接口把仿真器的 I/O 通道接到真实的传感器和执行器上做半实物仿真。还有一个方向是加历史回放功能把原始任务的数据流录下来仿真时按时间轴回放用于教学演示。我个人在实际操作中的体会是复刻老硬件最大的难点不在代码量而在对原始设计意图的理解。AGC 的很多设计在现代眼光看是“不合理”的比如 15 位字长、单累加器、Bank 切换但这些设计都是当时技术约束下的最优解。理解这些约束才能把仿真写对。踩过几次坑之后我养成了一个习惯每实现一个模块先找原始文档对照确认行为语义再写代码。这个习惯让后期调试省了很多时间。最后分享一个小技巧如果你也想做类似的复刻项目建议先从指令集仿真入手把 CPU 跑通再搞存储器和 I/O。指令集是核心其他都是外围。指令集跑通了后面就是体力活。指令集没跑通后面全是坑。另外原始文档一定要找齐尤其是指令手册和存储器布局图缺了这两样仿真根本没法写对。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。