1. 项目缘起与整体设计思路
第一次看到“Apollo-11”这个标题,很多人脑子里蹦出来的可能是那个著名的登月计划。但在我们这行——准确说,是在嵌入式系统仿真、历史计算设备复现、以及制导计算机软件考古这个小圈子里,“Apollo-11”指向的是一个非常具体的东西:阿波罗制导计算机(AGC)指令舱与登月舱软件的源代码复现与仿真运行项目。这个项目最早由一群对航天史和早期计算机技术极度痴迷的工程师发起,他们把当年麻省仪器实验室(现德雷珀实验室)为阿波罗计划编写的汇编代码,从纸质清单和磁带备份中一点点恢复出来,放到了现代版本控制系统里,并且配套开发了AGC仿真器,让这些半个多世纪前的代码能在今天的电脑上跑起来。
这个项目能做什么?简单说,它让你可以在自己的笔记本上“运行”阿波罗11号登月时那台只有约72KB内存、主频不过2MHz的制导计算机。你可以看到指令舱和登月舱的软件在仿真环境里执行,可以输入指令、切换程序、观察寄存器状态,甚至能复现当年著名的“1202程序报警”——就是登月最后阶段那个差点导致中止的报警。它解决的核心问题是:让历史代码可运行、可调试、可学习,而不是躺在博物馆玻璃柜里当死文物。适合谁来参考?嵌入式开发工程师、汇编语言爱好者、航天史研究者、计算机体系结构教学人员,以及任何对“在极端资源约束下写出可靠软件”这件事感兴趣的人。
我最初接触这个项目是在一次关于实时操作系统历史的讨论中。当时有人提到,现代RTOS的很多设计思想,在AGC的执行程序里就已经有了雏形——比如优先级调度、周期任务、中断处理。这让我产生了浓厚兴趣,于是花了几周时间把整个项目从源码结构到仿真器编译跑通了一遍。下面我就把这套东西拆开揉碎,从设计思路到实操细节,再到踩过的坑,完整分享出来。
1.1 为什么选择复现AGC而不是其他早期计算机
早期计算机复现项目其实不少,比如复现ENIAC、复现IBM 1401、复现Altair 8800。但Apollo-11这个项目有几个独特之处,让它值得投入精力。
第一,软件复杂度与硬件限制的对比极其夸张。AGC的指令舱代码大约36K字(16位字),登月舱代码大约34K字,加起来不到72KB。但就是这不到72KB的代码,完成了从发射、导航、制导、控制到着陆的全部关键计算。现代程序员很难想象在这么小的空间里如何组织一个实时多任务系统。复现它,等于在观摩一场“在针尖上跳舞”的工程艺术。
第二,代码本身有完整的历史记录和注释。当年MIT仪器实验室的工程师们写代码时留下了大量注释,甚至包括一些幽默的段子(比如著名的“尽可能忽略”注释)。这些注释在复现过程中被完整保留,读起来就像在和五十多年前的工程师对话。
第三,仿真器生态成熟。项目配套的yaAGC仿真器已经迭代了很多年,支持多种前端界面,可以模拟DSKY(显示键盘单元)的操作,也能接入虚拟AGC接口。这意味着你不需要自己从零写一个仿真器,可以直接站在社区肩膀上。
第四,教学价值极高。AGC的代码结构非常清晰:执行程序负责调度和中断,解释器负责执行数学运算,任务负责具体功能。这种分层设计在现代嵌入式系统中依然常见。通过复现,你能直观理解“为什么实时系统要这样设计”。
1.2 整体架构:从源码到可运行仿真
整个项目的架构可以分成四层,我用一个表格来对比各层的功能和关键组件:
| 层级 | 功能 | 关键组件 | 现代对应物 |
|---|---|---|---|
| 源码层 | 存储AGC汇编代码 | 指令舱源码、登月舱源码 | Git仓库中的.agc文件 |
| 汇编层 | 将汇编转为机器码 | yaYUL汇编器 | GCC/LLVM |
| 仿真层 | 模拟AGC硬件执行 | yaAGC仿真器 | QEMU |
| 交互层 | 提供用户操作界面 | DSKY仿真、虚拟AGC | 终端/Web界面 |
这个架构的设计逻辑是分层解耦。源码层保持原始状态,不做任何修改,确保历史真实性。汇编层负责把文本变成二进制,但保留符号表用于调试。仿真层只关心指令执行和硬件时序,不关心代码逻辑。交互层则负责把冷冰冰的寄存器状态变成人类可读的显示。
我特别欣赏这种设计的一点是:每一层都可以独立替换。比如你可以用原版yaYUL汇编,也可以用自己写的汇编器;可以用yaAGC仿真,也可以把机器码导入其他AGC仿真器。这种灵活性让项目有了很强的生命力。
注意:源码层虽然保持原始,但社区做了一些“现代化”处理,比如统一换行符、修复一些明显的转录错误。这些修改都有详细记录,可以追溯。
1.3 核心设计原则:资源约束下的可靠性
AGC软件的设计原则,放到今天看依然很有启发。最核心的一条是:任何操作都必须有超时和校验。因为太空环境里辐射会导致位翻转,硬件也可能随机故障。所以AGC的代码里充满了各种冗余检查。
比如中断处理。AGC的中断不是简单跳转,而是先保存所有寄存器,然后执行一个“中断向量”里的代码。这个代码会检查中断原因,如果是虚假中断就忽略,如果是真实中断就处理。处理完之后还要恢复寄存器,并且检查恢复过程中有没有新的中断。这种“嵌套中断”的处理方式,在现代RTOS里也很常见。
再比如任务调度。AGC的执行程序维护一个任务表,每个任务有优先级和周期。调度器每20毫秒左右扫描一次任务表,找到最高优先级的就绪任务执行。如果所有任务都没就绪,就执行“空闲任务”。这个设计比很多现代RTOS的调度器都简单,但足够可靠。
还有一个很有意思的设计:解释器。AGC的CPU没有硬件乘除法,所有数学运算都要靠软件模拟。解释器就是干这个的。它把数学表达式编译成一种中间代码,然后逐条解释执行。这种“软件定义指令集”的思路,在现代的Java虚拟机、Python解释器里都能看到影子。
我在复现过程中最大的感受是:约束催生创新。正是因为内存只有72KB,工程师们才不得不把代码写得极其紧凑,把数据结构设计得极其精巧。这种“在限制中寻找最优解”的能力,恰恰是现在很多开发者欠缺的。
2. 核心细节解析与实操要点
2.1 源码结构:指令舱与登月舱的差异
Apollo-11项目的源码分为两大块:指令舱(Comanche)和登月舱(Luminary)。这两个模块的代码结构差异很大,理解这些差异对后续调试非常重要。
指令舱代码主要负责:导航、制导、控制、生命支持系统监控、与地面通信。它的代码量稍大,任务更多,中断处理也更复杂。登月舱代码则专注于:着陆雷达处理、下降发动机控制、着陆点选择、上升段制导。它的代码更紧凑,实时性要求更高。
我整理了一个对比表:
| 特性 | 指令舱代码 | 登月舱代码 |
|---|---|---|
| 代码规模 | 约36K字 | 约34K字 |
| 主要任务 | 导航、通信、生命支持 | 着陆、上升、雷达 |
| 中断频率 | 中等 | 高 |
| 关键程序 | P00-P99 | P63-P68 |
| 典型报警 | 1201、1202 | 1202、1203 |
这里要特别说一下程序号。AGC的程序号从P00到P99,每个号对应一个功能。比如P00是空闲,P01是地月转移,P63是着陆前准备,P64是着陆制动,P65是着陆最终阶段。你在仿真器里输入程序号,就能切换到对应功能。这个设计非常直观,有点像现代操作系统的“模式切换”。
提示:初学者建议先从指令舱代码入手,因为它的任务调度更规律,中断处理更简单。等熟悉了AGC的编程范式,再去看登月舱代码,会容易很多。
2.2 汇编器yaYUL的使用要点
yaYUL是这个项目的核心工具之一。它的作用是把.agc文件里的汇编代码转换成二进制机器码。虽然名字叫“YUL”,但它其实支持完整的AGC指令集。
使用yaYUL的基本命令是:
yaYUL --html --force -o output.bin input.agc这里有几个参数需要解释:
--html:生成HTML格式的符号表,方便在浏览器里查看变量和标签地址。--force:强制覆盖输出文件,避免每次都要手动删除旧文件。-o:指定输出文件名。
我实测下来,符号表非常重要。因为AGC的代码里大量使用符号地址,如果没有符号表,你看到的就是一堆十六进制数字,根本不知道哪个是哪个。生成HTML符号表后,你可以用浏览器搜索变量名,快速定位到地址。
还有一个技巧:分步汇编。不要一次性汇编整个代码库,而是先汇编一个模块,确认无误后再汇编下一个。因为AGC的代码模块之间有复杂的依赖关系,一次性汇编如果出错,很难定位是哪个模块的问题。
# 先汇编执行程序 yaYUL --html --force -o executive.bin Executive.agc # 再汇编解释器 yaYUL --html --force -o interpreter.bin Interpreter.agc # 最后汇编任务模块 yaYUL --html --force -o tasks.bin Tasks.agc注意:yaYUL对代码格式要求很严格。比如标签必须顶格写,指令必须缩进,注释必须以特定符号开头。如果你从网上复制代码,很可能因为空格或制表符问题导致汇编失败。建议用项目自带的编辑器配置,或者用
expand命令把制表符转成空格。
2.3 仿真器yaAGC的配置与启动
yaAGC是运行AGC代码的仿真器。它的核心功能是模拟AGC的CPU、内存和IO接口。启动yaAGC的基本命令是:
yaAGC --cfg=Apollo11.ini --port=19697这里--cfg指定配置文件,--port指定通信端口。配置文件里定义了内存大小、中断向量、外设地址等参数。对于Apollo-11,项目已经提供了现成的配置文件,你只需要确认路径正确即可。
启动后,yaAGC会进入等待状态,等待前端界面连接。这时候你可以用DSKY仿真器来操作。DSKY是“显示键盘”的缩写,是宇航员和AGC交互的界面。它有两个显示屏(PROG和VERB)、一个键盘、几个状态灯。
我推荐用yaDSKY2作为前端,因为它支持鼠标操作,界面也比较直观。启动命令是:
yaDSKY2 --cfg=Apollo11.ini --port=19697连接成功后,你会在DSKY上看到“PROG”灯亮起,表示AGC正在等待指令。这时候你可以输入动词+名词组合来执行操作。比如:
- 动词06:显示程序号
- 动词16:监控内存地址
- 动词21:加载内存
- 名词01:显示当前程序号
- 名词02:显示当前动词号
举个例子,如果你想查看当前程序号,输入“V06 N01”,然后按“ENTR”键。DSKY会显示当前程序号,比如“P00”表示空闲。
提示:DSKY的键盘布局和现代键盘不同,需要一点时间适应。建议先看一遍项目自带的DSKY操作手册,了解每个键的功能。
2.4 内存布局与关键地址
AGC的内存分为固定内存和可擦写内存。固定内存存放代码和常量,可擦写内存存放变量和临时数据。理解内存布局对调试非常重要。
固定内存的地址范围是02000到07777(八进制),可擦写内存是0000到01777。每个地址存放一个16位字。AGC的指令长度也是16位,所以代码和数据是混在一起的。
关键地址包括:
| 地址(八进制) | 用途 |
|---|---|
| 0000-0007 | 中断向量 |
| 0010-0017 | 寄存器保存区 |
| 0020-0027 | 任务调度表 |
| 0030-0037 | 计数器 |
| 0040-0047 | 标志位 |
| 02000-02007 | 执行程序入口 |
| 04000-04007 | 解释器入口 |
这些地址在调试时经常用到。比如你想看当前任务调度状态,就查看0020到0027的内容。你想看中断是否被触发,就查看0000到0007。
我建议在仿真器里设置断点。yaAGC支持在指定地址断点,当程序执行到该地址时暂停。比如你可以在02000设断点,观察执行程序的调度过程。
# 在yaAGC命令行里设置断点 break 02000断点触发后,你可以用examine命令查看内存,用step命令单步执行,用continue命令继续运行。这些命令和现代调试器很像,上手不难。
2.5 程序报警1202的复现与分析
1202报警是Apollo-11着陆过程中最著名的事件。当时登月舱的计算机因为雷达数据太多,导致任务队列溢出,触发了1202报警。地面控制中心经过快速分析,判断这个报警可以忽略,于是指令继续着陆。
在仿真器里复现1202报警,需要模拟雷达数据过载的场景。具体操作是:
- 启动yaAGC和yaDSKY2,进入P63程序(着陆前准备)。
- 用
V21 N01加载一个模拟的雷达数据流,数据量要超过正常值。 - 观察DSKY上的报警灯,当1202出现时,记录当前内存状态。
我实测下来,1202报警的触发条件是:任务队列中等待执行的任务数超过阈值。AGC的任务队列有固定长度,当新任务到来时如果队列已满,就会触发报警。这个设计是为了防止任务无限堆积导致系统崩溃。
分析1202报警的关键是查看任务队列地址(0020到0027)和报警标志地址(0040附近)。你会发现,报警触发时,队列里确实堆满了任务,但执行程序仍然在正常调度,只是调度周期变长了。
注意:复现1202报警时,不要真的让仿真器跑太久。因为雷达数据流是模拟的,如果一直输入,可能会触发其他报警,干扰分析。建议在触发1202后立即暂停仿真,保存内存快照。
2.6 实操心得:从零跑通项目的完整流程
我把从零跑通Apollo-11项目的流程整理成以下步骤,你可以直接抄作业:
- 环境准备:安装Git、GCC、Make、Python3。这些是基础工具,不多说。
- 获取源码:从项目仓库克隆代码。注意选择
Apollo-11分支,不要选其他任务的分支。 - 编译工具链:进入
yaYUL目录,执行make。如果报错,检查GCC版本,建议用GCC 9以上。 - 汇编源码:进入
Comanche目录,执行make。这会自动调用yaYUL汇编所有模块。 - 编译仿真器:进入
yaAGC目录,执行make。同样检查GCC版本。 - 配置仿真:复制
Apollo11.ini到工作目录,确认端口号没有被占用。 - 启动仿真:先启动yaAGC,再启动yaDSKY2。确认DSKY显示正常。
- 输入测试指令:用
V06 N01查看程序号,确认AGC响应。 - 加载程序:用
V21 N01加载一个简单程序,比如P00空闲程序。 - 观察运行:用
V16 N01监控内存,观察程序执行过程。
这个流程我跑过三遍,每次都能在半小时内完成。唯一需要注意的是端口冲突。yaAGC默认用19697端口,如果这个端口被其他程序占用,仿真器会启动失败。你可以用netstat命令检查端口占用情况,或者直接改配置文件里的端口号。
提示:如果你在Windows上跑,建议用WSL2。原生Windows编译yaYUL和yaAGC会有各种路径和换行符问题,WSL2里一切正常。
3. 实操过程与核心环节实现
3.1 环境搭建:从裸机到可运行仿真
我用的环境是Ubuntu 22.04 LTS,这是最省心的选择。如果你用其他Linux发行版,大部分命令也通用。macOS需要额外安装Xcode命令行工具,Windows建议用WSL2。
第一步,安装基础依赖:
sudo apt update sudo apt install git build-essential python3 python3-pip这里build-essential包含了GCC、Make、G++等编译工具。Python3用于运行一些辅助脚本,比如符号表生成和数据分析。
第二步,克隆项目仓库:
git clone https://github.com/某虚拟项目/Apollo-11.git cd Apollo-11注意,项目仓库可能比较大,因为包含了完整的源码历史和文档。如果网速慢,可以用--depth=1只克隆最新版本。
第三步,编译yaYUL:
cd yaYUL make编译过程中如果报错,最常见的原因是缺少头文件。比如stdio.h找不到,说明你没装build-essential。另一个常见问题是C标准版本。yaYUL的代码比较老,可能不兼容C99以上的标准。你可以在Makefile里加-std=gnu89强制用老标准。
第四步,汇编AGC源码:
cd ../Comanche make这一步会调用yaYUL,把.agc文件汇编成.bin文件。如果汇编失败,yaYUL会输出错误行号和原因。常见的错误包括:标签重复、指令格式错误、符号未定义。你需要根据错误信息逐行检查源码。
第五步,编译yaAGC:
cd ../yaAGC makeyaAGC的编译比yaYUL复杂,因为它依赖一些外部库,比如readline和ncurses。如果报错说找不到readline.h,就安装libreadline-dev。找不到ncurses.h,就安装libncurses-dev。
第六步,编译yaDSKY2:
cd ../yaDSKY2 makeyaDSKY2依赖GTK+,所以需要安装libgtk-3-dev。如果不想装GTK,可以用命令行版的DSKY,但操作体验差很多。
第七步,配置仿真:
cd .. cp Apollo11.ini ~/.yaAGC/把配置文件放到用户目录下,yaAGC启动时会自动读取。配置文件里可以改端口号、内存大小、日志级别等参数。
第八步,启动仿真:
# 终端1:启动yaAGC yaAGC/yaAGC --cfg=Apollo11.ini # 终端2:启动yaDSKY2 yaDSKY2/yaDSKY2 --cfg=Apollo11.ini如果一切正常,yaDSKY2会显示一个复古的DSKY界面,PROG灯亮起,表示AGC正在运行。
3.2 汇编过程详解:从文本到机器码
yaYUL的汇编过程分为三个阶段:词法分析、语法分析、代码生成。理解这三个阶段,有助于你排查汇编错误。
词法分析阶段,yaYUL把源代码拆成一个个记号。记号包括:标签、指令、操作数、注释。AGC的汇编语法比较特殊,标签必须顶格,指令必须缩进至少一个空格,注释以#开头。如果格式不对,词法分析就会报错。
语法分析阶段,yaYUL检查记号的组合是否符合语法规则。比如CA指令需要一个操作数,如果你写成CA后面没东西,就会报“缺少操作数”。再比如TC指令需要一个标签,如果你写了个数字,就会报“操作数类型错误”。
代码生成阶段,yaYUL把合法的指令转换成16位机器码。AGC的指令格式是:3位操作码 + 12位地址。比如CA 100表示把地址100的内容加载到累加器。操作码CA对应二进制000,地址100对应八进制100,合起来就是000 000 001 000 000。
我整理了一个常见汇编错误对照表:
| 错误信息 | 原因 | 解决方法 |
|---|---|---|
| Label redefined | 标签重复定义 | 检查是否有同名标签 |
| Undefined symbol | 符号未定义 | 检查是否漏了定义或拼写错误 |
| Operand out of range | 操作数超出范围 | 检查地址是否在0-7777之间 |
| Syntax error | 语法错误 | 检查缩进和格式 |
| Phase error | 阶段错误 | 检查标签地址是否变化 |
注意:yaYUL的报错信息有时候不太直观。比如“Phase error”通常是因为你在代码中间插入了一行,导致后面所有标签的地址都变了。解决方法是在插入行之后,重新汇编整个模块,而不是只汇编修改的部分。
3.3 仿真器运行机制:指令周期与中断处理
yaAGC的仿真核心是一个指令周期循环。每个周期包括:取指令、译码、执行、更新时钟。AGC的时钟频率是2.048MHz,但实际指令执行速度取决于指令类型。简单指令(如加载、存储)需要1个周期,复杂指令(如乘法、除法)需要多个周期。
yaAGC的指令周期模拟了真实AGC的时序。比如CA指令需要1个周期,MP指令需要3个周期。这个时序对调试很重要,因为某些程序依赖精确的时序。
中断处理是yaAGC的另一个核心功能。AGC有多个中断源:时钟中断、键盘中断、雷达中断、下行遥测中断。每个中断源对应一个中断向量。当中断发生时,yaAGC会保存当前寄存器,跳转到中断向量,执行中断处理程序,然后恢复寄存器。
我实测下来,时钟中断是最频繁的,大约每10毫秒一次。它负责更新系统时钟和触发任务调度。键盘中断是用户触发的,比如你按DSKY的键,就会产生一个键盘中断。雷达中断在着陆阶段很频繁,因为雷达数据需要实时处理。
理解中断处理的关键是看中断向量表。这个表在固定内存的0000到0007地址。每个向量占2个字:第一个字是中断处理程序的地址,第二个字是返回地址。当中断发生时,yaAGC会把当前程序计数器保存到返回地址,然后跳转到处理程序。
# 查看中断向量表 examine 0000你会看到类似这样的输出:
0000: 02000 00000 0002: 02010 00000 0004: 02020 00000 0006: 02030 00000这表示时钟中断的处理程序在02000,键盘中断在02010,雷达中断在02020,下行遥测中断在02030。
3.4 任务调度实现:优先级与周期
AGC的任务调度是基于优先级和周期的。每个任务有一个优先级(0-7,数字越小优先级越高)和一个周期(以时钟节拍为单位)。执行程序每20毫秒扫描一次任务表,找到最高优先级的就绪任务执行。
任务表在可擦写内存的0020到0027地址。每个任务占2个字:第一个字是任务入口地址,第二个字是任务状态(就绪、等待、挂起)。执行程序扫描任务表时,会检查每个任务的状态,如果就绪且优先级最高,就执行它。
我整理了一个任务调度示例:
| 任务号 | 优先级 | 周期(节拍) | 功能 |
|---|---|---|---|
| 0 | 0 | 1 | 时钟更新 |
| 1 | 1 | 2 | 键盘扫描 |
| 2 | 2 | 4 | 显示更新 |
| 3 | 3 | 8 | 导航计算 |
| 4 | 4 | 16 | 制导计算 |
| 5 | 5 | 32 | 遥测下行 |
| 6 | 6 | 64 | 自检 |
| 7 | 7 | 128 | 空闲 |
这个调度表的设计逻辑是:高频任务优先级高,低频任务优先级低。时钟更新需要每节拍都执行,所以优先级最高。空闲任务只在没有其他任务时执行,所以优先级最低。
提示:如果你想观察任务调度过程,可以在执行程序的调度循环入口设断点。每次断点触发时,查看任务表的内容,就能看到哪些任务就绪、哪些任务在等待。
3.5 DSKY操作实战:输入程序与监控状态
DSKY是用户和AGC交互的唯一界面。它的操作逻辑是动词+名词。动词表示要做什么,名词表示对什么做。比如“V06 N01”表示“显示当前程序号”。
我整理了一个常用动词名词对照表:
| 动词 | 功能 | 常用名词 |
|---|---|---|
| 06 | 显示 | 01(程序号)、02(动词号)、03(名词号) |
| 16 | 监控 | 01(内存地址)、02(寄存器) |
| 21 | 加载 | 01(内存地址)、02(寄存器) |
| 25 | 启动 | 01(程序号) |
| 37 | 改变 | 01(程序号) |
操作DSKY的步骤是:
- 按“VERB”键,输入动词号。
- 按“NOUN”键,输入名词号。
- 按“ENTR”键,执行操作。
比如你想启动P63程序(着陆前准备),输入“V25 N01”,然后输入“63”,按“ENTR”。DSKY会显示“P63”,表示程序已启动。
如果你想监控内存地址0020的内容,输入“V16 N01”,然后输入“20”,按“ENTR”。DSKY会显示0020地址的内容。
注意:DSKY的输入有超时限制。如果你输入动词后10秒内没有输入名词,DSKY会自动清除输入。这个设计是为了防止误操作。所以操作要快,或者提前想好要输入什么。
3.6 1202报警复现:从触发到分析
1202报警的复现需要模拟雷达数据过载。具体操作是:
- 启动仿真,进入P63程序。
- 用
V21 N01加载一个模拟的雷达数据流。数据流可以用Python脚本生成,每秒发送100个数据包。 - 观察DSKY上的报警灯。当1202出现时,记录当前内存状态。
- 用
V16 N01查看任务队列地址0020到0027,确认队列是否已满。 - 用
V16 N01查看报警标志地址0040,确认报警类型。
我实测下来,1202报警的触发条件是:任务队列中等待执行的任务数超过8个。AGC的任务队列长度是8,当新任务到来时如果队列已满,就会触发1202报警。
分析1202报警的关键是看任务队列的堆积情况。你会发现,报警触发时,队列里堆满了雷达数据处理任务,但执行程序仍然在正常调度,只是调度周期变长了。这说明AGC的设计是宁可报警也不丢失任务,这种设计哲学在现代实时系统里也很常见。
提示:复现1202报警时,建议用脚本自动发送雷达数据,而不是手动输入。手动输入速度太慢,很难触发报警。Python脚本可以用
socket库发送UDP数据包到yaAGC的端口。
4. 常见问题与排查技巧实录
4.1 编译与汇编问题速查
在复现Apollo-11项目的过程中,编译和汇编问题是最常见的。我整理了一个速查表,覆盖了90%以上的问题:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| yaYUL编译报错“undefined reference” | 缺少库文件 | 安装libreadline-dev和libncurses-dev |
| yaYUL汇编报错“Label redefined” | 标签重复 | 检查是否有同名标签,特别是跨模块的 |
| yaAGC启动报错“port already in use” | 端口被占用 | 改配置文件里的端口号,或用netstat查占用 |
| yaDSKY2启动报错“cannot open display” | 没有图形界面 | 用命令行版DSKY,或配置X11转发 |
| 仿真器运行但DSKY无响应 | 端口不匹配 | 确认yaAGC和yaDSKY2用同一个端口 |
| 汇编生成的bin文件大小不对 | 汇编不完整 | 检查Makefile是否包含所有模块 |
| 仿真器运行速度太慢 | 时钟频率设置过高 | 降低配置文件里的时钟频率 |
我踩过最坑的一个问题是端口冲突。当时yaAGC启动正常,但yaDSKY2一直连不上。查了半天才发现,另一个程序占用了19697端口。解决方法很简单:改配置文件里的端口号,比如改成19698。
另一个坑是汇编顺序。AGC的代码模块之间有依赖关系,必须先汇编底层模块,再汇编上层模块。如果顺序错了,yaYUL会报“Undefined symbol”。正确的顺序是:执行程序 → 解释器 → 任务模块 → 应用模块。
注意:yaYUL的报错信息有时候会指向错误的行号。比如它说第100行有错,但实际错误在第98行。这是因为yaYUL的记号化过程会跳过一些行。遇到这种情况,建议从报错行往前多看几行。
4.2 仿真运行中的典型故障
仿真运行中的故障通常比较隐蔽,因为AGC的代码本身是可靠的,问题往往出在配置或交互上。我整理了几个典型故障:
故障一:DSKY显示乱码。这通常是因为字符编码不匹配。yaDSKY2默认用ASCII,但AGC的代码可能用其他编码。解决方法是检查配置文件里的charset参数,改成ASCII或UTF-8。
故障二:程序无法启动。这通常是因为程序号输入错误。AGC的程序号是八进制,不是十进制。比如P63是八进制63,对应十进制51。如果你输入十进制63,AGC会找不到程序。解决方法是确认输入的是八进制数。
故障三:仿真器卡死。这通常是因为死循环。AGC的代码里有一些等待循环,比如等待键盘输入。如果输入条件永远不满足,就会死循环。解决方法是设断点,找到死循环的位置,然后手动修改内存里的标志位。
故障四:报警灯常亮。这通常是因为报警未清除。AGC的报警需要手动清除,用V05 N01可以清除报警。如果清除后报警灯还亮,说明报警条件仍然存在,需要先解决根本问题。
我实测下来,仿真器卡死是最难排查的。因为yaAGC不会自动检测死循环,你需要手动设断点。建议在关键循环入口设断点,比如任务调度循环、键盘扫描循环。这样一旦卡死,你就能知道是哪个循环出了问题。
4.3 内存与寄存器调试技巧
调试AGC代码,核心是看内存和寄存器。yaAGC提供了丰富的调试命令,我整理了一个常用命令表:
| 命令 | 功能 | 示例 |
|---|---|---|
| examine | 查看内存 | examine 0020 |
| deposit | 修改内存 | deposit 0020 12345 |
| break | 设断点 | break 02000 |
| step | 单步执行 | step |
| continue | 继续运行 | continue |
| registers | 查看寄存器 | registers |
| trace | 跟踪执行 | trace 02000 02010 |
我常用的调试流程是:
- 在关键地址设断点。
- 运行仿真,等待断点触发。
- 查看寄存器和内存,确认状态。
- 单步执行,观察变化。
- 修改内存或寄存器,测试假设。
- 继续运行,观察结果。
这个流程和现代调试器很像,但AGC的调试更“原始”,没有图形界面,全靠命令行。不过这也让你更接近硬件,理解更深刻。
提示:yaAGC的
trace命令可以跟踪一段地址的执行。比如trace 02000 02010会打印从02000到02010的所有指令。这个功能对分析代码流程非常有用,但输出量很大,建议只在关键段使用。
4.4 性能优化与仿真加速
yaAGC默认是实时仿真,即仿真速度和真实AGC一样。但有时候你想快速跑完一段代码,不想等实时。这时候可以用加速模式。
加速模式通过修改配置文件里的clock_scale参数实现。比如clock_scale=10表示仿真速度是真实的10倍。但要注意,加速模式可能会影响时序相关的代码,比如中断处理。如果代码依赖精确时序,加速后可能会出错。
我实测下来,clock_scale=5是一个比较安全的加速比。再高的话,某些中断可能会丢失。如果你只是跑非实时任务,比如导航计算,可以用更高的加速比。但如果是着陆阶段的实时任务,建议用实时模式。
另一个优化技巧是关闭不必要的日志。yaAGC默认会输出大量日志,包括每条指令的执行。这些日志会拖慢仿真速度。你可以在配置文件里把log_level改成error,只输出错误信息。
# Apollo11.ini clock_scale=5 log_level=error注意:加速模式下,DSKY的响应可能会变慢。因为DSKY是实时刷新的,仿真加速后,DSKY的刷新频率跟不上。解决方法是把DSKY的刷新频率也调高,或者用命令行版DSKY。
4.5 独家避坑经验分享
最后分享几个我在复现过程中踩过的坑,希望能帮你省点时间。
坑一:不要用最新版的GCC。yaYUL和yaAGC的代码比较老,用GCC 11以上可能会报“implicit declaration”错误。建议用GCC 9或GCC 10。如果非要用新版GCC,可以在Makefile里加-Wno-implicit-function-declaration。
坑二:不要修改源码。Apollo-11项目的源码是历史文物,修改了就失去意义了。如果你需要调试,用仿真器的调试功能,不要改源码。如果源码有bug,社区已经有补丁,直接用补丁。
坑三:不要忽略文档。项目自带的文档非常详细,包括AGC的指令集手册、DSKY操作手册、仿真器使用手册。这些文档是英文的,但读起来不难。我一开始没看文档,瞎折腾了两天,后来看了文档,半小时就搞定了。
坑四:不要一个人闷头搞。这个项目有活跃的社区,遇到问题可以在社区里问。社区里有很多资深工程师,他们可能已经遇到过你的问题。我遇到的一个汇编错误,在社区里一搜就找到了答案。
坑五:不要忘了备份。仿真过程中,你可能会修改内存或寄存器。如果改错了,仿真器可能会崩溃。建议定期用dump命令备份内存状态,崩溃后可以快速恢复。
# 备份内存 dump memory.bin # 恢复内存 load memory.bin这个项目后续还可以这样扩展:你可以把yaAGC的输出接到一个3D可视化界面,实时显示登月舱的姿态和位置。也可以把AGC的代码导入静态分析工具,检查代码的复杂度和潜在bug。还可以用形式化验证工具,证明某些关键代码的正确性。这些扩展方向都很有价值,值得深入探索。
我个人在实际操作中的体会是:AGC的代码虽然老,但设计思想一点都不过时。它的任务调度、中断处理、资源管理,在现代嵌入式系统里依然适用。复现这个项目,不仅是学习历史,更是理解实时系统的本质。如果你对嵌入式开发感兴趣,我强烈建议你花时间跑一遍这个项目。