怎么读这一册。 它是这个系列的第三本,也是三本里最窄的一本——只讲一件事:这台机器凭什么算得准,误差在它手里怎么被管住。前两册已经讲过程序怎么运转、机器是什么做的,这一册不再重复。证明链 ①(表示) 见第 4 章:一个 15 位计数值,经过源码规定的尺度换算,变成 359.99 度——起点和终点都接在源码上,中间只用了乘法。证明链 ②(算法) 见第 5、6 章:定点量化、归一化、两次牛顿之后,最终误差给出解析上界,并配了可复跑的脚本。证明链 ③(执行) 见第 8 章——真实的 Luminary 099 绳芯镜像装进 yaAGC 跑了 72 点,最坏 0.925 个双精度最低位,与第 5、6 章的预测对得上。 证据分五类:机器实证 附命令与原始输出、可自己复跑;源码实证 标了文件与行号,外部资料 给了参考文献编号,作者推导 给出完整算式,解释性类比 只是解释工具不算证据。
先说清楚单位:位、字、以及没有字节 这一篇里「位」和「字」是两个不同层级的单位,而且这台机器里根本没有字节。先花一分钟分清,后面就不会读错。
位(bit) 最小单位。一个开关只有 0 或 1 两种状态,就是一个 bit。
操作码 3 位、地址 12 位,说的都是 bit。
字(word) 这台机器一次能处理的完整一份数据。1 个字 = 15 位数据(最高位是符号)+ 1 位奇偶校验;程序按字寻址,不按 8 位字节 ,物理上占 16 位。校验位由硬件自动生成与检查,程序碰不到。
内存容量全用字:可写 2,048 字,只读 36,864 字。
字节(byte) 这台机器里没有字节这个概念。 今天说的 1 字节 = 8 位,是后来才通行的。所以看到「2,048 字」不要按字节去想。
按今天的换算:2,048 字 ≈ 4 KB,36,864 字 ≈ 72 KB。
换算一次就明白了。 36,864 字 × 16 位 ÷ 8 ÷ 1,024 = 72 KB ;2,048 字 × 16 位 ÷ 8 ÷ 1,024 = 4 KB 。这就是常见说法里「阿波罗有 4 KB 内存、72 KB 只读存储」的来处——当年的人只说字,是后人按字节折算出来的。
五种内容,五种可信度 本册把事实和解释分开摆放。读到任何一句,先看清它属于哪一类。第 8 章把最硬的一档——机器实证——补齐了。
机器实证 把真实绳芯镜像装进仿真器(yaAGC)出来的运行结果,附完整命令与原始输出,可自己复跑复现。这是本册最强的一档证据——它不只说明源码写了什么,还说清了这台机器实际算出什么。
源码实证 可在仓库里按文件与行号直接核对的事实。引证固定在 commit 911e5c0。
外部资料 来自 NASA / MIT / 公开史料。不能回仓库核对,单独标出并给出参考文献编号。
作者推导 不是原文直接写明的,是我从源码数据算出来或推出的结论。会写明推导过程,读者可自行复核。
解释性类比 为了帮助理解而打的比方,属于解释工具,不是证据。
1 没有小数点,怎么算小数定点不是凑合,是一套写进注释的规矩
这台机器不认识小数,也没有浮点运算部件。可它每天要处理的量,小到角秒,大到几十万公里。它不是靠「想办法凑合」解决这个问题的——它把小数点的位置写成了一条人人都得遵守的规矩,然后让规矩代替硬件。
先打个比方
记账时约定所有金额都以「分」为单位写成整数。钱还是那笔钱,只是小数点的位置被挪到了大家说好的地方,账本上不再写小数点。这台机器做的是同一件事:位(bit)还是那些位,小数点在哪由尺度约定决定,机器自己不管。
当时的处境
1960 年代给一台航天计算机加浮点运算部件,代价是额外的大量逻辑门,还有更多的重量、功耗与故障点。这台机器全靠同一种基本逻辑门搭出来,团队最终选了另一条路:把小数点位置交给程序员管理,用移位指令显式调整。
位还是那些位,变的是小数点的位置
同样是 15 位数据字,按三种尺度读,得到三种完全不同的量
WHOLE 整数
▼小数点
1 格 = 1 单位
最大 16383 = 2¹⁴−1
FRACTIONAL 小数
▼
1 格 = 2⁻¹⁴
尾数 14 位 + 符号 1 位
CDU DEGREES 角度
▼
1 格 = 360/2¹⁵ 度
15 位全给大小,最大 359.99
小数点在哪,机器不管;写在尺度表里,由人遵守
图 2 同一个 15 位数据字(bit),小数点位置由尺度约定决定:整数型点在末尾,小数型点在符号位之后,角度型则乘一个 360/2 的 15 次方。源码实证 Comanche055/ASSEMBLY_AND_OPERATION_INFORMATION.agc:688-692
# NOUN SCALES AND FORMATS
#
# -SCALE TYPE- PRECISION
# UNITS DECIMAL FORMAT -- AGC FORMAT
# ------------ -------------- -- ---------- 源码里专门有一张表,逐个规定每个量「十进制长什么样」和「机器里每一位代表多少」。这就是那套规矩的正式文本——它不是给机器读的,机器只认位;它是给人读的,是全体程序员共同遵守的契约。
源码实证 Comanche055/INTERPRETER.agc:1470 / 1477
# 6. VSL1 TO VSL8 VECTOR SHIFT LEFT (NEVER ROUNDS).
# 6. VSL VECTOR SHIFT LEFT. 缩放是显式指令:想改小数点的位置,就执行一次左移或右移。它被列进解释器的指令清单里,和加减乘除平起平坐,不是编译器在背后代劳的。
作者推导 统计(可用 grep 复核)
grep -ro "SCALED" Comanche055 | wc -l
# 213
grep -roh "\bNORM\b|\bSL1\b|\bVSL1\b|\bSR1\b" Comanche055 | sort | uniq -c
# 137 NORM
# 115 SL1
# 92 VSL1
# 85 SR1 指令舱这一份代码里,注释中写明尺度的 SCALED 有 213 处;仅 NORM、SL1、VSL1、SR1 四类显式缩放指令就出现 429 次。这是频率证据,不是因果证据 :它只能说明手动管理小数点在这份代码里不是偶发技巧,而是日常操作;它不能说明这套做法为什么正确。频率不能证明机制为什么正确,但能证明它不是个例。 数字随统计口径(是否含注释行、是否跨目录)会略有出入,量级不变。
● 没有浮点不等于没有精度。 精度来自位宽与尺度约定,不来自浮点部件。
● 规矩写在源码里,而且是正式表格。 不是口耳相传的经验,是可以逐行核对的文本。
● 代价是人。 每次运算前后都要想清楚小数点在哪,想错了就是量纲错误——这也是为什么后面几章会看到那么多显式的移位与归一化。
2 七个字,三种数据类型MPAC —— 这台机器的算术中心,也是本册的主角
这台机器做算术的地方叫 MPAC,全称 MULTI-PURPOSE ACCUMULATOR(多用途累加器)。它在可写内存里只占 7 个字(word)。少到这个份上,它却要同时充当标量寄存器、高精度累加器和三维向量寄存器。做法是:同一块内存,用一个模式标记切换三种解释。
MPAC 只有 7 个字,却要当三种寄存器用
同一块内存,靠一个 MODE 寄存器切换解释方式
MPAC MPAC+1 MPAC+2 MPAC+3 MPAC+4 MPAC+5 MPAC+6
MODE = 0 双精度 2 字 低字 高字 0(标记) 不用 不用 不用 不用
MODE = +1 三精度 3 字 低 中 高 不用 不用 不用 不用
MODE = −1 向量 6 字 X 低 X 高 不用 Y 低 Y 高 Z 低 Z 高
7 = 3×2 + 1:中间那个字在向量模式下是空档,在三精度模式下是第三个字
图 3 MPAC 只有 7 个字(word),靠一个 MODE 寄存器在同一块内存上表示双精度标量、三精度标量与三维向量。中间那个空档是设计的关键。源码实证 Comanche055/ERASABLE_ASSIGNMENTS.agc:1620 | Luminary099/ERASABLE_ASSIGNMENTS.agc:376
MPAC ERASE +6 # MULTI-PURPOSE ACCUMULATOR.
MPAC ERASE +6 # MULTI-PURPOSE ACCUMULATOR. ERASE +6 表示从这里开始留 7 个字:MPAC 到 MPAC+6。指令舱与登月舱两份代码里这一行都存在,写法完全一致——同一个设计,两份实现。
源码实证 Comanche055/INTERPRETER.agc:63-77(装双精度)
# DLOAD LOADS MPAC, MPAC +1, LEAVING ZERO IN MPAC +2.
DLOAD EXTEND
INDEX ADDRWD
DCA 0 # LOAD DP C(C(ADDRWD)) INTO MPAC,MPAX +1
SLOAD2 DXCH MPAC
CAF ZERO # ZERO MPAC +2
# Page 1108
# AT THE END OF MOST INSTRUCTIONS, CONTROL IS GIVEN TO DANZIG TO DISPATCH THE NEXT OPERATION.
TS MPAC +2 # AND DECLARE DP MODE
NEWMODE TS MODE # PROLOGUE FOR MODE-CHANGING INSTRUCTIONS. 装双精度:用前两个字,第三个字清零,再由 NEWMODE 把 MODE 写成双精度值——清零和类型标记是分开的两步,不是一回事。
源码实证 Comanche055/INTERPRETER.agc:527-530(单精度装载也走同一条路)
SLOAD ZL # LOAD A SINGLE PRECISION NUMBER INTO
INDEX ADDRWD # MPAC, SETTING MPAC+1,2 TO ZERO. THE
CA 0 # CONTENTS OF THE REMAINING MPAC REGISTERS
TCF SLOAD2 # ARE IRRELEVANT. 注意最后一跳:SLOAD 直接跳进 SLOAD2——双精度装载的后半段,于是 MPAC+1、+2 同样被清零,MODE 同样记成 0。这说明 MPAC 并没有一个独立的单精度布局模式 ——单精度值只是「装进前两个字、其余清零」,类型标记仍然是双精度。这一点很重要:本册说 MPAC 有三种布局,是严格按 MODE 的三个取值说的,不包括单精度。
源码实证 Comanche055/INTERPRETER.agc:517-525(装三精度)
TLOAD INDEX ADDRWD
CA 2 # LOAD A TRIPLE PRECISION ARGUMENT INTO
TS MPAC +2 # THE FIRST THREE MPAC REGISTERS, WITH THE
EXTEND # CONTENTS OF THE OTHER FOUR IRRELEVANT.
INDEX ADDRWD
DCA 0
DXCH MPAC
TMODE CAF ONE
TCF NEWMODE # DECLARE TRIPLE PRECISION MODE. 装三精度:三个字全部用上,MODE 记成 +1(CAF ONE)。注意注释里那句「CONTENTS OF THE OTHER FOUR IRRELEVANT」——另外四个字是什么无所谓,因为类型已经由 MODE 决定了。
源码实证 Comanche055/INTERPRETER.agc:530-547(装向量)
TCF SLOAD2 # ARE IRRELEVANT.
VLOAD EXTEND # LOAD A DOUBLE PRECISION VECTOR INTO
INDEX ADDRWD # MPAC,+1, MPAC+3,4, AND MPAC+5,6. THE
DCA 0 # CONTENTS OF MPAC +2 ARE IRRELEVANT.
DXCH MPAC
ENDVLOAD EXTEND # PDVL COMES HERE TO FINISH UP FOR DP, TP.
INDEX ADDRWD
DCA 2
DXCH MPAC +3
+4 EXTEND # TPDVL FINISHES HERE.
INDEX ADDRWD
DCA 4
DXCH MPAC +5
VMODE CS ONE # DECLARE VECTOR MODE. 装向量:六个字分成三组(+0+1、+3+4、+5+6),每组两个字构成一个双精度分量;中间那个 MPAC+2 空着不用,MODE 记成 -1(CS ONE,取 ONE 的反码)。
源码实证 Comanche055/INTERPRETER.agc:473-495(写回也按 MODE 走)
STARTSTO EXTEND # MPAC,+1 MUST BE STORED IN ANY EVENT.
# ITRACE (5) REFERS TO "STARTSTO".
DCA MPAC
INDEX ADDRWD
DXCH 0
CCS MODE
TCF TSTORE
TC Q
VSTORE EXTEND
DCA MPAC +3
INDEX ADDRWD
DXCH 2
EXTEND
DCA MPAC +5
INDEX ADDRWD
DXCH 4
TC Q
TSTORE CA MPAC +2
INDEX ADDRWD 写回也分三种:向量写 6 个字(+0+1、+3+4、+5+6),三精度写 3 个字(多写 +2),双精度只写 2 个字。同一段存储代码,靠 MODE 分支成三条长度不同的路径。
作者推导 由上五条归纳
MODE = 0 双精度标量 用 MPAC+0、+1 (+2 恒为 0)
MODE = +1 三精度标量 用 MPAC+0、+1、+2
MODE = -1 三维向量 用 MPAC+0..+6 中的 6 个 (+2 空置)
7 = 3×2 + 1
└─ 三个双精度分量
└─ 中间那个空档 7 这个数字不是随便定的:三个双精度分量需要 6 个字,剩下的 1 个字放在正中间,在三精度模式下正好当第三个字用。一块内存,两种布局,靠一个 MODE 切换。这是本册最核心的一处设计——用类型标记换取内存复用 。
● MPAC 只有 7 个字 ,正式布局只有三种:双精度标量、三精度标量、三维向量。
● 没有独立的单精度布局模式 。单精度值装载后仍然走双精度的后半段,MODE 记 0——单精度是「只用前两个字」的用法,不是第四种模式。
● 切换靠一个 MODE 寄存器 :0 是双精度,+1 是三精度,-1 是向量。取值本身带符号,可以用 CCS 一次判断三种情况。
● 中间那个空档是关键 :它让向量布局和标量布局能共用同一段连续内存,省下来的每一个字在 2,048 字的可写内存里都是硬通货。
3 源码里那张尺度表从 A 到 J 共十型,逐个规定每一位代表多少
上一章说规矩写在源码里。这一章把那张表摊开看。它一共列了十种尺度类型(A 到 J),每种都写明三件事:十进制显示格式、精度(单 / 双)、以及机器里最低的那一位代表多少。
多一个字,精度跨一大截
每加一个字(word),尾数多 14 位(bit),相对精度提高约 1.6 万倍
单精度 SP
尾数 14 位 → 2⁻¹⁴ ≈ 6.1×10⁻⁵ → 约 4.2 位十进制
双精度 DP
尾数 28 位 → 2⁻²⁸ ≈ 3.7×10⁻⁹ → 约 8.4 位十进制
三精度 TP
尾数 42 位 → 2⁻⁴² ≈ 2.3×10⁻¹³ → 约 12.6 位十进制
反推验证:源码写双精度角度最低位 = 360/2²⁸ 度,正好说明双精度尾数是 28 位
图 4 单精度、双精度、三精度的有效位与角度分辨率对比。每加一个字,分辨率就跨一大截。外部资料 图 5 <b>Comanche055 原始打印清单第 19 页(尺度表 A–J)</b>。这一页是本册后续讨论所有尺度类型的源头:A 整型、B 小数(BIT 1 = 2⁻¹⁴)、C 整型(BIT 1 = 1 UNIT)、D 角度(BIT 1 = 360/2¹⁵ DEGREES)、E 高程、F 180 度、G/H 双精度角度、I/J 光学。本册也如实标出第 3 章 -B- 型 FRACTIONAL 的源码标注 MAX .99996与按 14 位尾数手算得到的 .99994 对不上——在图上 -B- 段那行 "(MAX .99996)" 就是这个存疑点的物理出处。源码实证 Comanche055/ASSEMBLY_AND_OPERATION_INFORMATION.agc:694-708
# -A-
# OCTAL XXXXX SP OCTAL
#
# -B- -14
# FRACTIONAL .XXXXX SP BIT 1 = 2 UNITS
# (MAX .99996)
#
# -C-
# WHOLE XXXXX. SP BIT 1 = 1 UNIT
# (MAX 16383.)
#
# -D- 15
# CDU DEGREES XXX.XX DEGREES SP BIT 1 = 360/2 DEGREES
# (MAX 359.99) (USES 15 BITS FOR MAGNI-
# TUDE AND 2-S COMP.) 四型并排看就很清楚了:同样的 15 位字,-A- 当八进制用,-B- 当纯小数用,-C- 当整数用,-D- 当角度用。变的只有「最低位代表多少」这一条约定。
作者推导 由 -C- 型复核
-C- WHOLE:BIT 1 = 1 UNIT,MAX 16383
16383 = 2^14 - 1
→ 15 位里,1 位是符号,14 位是尾数
→ 可表示范围:-16383 .. +16383 源码标的 MAX 16383 与「14 位尾数 + 1 位符号」完全对得上。这是这套约定最直接的一次自检:(外部资料也记明这台机器采用一补数表示,正负对称,因此最小值为 -16383 而非 -16384。)
作者推导 由 -D- 型复核(完整算式的手算见第 4 章)
-D- CDU DEGREES:BIT 1 = 360/2^15 DEGREES
360 / 2^15 = 360 / 32768 = 0.010986328125 度
32767 × 0.010986328125 = 359.989013671875 度
四舍五入到两位小数 → 359.99
源码标注 MAX 359.99 —— 一致 ✓ 注意 -D- 型后面那句括号:「USES 15 BITS FOR MAGNITUDE AND 2-S COMP.」。角度是 0 到 360,不需要符号位,所以 15 位全部拿来当尾数——这比 -C- 型多一位。同一台机器,不同量的尾数位数可以不同,全看约定怎么写。
源码实证 Comanche055/RT8_OP_CODES.agc:46-49(两种补数并存,且有专门的转换例程)
# CONVERT THE SINGLE PRECISION 2'S COMPLEMENT NUMBER ARRIVING IN MPAC (SCALED IN HALF-REVOLUTIONS) TO A
# DP 1'S COMPLEMENT NUMBER SCALED IN REVOLUTIONS.
CDULOGIC CCS MPAC这一行是本册处理「补数」问题最关键的证据。它说明两件事:第一,同一个程序里同时存在 二补数字段和一补数字段;第二,两种表示之间需要专门的转换例程 (这里叫 CDULOGIC,它同时还做了一次尺度换算:半圈 → 圈)。如果这台机器统一只用一种补数,这样的例程根本不会存在。
源码实证 Comanche055/P20-P25.agc:3092-3095(另一处转换,注释写得很直白)
CA TRUNION # REQUIRED TO CHANGE 2'S COMPLEMENT
EXTEND # TRUNION TO 1'S COMPLEMENT
MSU L # TRUNION (2'S)-00000 CONVERTS TRUNION TO
TS MPAC # 1'S. VARSUBL=00000. 注释里连减什么、为什么减都写了:二补数减 00000(借位)就得到一补数。同一份代码里还有更多同类声明,例如 `P20-P25.agc:1485 SP, 2'S COMPL, SCALED 180 DEGREES`、`P61-P67.agc:1160 ROLLC ROLL COMMAND DP 1'S COMP AT 1REV`、`P61-P67.agc:1168 CPHI GIMBAL ANGLES (O,I,M) 2'S COMP TP (O,I,M)/180`。逐个字段声明符号约定 ,是这份代码的实际做法。
作者推导 由上两条 + 第 7 章的 -0 证据归纳
基础算术体系 一补数
└ 证据:+0 与 -0 是两个不同的值,源码里至少 7 处显式处理(第 7 章)
单个数据字段的符号约定 逐个声明,两种都用
└ 证据:CDULOGIC 专门做 2'S → 1'S 转换
└ 证据:同一子程序的输入声明 1'S COMP、输出声明 2'S COMP
源码里某一行写的 2-S COMP.
→ 是**该数据格式自己的约定**,不代表这台机器的基础算术 厘清这一点之后,第 4 章那个 `2-S COMP.` 就不会和全篇的一补数叙述打架了。尺度表 -D- 型那一行同时写着「15 位用于大小」和「2-S COMP.」,对一个有符号量来说这两句其实是互相拉扯的——本册不替原件圆这个场,只把两种约定并存、且需要显式转换这个事实讲清楚。
作者推导 把上表归纳成一句话
现代写法: meters: number degrees: number
↑ 类型写在变量上,运行时看不出区别
这台机器的写法:
同一个 15 位(bit)数据
+ 尺度约定(每一位代表多少)
+ 精度约定(单 / 双 / 三)
+ 符号约定(一补数 / 二补数,逐字段声明)
─────────────────────────
= 这个量「实际上是什么」 这台机器的一部分类型系统,被编码进了尺度约定里 。机器看到的始终只是 15 个位,类型不占任何存储空间——它活在注释、尺度表和程序员的脑子里。这既是它省内存的原因,也是它要求极强纪律的原因:约定一旦写错,机器不会报错,只会算出一个错得很自信的数。
作者推导 一处对不上,如实标出
-B- FRACTIONAL:BIT 1 = 2^-14,源码标注 (MAX .99996)
按 14 位尾数算:MAX = (2^14 - 1) / 2^14 = 16383/16384
= 0.99993896484375
→ 两位小数为 0.99994
源码写的是 .99996,手算得 .99994。两者对不上。 指令舱与登月舱两份代码里这一行都写作 .99996,所以不是某一边的转录笔误。本册没有从源码里找到能解释这个差异的约定,如实标出,并且不基于这个数字做任何论证 。读者若知道出处,欢迎指出。
源码实证 Comanche055/ORBITAL_INTEGRATION.agc:427、433 | Comanche055/CONIC_SUBROUTINES.agc:1084
5/8 # 5 SCALED B3
D1/32 # 1 SCALED B5
D1/32 2DEC 1.0 B-5 这就是源码里写定点常数的样子。左边是常数的名字,右边注释说明它怎么缩放。两个名字本身就带着答案:5/8 和 D1/32(D 前缀表示双精度)。
作者推导 验算上一条的两个常数
5 SCALED B3 → 5 × 2^-3 = 5/8 = 0.625 与名字 5/8 一致 ✓
1 SCALED B5 → 1 × 2^-5 = 1/32 = 0.03125 与名字 D1/32 一致 ✓ 「X SCALED Bnn」就是「把整数 X 的小数点左移 nn 位」。两个例子都能用名字直接验算,这是本册敢把这条约定写进正文的原因。至于常数定义行里写作 `1.0 B-5` 的带负号写法,本册只记录它与名字 1/32 相符这一事实,不做推广断言。
作者推导 由 -H- 型反推双精度尾数位数
-H- DP DEGREES (360):BIT 1 OF LOW REGISTER = 360/2^28 DEGREES
(MAX 359.99)
→ 双精度的最低有效位权重是 2^-28
→ 双精度尾数 = 28 位 = 14(高字)+ 14(低字)
单精度 14 位,双精度 28 位:每加一个字,尾数多 14 位 ✓ 这是一次反向验证:源码没有直接写「双精度有 28 位尾数」,但它写了双精度角度最低位代表 360/2^28 度,从这一行就能反推出来,而且正好符合「每多一个字多 14 位」的规律。
● 十种尺度类型,规定的是同一件事 :这个量的最低位代表多少。
● 尾数位数可以按需分配 :整数型 14 位(留 1 位给符号),角度型 15 位(不需要符号)。
● 一部分类型系统被编码进了尺度约定 :机器只看得见 15 个位,「这个量是什么」活在注释和尺度表里,不占任何存储。
● 两种补数并存,且逐个字段声明 :基础算术是一补数(+0 / -0 为证),但具体字段可以声明为二补数,两者之间有专门的转换例程。源码里某一行写的 `2-S COMP.` 是字段级约定,不等于「这台机器用二补数」。
● 有一处源码与手算对不上 (-B- 型的 MAX),本册如实标出,不用它做论证。
● SCALED Bnn 就是「小数点左移 nn 位」 ,两个常数都能用名字当场验算。
4 一条数字的完整旅程从 15 位计数值到 359.99 度,每一步都能手算
光看规矩容易空。这一章挑一个真实的数字,让它从源码里走进来、走完尺度换算、再走出去变成一个能自己核对的角度。全程只有乘法和四舍五入,一支笔就能跟完。先说清这一章证明了什么 :它证明的是表示层自洽 ——源码规定的编码规则能把自己标注的量程原样还原出来。它还没有证明「这台机器在真实运算中把误差控制住了」,那是第 5、6 章的事。
一个数字的完整旅程:从 15 位计数值到 359.99 度
全程只有乘法和四舍五入,读者可以跟着算一遍
① 源头
CDU 送来 15 位(bit)整数 N,15 位全 1 时 N = 32767
② 尺度约定(源码规定)
BIT 1 = 360 / 2¹⁵ 度 = 0.010986328125 度
③ 换算
32767 × 0.010986328125 = 359.989013671875 度
④ 对照
显示格式 XXX.XX → 359.99,与源码标注的 MAX 359.99 吻合 ✓
图 6 一个 15 位(bit)的 CDU 计数值走完尺度约定、换算、显示格式三步,落到源码自己标注的 359.99 度上。源码实证 Comanche055/ASSEMBLY_AND_OPERATION_INFORMATION.agc:705-708
# -D- 15
# CDU DEGREES XXX.XX DEGREES SP BIT 1 = 360/2 DEGREES
# (MAX 359.99) (USES 15 BITS FOR MAGNI-
# TUDE AND 2-S COMP.) 这一行把三件事一次说清:最低位的权重(360/2^15 度)、能表示的最大值(359.99)、以及位是怎么分配的(15 位全给大小——源码括注 USES 15 BITS FOR MAGNITUDE)。防杠注:常见科普说角度是「尾数 14 位、一圈 = 2^14 格」,那是把尺度表里的 180° 档(180/2^14,MAX 179.99)记串到了全圈档上——360° 全圈档源码明确括注 USES 15 BITS FOR MAGNITUDE,1 格 = 360/2^15 ≈ 0.011 度,最大 359.99 正由 (2^15−1)×360/2^15 得出。本册的整条证据链就从这里出发。同行末尾那个 `2-S COMP.` 按第 3 章的处理:它是这个数据格式自己的符号约定 ,不等于这台机器的基础算术是二补数——这台机器同时存在一补数字段(第 7 章有证),两者之间还要靠 CDULOGIC 这类例程专门转换。
源码实证 Comanche055/ASSEMBLY_AND_OPERATION_INFORMATION.agc:723-726
# -H-
# DP DEGREES (360) XXX.XX DEGREES DP BIT 1 OF LOW REGISTER =
# 28
# (MAX 359.99) 360/2 DEGREES 同一个角度量的双精度版本。显示格式一样(都是 XXX.XX),但最低位权重从 360/2^15 变成 360/2^28——这就是多用一个字换来的东西。
WALKTHROUGH 一个 CDU 计数值怎么变成 359.99 度
取这个量能表示的最大计数值——15 位全是 1,即十进制 32767——让它按源码规定的尺度走一遍。最后拿结果和源码自己在同一行标注的最大值对照。
已知条件
· 源码规定:CDU DEGREES 型,单精度,最低位(BIT 1)= 360/2 的 15 次方度
· 15 位全部用来表示大小,最大计数值 = 2 的 15 次方 − 1 = 32767
· 十进制显示格式为 XXX.XX,保留两位小数
步 代码在算什么 为什么这么做 代入本例 结果 1 N = 32767 15 位全 1,就是这个量能取到的最大计数 2^15 - 1 32767 2 BIT 1 = 360/2^15 源码尺度表 -D- 型给出的最低位权重 360 / 32768 0.010986328125 度 3 N × BIT 1 计数值乘最低位权重,得到物理角度 32767 × 0.010986328125 359.989013671875 度 4 XXX.XX 按十进制格式四舍五入到两位小数 359.989013671875 → 359.99 359.99 度 5 MAX 359.99 与源码自己在同一行标注的最大值对照 359.99 = 359.99 一致 ✓
换一条路再算一遍
# 不乘小数,直接用比例
N / 2^15 × 360
= 32767 / 32768 × 360
= 0.999969482421875 × 360
= 359.989013671875 度
→ 四舍五入 359.99
# 源码标注:MAX 359.99
# Comanche055/ASSEMBLY_AND_OPERATION_INFORMATION.agc:705 两条路都算出 359.989013671875 度,四舍五入之后与源码标注的 359.99 吻合。这不是巧合:源码写的 MAX 359.99,正是按这套换算算出来的。这条链的价值在于它两头都接在源码上 ——起点是源码规定的位权,终点是源码标注的最大值,中间只用了乘法。
换成双精度,分辨率差多少
# 单精度 CDU:最低位 = 360/2^15 度
360 / 32768 = 0.010986328125 度
× 3600 = 39.55078125 角秒
# 双精度角度:最低位 = 360/2^28 度
360 / 268435456 = 1.341104507446289e-06 度
× 3600 = 0.004827977... 角秒
# 两者之比
39.55078125 / 0.0048279778 = 8192 = 2^13● 这条链两头都接在源码上 :起点是源码规定的位权,终点是源码标注的最大值。
● 中间只用乘法 ,所以谁都能复核。这也是本册选它当主证据链的原因。
● 多用一个字,角度分辨率提高 8,192 倍 (2 的 13 次方)——从约 39.6 角秒变成约 0.0048 角秒。
5 把精度从浪费里捞回来先左移到「够大」,记下移了几位,算完再挪回去
15 位能表达的精度是固定的,可数字有大有小。如果一个数很小,它高位上的 0 就占了地方却没派上用场。这台机器的处理办法很朴素:先把数左移到够大为止,记下移了几位,算完再按记录挪回去。左移本身不产生任何新信息,只是把已有的位挪到能派上用场的位置上。
归一化:左移 11 位,把有效位顶到尾数最高端
以 x = .000244 (= 4 × 2⁻¹⁴) 为例,源码 INTERPRETER.agc:1629–1651(TSLC 例程)实际跑出来就是 11 次左移。
左移前:
15 位寄存器的尾数 14 位中只有 1 位为 1,前面 11 个 0 没携带任何信息
0 符号 0 最高 0 0 0 0 0 0 0 0 0 0 1 0 0 最低
MPTEMP 0
左移 11 位后:
原本在尾数第 12 位(自左数)的那位 1,挪到了符号位右侧的尾数最高位,结果回到 ≥ 0.5
0 符号 1 最高 0 0 0 0 0 0 0 0 0 0 0 0 0 最低
MPTEMP 11
左移 11 位
MPTEMP 同步 +11
前导 0(高位的空位,不带信息)
有效位(携带信息的 1)
移位计数 MPTEMP
图 7 归一化前后位模式对比:以 .000244 (.25/2^10) 为例,原值 4=100₂,在 15 位中只占 3 个有效位,左移 11 位后用满 14 位尾数,左侧空白消失。源码实证 Comanche055/INTERPRETER.agc:1628-1652
# TSLC -- TRIPLE SHIFT LEFT AND COUNT. SHIFTS MPAC LEFT UNTIL GREATER THAN .5 IN MAGNITUDE, LEAVING
# THE COMPLEMENT OF THE NUMBER OF SHIFTS REQUIRED IN X.
TSLC2 TS MPTEMP # START BY ZEROING SHIFT COUNT (IN A NOW).
TC BRANCH # EXIT WITH NO SHIFTING IF ARGUMENT ZERO.
TCF +2
TCF ENDTSLC # STORES ZERO SHIFT COUNT IN THIS CASE.
TC TPAGREE # MAY CAUSE UPSHIFT OF ONE EXTRA PLACE.
CA MPAC # BEGIN NORMALIZATION LOOP.
TCF TSLCTEST
TSLCLOOP INCR MPTEMP # INCREMENT SHIFT COUNTER.
EXTEND
DCA MPAC +1
DAS MPAC +1
AD MPAC
ADS MPAC
TSLCTEST DOUBLE # SEE IF (ANOTHER) SHIFT IS REQUIRED
OVSK
TCF TSLCLOOP # YES -- INCREMENT COUNT AND SHIFT AGAIN.
ENDTSLC CS MPTEMP
TCF STORE1 # STORE SHIFT COUNT AND RETURN TO DANZIG. 注释把契约写得清清楚楚:左移到「绝对值大于 .5」为止,把移位次数记下来(存的是补数,最后 CS MPTEMP 取反)。循环体就是一次左移(DOUBLE 加进位)。在这一个循环里 ,溢出标志被主动当成「已经左移到目标幅值」的判定信号:OVSK 一跳,循环就结束。这是算法在消费溢出,不是把溢出当故障——但这句话只对这一段成立,不能推广成「这台机器把溢出当正常结果」(真正的非法状态走的是第 7 章的 POODOO)。
源码实证 Comanche055/INTERPRETER.agc:2509-2513
XCH MPAC +2 # MPAC IS ZERO -- SHIFT LEFT 14.
XCH MPAC +1
TS MPAC
CAF SEVEN # AUGMENT RIGHT SHIFT COUNTER.
TS MPTEMP 开平方例程里的一段:把三个字整体左移 14 位(一次字交换),然后把右移补偿计数加上 7。注意这个 7——下一章会用到。
作者推导 由上一条的 14 与 7 推出
左移 14 位 → 数值 × 2^14
开平方 → 指数减半:sqrt(x × 2^14) = sqrt(x) × 2^7
所以补偿 → 结果右移 7 位
源码里:CAF SEVEN / TS MPTEMP —— 正好是 7 这处细节最能说明「移位计数」是怎么被当成一个显式的量来管理的:左移 14 位之后,开方带来的指数变化是减半,所以右移补偿必须是 7 而不是 14。源码用 CAF SEVEN 直接写了 7,没有多余的解释——因为对写它的人来说这是常识。
源码实证 Comanche055/ORBITAL_INTEGRATION.agc:424-433 | 同文件:921
# AS VECTOR.
PUSH DMP # STORE COSPHI**2 SCALED B2 IN 8D
# Page 1343
5/8 # 5 SCALED B3
PDDL SR2 # PUT 5 COSPHI**2, D5, IN 8D. GET
# COSPHI**2 D2 FROM 8D
DAD BDSU # END UP WITH (1-7 COSPHI**2), B5
8D # ADDING COSPHI**2 B4 SAME AS COSPHI**2
# X 2 D5
D1/32 # 1 SCALED B5
5/8 2DEC 5 B-3 这一段的价值在于:注释把每一次运算之后的标度都写明了,于是可以拿源码自己的记账去对账。乘法的标度指数相加 (B2 × B3 → B5),加法和减法要求两个操作数同标度 ——所以常数 1 必须专门写成 B5 的 D1/32。这两条规则就是这台机器上「小数点跟着运算走」的全部内容。
WALKTHROUGH 一次定点乘法,标度是怎么记账的
取上面那段代码里的第一次乘法走一遍。全程用整数算,不引入任何小数——这正是这台机器实际在做的事。
已知条件
· COSPHI2 在 B2 标度下,本例存整数 2** → 实际值 2 × 2⁻² = 0.5
· 常数 5/8 定义为 `2DEC 5 B-3`,存整数 5 → 实际值 5 × 2⁻³ = 0.625
· 源码注释写明:这次乘法的结果落在 B5
步 代码在算什么 为什么这么做 代入本例 结果 1 DMP 整数相乘,机器眼里没有小数点 2 × 5 10 2 标度指数相加 2⁻² × 2⁻³ = 2⁻⁵,所以 B2 × B3 = B5 2 + 3 B5 3 10 × 2⁻⁵ 把整数结果按新标度还原成实际值 10 / 32 0.3125 4 0.5 × 0.625 直接用真值相乘,作为独立校验 0.5 × 0.625 0.3125 ✓
用整数运算独立验算
# 全程整数,不用小数
A = 2, 标度 2^-2 → 实际值 0.5
B = 5, 标度 2^-3 → 实际值 0.625
A × B = 10, 标度 2^-(2+3) = 2^-5
10 × 2^-5 = 10 / 32 = 0.3125
0.5 × 0.625 = 0.3125 一致 ✓
# 源码依据
# ORBITAL_INTEGRATION.agc:425-427 COSPHI**2 SCALED B2 / 5/8 # 5 SCALED B3
# ORBITAL_INTEGRATION.agc:429 PUT 5 COSPHI**2, D5, IN 8D
# ORBITAL_INTEGRATION.agc:921 5/8 2DEC 5 B-3 两条路都得到 0.3125。乘法的标度指数相加,加法的标度必须对齐 ——这两条规则覆盖了这台机器上绝大多数的「小数点管理」工作,也解释了为什么注释里到处都是 B 后面跟一个数字。
换成加减法,规则就不一样了
# 加减法不产生新的标度,但要求两个操作数本来就在同一标度上
# 所以源码要把常数 1 专门写成 B5 的 D1/32:
#
# D1/32 1 SCALED B5 存整数 1 → 1 × 2^-5
#
# 而不是写成 1。写成 1 的话,它在这个上下文里的实际值
# 就成了 1 × 2^-5 = 1/32,与参与运算的另一个 B5 量对不上。 ● 归一化解决的问题是浪费 :小数字的高位 0 不携带信息,却占了位置。
● 移位次数必须记下来 ,否则算完就还原不回去了。源码用一个 MPTEMP 专门存它。
● 在这一个归一化循环里,溢出被用作「到位」的信号 :TSLCTEST 靠 OVSK(溢出则跳过下一条)判断已经够大。这是算法主动消费溢出标志,只适用于这一段。
● 对照第 7 章 :真正算不下去时走的是完全不同的路——POODOO 加一个告警码,停在那里等上层处理。「可以消费的溢出」和「必须中止的非法状态」是两回事,这份代码把两者的边界切得很清楚。
6 两次牛顿就够了先用一条直线猜个初值,再迭代两次补到双精度
前面几章讲的都是规矩。规矩好不好用,得拿一个真实例程从头到尾走一遍才知道。这一章的实验台是开平方 ——不是因为开平方本身有多重要,而是因为它把归一化、线性近似、定点量化、误差收敛这几件事集中在一个不到 100 行的例程里,全过程都看得见。主角仍然是由尺度约定、归一化、多精度与软件算术构成的数值系统 ,开平方只是把它摆到台面上的那件样品。
开平方的五个阶段
归一化 → 线性近似 → 一次单精度牛顿 → 一次双精度牛顿 → 反归一化
输入 MPAC(三精度)
大小未知,可能很小
① 归一化
左移到 ≥ .5,移位次数记入 MPTEMP
② 线性近似取初值
一条直线,误差约 ±1%
③ 单精度牛顿
误差降到单精度分辨率量级
④ 双精度牛顿
再迭代一次,落到双精度最后一位
⑤ 反归一化
按 MPTEMP 右移还原
两次迭代不是随便定的:初值最坏误差 1.10%,两次之后落到 1.4~1.5 个双精度最低位
图 8 开平方的五个阶段:归一化、线性近似取初值、一次单精度牛顿、一次双精度牛顿,最后按记录的移位次数还原。
ARGHI/ARGLO 两段线性近似的相对误差
解析式 e(x) = ((2Sx + B) / (√2·√x) − 1) × 100%,S/B 取各段常数;平稳点 x* = B/(2S)。曲线由解析式计算,端点与源码常数核对一致。
−0.5%
0%
+0.5%
+1.0%
0.125
0.1875
0.25
0.3125
0.375
0.4375
0.5
线性近似的相对误差 e(%)
归一化自变量 x
ARGLO
ARGHI
ARGLO 段 [0.125, 0.25]
ARGHI 段 [0.25, 0.5]
+1.10%
+0.92%
+0.89%
+0.76%
x*=0.179, e=-0.49%
x*=0.356, e=-0.67%
图 9 ARGHI(x ∈ [.25, .5],S=.5884、B=.2096)与 ARGLO(x ∈ [.125, .25],S=.8324、B=.1487)两段全区间误差曲线。最大值出现在区间端点(解析上界),最小值在 x*=B/S 处(导数为零的平稳点)。外部资料 图 10 <b>Comanche055 原始打印清单第 1184 页(SQRTSUB 第一页)</b>。这是 1969 年 4 月 1 日那批绳芯源代码的物理复印件,由 MIT 仪器实验室在装订时刻印。图上能看到:注释把整段契约写在最上面,CAF ZERO 把右移计数先清零,CAF SEVEN 把 7 写进 MPTEMP(与第 5 章那段 14/7 的除法对应),最末两行 SQRTABRT TC POODOO 与 OCT 1302 是算不下去时的告警路径。本册第 6 章引用的行号(自 2497 行起,即打印清单页 1184)就是这张图上能数到的物理行号。源码实证 Comanche055/INTERPRETER.agc:2497-2499
# DOUBLE PRECISION SQUARE ROOT ROUTINE. TAKE THE SQUARE ROOT OF THE TRIPLE PRECISION (MPAC +2 USED ONLY
# IN NORMALIZATION) CONTENTS OF MPAC AND LEAVE THE NORMALIZED RESULT IN MPAC (C(MPAC) GREATER THAN OR EQUAL TO
# .5). THE RIGHT SHIFT COUNT (TC UNNORMALIZE) IS LEFT IN MPTEMP. 三行注释把主契约说完了:输入是三精度,输出是归一化形式(≥ .5),右移补偿次数留在 MPTEMP 里交给调用方。注释之外,这段代码还处理了四件边界事:第三字(MPAC+2)只在归一化时用;参数为零直接返回零(SQRT(0) = 0);负的小值(幅值小于 10⁻⁴)按零处理;出口前专门检查不留负零(FIXROOT)。输入输出边界写得这么清楚,是这份代码给我印象最深的地方之一。
源码实证 Comanche055/INTERPRETER.agc:1748、2186、1721、1964(四个常数)
SLOPEHI DEC .5884 # SQRT CONSTANT.
SLOPELO DEC .8324
BIASLO DEC .2974 B-1 # SQRT CONSTANT
BIASHI DEC .4192 B-1 # SQRT CONSTANTS. 两组斜率和截距,对应归一化之后的两段区间。源码把它们叫做 SQRT CONSTANT,没有解释怎么来的——但可以用代入验算反推,见下面的手算。
源码实证 Comanche055/INTERPRETER.agc:2556-2559(线性近似取初值)
ARGHI CAF SLOPEHI # ARGUMENT BETWEEN .25 AND .5, GET A
EXTEND # LINEAR APPROXIMATION FOR THIS RANGE.
MP MPAC
AD BIASHI # X0/2 = (MPAC/2)(SLOPHI) + BIASHI/2. 就三行:取斜率、乘上自变量、加截距。注释里连公式都写好了——X0/2 = (MPAC/2)×SLOPEHI + BIASHI/2。注意这里算的是 X0 的一半,后面所有运算都保持在「半值」上,为的是不溢出。
源码实证 Comanche055/INTERPRETER.agc:2561-2582(两次牛顿迭代)
+4 TS BUF # X0/2 (ARGLO ENTERS HERE).
CA MPAC # SINGLE-PRECISION THROUGHOUT.
ZL
EXTEND
DV BUF # (MPAC/2)/(X0/2)
EXTEND
MP HALF
ADS BUF # X1 = X0/2 + .5(MPAX/2)/(X0/2)
EXTEND
MP HALF # FORM UP X1/2.
DXCH MPAC # SAVE AND BRING OUT ARGUMENT.
EXTEND # TAKE DP QUOTIENT WITH X1.
DV BUF
TS BUF +1 # SAVE MAJOR PART OF QUOTIENT.
CAF ZERO # FORM MINOR PART OF QUOTIENT USING
XCH L # (REMAINDER,0).
EXTEND
DV BUF
TS L # IN PREPARATION FOR DAS.
CA BUF +1
DAS MPAC # X2 = X1/2 + (MPAC/2)X1 第一次迭代那行写着 SINGLE-PRECISION THROUGHOUT(全程单精度),用的是单精度除法 DV;第二次迭代改用双精度,还专门处理了余数(CAF ZERO / XCH L / DV BUF 那段)。先粗后精,各花各的代价。
作者推导 verify_accurate.py(本册配套脚本,可自己跑)
$ python verify_accurate.py
# B 段:全区间误差上界(解析求极值)
ARGHI → 全区间最大 |e| = 0.8900%(出现在 x = 0.250)
ARGLO → 全区间最大 |e| = 1.1000%(出现在 x = 0.125)
# C 段:实数域牛顿 vs 定点量化牛顿
量化方式:截断
实数域牛顿,最大相对误差 = 7.7046e-10 = 0.21 个双精度最低位
定点量化牛顿,最大相对误差 = 5.2568e-09 = 1.41 个双精度最低位
量化带来的额外误差 = 6.8 倍 脚本只用标准库,直接在命令行跑。它做的两件事,正好是这一章最需要补的两个洞:把「抽样验证」升级成解析上界 ,以及把「实数域收敛」和「定点实现误差」拆开算。必须说清楚它的边界 :这是本册按源码约定的位宽(单精度 14 位尾数、双精度 28 位尾数)建立的量化模型,不是 在真实 AGC 或模拟器上跑出来的结果。它证明的是「在这套位宽约定下,这个算法的误差量级是多少」,不是「yaAGC 跑出来就是这个数」。真正的执行证据在第 8 章:那里让真实的 Luminary 099 绳芯镜像跑了 72 点,最坏 0.925 个双精度最低位——比这里的模型上界还小 ,说明这个模型确实是覆盖全情形的上界,而不是一句估摸。
WALKTHROUGH 源码里那条直线,最坏能差多少
不抽样、不碰运气。把源码给的两个常数当成一条数学函数,直接求它在整个规定区间上的最坏相对误差 ——这是一个闭式解,不是估计。
已知条件
· 源码公式:X0/2 = SLOPE × x + BIASHI;真值:f(x) = √(2x) / 2
· ARGHI 段:SLOPEHI = .5884,BIASHI 本册取 .2096 (源码写作 `DEC .4192 B-1`),x ∈ [.25, .5]
· ARGLO 段:SLOPELO = .8324,BIASLO 本册取 .1487 (源码写作 `DEC .2974 B-1`),x ∈ [.125, .25]
· 相对误差:e(x) = (S·x + B) / f(x) − 1 = √2 · (S·√x + B/√x) − 1
· BIASHI / BIASLO 取值的依据:只有取成 .2096 / .1487,这条直线在各自区间两端才都落在 1% 量级;这是代入验证,源码未写明该常数的取值方向
步 代码在算什么 为什么这么做 代入本例 结果 1 e′(x) = (S·x − B) / (2·x^1.5) 对相对误差函数求导,找极值点 令 e′(x) = 0 唯一驻点 x = B/S 2 x* = B / S 导数在 x* 左侧为负、右侧为正 ⇒ 这是**极小值**,不是极大值 ARGHI:.2096/.5884 0.356220 3 最大值只能在端点 区间内只有一个极小值、没有极大值,所以最坏情况必在两端 比较 e(左端) 与 e(右端) 取绝对值较大者 4 ARGHI 两端 x = .25 与 x = .5 e(.25) = +0.8900% e(.5) = +0.7600% 最坏 0.8900% 5 ARGLO 两端 x = .125 与 x = .25,同一套做法 e(.125) = +1.1000% e(.25) = +0.9183% 最坏 1.1000%
跑一遍脚本,自己复核
$ python verify_accurate.py # 本册配套脚本,只用标准库
ARGHI 区间 [0.250, 0.500] S=0.5884 B=0.2096
导数零点 x* = B/S = 0.356220 (极小值点)
左端点 x=0.250 e = +0.8900%
右端点 x=0.500 e = +0.7600%
极小值 e = -0.6708%
→ 全区间最大 |e| = 0.8900%(出现在 x = 0.250)
ARGLO 区间 [0.125, 0.250] S=0.8324 B=0.1487
导数零点 x* = B/S = 0.178640 (极小值点)
左端点 x=0.125 e = +1.1000%
右端点 x=0.250 e = +0.9183%
极小值 e = -0.4900%
→ 全区间最大 |e| = 1.1000%(出现在 x = 0.125) 初始最坏相对误差:ARGHI 段 0.8900%,ARGLO 段 1.1000%。 这两个数是解析上界,不是抽样碰运气。需要说明的是:本册只证明「这条直线在区间上的误差不超过这两个值」,没有论证它是不是某种最优拟合 ——源码没有给出拟合准则,任何更强的说法都是猜的。
关键一问:实数域牛顿,和这台机器上的定点牛顿,是一回事吗
$ python verify_accurate.py # 同一脚本的 C 段
双精度最低位权重 2^-28 = 3.7253e-09
量化方式:截断
实数域牛顿,最大相对误差 = 7.7046e-10 = 0.21 个双精度最低位
定点量化牛顿,最大相对误差 = 5.2568e-09 = 1.41 个双精度最低位
量化带来的额外误差 = 6.8 倍
量化方式:四舍五入
实数域牛顿,最大相对误差 = 7.7046e-10 = 0.21 个双精度最低位
定点量化牛顿,最大相对误差 = 5.6968e-09 = 1.53 个双精度最低位
量化带来的额外误差 = 7.4 倍 ● 开平方是实验台,不是主角。 它只是把归一化、线性近似、定点量化、误差收敛集中在一个短例程里,方便我们把整条链看清楚。
● 初始最坏误差是解析上界 :ARGHI 段 0.8900%、ARGLO 段 1.1000%,由 e′(x) 的符号确定极值只能落在端点,不是抽样碰运气。
● 实数域牛顿和定点牛顿不是一回事 :实数域两次迭代只有 7.7×10⁻¹⁰(约 0.2 个双精度最低位),把 14 位/28 位定点量化加进去之后变成约 5×10⁻⁹(1.4~1.5 个双精度最低位)。最终误差的大头来自定点量化,不是牛顿收敛。
● 两次真的够了 :再迭代一次,收益会被双精度自身的分辨率吃掉。这也解释了源码为什么只写两次。
● 本册没有论证那条直线是最优拟合 ,只给出它在规定区间上的误差上界。源码没有给出拟合准则,更强的说法本册不写。
7 误差的另一半:负零、溢出和告警算不下去的时候,停下来报错,不硬算下去
前面六章都在讲怎么算得准。这一章讲另一半:算不下去的时候怎么办,以及把前六章那些零散的结论收成一个模型。这台机器对「算不下去」的回答很干脆——停下,写一个告警码,交给上层。它甚至要为「负零」这种东西单独写处理代码,因为在这台机器上,零确实有两个写法。
源码实证 Comanche055/INTERPRETER.agc:733-735
# L+1 IF MPAC IS GREATER THAN ZERO.
# L+2 IF MPAC IS EQUAL TO +0 OR -0.
# L+3 IF MPAC IS LESS THAN ZERO. 分支指令的出口说明里,把「等于 +0 或等于 -0」单列成一路。如果只有一个零,这里完全不需要写两个。这一行本身就是一补数的直接证据。
源码实证 Comanche055/INTERPRETER.agc:2529
TCF FIXROOT # DO NOT LEAVE SQRTSUB WITH -0 IN MPAC. 开平方例程结束前专门检查一遍:不能给调用方留下 -0。一条注释、一次跳转,防的是一个只在特定表示法下才存在的坑。
源码实证 Comanche055/INTERPRETER.agc:1793、1837、1853、2331(除法里的符号处理)
TS DVSIGN # +-1 FOR POSITIVE QUOTIENT -- -0 FOR NEG.
INCR DVSIGN # NOW -0.
INCR DVSIGN # NOW +1 OR -0.
UMPAC- CS ZERO # IF NEGATIVE, SET -0 IN DVSIGN FOR FINAL 除法里 -0 被当成一个有意义的状态 在用:CS ZERO 得到 -0,当作「结果为负」的记号,后面 INCR 一句就能在 -0 和 +1 之间切换。把 -0 当哨兵用,是这段代码里的常见手法。
源码实证 Comanche055/INTERPRETER.agc:2540-2541(算不下去的时候)
SQRTABRT TC POODOO
OCT 1302 开平方遇到负数(或负得离谱、修正后仍超范围),不硬算,直接跳 POODOO 并带上八进制码 1302。POODOO 是这份代码里的「放弃并报警」例程——相当于今天抛出一个带编号的致命异常。给每个失败一个编号 ,是这套系统能被诊断、能被复盘的前提。
源码实证 Comanche055/INTERPRETER.agc:229
AD -OCT10 # (NO -0). 注释只有四个字:(NO -0)。写这行的人知道这里不会出现负零,并且把这个判断写进了注释——这样下一个人读到这里就不用再推一遍。
作者推导 由上五条归纳
本册在 Comanche055/INTERPRETER.agc 里找到的 -0 相关显式处理:
:734 分支出口把 +0 与 -0 并列
:1793 除法符号位用 -0 表示「负」
:1837 INCR 之后变成 -0
:1853 INCR 之后变成 +1 或 -0
:2331 CS ZERO 造出 -0 当哨兵
:2529 开平方结束前检查不能留下 -0
:229 注释声明此处不会出现 -0
→ 一补数有两个零,这件事在源码里不是理论,是日常 七处,分散在分支、除法、开平方三个不同的地方。这说明「负零」不是某个例程的局部怪癖,而是这套表示法给整个代码库带来的普遍负担——每一处判断符号的地方都得想着它。
作者推导 把前七章归纳成一个模型
这台机器一次数值计算的误差,只有六个来源
① 表示误差 定点量化。尾数 14 / 28 / 42 位,装不下的部分只能丢掉
② 运算误差 乘法与除法的截断、舍入
③ 缩放误差 左移右移、归一化与反归一化带来的位移损失
④ 迭代误差 牛顿这类迭代算法的收敛残差
⑤ 范围错误 上溢、下溢、定义域非法
⑥ 接口错误 两个量的尺度没对齐、符号约定没对齐
前四项是**精度**问题,机器不报错,只会安静地算出一个略微偏差的数;
后两项是**正确性**问题,机器会停下来报错。 把前七章串起来看,结论其实不是「没有浮点也能算准」,而是:这台机器没有消灭误差,它把误差变成了一个可以被设计、被传播、被检查、被约束的量。 精度类误差靠位宽、尺度约定和归一化去压——压到多少是可以算出来的(第 6 章给出了具体数字);正确性类误差靠显式检查去拦——拦下来就报一个编号(POODOO 加告警码)。能算清楚的让它安静地算,算不清楚的让它响亮地停。 这比「凭惊人智慧克服了硬件限制」这种说法准确得多,也更值得学习。
● 这台机器用一补数,所以零有两个写法 :+0 和 -0。源码里至少七处为它单独写处理。
● -0 不只是麻烦,也被当成工具用 :除法里拿它当「结果为负」的哨兵,一个 INCR 就能切换状态。
● 算不下去就报编号 :开平方遇到负数直接跳 POODOO 并带码 1302。给失败编号,才谈得上诊断。
8 把机器叫起来问一次从源码到仿真器:执行证据链在这一章闭合
前面七章的结论,都能靠读源码加手算核对,第 6 章还把它们写成了脚本。但那终究是按源码约定搭出来的数值模型 ,不是机器。这一章换一种问法:把当年那份真实的 Luminary 099 装进 AGC 仿真器,让机器自己算一遍 √x,然后把寄存器里的位模式读出来,看它和第 5、6 章的预测对不对得上。
背景 图 11 <b>背景图:Apollo 11 登月舱上升段</b>。本章在 yaAGC 上跑的那份 Luminary 099,就是这台 LM 登月时实际在用的绳芯镜像。本图不作为任何数字的证据;它只用来把读者放在「这台机器真的飞过」的位置上。
72 个 x 值在 yaAGC 指令级模拟器上的实测 |e|(单位:DP LSB)
真实绳芯镜像 + yaAGC 实测(红 = 正值,绿 = 负值,灰 = 接近 0)
+1 LSB
0
−1 LSB
0.25
0.375
0.5
0.75
1.0
x = m / 2¹⁴
绝对误差 e(单位:DP LSB = 2⁻²⁸ ≈ 3.7e-9)
最大 |e| = 0.925 LSB(x = 0.6650,图中圆圈处)
图 12 72 个 x 值上72 个 x 值上仿真器(yaAGC)运行的相对误差 e/x 分布(×2^-28 单位),与 ±1 个双精度最低位(DP LSB)对照。47 个点完全正确(四舍五入与 28 位正解一致),24 个点低 1 个 LSB,1 个点高 1 个 LSB,没有点超过 ±1 LSB。源码实证 Apollo-11/README.md:101-109(Attribution 表)
Comanche055 | ... `Assemble revision 055 of AGC program Comanche by NASA`
| `2021113-051. 10:28 APR. 1, 1969`
Luminary099 | ... `Assemble revision 001 of AGC program LMY99 by NASA`
| `2021112-061. 16:27 JUL. 14, 1969`
Assembler | yaYUL
Contact | Ron Burkey <info@sandroid.org>
Website | www.ibiblio.org/apollo 这张表交代了三件事:两份程序的正式编号与装订记时、汇编器是 yaYUL、以及源码的数字化出自 MIT 博物馆藏硬拷贝。用 yaYUL 不是本册挑的,是这份源码自己指定的。 本册引用的「16:27 JULY 14, 1969」也来自这里——它是源码自身的装订记时,和仓库 commit 的提交时间完全是两件事。
外部资料 参考文献 [15]:Virtual AGC 项目
yaYUL —— AGC 的交叉汇编器(当年 YUL 的开源重实现)
yaAGC —— AGC 的指令级模拟器
yaDSKY —— DSKY 显示键盘模拟器
项目:github.com/virtualagc/virtualagc
网站:virtualagc.github.io/virtualagc/ 与 ibiblio.org/apollo 这一档证据不能回仓库核对,所以单独标出来。它在本书里的角色很明确:给真实源码准备一套能跑的机器。 yaYUL 负责把 MAIN.agc 变成 36 个 bank 的绳芯镜像,yaAGC 负责让这份镜像在今天的电脑上执行。两点边界要说清:yaAGC 只模拟计算机本体,不模拟外设(DSKY、IMU 等另有模拟器);本册实验用的是从源码自建的版本,不是官方发布的二进制包,所以版本横幅里没有版本号,只有构建日期。
机器实证 工具链搭建与两份绳芯镜像产出(真实命令与输出)
环境:Ubuntu 26.04 LTS(WSL2)× gcc 15.2.0
构建:yaYUL ── 290,264 B yaAGC ── 170,952 B 两者 exit=0
启动横幅:Apollo Guidance Computer (AGC) assembler, built Sep 10 2026, target AGC4
【登月舱】Luminary 099(装订记时 16:27 JUL. 14, 1969)
$ yaYUL MAIN.agc > lum099.lst 2> lum099.err
yaYUL exit=0 清单 67,485 行 stderr 1 行
MAIN.agc.bin → 73,728 字节
stderr 唯一一行:ERASABLE_ASSIGNMENTS.agc:987: Warning: May overflow memory bank.
【指令舱】Comanche 055(装订记时 10:28 APR. 1, 1969)
$ yaYUL MAIN.agc > com055.lst 2> com055.err
yaYUL exit=0 stderr 0 行
MAIN.agc.bin → 73,728 字节 73,728 字节这个数值得停一下:它正好等于 36,864 个字(word)× 16 位(bit)÷ 8 ÷ 1024 = 72 KB ,和源码文档里写的只读存储容量分毫不差。汇编器吐出来的不是一个「差不多」的文件,而是把 36,864 个字的容器一个不多、一个不少地填满了 ——这份 1969 年的代码,到今天仍然是完整可汇编的。(填满证明的是容器尺寸严丝合缝;与飞行绳芯的逐字关系取决于转录保真度,需另行比对。)本册实验用的镜像 MD5 为 `4531dff39d0b8379761f19eb2f67f792`。
作者推导 Luminary099/MAIN.agc:34、88(两处对不上的文件名)
# MAIN.agc 的包含清单里写着:
$LAMBERT_AIMPOINT_GUIDANCE.agc # pp. 651-653
$TRIM_GIMBAL_CNTROL_SYSTEM.agc # pp. 1472-1484
# 而目录里实际躺着的文件是:
GENERAL_LAMBERT_AIMPOINT_GUIDANCE.agc
TRIM_GIMBAL_CONTROL_SYSTEM.agc第一次汇编直接失败:`yaYUL exit=12`,0.156 秒,stderr 2,578 行,全是 `Symbol "ZEROVECS" undefined or offset bad` 这类符号找不到。顺着报错往回查,根因不在代码,而在两个文件名 :清单里要的是 `CNTROL`(少一个 O,是当年硬拷贝上的拼写),目录里的文件被整理成了正确的 `CONTROL`;另一个则是目录里的文件前面多了 `GENERAL_`。把这两个文件改成清单里要的名字之后,`exit=0`,清单 67,485 行,stderr 只剩 1 行。这条本册单独列出来,是因为它说明一件事 :读源码和跑源码是两种不同的检验。前者看内容对不对,后者才会撞上这类「内容全对但拼不上」的问题。至于那 2,578 行符号报错,全是这一个根因的下游噪音,改完自动消失。
作者推导 怎么让这台机器只算一次开平方(本册的做法)
AGC 复位后从固定存储器 0o4000 开始执行(线性地址 0x800):
原样: INHINT / CAF GOBB / XCH BBANK / TCF GOPROG ← 开机自检的入口
本册把它替换成一段桩程序,直接调用 SQRTSUB:
0o4000 INHINT 00004 ← 必须保留,否则 DOWNRUPT 会插进来打断
0o4001 TC SQRTSUB 03343
0o4002 CA MPAC 30154
0o4003 TS 0o0371 54371
0o4004 CA MPAC+1 30155
0o4005 TS 0o0372 54372
0o4006 CA MPAC+2 30156
0o4007 TS 0o0374 54374
0o4010 CA MPTEMP 30135
0o4011 TS 0o0373 54373
0o4012 TC 0o4012 04012 ← 停在这里,等我们来读
线性地址换算(本册用四个断点实测反推并交叉验证):
固定存储器:0x800 + FB×1024 + (AGC地址 & 0o1777)
可擦写内存:EBANK×256 + (AGC地址 & 0o0377) 这段桩程序是本册手写的,用的是从真实汇编清单里反推出来的指令编码 (`TC`=0o00000、`CA`/`CAF`=0o30000、`TS`=0o54000、`INHINT`=0o00004…),不是照抄任何现成脚本。为什么要有 INHINT:第一轮尝试没有加它,机器确实执行了 `TC SQRTSUB`(中断记录里留着 `ZRUPT 0x0801`、`BRUPT 0x06e3` 的证据),但随即被 DOWNRUPT 打断,结果全是零。补回 INHINT 之后才拿到干净的值。这一段失败与修正的经过,也一并留在这里。
机器实证 72 点实测:让真实 SQRTSUB 自己算一遍 √x
m x MPAC(八进制) 机器结果 真值 误差/LSB
--------------------------------------------------------------------------
4096 0.2500000 020000/000000 0.5000000000 0.5000000000 +0.000
4266 0.2603760 020250/010544 0.5102704912 0.5102704935 -0.613
……
11236 0.6857910 032400/000000 0.8281250000 0.8281250000 +0.000
……
16166 0.9866943 037622/024243 0.9933248870 0.9933248894 -0.647
共 72 点,输入区间 x ∈ [0.25, 0.9867]
最大 |e| = 0.925 个双精度最低位
出现在 x = 0.6650391(该点相对误差 4.2263e-09)
平均 |e| = 0.373 个双精度最低位
最大 |e/x| = 4.5464e-09
出现在 x = 0.5197754(该点绝对误差 0.880 个最低位)
※ 两个最大值不在同一个点上,本册分开列出,不合成一个数
误差为负(低估)56 点,为正(高估)16 点
与「28 位下正确舍入值」逐点比对:
完全相同的点 47 点
低 1 个最低位的点 24 点
高 1 个最低位的点 1 点
差 2 个最低位以上的点 0 点
72 次运行,停机位置 Z 全部等于 0x080a(本册自陷循环首址)
⇒ 72 次 SQRTSUB 全部正常返回,没有一次被中断打断或卡死 这就是本册想要的执行证据 :不是模型说它应该等于多少,而是机器自己算出来、我们去读寄存器。每个点都是一次独立的 yaAGC 启动:先把输入写进 MPAC,让桩程序调用真实的 SQRTSUB,然后读 MPAC 和 MPAC+1 两个字的实际位模式,按 28 位小数反解成十进制。最坏情况:72 点全部落在 1 个双精度最低位以内 ,其中 47 点与 28 位下的正确舍入值一模一样,剩下 25 点也只差 1 个最低位,没有任何一点差到 2 个最低位。选 72 个点而不是 3 个,是为了让输入同时压住两条逼近分支 :x ≥ 0.5 的点折半后落进 ARGHI,x ∈ [0.25, 0.5) 的点折半后落进 ARGLO————第 5 章算出来最坏差 0.89% 与 1.10% 的那两条直线。两条都跑到,本章的结论才覆盖得住第 5 章。小数点另一头也不含糊:x = 0.25 和 x = 0.685791 这两个点上,机器给出的 0.5 与 0.828125 是精确值 ,低位字读出来就是 000000——不是巧合,是牛顿迭代收敛到了能精确表示的那个点上。
作者推导 实测 0.925 对模型 1.53:第 6 章的上界站住了,而且偏保守
第 6 章数值模型的预测(脚本口径:最大相对误差)
定点截断模型:5.2568e-09 (脚本读作 1.41 个双精度最低位)
定点舍入模型:5.6968e-09 (脚本读作 1.53 个双精度最低位)
第 8 章真实机器的实测(同一口径:最大相对误差)
4.5464e-09 出现在 x = 0.5197754
实测 / 模型 = 0.80 倍 ⇒ 模型没被实测顶破
换成绝对误差口径,再核一遍(以双精度最低位计,1 个 = 2^-28)
模型 1.53 个最低位 (脚本口径,隐含「结果幅值 ≈ 1」)
模型 1.08 个最低位 (按最坏点幅值 0.707 折算成真绝对误差)
实测 0.925 个最低位 出现在 x = 0.6650391
实测 / 模型 = 0.61 ~ 0.86 倍 ⇒ 两个口径下模型都是上界
为什么是这个量级?把两条误差来源分开算
① 牛顿截断(源于第 5 章那条直线的最坏 1.1% 相对误差)
手算(牛顿误差按平方缩):第 1 次迭代后 ≈ 0.011² / 2 = 6.05e-05
第 2 次迭代后 ≈ (6.05e-05)² / 2 = 1.83e-09
脚本算(第 6 章 verify_accurate.py,实数域牛顿,20 万点,仅覆盖 ARGHI 路径):7.7046e-10
两者同量级(相差 2.4 倍),说明「误差按平方缩」确实在起作用
② 定点量化底噪(28 位小数的表示精度)
结果落在 [0.5, 1),1 个最低位 ≈ 2^-28 / 0.5 = 7.45e-09
① 比 ② 小约 10 倍(按脚本值)⇒ 两次迭代之后,
剩下的误差**几乎全是量化底噪**,堆不出更大的东西来。 这一段是本册和第 6 章对表的地方,也是三条证明链扣在一起的地方:第 5 章算出那条直线的近似最坏差 1.1%(解析解),第 6 章算出两次牛顿之后定点误差约 1.4–1.5 个最低位(数值模型),第 8 章让仿真器(yaAGC)跑出来 0.925 个最低位(仿真器输出)。这三个数不共用一个单位——1.1% 是近似阶段的相对误差,后两个才是输出端的最低位口径:模型与仿真输出同为最低位口径、量级吻合;1.1% 讲的是误差的起点,不与最低位直接比大小,而且仿真输出比模型预测更小——这正是守恒的表现:模型为了覆盖所有情形,必然比某一条具体轨迹略悲观。如果仿真输出反而比模型大一截,那就说明模型漏掉了某项误差来源,第 6 章的结论就得推翻重来。现在不用。 余量在两个口径下不一样(0.80 倍与 0.61~0.86 倍),原因不难解释:脚本是用相对误差表达结果的,而相对误差要除以结果幅值——同一个绝对误差,结果越小看起来就越大。本册把两个口径都列出来,不挑好看的那一个。
方法论 三条证明链,现在都闭合了
✅ 证明链 1(表示):15 位数据 → 尺度 → 物理量
第 4 章 起点终点都接在源码上,中间只用乘法
✅ 证明链 2(算法):定点 → 归一化 → 乘除 → 牛顿 → 量化 → 最终误差
第 5、6 章 误差上界由解析式给出,配可复跑脚本 verify_accurate.py
✅ 证明链 3(执行):源码 → yaYUL → 绳芯镜像 → yaAGC → 寄存器里的实际数值
第 8 章 真实 Luminary 099 镜像,72 点实测,最坏 0.925 个双精度最低位
证据分层(本册用五档,第 8 章用到了最高一档)
① 机器实证 真实绳芯镜像上的运行结果,可复跑 ← 本册最强的一档
② 源码实证 仓库里按文件行号可核对的事实
③ 外部资料 NASA / MIT / 公开史料,给参考文献编号
④ 作者推导 我从源码数据算出来或推出的结论,写明算式
⑤ 解释性类比 解释工具,不算证据 把这三条链分开,是本册的证据纪律:一个结论是「源码这么写的」、是「我算的」、还是「机器跑出来的」,可信度并不一样,读者有权知道自己读到的是哪一种。第 6 章的脚本再严谨,也只是链 2;它不能冒充链 3,所以本册在这一版之前一直把链 3 标成未闭合。现在链条接上了,本册把「机器实证」单列一档放在最前面——证据越硬,越应该被单独标出来,而不是混在「作者推导」里含糊过去。
CONTROL FLOW 实际跑过的四步(命令都是真跑过的,不是路径示意) 环境 Ubuntu 26.04 LTS (WSL2) × gcc 15.2.0;工具链为 Virtual AGC 源码自建
1 取源码并固定版本
克隆 Apollo-11 仓库,切到本册固定的 commit。行号以这个版本为准。同时取 Virtual AGC 的源码树,用它的 yaYUL / yaAGC。
git checkout 911e5c0283c629c50cb97666f34065e8c07d71a5
│ ▼
2 汇编,拿到绳芯镜像
在程序目录里对 MAIN.agc 跑 yaYUL。这里会撞上两个文件名对不上的问题,改完名字之后 exit=0,产出 73,728 字节的镜像——正好把 36,864 个字填满。
cd Luminary099 && yaYUL MAIN.agc > lum099.lst 2> lum099.err
│ ▼
3 把复位向量改成一段桩程序
复位后机器会从 0o4000 开始执行开机自检。本册把它换成「INHINT → TC SQRTSUB → 把 MPAC / MPAC+1 / MPAC+2 / MPTEMP 抄到暂存 → 停住」。地址换算用固定存储器的公式:0x800 + FB×1024 + (AGC地址 & 0o1777)。
set *0x801=1763 # TC SQRTSUB(这之前先 set *0x800=4 补回 INHINT)
│ ▼
4 启动、单步、读寄存器,逐点扫描
每点独立启动一次 yaAGC,把输入写进 MPAC,用 --command 脚本自动走完全程,再读 MPAC 与 MPAC+1 的实际位模式。72 点扫完,停机位置全部落在自陷循环里,说明每次 SQRTSUB 都正常返回。
yaAGC --exec=lum099.bin --symbols=lum099.symtab --command=one.cmd --no-resume --inhibit-alarms
● 用 yaYUL 不是本册挑的,是源码自己指定的 :chrislgarry/Apollo-11 仓库 README 的 Attribution 表里,Assembler 一栏写的就是 yaYUL。
● 读源码和跑源码是两回事 :这份 1969 年的代码内容完好,却因为两个文件名拼不上而过不了汇编——静态阅读永远撞不见这类问题。
● 第 3 条证明链闭合了 :真实 Luminary 099 绳芯镜像在 yaAGC 上跑出 √x,72 点全部落在 1 个双精度最低位以内,最坏 0.925 个最低位。
● 模型是上界,不是预言 :第 6 章的 1.41–1.53 个最低位压住了仿真输出的 0.925,这是模型该有的样子——覆盖所有情形,因此比具体某一条轨迹略悲观。
● 上界与仿真输出的关系是判断模型对错的尺子 :仿真输出若反超上界,就说明模型漏了误差来源,结论要推翻;这次没有。
● 本册有一处存疑如实标出,且与本章无关 :第 3 章 -B- 型 FRACTIONAL 的源码标注 MAX .99996 与按 14 位尾数手算得到的 .99994 对不上,本册不用它做任何论证。
● 还有一处尚未追到底,一并标出 :本册另外拿 Apollo-11 仓的 Luminary099 目录(只改了那两个文件名)编了一份镜像,与 Virtual AGC 维护版逐字节比对,73,728 字节里只有 4 个字不同,首差在第 36,343 字节。这 4 个字所在的地址在汇编清单里没有任何源码行与之对应,——落在没被源码填满的存储区里;本册没能进一步定位根因,如实记下,不用它做任何结论。
怎么复核本册的每一条 下面几条命令可以直接粘贴执行。原始源码属公有领域,任何人都能拿到同一份底本。
1. 把仓库拉到本地,并切到本册固定的 commit
git clone https://github.com/chrislgarry/Apollo-11
cd Apollo-11
git checkout 911e5c0283c629c50cb97666f34065e8c07d71a5
2. 核对尺度表(A–J 十型)与 CDU 角度那一行
sed -n '686,730p' Comanche055/ASSEMBLY_AND_OPERATION_INFORMATION.agc
3. 核对 MPAC 只有 7 个字,以及三种装载方式
grep -n 'MPAC' Comanche055/ERASABLE_ASSIGNMENTS.agc | head -5
sed -n '63,74p;517,547p' Comanche055/INTERPRETER.agc
4. 核对归一化的契约与开平方的四个常数、两次牛顿迭代
sed -n '1629,1651p;2497,2516p;2556,2579p' Comanche055/INTERPRETER.agc
grep -n 'SLOPEHI\|BIASHI\|SLOPELO\|BIASLO' Comanche055/INTERPRETER.agc
5. 核对定点常数的写法(5/8 与 D1/32)
grep -n '5/8\|D1/32' Comanche055/ORBITAL_INTEGRATION.agc Comanche055/CONIC_SUBROUTINES.agc
6. 核对源码里两种补数并存、且有专门的转换例程
grep -rn "2'S COMPLEMENT\|1'S COMPLEMENT\|2'S COMP\|1'S COMP" Comanche055
7. 把本册两条推导跑一遍(只用标准库)
python verify_accurate.py
8. 把执行证据链跑一遍(第 8 章,需自行编译 Virtual AGC)
# 1. 建工具链(yaYUL 的 Makefile 用老式小写 ${cc},所以显式传 cc=gcc)
sudo apt-get install -y build-essential libreadline-dev
cd virtualagc/yaYUL && make cc=gcc && cd ../yaAGC && make cc=gcc
# 2. 汇编,拿到绳芯镜像(应为 73,728 字节)
cd Luminary099 && ../../yaYUL/yaYUL MAIN.agc > lum099.lst 2> lum099.err
stat -c %s MAIN.agc.bin
# 3. 把复位向量改成调用 SQRTSUB 的桩程序,再单步读 MPAC
echo 'set *0x800=4' > one.cmd # INHINT
echo 'set *0x801=1763' >> one.cmd # TC SQRTSUB
echo 'set *0154=8192' >> one.cmd # x = 0.5 写进 MPAC
seq 1 140 | sed 's/.*/step/' >> one.cmd
echo 'print *0154' >> one.cmd # 结果高位字
echo 'print *0155' >> one.cmd # 结果低位字
yaAGC/yaAGC --exec=MAIN.agc.bin --symbols=MAIN.agc.symtab \
--command=one.cmd --no-resume --inhibit-alarms --quiet
# 期望:0154 读出 026501(八进制),0155 读出 07463
· 「机器实证 」条目附了完整命令与原始输出,照抄即可复跑一遍;若你复跑出来的数与本册不同,以你的为准,并请指出错处。
· 所有「源码实证」条目都标了文件与行号。行号以 commit 911e5c0 为准;仓库后续若有变动,请先 git checkout 911e5c0 再核对。
· 「外部资料」条目不能回仓库核对,都给了参考文献编号,按编号去原文查。
· 「作者推导」条目给出了完整算式,你可以自己重算一遍。若你的结果与本册不同,以源码为准,并请指出错处。
· 「解释性类比」只是解释工具,不是证据,不必去源码里找。
[1] Apollo-11 源码仓库(Luminary099 登月舱 / Comanche055 指令舱)
chrislgarry(整理),MIT Museum 藏硬拷贝数字化(Paul Fjeld 扫描) · 源码
https://github.com/chrislgarry/Apollo-11,commit 911e5c0283c629c50cb97666f34065e8c07d71a5。Public domain(公有领域)。源码自身的装订记时为 16:27 JULY 14, 1969;仓库 commit 的提交时间为 2026-07-15(该日期为仓库 commit 的提交时间,与源码内容无关,可自行用 git log -1 核实)。两者相差 57 年,不要混为一谈。
用于:全文所有「源码实证」条目的出处。
[2] MIT Instrumentation Laboratory Report R-500:Apollo Guidance and Navigation — Part VI, Guidance Computer Design
MIT Instrumentation Laboratory · 一手技术报告
MIT,1960 年代原始设计报告(klabs.org 等处存有扫描件)。
用于:指令字格式(3 位操作码 + 12 位地址)、EXTEND 与 partial codes 两种扩充手法、Block I 指令清单。
[3] NASA SP-8015:Guidance and Navigation for Entry Vehicles(原稿误引条目,已更正)
NASA · 一手技术报告
NASA NTRS 19690010164(1968)。本次对 NTRS 原页核验:该号实为再入飞行器导航专册,与 AGC 无关;原稿据其引述的 AGC「通用计算机」表述,已改由 [4] 的 Appendix F 承担(该附录取自 Draper Lab Report R-700,对页核验通过)。
用于:文献纠错记录。
[4] Apollo GN&C System Components(Appendix F)
NASA · 一手技术报告
NASA NTRS 20230005922(NASA/TP-20230005922,2023)。Appendix F 取自 Draper Lab Report R-700;PDF p288 含 AGC「general purpose computer…priority driven operating system」原话,p292(印刷页 271)含 AGC 规格表——均已对页核验。
用于:机载制导导航控制系统的职责边界描述。
[5] Apollo Onboard Navigation Techniques
NASA · 一手技术报告
NASA NTRS 20090016292。
用于:机载导航与地面测控的分工、滤波方法的工程化过程。
[6] Apollo 11 Mission Report / 任务相关技术文件
NASA · 一手技术报告
NASA NTRS 19700008096(MSC-00171)。原稿误引的 19700014507 经 NTRS 原页核验为月球大地测量报告,与任务报告无关,已更正;表 5-I 为着陆事件时间线(含 1202/1201 序列)
用于:任务时间线与测控(MSFN)定轨职责的表述。
[7] Apollo 11 Lunar Surface Journal — Program Alarms(1201 / 1202)
NASA 历史部(ALSJ) · 史料 / 综述
nasa.gov 历史档案,含当事软件人员对 1202/1201 的回顾。
用于:1202/1201 与 core set / VAC 耗尽的关联,及重启后恢复的口述佐证。
[8] Core rope memory(含其中引用的 Ken Shirriff 等人资料)
Wikipedia 条目及其引用 · 史料 / 综述
公开条目。注意:该条目关于存储密度的部分自身带有 citation needed 标记。
用于:仅用于 LOL memory 这个行话外号。原条目给出的密度数字 72 KB/ft³ 因来源过弱已从正文删除。
[9] A New Approach to Linear Filtering and Prediction Problems
R. E. Kalman · 史料 / 综述
Journal of Basic Engineering, 1960。
用于:最优估计的理论起点。
[10] James Potter 讣闻(MIT News)
MIT News · 史料 / 综述
news.mit.edu,2005。
用于:平方根形式滤波的提出者及其在阿波罗上的作用。
[11] Apollo 11 任务时间线
Planetary Society / 史密森尼国家航空航天博物馆 · 史料 / 综述
两处公开资料。
用于:第 4 章时间线。两源在地月转移点火时刻上存在约一分钟差异,正文已标明。
[12] Apollo Guidance Computer 公开技术综述(Wikipedia 等)
Wikipedia 等 · 史料 / 综述
公开条目。其中「Minuteman II 混用多种逻辑族」一句自身标有 citation needed。
用于:Block I / Block II 芯片数与封装、整机外形尺寸。等级偏低,正文已单独标注;功耗重量与容量改用 NASA NTRS 20230005922 原文。
[13] AGC Block II Instructions(Digital Development Memo #193)
Albert Hopkins,MIT Instrumentation Laboratory · 一手技术报告
MIT 仪器实验室内部备忘录,ibiblio.org/apollo/Documents/dd_memo_193.pdf。
用于:Block II 指令集扩充的两条机制(extension 与 partial codes)及 quarter code 的位来源。
[14] Apollo Guidance Computer Block II — CMC Data Cards(基本指令与操作码表)
F. Bernardini 整理,基于 CMC/AGC 原始手册 · 一手技术报告
ibiblio.org/apollo/CMC_data_cards_15_Fabrizio_Bernardini.pdf(整理稿,非 NASA 原件)。
用于:基本指令的操作码八进制值(TC=00、CCS=01、CA=03、AD=06 等)与 EXTEND=0.0006。
[15] Virtual AGC 项目:yaYUL 汇编器与 yaAGC 模拟器
Ron Burkey 等,开源项目 · 公开网页
github.com/virtualagc/virtualagc;项目网站 virtualagc.github.io/virtualagc/ 与 ibiblio.org/apollo。chrislgarry/Apollo-11 仓库 README 的 Attribution 表里,Assembler 一栏写的就是 yaYUL(2026-09-15 对其 raw README 逐字核对)。
用于:把本册的源码实证变成可执行证据:汇编与仿真复现的工具链出处。
[16] chrislgarry/Apollo-11 仓库 README 的 Attribution 表(源码来历与装订记时)
chrislgarry/Apollo-11 仓库 README · 源码
Attribution 表在 chrislgarry/Apollo-11 的 README(不是 virtualagc 仓库的 README)。2026-09-15 逐字核对:Luminary099 一栏写明 「Assemble revision 001 of AGC program LMY99 by NASA」与「2021112-061. 16:27 JUL. 14, 1969」;Comanche055 一栏为「10:28 APR. 1, 1969」。
用于:装订记时、程序正式编号;汇编器为 yaYUL。
两个日期,不要混。 源码自身的装订记时 16:27 JULY 14, 1969 ,写在源码头部,指这份程序当年装订的那个时刻;仓库 commit 911e5c0283c629c50cb97666f34065e8c07d71a5 ,提交时间 2026-07-15,只是仓库整理者的提交时间,与源码内容无关,可用 git log -1 911e5c0 自行核实。两者相差 57 年。若仓库后续变动,请以该 commit 为准核对行号。
术语速览 读正文卡住的词,这里一句话讲清
位(bit) 最小单位。一个开关只有 0 或 1 两种状态,就是一个 bit。位宽、操作码位数说的都是它。 字(word) 这台机器一次能处理的完整一份数据。1 个字 = 15 位数据 + 1 位奇偶校验,物理上占 16 位。 定点(fixed-point) 小数点的位置固定,并且由人来约定。机器只认整数位,小数点的位置写在注释和尺度表里。 尺度(scale) 「每一位代表多少」的约定。同一个 15 位字,按不同尺度读,可以是整数、小数,也可以是角度。 尾数(magnitude bits) 去掉符号位之后,真正用来表示大小的那些位。单精度 14 位,双精度 28 位,三精度 42 位。 MPAC 多用途累加器(MULTI-PURPOSE ACCUMULATOR)。这台机器做算术的地方,可写内存里只有 7 个字。 MODE 可写内存里的一个字(不是硬件寄存器),记录 MPAC 当前被当作哪种类型用:0 双精度、+1 三精度、-1 三维向量。 归一化(normalize) 把一个数左移到「够大」为止(通常要求 ≥ .5),让有效位不浪费在高位的 0 上,同时记下移了几位。 反归一化 算完之后按记录的移位次数挪回去,把规范形式还原成真正的数值。 一补数(one's complement) 一种表示负数的方法,特点是 0 有两个写法:+0 和 -0。这台机器用的就是它。 POODOO 源码里的「放弃并报警」例程,后面跟一个八进制告警码。相当于今天抛出一个致命异常。 SCALED Bnn 源码注释里的定点写法:整数 X 配上 Bnn,表示 X × 2 的负 nn 次方。例:5 SCALED B3 = 5/8。 牛顿迭代 求平方根的经典迭代:x' = (x + N/x)/2。误差按平方收敛——第一次迭代后误差约是原来的平方的一半。 yaYUL 当代开源的 AGC 汇编器。chrislgarry/Apollo-11 仓库 README 的 Attribution 表里,Assembler 一栏写的就是它。 yaAGC 当代开源的 AGC 指令级模拟器,配 yaYUL 用,可以在今天的电脑上跑当年的程序。
证据来源。 源码实证来自本地 Apollo-11 仓库(chrislgarry/Apollo-11,MIT 博物馆藏硬拷贝数字化,公有领域),均标注文件与行号。机器实证来自第 8 章:Virtual AGC 的 yaYUL / yaAGC(源码自建,Ubuntu 26.04 × gcc 15.2.0),汇编与运行的命令、原始输出、镜像 MD5 都写在正文里,可自行复跑。其余外部资料为 NASA、MIT 公开技术报告,已单独标记。一处存疑。 第 3 章 -B- 型 FRACTIONAL 的源码标注 MAX .99996,与按 14 位尾数手算得到的 .99994 对不上。指令舱与登月舱两份代码都写作 .99996,本册未找到能解释差异的约定,如实标出,且不用它做任何论证。一处未追到底。 本册另用 Apollo-11 仓的 Luminary099 目录(仅修正两个文件名)编了一份镜像,与 Virtual AGC 维护版逐字节比对,73,728 字节中仅 4 个字不同。这 4 个字所在地址在汇编清单里没有对应源码行,本册未能进一步定位根因,如实记下,不用它做任何结论。本页自包含 ,无外部依赖,无脚本,无 class,可直接粘进微信公众号与各类笔记软件。