ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Booth算法六位补码乘法器:汇编实现与调试避坑指南

Booth算法六位补码乘法器:汇编实现与调试避坑指南

简介:这份文档面向计算机组成原理课程的学习者与课程设计者,聚焦定点补码一位乘(Booth算法)的程序设计实现,帮助读者理解从算法原理到实验箱编程落地的完整过程。内容围绕设计原理、设计思路、总体与详细设计方案、流程图分析以及程序调试与结果测试展开,涉及[X]补、[-X]补、[Q]补的初始化,YnYn+1状态判断、部分积右移与末步不移位等关键环节,并基于伟福COP2000实验仪指令集完成汇编编程,对课程设计报告撰写与实验复现均有参考价值。资源包共1个doc文件,约639KB,为完整的课程设计报告文档,含目录、章节正文、参考文献与附录,结构规范便于直接借鉴。目前已有1607人学习下载,适合需要完成补码乘法器设计、梳理Booth算法流程或准备组成原理实验考核的读者参考使用。

1. 六位补码乘法器:从开关输入到 OUT 寄存器出结果,这套 Booth 算法程序到底怎么跑

把两个六位数从实验箱开关拨进去,按下运行,OUT 寄存器先亮出乘数、再亮出部分积——听起来像玩具,但这套跑在 COP2000 实验仪上的定点补码一位乘程序,恰恰是计算机组成原理课程设计里最容易翻车的一环。它用 Booth 算法把原码输入转成补码运算,乘数、被乘数各六位(首位符号位),循环五次移位加一次不移位的收尾,最终在寄存器里拼出十二位乘积。适合正在做组成原理课设、需要一份能对着机器码逐行核对的汇编实现参考的读者,也适合想搞懂 Booth 算法“为什么最后一步不移位”的开发者。下面按初始化、循环相乘、移位细节、调试排错、二次改造的顺序,把这套程序拆开讲。

2. 总体设计:六位补码乘法器的寄存器分配与数据通路

这套程序的核心约束很硬:乘数和被乘数规定为六位,首位是符号位,数据位只有五位,所以循环次数 n=5,加上最后一步不移位的修正操作,一共要做六次判断。数据从实验箱开关输入,结果从 OUT 寄存器输出,中间所有中间量都落在寄存器 R0~R3 和几个固定存储单元里。理解这套程序的第一个门槛不是 Booth 算法本身,而是搞清楚哪个值放在哪、为什么放那儿。

2.1 寄存器与存储单元的职责划分

程序对资源的占用是有讲究的,不是随手分配。把关键位置列成表更清楚:

位置存放内容说明
R0部分积初始为 0,每轮移位后更新
R1被乘数 X 原码从开关输入,用于求 [X]补 和 [-X]补
R2乘数 Y 原码从开关输入,用于求 [Y]补
R3循环计数 n初始 5,每轮减 1
0E0H附加位 Yn+1初始 0,随乘数末位同步更新
0F0H[X]补被乘数补码
0F1H[-X]补被乘数负补码
0F2H[Y]补乘数补码,运算中兼作乘数寄存器

这里有个血泪经验值得先说:程序里大量使用了 0F0H 到 0F2H 这几个靠后的存储单元,而不是随手抓一个空闲寄存器。原因在调试章节会细讲——EM 程序运行时前面的寄存器会被系统占用,早期把中间量塞进低地址寄存器,跑到一半数据被覆盖,现象是部分积莫名其妙变成别的值,查了半天才发现是寄存器冲突。所以这套程序从一开始就把补码缓存放在 0F0H 往后,属于被坑过之后的习惯做法。

2.2 初始化阶段:三个补码怎么算出来

初始化要解决三件事:部分积和附加位清零、循环计数置 5、把输入的原码转成 [X]补、[-X]补、[Y]补。前两件是赋值,第三件才是真正需要判断符号位的逻辑。以被乘数 X 为例,程序先把它和 20H(即二进制 100000B)做与运算,判断符号位:

