资讯动态

Apollo-11制导计算机源码解析:从AGC汇编到模拟器实操

发布时间:2026/10/15 1:26:36 来源:尧图企业网站定制
1. 项目缘起与核心定位Apollo-11 这个标题第一次看到的人多半会愣一下——它既像一段历史代号又像某个技术项目的内部命名。实际上在开源软件和计算机历史圈子里Apollo-11 指向的是一个非常具体的东西阿波罗11号制导计算机AGC指令舱与登月舱软件的完整源代码托管项目。这个项目把当年让人类第一次踏上月球的飞行软件以公开、可读、可编译的形式重新整理出来放到现代开发者面前。它解决的核心问题很直接那段代码原本是写在磁芯存储器里的用的是自定义汇编语言运行在算力和内存都极其有限的机器上。几十年过去原始文档散落、硬件早已停产普通人根本无从下手去理解它到底怎么工作。Apollo-11 项目做的事情就是把这些代码从历史档案里“捞”出来做数字化整理、注释、语法高亮甚至提供模拟器让你能在现代电脑上把它跑起来。适合谁来参考三类人最值得花时间一是对嵌入式系统和实时控制感兴趣的工程师想看看在没有操作系统、没有高级语言、内存以 KB 计算的年代软件是怎么做到万无一失的二是计算机历史爱好者想亲手翻一翻那段改变人类进程的代码长什么样三是教学场景下的老师或学生拿它当“极限约束下的软件工程”案例比任何教科书都生动。我最初接触这个项目是因为在做一个小型实时控制器的固件优化总觉得现代工具链把很多底层细节藏得太深。翻完 AGC 的代码结构之后很多关于“资源怎么省、优先级怎么排、错误怎么兜底”的思路突然就通了。这也是我写这篇博文的出发点不把它当历史文物供着而是当一份可以拆解、可以借鉴的工程样本。2. 代码仓库的整体结构与设计思路2.1 为什么用汇编而不是高级语言Apollo-11 项目里的代码全部是 AGC 汇编语言。你打开任何一个.agc文件看到的都是类似TC、CCS、XCH这样的助记符没有函数、没有类、没有标准库。这不是当年的人不想用高级语言而是硬件条件根本不允许。AGC 的内存分两部分核心存储Core Rope和 erasable memory。核心存储是只读的容量大约 36K 字每个字 15 位其中 1 位奇偶校验14 位有效数据。erasable memory 更小只有 2K 字左右。你拿现代的眼光看这点空间连一个像样的 JSON 解析器都放不下。所以每一行代码都必须精打细算每一个跳转都要考虑周期数。项目在整理时保留了这种原始形态没有试图把它翻译成 C 或 Python。这个选择非常关键一旦翻译时序关系、内存布局、中断响应全部会失真。保留汇编等于保留了那段代码的“物理真实性”。你在阅读时能直接感受到为什么某个变量要放在固定地址、为什么某个子程序要用 fall-through 而不是跳转——这些都是被内存和周期逼出来的设计。2.2 仓库目录的划分逻辑整个仓库的目录结构并不复杂但每一层都有明确意图。根目录下通常能看到几个核心文件夹Comanche系列对应指令舱代码Luminary系列对应登月舱代码另外还有Validation、Test之类的辅助目录。Comanche 和 Luminary 的命名来自当年的项目代号不是随便起的。指令舱和登月舱的软件虽然共享很多底层例程但任务剖面完全不同指令舱要负责导航、姿态控制、返回地球登月舱要负责下降、着陆、上升。两者对实时性的要求、对故障的容忍度都不一样。所以代码被拆成两个独立构建目标各自有独立的入口和中断向量表。这种“同一套底层不同任务剖面”的组织方式在现代嵌入式项目里也很常见。比如同一个 MCU 平台做电机控制和做传感器采集底层驱动可以复用但应用层调度策略必须分开。Apollo-11 项目把这个思路用最原始的方式呈现出来反而更容易看清本质。2.3 构建系统与模拟器的配合项目里包含了一套构建脚本用来把汇编源码编译成 AGC 可执行格式。这套脚本通常依赖一个叫yaYUL的汇编器它是后来社区根据当年文档重新实现的。yaYUL 不是官方工具但它的行为经过大量比对能正确生成核心存储的二进制镜像。编译出来的镜像不能直接在现代 CPU 上跑所以项目还配套了模拟器。模拟器负责模拟 AGC 的指令周期、寄存器行为、内存读写和中断响应。你在本地跑起来之后可以看到代码在“虚拟 AGC”上一步步执行甚至能接入一个简单的 DSKY显示键盘模拟界面输入指令、查看寄存器状态。这个构建加模拟的组合是整个项目最实用的部分。没有它代码就只是一堆文本有了它你才能做实验改一个参数、加一条指令、观察时序变化。我建议任何想深入的人都先把模拟器跑通再回头读代码效率会高很多。3. 核心细节解析与实操要点3.1 内存布局与变量命名规则AGC 汇编里没有“变量声明”这个概念。所有 erasable memory 的地址都是硬编码的通常用ERASE或EQUALS这样的伪指令来分配。你在代码里会看到大量类似TEMPFLG、RADMOD、PIPAX这样的符号它们背后都是固定地址。这种做法的好处是任何时刻你都知道某个数据在内存的哪个位置调试时可以直接看地址。坏处也很明显地址冲突、覆盖、越界全靠人工检查。项目在整理时保留了一套命名约定比如以T开头的多半是临时变量以R开头的多半是寄存器映射以C开头的多半是计数器。这套约定不是强制的但读代码时能帮你快速判断一个符号的用途。注意不要试图去“优化”这些地址分配。AGC 的 erasable memory 只有 2K 字很多地址被多个子程序分时复用。你一旦改动可能破坏某个中断服务程序的隐式约定导致模拟器里跑飞。3.2 中断处理与优先级设计AGC 的中断机制非常原始没有现代意义上的中断控制器。它靠一套“中断向量”加“优先级排队”的逻辑来工作。高优先级的中断可以打断低优先级的处理但低优先级不能嵌套高优先级。项目代码里每个中断服务程序都很短通常只做几件事保存现场、置标志、跳回主循环。这种设计的原因是AGC 的周期数极其宝贵中断服务程序如果太长会挤占导航和姿态控制的计算时间。所以你会看到很多实际处理逻辑并不在中断里完成而是被“标记”下来等主循环轮到的时候再处理。这就是典型的“前后台”架构前台是中断后台是主循环。我在做现代嵌入式项目时也经常用这个思路中断里只做最紧急的事比如清标志、存数据复杂计算全部丢给主循环。Apollo-11 的代码把这个原则执行到了极致值得反复看。3.3 数值表示与溢出处理AGC 用的是 15 位字长其中 14 位有效数据。它支持定点数和一种特殊的“双精度”表示。很多计算涉及角度、速度、加速度单位不是现代的标准单位而是 AGC 自己的“脉冲”或“计数”。项目里保留了一套转换例程用来在内部表示和物理量之间换算。溢出处理是另一个重点。AGC 没有硬件浮点所有运算都是定点或整数。一旦溢出结果会出错而且不会自动报错。所以代码里到处是溢出检查先判断符号位再判断高位最后才做运算。这种“先检查后运算”的模式在现代安全关键系统里依然是标准做法。提示读代码时遇到OVF或CHECKOVF之类的标签不要跳过。它们往往是整个子程序最核心的安全逻辑。4. 实操过程与核心环节实现4.1 本地环境搭建与模拟器编译要在本地跑起来第一步是准备工具链。项目通常提供两种方式一种是直接用预编译的模拟器二进制另一种是从源码编译。我建议从源码编译因为你能顺便看到模拟器是怎么实现指令周期的。以常见的 Linux 环境为例大致流程如下# 克隆仓库这里用通用描述实际地址以项目为准 git clone apollo-11-repo-url cd apollo-11 # 安装依赖汇编器、模拟器构建工具 # 通常需要 C 编译器、make、以及 yaYUL 的源码 make yaYUL make simulator编译完成后你会得到yaYUL和agc_sim两个可执行文件。接下来是编译源码# 进入 Comanche 或 Luminary 目录 cd Comanche # 用 yaYUL 汇编主程序 ../yaYUL MAIN.agc MAIN.bin生成的MAIN.bin就是核心存储镜像。然后启动模拟器../agc_sim -f MAIN.bin模拟器启动后通常会显示一个简单的命令行界面或者一个 DSKY 模拟窗口。你可以输入V35E之类的指令查看当前状态。4.2 关键参数计算周期与内存的权衡AGC 的指令周期大约是 11.7 微秒具体取决于指令类型。这意味着每秒最多执行约 85,000 条指令。听起来不少但导航计算涉及大量三角函数和矩阵运算实际可用余量很小。项目里有一个经典的例子姿态控制子程序PITCH和YAW的计算需要在 2 毫秒内完成一次迭代。2 毫秒等于约 170 个指令周期。在这 170 个周期里要完成读取传感器、计算误差、输出控制量、检查溢出。代码里用了大量查表和近似就是为了把周期数压下来。我在模拟器里做过一个实验把某个查表改成实时计算结果迭代时间从 1.8 毫秒涨到 3.2 毫秒直接导致控制周期失稳。这个实验让我彻底理解了“为什么当年要用查表”——不是偷懒是物理约束。4.3 从源码到运行一次完整的实验记录我选了一个最简单的子程序做实验DISPLAY例程负责把数据送到 DSKY 显示。这个例程不涉及复杂计算但涉及 I/O 操作和中断配合。步骤一在模拟器里加载 Luminary 镜像启动后进入待机状态。 步骤二用调试命令查看DISPLAY的入口地址设置断点。 步骤三手动触发一次显示更新观察寄存器变化。 步骤四修改显示缓冲区的一个值重新运行看 DSKY 模拟界面是否变化。整个过程大概花了半小时但收获很大我看到了 I/O 指令如何与中断标志配合看到了显示刷新如何被主循环调度。这些细节在纯读代码时很容易忽略只有跑起来才会注意到。注意模拟器的时序和真实 AGC 有细微差别不要用它来验证极端实时性。它的价值在于理解逻辑不在于精确复现硬件。5. 常见问题与排查技巧实录5.1 汇编器报错符号未定义或地址冲突这是新手最常遇到的问题。yaYUL 在汇编时如果遇到未定义的符号会直接报错并停止。常见原因有三个一是符号拼写错误AGC 汇编对大小写敏感二是符号定义在另一个文件里但没有正确包含三是地址冲突两个变量被分配到同一个 erasable 地址。排查方法先看报错行号确认符号名。然后在仓库里全局搜索这个符号看它在哪里定义。如果是地址冲突检查ERASE伪指令的分配范围确保没有重叠。5.2 模拟器跑飞PC 跳到非法地址模拟器跑飞通常表现为程序计数器突然跳到一个没有代码的地址或者陷入死循环。原因可能是中断向量配置错误、堆栈溢出、或者某个跳转指令的目标地址算错了。我的排查习惯是先在模拟器里打开指令跟踪看最后几条执行的指令。然后检查跳转前的条件判断看是不是某个标志位被意外置位。AGC 的跳转指令很多是条件跳转条件判断依赖累加器的值而累加器的值又依赖前一条指令。所以往往要往前追好几步才能找到根因。5.3 显示不更新I/O 通道与中断标志DSKY 显示不更新通常不是显示例程本身的问题而是 I/O 通道没被正确选中或者中断标志没被清除。AGC 的 I/O 是通道映射的每个外设对应一个通道号。显示更新前必须先选中对应通道然后写数据最后清标志。我遇到过一次显示例程逻辑完全正确但 DSKY 就是不动。后来发现是中断标志在别的地方被意外清除了导致显示更新请求被吞掉。这种问题在单步调试时很难发现只有连续运行才会暴露。5.4 常见问题速查表问题现象可能原因排查方法汇编报错符号未定义拼写错误、未包含定义文件全局搜索符号检查包含关系汇编报错地址冲突ERASE 分配重叠检查 erasable 地址分配表模拟器跑飞中断向量错误、堆栈溢出打开指令跟踪回溯跳转条件显示不更新I/O 通道未选中、中断标志被清检查通道选择指令和标志清除位置计算结果偏差大溢出未检查、单位换算错误检查溢出逻辑和转换例程模拟器启动失败镜像格式不对、依赖缺失确认 yaYUL 版本和模拟器参数提示遇到问题时先把模拟器的日志级别调到最高很多错误在日志里都有线索。不要一上来就改代码先确认是环境问题还是逻辑问题。6. 从 Apollo-11 项目里能带走什么这个项目最打动我的地方不是它有多“古老”而是它把工程约束下的决策过程完整保留了下来。现代开发里我们很少需要为 2K 内存写代码但“资源有限时怎么排优先级”“怎么在中断和主循环之间分配任务”“怎么用查表换时间”这些思路在任何嵌入式场景里都适用。我后来在自己的项目里借鉴了 AGC 的“前后台”架构中断只做标志置位主循环做状态机调度。代码量没增加多少但实时性和可维护性明显提升。另外AGC 的溢出检查习惯也让我改掉了“先算再说”的毛病现在写任何数值计算都会先想边界条件。如果你只想快速体验一下建议直接跑模拟器输入几条 DSKY 指令感受一下当年的交互方式。如果你想深入就从Luminary的MAIN.agc开始读顺着中断向量表往下追大概两三天能摸清主循环的骨架。再往后就是无穷无尽的细节了——但那些细节才是这段代码真正值钱的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