☰
Apollo-11制导计算机源码解析:实时调度、内存管理与容错设计
2026/10/15 1:24:18 网站建设 项目流程

1. 从“Apollo-11”这个名字说起:它到底指什么

第一次看到“Apollo-11”这个标题,很多人脑子里蹦出来的第一反应是上世纪的登月计划。但在技术圈子里,尤其是做嵌入式和航天软件的人眼里,这四个字加一个数字,指向的是另一件东西——一段被反复研究、被无数工程师当作“教科书级反面教材”的制导计算机源码。我最早接触它是在做一个高可靠性实时系统的项目,当时团队里有人甩过来一句“你去看看Apollo-11的源码怎么写的”,我以为是开玩笑,结果翻完之后确实被震住了:那套代码里藏着大量今天依然适用的工程智慧,也藏着一些让人哭笑不得的历史包袱。

这篇文章不聊历史八卦,也不做源码逐行翻译。我想做的是把“Apollo-11”这个项目当作一个软件工程案例来拆,讲清楚它为什么值得今天的开发者花时间看,它的核心机制是怎么运转的,以及如果你手头正好有一个资源极度受限、可靠性要求又极高的系统,能从里面抄到哪些作业。关键词就三个:实时调度、内存管理、容错设计。适合谁看?做嵌入式、航天软件、工业控制、高可靠系统的工程师,以及任何对“在极端约束下写出能跑的代码”这件事感兴趣的人。

先说一个反直觉的结论:Apollo-11的制导计算机代码,按今天的标准看,写得并不“漂亮”。它没有优雅的抽象,没有清晰的模块边界,变量命名充斥着缩写和数字。但它在一个主频不到1MHz、内存以KB计、没有操作系统概念的硬件上,完成了载人登月的导航、制导与控制。这件事本身说明,代码质量的定义是随约束条件变化的。在资源无限的环境里追求可读性和可维护性是对的;在资源极度受限的环境里,第一优先级是“能不能在截止时间前算完”。这个判断,是我看完这套代码之后最大的收获。

2. 硬件约束倒逼出来的软件架构

2.1 那台计算机到底有多“寒酸”

要理解Apollo-11的软件设计,必须先理解它跑在什么硬件上。制导计算机(AGC)的核心参数大致是这样的:字长15位(加上一位奇偶校验位凑成16位),主频约2MHz,内存分为两部分——核心存储区(Core Rope Memory)约36K字,用于存放固化程序;可擦写存储区约2K字,用于运行时数据。注意,这里的单位是“字”,不是字节,而且36K字是只读的,程序在出厂时就被“编织”进硬件里,想改一行代码等于重新做一块硬件。

我第一次看到“核心存储区”这个概念时愣了半天。它的原理是用导线穿过磁环,穿过去代表1,绕过去代表0,程序就是这样一根线一根线“编”出来的。这意味着什么?意味着软件和硬件在物理层面是绑死的,没有“重新部署”这回事。今天你改个配置重启一下就行,当年改一个常量可能要重新安排整块存储板的走线。这种约束直接导致了两个设计后果:第一,程序必须一次做对,测试成本极高;第二,代码必须极度紧凑,每一个字都要省着用。

2.2 为什么没有“操作系统”反而是一种优势

今天的开发者习惯了操作系统提供进程调度、内存管理、中断处理这些基础设施。Apollo-11的AGC没有这些,它只有一个**执行调度器(Executive)和一套等待列表(Waitlist)**机制。执行调度器负责在多个任务之间切换,等待列表负责管理定时任务。听起来很原始,但恰恰是这种“没有中间层”的设计,让它的时间行为完全可预测。

我打个比方:操作系统像一个大公司的行政体系,什么都要走流程,灵活但开销大;AGC的调度器像一个小作坊的老板,谁该干活直接喊一嗓子,没有中间商赚差价。在实时系统里,可预测性比灵活性重要得多。你知道每个任务最坏情况下多久能跑完,才能保证整个系统在截止时间前响应。AGC的调度器把任务分成不同优先级,高优先级任务可以抢占低优先级任务,但抢占的粒度是精心设计过的,不会出现今天某些RTOS里那种优先级反转的坑。

这里有一个细节值得说:AGC的调度器不是基于时间片轮转的,而是基于任务就绪状态。每个任务执行到某个点会主动让出CPU,或者被更高优先级任务打断。这种协作式调度在今天的眼光看很危险——一个任务如果死循环不让出,整个系统就挂了。但当年的工程师通过严格的代码审查和测试,保证了每个任务都会在有限步内让出。这给我的启示是:协作式调度不是不能用,而是对代码纪律要求极高。如果你的团队能做到每个人都清楚自己的任务最长执行时间,协作式调度反而比抢占式更简单、更可预测。

