APOLLO 11 · AGC · 第三册 · PUBLIC DOMAIN

算得准的机器
15 位定点计算机如何管理精度、范围与误差

它没有浮点单元,也不认识小数。这一册只盯一件事:它凭什么算得准,以及误差在它手里是怎么被管住的。
本册建了三条证明链:① 表示——15 位数据经尺度约定变成物理量,起点终点都接在源码上;② 算法——定点量化、归一化、牛顿迭代之后,最终误差是多少,给出解析上界并跑脚本复核;③ 执行——把真实绳芯镜像装进 AGC 仿真器跑一遍,读寄存器里的实际位模式,与上面两条链对照。
三条链都已闭合。每条结论分五类证据,源码实证可按文件行号回仓库核对,机器实证附完整命令与原始输出、可自己复跑;凡源码与手算对不上的地方,本册如实标出,不用它做论证。

数据字宽 15 位(bit)尾数位数 14 / 28 / 42MPAC 7 字(word)尺度类型 10 型(A–J)定点常数注释 213 处
背景图 1 <b>开篇配图:1965 年的 AGC 主机本体</b>(MIT 仪器实验室)。这台机器全机由同一种双三输入 NOR 门搭成,本册讲的 15 位字、尺度约定、MPAC、开平方例程,都发生在这块板子里。这张图<b>不为任何一条数字背书</b>——它只负责让人知道自己在读一台真实的机器;本册所有数字都以源码行号和仿真器读出的位模式为准。
目录
1没有小数点,怎么算小数定点不是凑合,是一套写进注释的规矩2七个字,三种数据类型MPAC —— 这台机器的算术中心,也是本册的主角3源码里那张尺度表从 A 到 J 共十型,逐个规定每一位代表多少4一条数字的完整旅程从 15 位计数值到 359.99 度,每一步都能手算5把精度从浪费里捞回来先左移到「够大」,记下移了几位,算完再挪回去6两次牛顿就够了先用一条直线猜个初值,再迭代两次补到双精度7误差的另一半:负零、溢出和告警算不下去的时候,停下来报错,不硬算下去8把机器叫起来问一次从源码到仿真器:执行证据链在这一章闭合
怎么读这一册。它是这个系列的第三本,也是三本里最窄的一本——只讲一件事:这台机器凭什么算得准,误差在它手里怎么被管住。前两册已经讲过程序怎么运转、机器是什么做的,这一册不再重复。
证明链 ①(表示)见第 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 寄存器切换解释方式 MPACMPAC+1MPAC+2MPAC+3MPAC+4MPAC+5MPAC+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,保留两位小数
步代码在算什么为什么这么做代入本例结果
1N = 3276715 位全 1,就是这个量能取到的最大计数2^15 - 132767
2BIT 1 = 360/2^15源码尺度表 -D- 型给出的最低位权重360 / 327680.010986328125 度
3N × BIT 1计数值乘最低位权重,得到物理角度32767 × 0.010986328125359.989013671875 度
4XXX.XX按十进制格式四舍五入到两位小数359.989013671875 → 359.99359.99 度
5MAX 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最高0000000000100最低 MPTEMP0 左移 11 位后: 原本在尾数第 12 位(自左数)的那位 1,挪到了符号位右侧的尾数最高位,结果回到 ≥ 0.5 0符号1最高0000000000000最低 MPTEMP11 左移 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
步代码在算什么为什么这么做代入本例结果
1DMP整数相乘,机器眼里没有小数点2 × 510
2标度指数相加2⁻² × 2⁻³ = 2⁻⁵,所以 B2 × B3 = B52 + 3B5
310 × 2⁻⁵把整数结果按新标度还原成实际值10 / 320.3125
40.5 × 0.625直接用真值相乘,作为独立校验0.5 × 0.6250.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% 量级;这是代入验证,源码未写明该常数的取值方向
步代码在算什么为什么这么做代入本例结果
1e′(x) = (S·x − B) / (2·x^1.5)对相对误差函数求导,找极值点令 e′(x) = 0唯一驻点 x = B/S
2x* = B / S导数在 x* 左侧为负、右侧为正 ⇒ 这是**极小值**,不是极大值ARGHI:.2096/.58840.356220
3最大值只能在端点区间内只有一个极小值、没有极大值,所以最坏情况必在两端比较 e(左端) 与 e(右端)取绝对值较大者
4ARGHI 两端x = .25 与 x = .5e(.25) = +0.8900% e(.5) = +0.7600%最坏 0.8900%
5ARGLO 两端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,可直接粘进微信公众号与各类笔记软件。