☰
补码一位乘与Booth重编码:从原理到汇编实现的位级优化
2026/10/28 14:25:04 网站建设 项目流程

简介:这份文档面向学习计算机组成原理、需要完成定点补码乘法器课程设计的学生,围绕补码一位乘的程序设计展开,重点解决Booth算法从原理理解到COP2000实验仪编程实现的衔接问题。压缩包内共1个doc文件,约639KB,内容按总体设计方案、详细设计方案、程序调试与结果测试组织,包含设计原理、初始化与运算思路、流程图分析、汇编指令说明及结果验证等模块,可作为课程设计报告的参考范例。目前已有1607人学习,说明该题目在组成原理实验中具有一定代表性。读者可从中梳理Booth算法中YnYn+1状态判断、部分积右移、末步不移位等关键处理,并借鉴实验数据输入、OUT寄存器显示与调试排错的组织方式,适合需要撰写实验报告或复现COP2000补码一位乘程序的读者参考。

1. 补码一位乘的直觉:乘一个数为什么只需要加减和右移

补码一位乘最反直觉的地方在于:它并不打算让乘法变快,而是先把乘数重新编码,让一整串连续的 1 只触发两次动作。很多人第一次对着 Booth 的操作表看,会觉得 00 和 11 都“什么都不做”是个偷懒的约定,其实这条约定正是全部收益的来源——乘数里 0111 1100 这样的串,逐位相乘要加七次,重编码后只在 1 串的头和尾各动一次手。

搞懂它值当的地方不止于考试。这套“用相邻两位的差来驱动加减”的思路,后面在阵列乘法器、定点 DSP、乃至各种位级优化里反复出现;而它落在 6 位数据、8 位寄存器的实验箱上时暴露出来的符号扩展、移位联动、附加位时序,才是真正会翻车的地方。

2. 补码的模运算基础:减法为什么能被“加一个负数”顶替

2.1 取反加一不是技巧,是模 2^n 下的求逆

n 位寄存器里能表示的只有 2^n 个状态,加法天然是模 2^n 的。要让 x + (-x) ≡ 0 (mod 2^n),就必须有 -x ≡ 2^n - x。把 2^n - x 拆开写:

2^n - x = (2^n - 1 - x) + 1

而 2^n - 1 是全 1 的掩码,任何数减去 x 等价于对 x 按位取反。于是“取反加一”根本不是什么记忆口诀,它是模运算下求加法逆元唯一的机械做法:先按位取反得到 2^n-1-x,再加 1 补上那一步进位。

这个视角一旦立住,后面所有的“加 [X]补 就是加 X”“加 [-X]补 就是减 X”都不需要再解释,它们只是同一件事的两种说法。硬件上也因此只需要一个加法器——减法从来不存在,存在的只是加一个已经求好逆元的数。

2.2 [-X]补 在代码里是怎么求的

真正的实现比教科书多一层取舍。被乘数 X 以原码形式从开关输入,6 位、首位符号位。程序拿到它之后要同时准备 [X]补 和 [-X]补,两条路径不一样:

; 输入 X 存入 R1 后,先用掩码判断符号位 MOV A, R1 AND A, #20H ; 0x20 是 6 位数的符号位 JZ POS_X ; 结果为 0 说明是正数 ; ---- 负数分支:X 是负数,[-X]补 就是 |X| ---- MOV A, R1 CPL A ; 低 5 位取反 ADD A, #21H ; 加 1 求补,同时把符号位铺成 1 MOV 0F0H, A ; 存 [X]补 MOV A, R1 AND A, #1FH ; 直接截出低 5 位,符号位清零 MOV 0F1H, A ; 存 [-X]补,此时它是正数 JMP NEXT POS_X: MOV A, R1 MOV 0F0H, A ; 正数的补码等于原码 CPL A ADD A, #01H ; 整体取反加一,得到 -X MOV 0F1H, A NEXT:

注意ADD A, #21H里的 0x21 而不是 0x01:0x20 负责把被取反抹掉的符号位重新置 1,0x01 负责完成“加一”。如果这里写成 0x01,负数的符号位会变成 0,整个数被解释成正数,后面的部分积会一路错到底。反过来,负数的 [-X]补 不需要取反加一,因为 X 是负的,-X 就是正的,正数的补码就是它本身的低 5 位,一条AND A, #1FH就够了。这一步省下来的不只是指令,更是避免了对符号位的二次折腾。

2.3 符号位凭什么能参与运算