2.3 内存管理:把每一“字”都花在刀刃上

AGC的可擦写内存只有约2K字,这2K字要存放所有运行时变量、堆栈、临时计算结果。没有虚拟内存,没有垃圾回收,没有动态分配——所有内存都是静态分配的。这意味着程序员在写代码时就要想清楚:这个变量放在哪个地址,那个缓冲区多大,堆栈深度多少。听起来很痛苦,但好处是内存行为完全确定,不会出现今天常见的“内存泄漏”“堆碎片”这类问题。

我实际做过一个对比:在一个资源受限的嵌入式项目里,我们一开始用了动态内存分配,结果跑了几天就出现碎片导致的分配失败。后来改成全静态分配,虽然写代码时麻烦一点,但系统稳定性大幅提升。AGC的做法印证了这个经验:在可靠性要求高的系统里,静态分配是更稳妥的选择。当然,静态分配的前提是你对系统的内存需求有清晰的预估,这需要前期做足功课。

还有一个细节:AGC的堆栈是固定深度的,而且每个任务的堆栈是分开的。这样即使某个任务堆栈溢出,也不会影响其他任务。今天的RTOS通常也支持每个任务独立堆栈,但很多开发者为了省内存会共用堆栈,这在AGC的视角看是不可接受的。隔离性是可靠性的基础,这个原则从当年到现在都没变。

3. 那些今天依然值得抄的代码设计

3.1 数字命名法:看起来丑,用起来真香

AGC代码里大量使用数字命名,比如“T3RUPT”“KEYRUPT”“DOWNRUPT”这些中断处理程序的名字,以及像“V37”“R12”这样的变量名。第一次看会觉得这是什么鬼,但用久了会发现,数字命名在极端受限的环境里有它的道理。首先,数字比单词短,省内存;其次,数字在汇编层面直接对应地址或寄存器,减少了一层映射;最后,数字命名强迫你记住每个数字的含义,反而减少了“这个名字到底啥意思”的歧义。

当然,我不是建议你今天写代码也用数字命名。我的意思是,命名策略要服务于约束条件。在内存充裕、团队规模大的项目里,用描述性命名是对的;在资源极度受限、团队规模小的项目里,短命名甚至数字命名可能是更务实的选择。关键是团队内部要有一份清晰的对照表,并且严格执行。我在一个工业控制项目里试过混合命名:核心变量用短名,业务逻辑用长名,效果还不错。

3.2 中断处理:快进快出,绝不恋战

AGC的中断处理程序有一个铁律:尽可能短。中断发生时,CPU会跳转到对应的处理程序,处理程序只做最紧急的事情——比如读取一个输入、设置一个标志位——然后立刻返回。耗时的计算留给主循环或低优先级任务去做。这个原则今天依然适用,但很多开发者做不到,因为他们在中断里调用了耗时函数,比如打印日志、做浮点运算、甚至等待信号量。

我踩过这个坑:在一个电机控制项目里,我在中断里做了一次浮点除法,结果中断响应时间从几微秒变成了几十微秒,导致电机抖动。后来把除法移到主循环,中断只负责采集数据,问题就解决了。AGC的代码里几乎看不到在中断里做复杂运算的情况,中断是“通知”,不是“处理”,这个分工要明确。

还有一个细节:AGC的中断优先级是硬件固定的,高优先级中断可以打断低优先级中断。但工程师在设计时尽量避免中断嵌套,因为嵌套会增加堆栈深度和时序分析的复杂度。今天的RTOS通常支持中断嵌套,但我的经验是:除非必要,不要嵌套。嵌套带来的收益往往抵不上它增加的调试难度。

3.3 自检与冗余:不相信任何单一组件

Apollo-11的软件里有一套完整的自检机制。系统启动时会跑一系列测试,检查内存、寄存器、中断系统是否正常。运行过程中也会定期做校验,比如对关键变量做奇偶校验,对程序代码做校验和。如果发现异常,系统会尝试恢复或者切换到备份模式。这种“不相信任何单一组件”的思路,是可靠性设计的核心。