; 输入 X 存入 R1,判断符号位求 [X]补 08 C0 IN ; 从输入端口读入 X 到累加器 A 09 81 MOV R1, A ; X 原码存入 R1 0A 5C 20 AND A, #20H ; 与 100000B 相与,提取符号位 0C A4 1C JZ DAY1 ; 结果为 0 说明是正数,跳转 DAY1 0E 71 MOV A, R1 ; 负数分支:取回 X 0F E4 CPL A ; 按位取反 10 1C 21 ADD A, #21H ; 加 21H 完成求补(六位模 40H 的补码修正) 12 88 F0 MOV 0F0H, A ; [X]补 存入 0F0H 14 71 MOV A, R1 ; 再取 X 求 [-X]补 15 5C 1F AND A, #1FH ; 与 1FH 相与,取低五位数据位 17 88 F1 MOV 0F1H, A ; [-X]补 存入 0F1H 19 AC 23 JMP DAY2 ; 跳到乘数处理 1B 71 DAY1: MOV A, R1 ; 正数分支:X 本身就是 [X]补 1C 88 F0 MOV 0F0H, A ; 直接存入 0F0H 1E E4 CPL A ; 取反求 [-X]补 1F 1C 01 ADD A, #01H ; 加 1 完成求补 21 88 F1 MOV 0F1H, A ; 存入 0F1H

这段代码里有两个参数值得掰开说。第一个是#20H,它对应二进制 100000B,正好是六位数的符号位掩码,与运算结果非零即负、为零即正,这是判断补码符号最省指令的写法。第二个是负数求补时加的#21H而不是#01H,原因是机器按八位识别,六位数的高两位需要和符号位保持一致(符号扩展),21H 相当于在六位模 40H 的基础上补上了符号扩展的偏移,这样算出来的 [X]补 高两位自动填成符号位,后续加减运算不会因为高位垃圾值出错。正数分支加#01H是因为正数符号扩展位本来就是 0,不需要额外偏移。

乘数 Y 的处理逻辑完全对称,只是结果存到 0F2H,并且正数分支直接存入、负数分支取反加 21H。这里不再重复贴代码,但要注意一个细节:乘数补码在运算过程中会被反复读写(判断末位、右移、拼接部分积末位),所以 0F2H 既是 [Y]补 的初始存放处,也是运行时的乘数工作寄存器,一个单元身兼两职,省资源但也要求写代码时心里清楚它当前处于哪个语义。

2.3 数据通路的整体走向

把初始化之后的运算链路串起来看:部分积在 R0,乘数在 0F2H,附加位在 0E0H,[X]补 和 [-X]补 在 0F0H、0F1H 待命。每一轮的核心动作是“看 YnYn+1 决定加什么 → 右移 → 计数减一”,右移时乘数最低位移进附加位、部分积最低位移进乘数最高位、部分积符号位保持不变。这条通路决定了后面所有代码的组织方式,也是理解移位那几段汇编的前提。

3. 核心循环拆解:Booth 判位、加减与三段式移位

循环部分是整套程序的主体,也是最容易写错的地方。Booth 算法的操作表本身不复杂:YnYn+1 为 00 或 11 时只移位,为 01 时部分积加 [X]补 再移位,为 10 时部分积加 [-X]补 再移位。真正难的是“移位”在六位定长、符号位固定的约束下怎么拆成三步,以及最后一步为什么不移位。

3.1 判位逻辑:先看 Yn 再看 Yn+1

程序进入循环后,第一件事是提取乘数末位 Yn 和附加位 Yn+1,组合成两位状态再分派操作。判位代码用两次与 01H 的运算分别取位:

; 判断 YnYn+1 状态 34 78 F2 DAY0: MOV A, 0F2H ; 取乘数 36 5C 01 AND A, #01H ; 取 Yn 末位 38 A4 85 JZ DAY4 ; Yn=0 跳 DAY4 继续判附加位 3A 78 E0 MOV A, 0E0H ; Yn=1,取附加位 3C 5C 01 AND A, #01H ; 取 Yn+1 3E A4 7F JZ DAY5 ; Yn+1=0 即 10 状态,跳 DAY5 加 [-X]补 40 78 F2 DAY6: MOV A, 0F2H ; 11 状态,直接进入移位

这段的分支结构是:Yn=1 且 Yn+1=0 走 DAY5(加 [-X]补),Yn=1 且 Yn+1=1 走 DAY6(只移位),Yn=0 时跳到 DAY4 再判 Yn+1,Yn+1=1 走加 [X]补、Yn+1=0 走只移位。四个状态对应操作表四行,一一对齐。参数上#01H是取最低位的标准掩码,0F2H和0E0H分别是乘数和附加位的地址,这两个地址在整段循环里反复出现,读代码时记住它们就不会迷路。

加 [X]补 和加 [-X]补 的两段代码很对称,都是把对应补码取到累加器、加上 R0 里的部分积、再写回 R0:

; 10 状态:部分积 + [-X]补 7F 78 F1 DAY5: MOV A, 0F1H ; 取 [-X]补 81 10 ADD A, R0 ; 加上当前部分积 82 80 MOV R0, A ; 结果写回 R0 83 AC 40 JMP DAY6 ; 进入移位 ; 01 状态:部分积 + [X]补 85 78 E0 DAY4: MOV A, 0E0H ; 取附加位 87 5C 01 AND A, #01H ; 判 Yn+1 89 A4 40 JZ DAY6 ; 为 0 即 00 状态,只移位 8B 78 F0 MOV A, 0F0H ; 为 1 即 01 状态,取 [X]补 8D 10 ADD A, R0 ; 加上部分积 8E 80 MOV R0, A ; 写回 R0 8F AC 40 JMP DAY6 ; 进入移位

注意这里加法直接作用在八位累加器上,六位数的符号扩展位在初始化时已经处理好,所以加减不会溢出到错误的高位。这是前面初始化阶段加 21H 那个设计的回报,如果初始化时没做符号扩展,这里加出来的结果高位就是脏的,最终输出会莫名其妙多出几个 1。

3.2 三段式移位:附加位、乘数、部分积依次右移

移位是这套程序最绕的部分,因为它不是一条指令搞定,而是拆成三段有依赖关系的操作。第一段处理附加位:先看乘数末位 Yn,Yn 为 1 就把附加位置 1,否则置 0,然后乘数整体右移一位。第二段看部分积末位,为 1 就把移位后的乘数最高位置 1,否则清 0。第三段看部分积符号位,右移后符号位保持不变。

; 第一段:附加位跟随 Yn,乘数右移 40 78 F2 DAY6: MOV A, 0F2H ; 取乘数 42 5C 01 AND A, #01H ; 判 Yn 44 A4 69 JZ DAY7 ; Yn=0 跳 DAY7 清附加位 46 78 E0 MOV A, 0E0H ; Yn=1 48 6C 01 OR A, #01H ; 附加位置 1 4A 88 E0 MOV 0E0H, A 4C 78 F2 LOOP: MOV A, 0F2H ; 乘数右移 4E D0 RR A 4F 88 F2 MOV 0F2H, A ; 第二段:部分积末位拼进乘数最高位 51 70 MOV A, R0 52 5C 01 AND A, #01H ; 判部分积末位 54 A4 71 JZ DAY8 ; 为 0 跳 DAY8 清乘数最高位 56 78 F2 MOV A, 0F2H 58 6C 20 OR A, #20H ; 乘数最高位置 1 5A 88 F2 MOV 0F2H, A 5C 70 LOOP1: MOV A, R0 ; 部分积右移 5D D0 RR A 5E 80 MOV R0, A ; 第三段:部分积符号位保持 5F 5C 10 AND A, #10H ; 判部分积符号位(六位数的符号位在 bit4) 61 A4 79 JZ DAY9 ; 为 0 跳 DAY9 清符号位 63 70 MOV A, R0 64 6C 80 OR A, #80H ; 符号位置 1(八位视角的最高位) 66 80 MOV R0, A

这三段里参数的含义要特别留意。#20H在第二段是乘数最高位掩码(六位数的 bit5),#10H在第三段是部分积符号位掩码(六位数的 bit4),#80H是八位视角的最高位。为什么符号位判断用 10H 而置位用 80H?因为部分积在八位累加器里是符号扩展过的,六位数的符号位落在 bit4,但右移后要保持的是八位意义上的符号一致性,所以置位时操作 bit7。这个细节如果搞混,现象是负数部分积右移后符号位丢失,最终乘积符号错误——调试章节里提到的“最高位没置 1 导致程序出错”就是这个坑。

3.3 收尾:最后一步只加不移位

循环计数 R3 从 5 减到 0 之后,程序不是直接结束,而是再判一次 YnYn+1,按操作表做最后一次加减,但跳过移位。这就是 Booth 算法“第 n+1 步不移位”的落地:

; 循环计数减一,判是否结束 91 73 DAY10: MOV A, R3 92 3C 01 SUB A, #01H ; 计数减一 94 83 MOV R3, A 95 6C 00 OR A, #00H ; 影响零标志 97 A4 9B JZ DAY11 ; 减到 0 跳收尾 99 AC 34 JMP DAY0 ; 否则继续循环 ; 收尾:再判一次 YnYn+1,只加减不移位 9B 78 F2 DAY11: MOV A, 0F2H 9D 5C 01 AND A, #01H ; 判 Yn 9F A4 B2 JZ DAY12 ; 为 0 继续判附加位