因为补码里符号位和数值位根本没有物理区别,只是权重不同:最高位权重是负的 2^{n-1},其余位是正的 2^k。加法器一视同仁地把它们加起来,溢出位自然丢弃,模运算自动把结果折回合法范围。所以乘法的累加过程完全不需要“先算数值再补符号”,符号位跟着加减法一起滚,滚出来的结果天然正确。

这也是后面判断部分积符号位的依据:部分积是 8 位寄存器,符号位在 bit7,而不是 6 位数的 bit5。这个位宽错位,是实验箱上最容易出错的地方之一。

3. Booth 重编码:两位状态如何把连续 1 串压成一次加减

3.1 从权重展开推导四种状态

把 n 位乘数 Y 按补码权重展开:

Y = -y[n-1]·2^(n-1) + Σ(k=0..n-2) y[k]·2^k

补一个虚拟位 y[-1] = 0,上式可以改写成相邻两位之差的形式:

Y = Σ(k=0..n-1) (y[k-1] - y[k])·2^k

拿实验里那组数据验证一下:Y = 011000(十进制 24)。

k=0: (y[-1]-y[0]) = 0-0 = 0 → 0 k=1: (y[0]-y[1]) = 0-0 = 0 → 0 k=2: (y[1]-y[2]) = 0-0 = 0 → 0 k=3: (y[2]-y[3]) = 0-1 = -1 → -8 k=4: (y[3]-y[4]) = 1-1 = 0 → 0 k=5: (y[4]-y[5]) = 1-0 = +1 → +32 合计: -8 + 32 = 24 ✓

六个位里只有两处系数非零,其余全是 0。一长串连续的 1 被折叠成了“开头 +1、结尾 -1”两次动作,这就是 Booth 省操作的数学来源。把 (y[k-1], y[k]) 记成 (Yn+1, Yn),四种组合直接映射成操作表:

Yn (当前位)Yn+1 (附加位)系数操作
000部分积不变
01+1部分积加 [X]补
10-1部分积加 [-X]补
110部分积不变

表格里 Yn 是“现在这一位”,Yn+1 是“刚移出去的那一位”,它其实就是上一轮的 Yn。所以附加位这个寄存器不是额外发明出来的东西,它是把上一轮的信息留了一份,好让相邻两位能同时被看到。

3.2 最后一步为什么不移位

循环体内的“加完右移”有双重身份:加法负责把这一位的贡献累加进部分积,右移负责把部分积的权重降一档,为下一位腾位置。移位等价于除以 2,执行 n 次之后所有位刚好对齐。

第 n 次操作是最后一次,此时乘积已经在寄存器里拼装完毕,再移一次就等于把结果又除了一次 2,直接减半。所以标准的 Booth 实现是:前 n 次“判断—加/减—右移”,第 n+1 次只“判断—加/减”,不移位。原文的实现用 R3 从 5 递减到 0 来控制前 5 次循环,出循环后再补一次只加不移的操作,正是这个结构。

这个细节如果漏掉,症状很有迷惑性:结果不是全错,而是刚好少一半,而且只在最后一次操作非零(即乘数最高位附近的两位不同)时才错。

3.3 六位数据挤进八位机器的符号扩展

实验箱的字长是 8 位,规定数据只有 6 位,中间空出来的两位就是隐患。一个 6 位负数 100110(原码,值 -6),从开关读进累加器是 0x26 = 0010 0110,bit7 和 bit6 都是 0。要是直接拿它做 8 位加法,它会被当成 +38,符号彻底反了。

正确的做法是把 6 位补码扩展到 8 位:真值 -6 的 6 位补码是 111010,扩到 8 位是 11111010,也就是高两位要复制符号位。程序里那句ADD A, #21H就是这个扩展的压缩写法:低 5 位取反加一算出数值部分,bit5 单独置 1 补上符号,bit7/bit6 因为原输入是 0、取反后是 1,天然就跟符号位一致了。