我印象最深的是AGC的双重计算机制:对于关键的计算结果,系统会用两种不同的方法算一遍,然后比对结果。如果一致,才认为计算有效;如果不一致,说明某个环节出了问题,系统会报警或者进入安全模式。这种做法在今天的高可靠系统里依然常见,比如航空电子的飞控计算机、高铁的列控系统。冗余不是浪费,是保险。当然,冗余会增加成本和复杂度,所以要根据系统的失效后果来决定冗余级别。人命关天的系统,冗余是必须的;普通消费电子,可能就不需要。

这里有一个实操建议:如果你在做高可靠系统,至少要对关键数据做范围检查和变化率检查。范围检查是看数据是否在合理区间内,变化率检查是看数据的变化速度是否合理。比如温度传感器读数突然从25度跳到200度,变化率明显异常,系统就应该报警而不是直接采信。AGC的代码里有很多这类检查,虽然简单,但非常有效。

4. 从Apollo-11看实时系统的调试与验证

4.1 没有调试器的年代,怎么找bug

AGC的调试手段极其有限。没有断点,没有单步执行,没有变量监视窗口。工程师主要靠灯和示波器:计算机面板上有一排灯,显示寄存器的内容;示波器用来观察时序信号。想调试一个逻辑错误,往往要花几个小时甚至几天,通过反复运行、观察灯的变化、推断问题所在。

这种条件下,工程师养成了一个习惯:在代码里埋大量的“检查点”。每个关键步骤执行后,会更新一个特定的内存地址或者输出一个特定的信号。如果程序跑飞了,通过观察最后更新的检查点,就能大致定位问题范围。这个思路今天依然有用:在复杂的分布式系统里,我们通过日志和追踪来定位问题,本质上和当年的检查点是一样的。可观测性是调试的基础,无论什么年代。

我自己的经验是:在嵌入式项目里,至少要在每个任务的关键节点打一个标记,可以用GPIO翻转、可以用内存写特定值、可以用串口输出一个字符。这些标记在正常运行时开销很小,但出问题时能救命。AGC的工程师当年就是这么干的,只不过他们用的是灯和示波器,我们用的是逻辑分析仪和串口。

4.2 测试用例的设计:覆盖所有“不可能发生”的情况

Apollo-11的测试极其严苛。工程师会模拟各种异常情况:传感器故障、通信中断、电源波动、甚至宇航员误操作。每一个异常情况都有对应的测试用例,确保系统能正确处理。这种“把不可能发生的情况都测一遍”的思路,是航天软件的标配。

我参与过一个医疗设备的项目,监管要求我们必须做故障模式与影响分析(FMEA),列出所有可能的故障模式,评估其影响,然后设计测试用例覆盖。这个过程很繁琐,但确实能发现一些平时想不到的问题。比如我们当时发现,如果某个传感器的采样值恰好等于一个特殊值(比如0xFFFF),会导致后续计算溢出。这个问题在正常测试中根本不会出现,但在FMEA的引导下被找出来了。

AGC的代码里有很多边界检查,比如数组访问前检查索引范围,除法前检查除数是否为零。这些检查在今天的很多代码里被省略了,理由是“性能”或者“不可能发生”。但我的经验是:边界检查的成本远低于故障排查的成本。尤其是在高可靠系统里,宁可多花几个时钟周期做检查,也不要让一个未定义行为毁掉整个系统。

4.3 版本管理与变更控制:没有Git的年代怎么管代码

AGC的代码是用纸带和卡片管理的,每次修改都要经过严格的审批流程。修改后的代码要重新“编织”到核心存储区,这个过程本身就是一个物理上的版本控制。虽然原始,但效果很好:每一行代码的变更都有记录,每一次发布都有对应的硬件版本。

今天的开发者有Git,但很多团队并没有用好。我见过太多项目,代码提交信息写的是“fix bug”“update”,根本不知道改了什么、为什么改。AGC的经验告诉我们:变更控制的核心不是工具,是纪律。每次修改都要说清楚改了什么、为什么改、影响范围是什么。这个习惯在团队协作里尤其重要,因为你的修改可能影响别人的模块。

我的做法是:提交信息必须包含三部分——问题描述、修改方案、测试结果。比如“修复温度传感器读数溢出问题:将int16改为int32,测试了-40到125度范围,正常”。这样即使过了半年,回头看也能快速理解当时的上下文。AGC的工程师当年在纸上写变更记录,我们今天在Git里写,本质是一样的。

5. 如果今天要做类似系统,哪些经验可以直接用

5.1 资源受限系统的设计 checklist

如果你手头有一个资源受限的实时系统,比如单片机、DSP、FPGA软核,可以从Apollo-11的经验里提炼出这样一份checklist:

设计维度AGC的做法今天的对应实践
内存分配全静态分配,无动态内存使用静态数组或内存池,避免malloc/free
任务调度协作式调度,任务主动让出如果任务执行时间可控,可用协作式;否则用抢占式RTOS
中断处理快进快出,只做通知中断里只设标志位,耗时操作放主循环
错误处理自检+冗余+安全模式关键数据做范围检查和变化率检查,异常时进入安全状态
调试手段检查点+灯+示波器GPIO翻转+串口日志+逻辑分析仪
变更控制纸质记录+审批Git提交信息规范+代码审查

这份checklist不是让你照搬AGC的做法,而是提醒你:在资源受限的环境里,每一个设计决策都要有明确的理由。为什么用静态分配?因为动态分配有碎片风险。为什么中断要短?因为长中断会影响实时性。为什么要有冗余?因为单点故障可能导致灾难性后果。把这些“为什么”想清楚,你的设计就不会跑偏。

5.2 实时性分析的实操方法

AGC的实时性分析是靠人工推算的:每个任务最坏执行时间多少,中断响应时间多少,任务切换开销多少,加起来看是否满足截止时间。今天我们有更先进的工具,比如静态时序分析、最坏执行时间分析,但核心思路没变:你必须知道每个任务的最坏执行时间。

我做过一个电机控制项目,要求控制周期100微秒。一开始我们凭感觉写代码,结果偶尔会出现控制周期超标。后来用示波器测量每个函数的执行时间,发现有一个查表函数在特定输入下耗时特别长。优化之后,最坏执行时间降到了80微秒以内,留出了20%的余量。这个余量很重要:实时系统不能卡着截止时间设计,必须留余量,因为实际运行环境比实验室复杂得多。

AGC的工程师当年也是这么做的:他们会计算每个任务的最坏执行时间,然后留出足够的余量。这个余量不是拍脑袋定的,而是基于对硬件时序、中断延迟、内存访问时间的精确测量。今天的开发者有更好的工具,但测量的习惯不能丢。不要凭感觉说“应该够快”,要实际测。

5.3 团队协作与代码审查的要点

AGC的代码是多人协作的产物,但那个年代没有Pull Request,没有代码审查工具。他们靠的是严格的评审流程:每一行代码都要经过至少两个人的审查,审查内容包括逻辑正确性、时序影响、内存使用、边界条件。这种流程虽然慢,但保证了代码质量。

今天的团队协作工具更先进,但审查的深度往往不够。我见过很多代码审查只关注“能不能跑”,不关注“边界条件”“异常处理”“资源使用”。AGC的经验是:审查要覆盖所有维度,尤其是那些容易被忽略的——比如中断里有没有调用不可重入函数,堆栈深度是否足够,变量初始化是否完整。

我的做法是:在代码审查时,除了看逻辑,还会问三个问题——最坏情况是什么?异常情况怎么处理?资源够不够?这三个问题能逼出很多隐藏问题。AGC的工程师当年在纸上做审查,我们今天在GitLab或GitHub上做,工具变了,但审查的深度不能变。

6. 我踩过的坑和总结出的几条硬经验

6.1 不要迷信“先进”的工具和框架

我刚开始做嵌入式时,总想用最新的RTOS、最全的中间件、最花哨的调试工具。结果发现,很多“先进”的东西在资源受限的环境里根本跑不动,或者引入了不可预测的行为。AGC的代码里没有任何“框架”,就是最朴素的汇编和调度逻辑,但它能跑,而且跑得稳。

我的经验是:在资源受限的系统里,简单比先进重要。能用裸机就不用RTOS,能用静态分配就不用动态分配,能用轮询就不用中断(如果实时性要求不高)。每引入一个抽象层,就多一份开销和一份不确定性。当然,这不是说不能用RTOS,而是说引入任何东西都要有明确的理由,不能因为“别人都在用”就用。

6.2 时序问题是最难查的,预防比调试重要

我遇到过最头疼的bug是时序相关的:系统大部分时间正常,偶尔在特定条件下出错,而且很难复现。这种问题往往是因为某个操作的执行时间超过了预期,导致后续任务错过了截止时间。AGC的工程师当年也面临同样的问题,他们的对策是在设计阶段就做时序分析,而不是等到出问题再查。

具体做法:列出所有任务的执行路径,计算每条路径的最坏执行时间,然后看是否满足截止时间。如果某条路径超标,要么优化代码,要么调整任务优先级,要么增加硬件资源。这个分析要在写代码之前做,而不是之后。事后补时序分析,往往意味着要重构,成本高得多。

