简介:这份资源面向计算机组成原理课程的学习者与课程设计者,聚焦定点补码一位乘(Booth算法)的程序设计实现,帮助读者理解从算法原理到实验箱编程落地的完整过程。资源以沈阳航空航天大学课程设计报告形式组织,涵盖总体设计方案、详细设计方案、程序调试与结果测试等章节,具体展开Booth算法操作表、[X]补与[-X]补的初始化、部分积与附加位的循环移位规则,以及基于伟福COP2000指令集的汇编实现思路。压缩包内为1个doc文档,约639KB,内容完整、结构清晰,适合对照实验环境复现与查阅。该资源已有1607人学习下载,可作为组成原理课程设计、实验报告撰写与Booth乘法器调试的参考材料,帮助读者快速把握算法要点与编程实现路径。
1. 补码一位乘:为什么两个符号位能“自动”算出正确符号
两个 6 位数相乘,结果符号本该单独判断,可 Booth 算法偏偏不这么做——它让符号位跟着数据一起进加法器、一起右移,最后符号自己就对了。这不是巧合,而是补码算术右移的封闭性在兜底:算术右移保持最高位不变,等价于对负数做除 2 取整,于是符号信息在移位链里被一路“携带”下去。搞懂这一点,你才算真正理解定点补码乘法器为什么能省掉独立的符号处理逻辑,也才能看懂 COP2000 实验箱上那段汇编里每一次RR A背后的意图。下面从操作表出发,把 Booth 的判定、累加、移位三件事拆到寄存器级。
2. Booth 重编码:Yn 与 Yn+1 两位到底在编码什么
2.1 从“加四次”到“加减各半”的动机
最朴素的乘法是逐位判断乘数、为 1 就加被乘数。对补码来说这条路走不通:乘数最高位是符号位,权重是负的,不能简单当 1 处理。Booth 的做法是把乘数按相邻两位重新编码,把“加被乘数”改写成“加或减被乘数”,让符号位自然融入运算。
操作表只有四行,但每一行都对应一个重编码值:
| Yn | Yn+1 | 操作 | 重编码含义 |
|---|---|---|---|
| 0 | 0 | 部分积不变,右移 | 0 |
| 0 | 1 | 部分积加 [X]补,右移 | +1 |
| 1 | 0 | 部分积加 [-X]补,右移 | -1 |
| 1 | 1 | 部分积不变,右移 | 0 |
关键在于 01 和 10 这两行:它们分别代表乘数在这一位从 0 跳到 1、从 1 跳到 0,也就是一段连续 1 的起点和终点。Booth 把“一串 1”压缩成“起点减一次、终点加一次”,加法次数从最多 n 次降到平均 n/2 次。这是重编码带来的直接收益,也是它在硬件乘法器里长期占位的根本原因。
2.2 附加位 Yn+1 的初始化与“多算一步”的由来
附加位初值必须是 0,这一点常被忽略。原因在于最低位 Y0 的右边没有“更低位”,Booth 需要虚构一个 Yn+1=0 来构成第一对判定。如果初值给错,第一次判定就会把最低位的 0 误读成一段 1 的终点,结果整体偏移。
另一个容易翻车的点是循环次数。6 位数(1 位符号 + 5 位数据)需要做 6 次判定,但只有前 5 次带移位,第 6 次只做加减不移位。原文在 R3 里存 n=5,循环体里每次减 1,减到 0 后再补一次“只加减不移位”的收尾。这个设计不是随意定的:最后一次判定处理的是符号位与它左边虚构位的组合,此时再右移会把已经算好的符号位挤出去。所以“n 次移位 + 1 次纯加减”是 Booth 在定长补码下的标准收尾方式。
提示:把循环次数写成 6 并在循环体内无条件移位,是这类实验里最高频的逻辑错误,结果往往只错在符号位上,肉眼很难第一时间发现。
2.3 算术右移为什么能“带着符号走”
普通逻辑右移补 0,算术右移补符号位。补码乘法的部分积可能是负数,右移时必须保持最高位不变,否则负数会被当成大正数。原文的移位流程分三段处理:先看乘数末位决定附加位,再看部分积末位决定乘数新首位,最后看部分积首位决定部分积新首位。这三步合起来,等价于把“部分积 + 乘数 + 附加位”当成一个整体做一次算术右移。
用代码表达这个整体移位更直观:
def arithmetic_shift_right(partial, multiplier, extra, width=6): # 取出乘数最低位,作为新的附加位 new_extra = multiplier & 1 # 部分积最低位进入乘数最高位 new_multiplier = ((partial & 1) << (width - 1)) | (multiplier >> 1) # 部分积算术右移:最高位保持原符号 sign = partial & (1 << (width - 1)) new_partial = (partial >> 1) | sign return new_partial, new_multiplier, new_extrasign这一行是灵魂:它把移位前的最高位原样贴回移位后的最高位,负数的符号因此不会在移位中丢失。new_multiplier的拼接则保证部分积溢出的最低位不会丢,而是被“接”进乘数的高位——这正是把两个寄存器串成一个移位链的硬件含义。参数width=6对应题目规定的 6 位字长,改字长时三处位宽必须同步改,否则拼接会错位。
3. 寄存器级实现:从开关输入到 OUT 输出的完整链路
3.1 初始化阶段:三个补码值怎么算出来
程序先把 [X]补、[-X]补、[Y]补分别落到 0F0H、0F1H、0F2H 三个存储单元。求补码的逻辑靠符号位判断分支:把输入值与 20H(即二进制 100000)做AND,结果为 0 说明是正数,补码等于原码;结果为 20H 说明是负数,走“取反加 21H”的路径。
这里 21H 而不是 01H,是 6 位字长下的一个细节:取反后加 1 得到的是 6 位补码,但机器按 8 位识别,需要把高两位补成与符号位一致。加 21H 相当于在低 6 位加 1 的同时把高两位也置成符号扩展的正确形态。原文对此的说明是“令前两位与符号位保持相同,方便运算”,本质就是手工做符号扩展。
; 负数求补码:取反后加 21H,完成 6 位补码 + 符号扩展 MOV A, R1 CPL A ; 按位取反 ADD A, #21H ; 低 6 位加 1,高 2 位补符号 MOV 0F0H, A ; 存入 [X]补CPL是对累加器整体取反,ADD #21H一次性完成“加 1 + 符号扩展”两件事。如果字长改成 8 位数据,这个立即数就要跟着改,否则高位扩展会错。求 [-X]补 时原文用AND #1FH再存 0F1H,这是把 X 的低 5 位数据位取出后按另一条路径处理,读者对照附录机器码可以看到两条分支的对称结构。
3.2 主循环:判定、累加、移位的三段式
主循环从 DAY0 开始,先取 0F2H 里的乘数,AND #01H判断 Yn;再取 0E0H 里的附加位,AND #01H判断 Yn+1。两次判断组合出四种分支,分别跳到“加 [X]补”“加 [-X]补”或“直接移位”。
DAY0: MOV A, 0F2H AND A, #01H ; 取 Yn JZ DAY4 ; Yn=0 转去判附加位 MOV A, 0E0H AND A, #01H ; 取 Yn+1 JZ DAY5 ; Yn=1,Yn+1=0 -> 加 [-X]补 ; 否则 YnYn+1=11,直接移位 JMP DAY6这段分支结构把操作表翻译成了跳转表。DAY5分支执行ADD A, R0完成部分积加 [-X]补,DAY4分支在附加位为 1 时执行加 [X]补。注意加完后统一跳到 DAY6 做移位,移位逻辑被抽成公共段,避免在每个分支里重复写三行移位代码——这是汇编里典型的“用跳转换代码体积”的取舍。
3.3 移位段的三个判断:附加位、乘数首位、部分积首位
DAY6 之后的移位段是全程序最容易写错的地方。它按顺序做三件事:判断乘数末位决定新附加位、判断部分积末位决定乘数新首位、判断部分积首位决定部分积新首位。原文用OR #01H、OR #20H、OR #80H分别把对应位置 1,用AND #DFH、AND #7FH把对应位清 0。
以乘数新首位为例:部分积末位为 1 时,移位后乘数最高位要置 1,对应OR #20H(6 位字长下最高位是 bit5)。部分积新首位则看移位前最高位,为 1 时OR #80H保持符号。这两步合起来就是 2.3 节那段 Python 的硬件版。如果漏掉OR #80H,负数部分积右移后符号丢失,最终结果会偏出一个很大的正数——原文调试记录里“最高位是 1 时没有置 1 导致程序出错”说的就是这一条。
3.4 收尾:R3 减到 0 后的“最后一次不加移位”
DAY10 段把 R3 减 1,用OR #00H触发零标志判断,非零就跳回 DAY0 继续循环。R3 归零后进入 DAY11,再判一次 YnYn+1,按操作表做最后一次加减,但跳过整个移位段直接输出。输出顺序是先乘数后部分积,分别经 OUT 寄存器显示。
这个收尾结构对应 2.2 节讲的“n 次移位 + 1 次纯加减”。把收尾单独写成一段而不是塞进循环体,好处是循环体内的移位逻辑不用加条件判断,硬件执行路径更短;代价是代码多了一段重复的判定逻辑。在 COP2000 这种指令条数有限的实验环境里,这个取舍是合理的。
4. 调试避坑:寄存器冲突、指令误用与符号位丢失
4.1 现象:程序跑到一半结果全乱,寄存器值被莫名覆盖
原因:COP2000 的 EM 程序在运行时会占用一批低地址寄存器,编程时如果随手用 R0、R1 之外的通用寄存器存中间值,可能和系统占用区撞车。原文调试记录第一条说的就是这个——写到一半发现“所需要的寄存器被占用了”。
解决:中间变量尽量往高地址放,比如 0F0H 之后的存储单元;核心数据固定用 R0、R1、R2、R3 这几个明确可用的寄存器,并在注释里标清每个寄存器的用途,避免后期改动时互相踩踏。
4.2 现象:MOV A, 0AH编译能过,但取到的不是立即数 0AH
原因:COP2000 汇编里立即数必须带#前缀。MOV A, 0AH被解释成从地址 0AH 取数,而不是把 0AH 装进累加器。原文调试记录第二条明确记了这个坑。
解决:凡是字面常量一律写#,例如MOV A, #0AH、ADD A, #21H。养成“看到裸数字先怀疑是不是漏了 #”的习惯,这类错误在汇编里不会报错,只会静默取错值。
4.3 现象:正数乘法结果正确,负数乘法结果符号反了
原因:部分积算术右移时没有保持最高位。逻辑右移补 0 会把负数变成大正数,后续累加全部建立在错误的部分积上。原文调试记录第三条“最高位是 1 时移位后没有置 1”就是它。
解决:移位段固定加一步“判部分积首位,为 1 则OR #80H”。更稳妥的做法是把移位写成子程序,所有调用点共用同一份逻辑,避免某个分支漏写。
4.4 现象:最后一次循环后结果差一个 [X]补 或 [-X]补
原因:循环次数或收尾逻辑错位。把 n=5 的循环写成 6 次带移位,或者收尾时又移了一次位,都会让最后一次判定白做甚至反向。
解决:明确“数据位 5 次移位 + 符号位 1 次纯加减”的结构,R3 只控制前 5 次,收尾段独立于循环。测试时用 Y=011000B 这类中间有连续 1 的数据,能同时覆盖 01 和 10 两种重编码分支。
4.5 现象:OUT 显示的顺序和预期相反
原因:输出段先送乘数还是先送部分积,取决于结果在寄存器里的拼接方式。原文规定先输出乘数再输出部分积,如果代码顺序写反,读出来的数看着像“高低位颠倒”。
解决:输出前在注释里写清“先乘数后部分积”,并用一组已知结果(如 X=100110B、Y=011000B 得到部分积 FBH、乘数 20H)做基准比对,顺序错了立刻能看出来。
5. 设计取舍:为什么 Booth 在定长补码里仍是“够用且优雅”的解
Booth 算法最容易被误解成“只是省了几次加法”。真正值得琢磨的是它对符号的处理方式:不设独立的符号判断电路,而是让符号位作为数据的一部分参与运算,靠算术右移的封闭性保证正确性。这个设计在 6 位字长的实验环境里体现得尤其清楚——符号扩展、附加位初值、最后一次不移位,三个细节环环相扣,少一个结果就错。
从实现代价看,Booth 用“多一个附加位 + 一套四分支判定”换掉了“独立的符号处理 + 绝对值乘法 + 符号回填”三块逻辑。在指令集精简、寄存器紧张的实验平台上,这个交换是划算的:分支虽然多,但每条分支都只是简单的加或减,移位段还能抽成公共代码。代价是控制流变复杂,调试时不容易一眼看出哪一步错了,这也是第 4 章那些坑的根源。
如果想进一步验证自己是否真懂了,可以做一个改动实验:把字长从 6 位改成 8 位,同步修改 20H、21H、1FH、DFH、7FH 这几个掩码和立即数,再跑同一组测试数据。改完如果结果仍然正确,说明你理解了每个常量的位宽含义;如果错了,错在哪一个常量上,就对应哪一段位宽没对齐。这种“改参数看哪里崩”的验证方式,比通读代码更能暴露理解盲区。
我自己的习惯是:每次碰定点乘法这类位级运算,先把操作表、移位规则、收尾条件三样东西手写一遍,再对照代码逐行核,绝不直接抄现成程序。血泪经验是,位级 bug 不会在编译期报错,只会在某组特定输入下悄悄给出错误结果,事后排查的成本远高于事前手推一遍。这份课程设计里的汇编清单和调试记录,正好提供了从操作表到机器码的完整对照,适合拿来当手推的参照物。希望帮到你。
本文还有配套的精品资源,点击获取