理解了这一层,再看部分积为什么用 8 位寄存器、符号位按 bit7 判断(代码里用OR A, #80H置位、AND A, #7FH清零),就不会觉得前后矛盾——乘数按 6 位处理,部分积按 8 位处理,两套位宽在同一段代码里并存,是这一版实现最需要留神的地方。

4. 汇编实现:存储布局、分支树与算术右移的三处联动

4.1 初始化阶段的地址规划

指令集只有 R0-R3 四个通用寄存器和一批可直接寻址的存储单元,所以常量必须提前落到内存里,寄存器留给会在循环里变动的东西。实际布局:

位置内容为什么放这
R0部分积每轮都要读写,必须在寄存器
R1原始被乘数 X求补时还要回头取
R2原始乘数 Y求补时还要回头取
R3循环计数 n每轮减一,跳转要用
0E0H附加位 Yn+1沿用原设计的约定单元
0F0H[X]补加操作时取用
0F1H[-X]补减操作时取用
0F2H[Y]补(乘数)每轮右移,也在循环里更新

初始化的三件事:部分积清零、附加位清零、计数置 5。附加位初值必须是 0 而不是 1,因为它代表“乘数最低位右边那一位”,补码数在最低位右边没有东西,按定义补 0。这一点如果写反,第一轮的判断就会把 00 当成 01,凭空多出一项 [X]补。

4.2 分支树到指令序列

每轮循环开头先取 Yn,再取 Yn+1,两两组合落到四个分支:

DAY0: MOV A, 0F2H ; 取乘数 AND A, #01H ; 只看最低位 Yn JZ DAY4 ; Yn=0 走另一条路 ; ---- Yn = 1 ---- MOV A, 0E0H AND A, #01H JZ DAY5 ; Yn=1, Yn+1=0 → 加 [-X]补 JMP DAY6 ; Yn=1, Yn+1=1 → 不改部分积 DAY4: MOV A, 0E0H AND A, #01H JZ DAY6 ; Yn=0, Yn+1=0 → 不改部分积 MOV A, 0F0H ; Yn=0, Yn+1=1 → 加 [X]补 ADD A, R0 MOV R0, A JMP DAY6 DAY5: MOV A, 0F1H ; 减 X 用加 [-X]补 实现 ADD A, R0 MOV R0, A DAY6: ; 进入移位阶段

有两点值得说。一是ADD A, R0的加数顺序:累加器里放的是补码常量,R0 才是部分积,加完写回 R0,这条ADD直接把“部分积加常量”做掉了,不需要多余搬运。二是四个分支里有两条直接跳到移位,这就是 Booth 在白捡便宜的证明——约一半的位组合根本不进加法器。

4.3 移位阶段的三件事与顺序

移位看起来只有一条右移指令,实际上是三个动作联动,而且顺序不能换:

; 第一步:用移位前的 Yn 更新附加位 MOV A, 0F2H AND A, #01H JZ CLR_ADD MOV A, 0E0H OR A, #01H ; 附加位置 1 MOV 0E0H, A JMP SHIFT_Q CLR_ADD: MOV A, 0E0H AND A, #00H ; 附加位清零 MOV 0E0H, A ; 第二步:乘数右移,空出的高位用部分积的旧末位填 SHIFT_Q: MOV A, 0F2H RR A MOV 0F2H, A MOV A, R0 AND A, #01H ; 取部分积末位 JZ SET_Q0 MOV A, 0F2H OR A, #20H ; 6 位乘数的首位是 bit5 MOV 0F2H, A JMP SHIFT_A SET_Q0: MOV A, 0F2H AND A, #0DFH ; 1101 1111,清 bit5 MOV 0F2H, A ; 第三步:部分积算术右移,符号位保持 SHIFT_A: MOV A, R0 RR A MOV R0, A AND A, #80H ; 检查移位后的 bit7 JZ DONE MOV A, R0 OR A, #80H ; 负数补回符号位 MOV R0, A DONE:

为什么必须是这个顺序?附加位要存的是“这一轮开始时的 Yn”,一旦乘数先右移,最低位就换人了,存进去的是下一位,权重整体错一档。乘数的新首位要取“部分积右移之前的末位”,如果先移部分积,取到的已经是下一个位,同样错位。所以三件事的因果链是:先采样 Yn 给附加位,再把部分积末位送进乘数首位,最后才让部分积自己移位。

第三件事里的符号保持尤其关键。RR A是逻辑右移,高位补 0,负数做几次就变成正数。补回来的动作是右移之后判断最高位,为 1 就OR回去。这段判断-回填如果省掉,部分积会越移越大,最后得到一个巨大的正数,而乘数部分看起来完全正常,排查时很容易怀疑错对象。

5. 常见问题与排查:实验箱上跑不通的五种典型现象

5.1 单步到一半数据突然被改写

现象:程序前半段跑得好好的,走到循环中段,0F0H 里的 [X]补 莫名其妙变了,后续结果全乱。

原因:通用寄存器数量有限,程序里又频繁拿 R0-R3 做临时周转,一旦某个暂存需求落到了低地址单元,就会和指令区或系统占用的空间重叠。低地址往往被复位向量、中断入口、堆栈占用,写进去当下看不出问题,等系统自己用到时才互相覆盖。

解决:所有长期驻留的常量一律放到高地址,教程里用 0F0H-0F2H 不是随便挑的,那是留给用户的高端区。临时变量也尽量复用 R0-R3,用完立刻释放,不要图省事在低地址开新单元。

5.2 立即数被当成地址,取回一个无关的值

现象:MOV A, 0AH本意是让累加器等于 10,结果 A 里出现的是某个存储单元的内容,后面所有运算都偏了。

原因:这套指令集里带#才是立即数,不带#表示直接寻址。两条指令机器码不同,汇编器不会替你纠正,因为语法上两者都合法。

解决:凡是常数,强制写#。写完一段程序回头扫一遍所有MOV,把没有#又本来想表达常数的行单独标出来对一遍。这个错误编译期不报,运行期也不崩,只错结果,是典型的黑匣子问题。

5.3 负数部分积移几次就“转正”了

现象:输入一组负数,前几轮部分积还是正常的补码形式,移位两三遍之后变成一个很大的正数,和手算完全对不上。

原因:右移指令是逻辑右移,高位补 0,而算术右移要求高位复制符号位。教科书写“算术右移”四个字带过,落到只有一条RR的指令集上,得自己补这一步。

解决:每次右移后判断原符号位,为 1 就把它OR回去。判断的时机要看清楚——RR之后 bit7 已经是 0 了,所以得在移位前记下符号,或者移完立刻用掩码查一次再回填。原文实现里移位后紧跟一条AND A, #80H加条件OR,就是这个套路。

5.4 附加位更新晚了一拍

现象:单看每一轮的加法都对,但最终乘积总是差 2 的整数倍,有时是两倍,有时是一半,且只在乘数含连续 1 时出现。

原因:附加位保存的是上一轮的 Yn,必须在乘数右移之前采样。如果先移乘数再回填附加位,存进去的其实是下一位,等于把 Yn+1 又看了一遍,相邻两位的比较关系彻底错位,连续 1 串的折叠就失效了。

解决:把“采样 Yn 写附加位”“乘数右移并填首位”“部分积算术右移”三件事的顺序钉死,写成注释放在代码里。调试时单步走到移位前,先看 0E0H 是否等于当前乘数的最低位,再按下一条指令看乘数是否变化,一眼就能分辨。

5.5 六位数没做符号扩展,高两位污染结果

现象:被乘数为负时,[X]补 存进 0F0H 的值低 6 位是对的,高两位却是 0,累加几轮之后部分积的符号位判断失灵。

原因:8 位机把输入读成 8 位,6 位数的符号位在 bit5,bit7/bit6 是空的。补码运算要求符号位铺满所有高位,否则这个数在 8 位加法里会被当成一个正数参与运算。

解决:求补时不要只加 0x01,按符号位的位置加 0x21(或者先OR上 0xE0 再统一处理)。验证方法是看中间量:负数输入的 [X]补 高两位必须是 1,正数必须是 0,这一步对不上,后面的所有调试都是白费。

6. 取舍:串行一位乘在今天还值不值得看

这套实现从头到尾只用了加法、取反、右移和一个循环计数器,代价是 n 个时钟周期才能出一个积,关键路径上还挂着一次加法和一次移位。对比另外两条路,取舍一目了然:

方案面积开销每积周期关键路径适用场景
串行一位乘(Booth)一个加法器加移位寄存器n加法器加移位资源极度受限、教学验证
阵列乘法器n² 个全加器1(流水后更高)进位链通用处理器、DSP
查表法随位宽指数增长1存储访问固定小位宽、FPGA 小表

真正值得琢磨的是 Booth 在这条权衡曲线上的位置:它不减少加法器的数量,只减少加法的次数。乘数里连续 1 越多,它省下的周期越多;碰上 0101 0101 这种交替模式,一次加法都省不掉,退化成最朴素的移位加法。也就是说,Booth 的收益是数据相关的,这一点在评估它的实际价值时经常被忽略。

验证这类实现,我习惯不看最终结果先看中间量:把 [X]补、[-X]补、[Y]补 三个常量先对一遍,再逐轮盯附加位和乘数首位的填法,最后才对乘积。用例上先跑 0、±1、符号位单独为 1 的极端组合,再跑连续 1 串,基本能把符号扩展和移位时序这两类错误全部逼出来。

从那以后我每次碰定点运算的位级实现,都会先把“位宽、符号位位置、移位方向”三件事写在纸上,再动键盘。这套东西的坑不在算法本身,全在机器字长和逻辑字长的那点缝隙里。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询