收尾段的分支结构和循环内判位完全一致,区别只在于加减之后直接跳到输出,不再进移位段。这个设计的好处是判位逻辑只写一遍、复用两次,坏处是读代码时容易以为 DAY11 之后还有移位,实际没有。输出阶段先把 0F2H 里的乘数送到 OUT 显示,再把 R0 里的部分积送到 OUT 显示,两次输出拼起来就是十二位乘积。

4. 避坑与排查:寄存器冲突、立即数写法和符号位丢失

这套程序在调试阶段踩的坑很典型,基本覆盖了汇编课设的高频翻车点。下面四条按“现象 → 原因 → 解决”整理,都是实际跑程序时能对上的。

4.1 程序跑到一半数据被覆盖

现象:单步执行到循环中段,R0 里的部分积突然变成和预期无关的值,重新从头跑又正常,跑快一点就出错。原因:EM 程序运行时前面的寄存器会被系统占用,早期把中间量放在低地址寄存器,循环次数一多就被覆盖。解决:中间量和补码缓存统一放到 0F0H 往后的高地址单元,R0~R3 只放程序自己完全掌控的变量,并且初始化时显式清零,不依赖默认值。

4.2 立即数被当成地址

现象:MOV A, 0AH执行后累加器里不是 0AH,而是一个内存地址里的内容,后续运算全错。原因:COP2000 指令集里立即数必须带#前缀,MOV A, 0AH是取 0AH 地址的值,MOV A, #0AH才是送立即数。解决:所有常量操作数统一加#,写完一段就反汇编核对一遍,机器码里立即数指令和地址指令的编码不同,反汇编能直接看出来。

4.3 负数部分积右移后符号丢失

现象:被乘数为负时,最终乘积符号位是 0,结果比正确值小了一个符号量。原因:部分积右移后没有把符号位重新置 1,六位数的符号位在 bit4,右移会把 1 移走。解决:右移后单独判一次符号位,为 1 就 OR 上 80H 保持八位符号一致,这段就是 3.2 节第三段移位代码,漏写这一段必然翻车。

4.4 循环次数多算或少算一次

现象:乘积比正确值多移了一位或少移了一位,表现为结果差 2 倍或 1/2。原因:六位数数据位是五位,循环 n=5 次移位加一次不移位收尾,如果 R3 初值设成 6 或者收尾段又移了一次,次数就错。解决:R3 初值固定 5,循环体内减一判零,收尾段只加减不移位,用一组已知数据(如 X=100110B、Y=011000B)单步核对每一轮的部分积和乘数,对不上就数移位次数。

5. 二次改造:把六位乘法器扩成八位并加自动校验

这套程序跑通之后,最自然的改造方向是扩位和加校验。扩到八位不是把常量从 20H 改成 80H 那么简单,符号位位置、掩码、循环次数、符号扩展偏移全要跟着动,改错一处就是连锁反应。

5.1 扩位改造的四个改动点

第一,符号位掩码从 20H 改成 80H,判位逻辑不变但常量全换。第二,循环次数从 5 改成 7,R3 初值同步改。第三,负数求补的偏移从 21H 改成 81H,保证八位符号扩展正确。第四,移位段的符号位掩码从 10H 改成 40H、置位从 80H 改成保持 bit7 不变。改完用一组正数、一组负数、一组正负混合各跑一遍,重点看负数部分积右移后的符号位。

; 八位改造后的关键常量对照(改前 → 改后) ; 符号位判断 AND A, #20H → AND A, #80H ; 负数求补偏移 ADD A, #21H → ADD A, #81H ; 循环初值 MOV A, #05H → MOV A, #07H ; 部分积符号位 AND A, #10H → AND A, #40H

5.2 加自动校验的思路

手工单步核对太慢,可以在输出前加一段比对:把乘积的低六位和高六位分别和预期值比较,不等就点亮一个标志位。预期值不用写死,用移位加法算一遍参考结果存到固定单元,运行时和 Booth 结果比对。这样每次改完代码跑一遍,标志位不亮就说明逻辑没跑偏,比人眼盯寄存器靠谱得多。

改造项改动位置验证方法
位宽 6→8掩码、偏移、循环初值正/负/混合三组数据
自动校验输出前比对段故意改错一位看标志是否点亮
结果输出OUT 输出顺序先乘数后部分积,拼成完整乘积

我一般改完这类定长运算程序,都会先用一组边界数据(全 1、全 0、最小负数)跑一遍再碰正常数据,因为边界值最容易暴露符号位和移位的问题。从那以后每次动掩码或循环次数,我都强制走一遍边界用例加反汇编核对,省下来的调试时间远比多花的几分钟多。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进