6.3 文档和注释:写给自己看的,也是写给未来的自己看的

AGC的代码里有大量注释,虽然简短,但关键信息都有。比如一个中断处理程序,注释会说明它响应什么中断、做什么处理、有什么副作用。这些注释在当年帮助工程师维护代码,在今天帮助我们理解代码。

我的习惯是:注释要解释“为什么”,而不是“是什么”。比如“将变量A加1”这种注释没有价值,因为代码本身就说清楚了。有价值的注释是“这里加1是因为硬件寄存器在读取后会自减,需要补偿”。这种注释能帮助未来的你(或者接手你代码的人)理解当时的决策背景。AGC的代码里有很多这类注释,虽然简短,但信息量很大。

6.4 测试要覆盖“非正常路径”

正常路径的测试很容易写,也容易通过。但真正考验系统的是非正常路径:传感器故障、通信超时、电源波动、用户误操作。AGC的测试用例里,非正常路径的测试占了很大比例。我的经验是:测试用例的设计要“悲观”一点,假设所有可能出错的地方都会出错,然后验证系统是否能正确处理。

具体做法:列出所有外部输入和内部状态,对每一个输入和状态,考虑它可能出现的异常值,然后设计测试用例。比如一个温度传感器,正常范围是-40到125度,那就要测试-41度、126度、传感器断线、传感器短路这些情况。AGC的代码里有很多这类边界测试的痕迹,虽然当年的测试手段有限,但测试的思路是完整的。

7. 从Apollo-11到今天的工程实践:变与不变

7.1 变了的是工具,不变的是约束

今天的开发者有强大的IDE、丰富的库、高速的处理器、大容量的内存。但如果你做的是嵌入式、航天、工业控制这些领域,约束依然存在:CPU速度有限、内存有限、功耗有限、可靠性要求高。AGC的经验在这些领域依然适用,因为约束的本质没变。

我做过一个对比:AGC的CPU主频约2MHz,内存36K字;今天一颗普通的STM32主频100MHz以上,内存几百KB。看起来资源丰富了50倍以上,但今天的系统功能也复杂了50倍以上。结果就是,资源约束依然紧张,只是紧张的程度不同。AGC的工程师在2MHz上做导航计算,我们在100MHz上做电机控制,本质上都是在有限的资源里做尽可能多的事。

7.2 不变的是对“确定性”的追求

AGC的代码里,每一个操作的时间都是确定的,每一个内存访问的地址都是确定的,每一个中断的响应时间都是确定的。这种确定性是可靠性的基础。今天的系统里,很多不确定性被引入了:动态内存分配、缓存、多核并行、操作系统调度。这些机制提高了平均性能,但增加了最坏情况的不确定性。

我的观点是:在可靠性要求高的系统里,要尽量消除不确定性。能用静态分配就不用动态分配,能关缓存就关缓存(如果性能允许),能用单核就不用多核(如果算力够)。AGC的工程师当年没有这些选择,他们只能做确定的事。我们今天有选择,但选择多了反而容易迷失。回归确定性,是提高可靠性的有效路径。

7.3 给今天开发者的三条实操建议

第一条:先想清楚最坏情况。写代码之前,先问自己:这个函数最坏执行时间多少?这个缓冲区最大会用到多少?这个中断最坏会嵌套几层?把最坏情况想清楚,再动手写代码。AGC的工程师当年就是这么做的,因为他们的资源不允许“先写了再说”。

第二条:把检查点埋进代码里。在每个关键步骤后加一个标记,可以是GPIO翻转、内存写特定值、串口输出字符。这些标记在正常运行时开销很小,但出问题时能快速定位。AGC的工程师用灯和示波器,我们用逻辑分析仪和串口,工具变了,思路没变。

第三条:测试要覆盖异常路径。正常路径的测试只能证明系统“能跑”,异常路径的测试才能证明系统“可靠”。列出所有可能的异常情况,设计测试用例,逐一验证。AGC的测试用例里,异常路径占了很大比例,这是它可靠性的重要保障。

最后分享一个我自己的体会:看AGC的代码,最大的收获不是某个具体的技术点,而是一种工程态度——在极端约束下,依然追求正确、可靠、可维护。这种态度比任何技术都重要。技术会过时,工具会更新,但对质量的追求不会过时。如果你也在做资源受限、可靠性要求高的系统,不妨找一份AGC的代码翻翻,不一定能直接抄到什么,但那种“把每一字都用在刀刃上”的精神,会让你对自己的代码有新的要求。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询