1. 先搞清楚“原码一位乘”到底在解决什么问题
如果你刚开始学计算机组成原理,或者在看一些底层运算优化的资料,大概率会遇到“原码一位乘”这个词。它听起来很学术,但核心解决的问题非常具体:在没有硬件乘法器的早期计算机,或者在一些对电路面积、功耗极其敏感的嵌入式场景里,如何只用最基本的加法器和移位器,来实现两个整数的乘法运算。
这和你平时在编程里写a * b或者用计算器点乘号完全不同。高级语言和现代CPU的乘法指令背后是高度优化的硬件电路,速度极快。而原码一位乘是一种算法层面的、串行执行的乘法实现方案。它的价值不在于今天让你写程序算乘法更快,而在于帮你理解乘法运算在计算机底层是如何“拆解”成更基本操作的。这也是为什么相关热搜里会出现“变夸导模拟乘法电路”这类词——它本质上就是一种用基础门电路模拟乘法流程的设计思路。
所以,这篇文章适合两类人看:一是正在学习计组、需要掌握乘法器原理的学生;二是偶尔需要涉足底层算法或硬件模拟的开发者,想弄明白这种基础运算的来龙去脉。最关键的是,你要能通过几个明确的步骤,手动演算出整个过程,并理解每一步的硬件行为对应什么软件或逻辑操作。
2. 手动演算:原码一位乘的核心四步拆解
原码一位乘的算法是固定的,我们可以把它总结为四个步骤,然后通过一个具体的例子走一遍。这里先记住两个关键前提:
- 操作数采用原码表示:这是算法的名字来源。我们只处理数的绝对值部分,符号位单独处理(同号为正,异号为负)。
- 从乘数的最低位开始:算法是顺序的,一次只看乘数的一位。
下面以计算+13乘以+11为例(用8位二进制原码演示,忽略符号位后,数值部分各占4位):
- 被乘数
X= 13 (十进制) = 1101 (二进制) - 乘数
Y= 11 (十进制) = 1011 (二进制) - 初始部分积
P= 0000 (4位,与数值位等长)
2.1 第一步:判断当前乘数位
检查当前乘数Y的最低位(LSB, Least Significant Bit)。
- 如果该位为
1,则进入第二步。 - 如果该位为
0,则直接跳到第三步。 在我们的例子中,初始乘数Y=1011,最低位是1。
2.2 第二步:部分积加上被乘数
将当前的部分积P与被乘数X相加,结果写回部分积P。
- 初始:
P= 0000,X= 1101。 - 执行加法:0000 + 1101 = 1101。
- 更新部分积:
P= 1101。
2.3 第三步:部分积与乘数联合右移一位
这是最关键的一步,完成一次“乘数位”的消费。将部分积P和乘数Y视为一个整体,共同向右移动一位。
- 移位的规则:整体右移。部分积的最低位(LSB)移入乘数的最高位(MSB);部分积的最高位(MSB)补0。
- 移位前:
P= 1101,Y= 1011。 - 整体右移一位:
P右移:1101->0110(最高位补0)。Y右移:1011->1101(原P的最低位1移入Y的最高位)。
- 移位后:
P= 0110,Y= 1101。注意:乘数Y被右移后,其最低位被“挤掉”丢弃。我们下次判断的“当前乘数位”就是新的最低位。
2.4 第四步:重复循环
重复执行第一步到第三步,循环次数等于乘数数值部分的位数(本例中是4位)。 我们已经完成了一轮循环。现在开始第二轮:
- 判断当前乘数位:
Y=1101,最低位是1。 - 部分积加被乘数:
P(0110) +X(1101) =10011。注意这里产生了5位结果(1 0011)。在固定位宽(4位)的运算中,我们通常只取低4位,高位进位可能会被暂时存储或丢弃(具体硬件有累加寄存器处理)。为简化演示,我们取低4位:0011。更新P= 0011。(实际硬件有进位位,此处理解运算逻辑即可) - 联合右移:
P(0011)和Y(1101)整体右移。P右移为0001(最高位补0),Y右移为1110(P的原最低位1移入)。结果:P= 0001,Y= 1110。
继续第三轮、第四轮循环,直到完成4次循环。下表完整展示了整个过程:
| 循环次数 | 当前乘数位 (Y最低位) | 操作 (加X?) | 部分积 P (操作前) | 部分积 P (操作后) | 乘数 Y (操作前) | 右移后 (P, Y) |
|---|---|---|---|---|---|---|
| 初始 | - | - | 0000 | - | 1011 | - |
| 1 | 1 | P = P + X | 0000 | 1101 | 1011 | P=0110, Y=1101 |
| 2 | 1 | P = P + X | 0110 | 0011 (取低4位) | 1101 | P=0001, Y=1110 |
| 3 | 0 | (不加) | 0001 | 0001 | 1110 | P=0000, Y=1111 |
| 4 | 1 | P = P + X | 0000 | 1101 | 1111 | P=0110, Y=1111 (最终) |
循环结束后:最终的乘积由最终的部分积P和最终的乘数Y拼接而成。即P作为高4位,Y作为低4位。
- 最终
P= 0110 (二进制) = 6 (十进制) - 最终
Y= 1111 (二进制) = 15 (十进制) - 拼接结果:0110 1111 (二进制) = 6 * 16 + 15 = 111 (十进制)。
验证:13 * 11 = 143。等等,我们算出来是111?这里出错了。问题在于我们演示时简化了进位。在第二步的实际硬件运算中,加法产生的进位必须被保留在部分积的高位。让我们修正一下,用一个更严谨的表格,并假设部分积寄存器有足够的宽度(例如8位,是结果位数)来存放中间结果,但逻辑不变。
3. 纠偏与深化:从算法逻辑到硬件映射
上面的例子揭示了学习原码一位乘时最容易卡住的地方:位宽和进位。在纸上演算时,我们容易忽略寄存器位宽限制,而硬件是严格按位宽操作的。为了正确理解,我们需要明确几个硬件实现细节:
3.1 明确的寄存器结构与位宽
通常需要三个寄存器:
- 被乘数寄存器 (X):存放被乘数原码的数值部分,位宽为n位。
- 乘商寄存器 (Y):初始存放乘数原码的数值部分,位宽为n位。在运算过程中,它既存放剩余的乘数,也最终存放乘积的低n位。
- 累加寄存器 (A):初始为0,位宽为n+1位或n位(带进位位)。它存放部分积(Partial Product)。采用n+1位可以更方便地处理加法进位。
对于两个n位数相乘,乘积最多有2n位。因此,最终结果的高n位在A(的部分),低n位在Y。
3.2 修正后的演算流程(n=4)
我们重新计算13 * 11(1101 * 1011)。设定A寄存器为5位(4位数值+1位隐含高位),初始为0 0000。X=1101,Y=1011。
| 循环 | 判断位 (Y最低位) | 操作 | 操作前 (A, Y) | 加法操作 (A + X) | 操作后 (A, Y) | 右移后 (A, Y) |
|---|---|---|---|---|---|---|
| 初始 | - | - | A=0 0000, Y=1011 | - | - | - |
| 1 | 1 | A = A + X | A=0 0000 | 0 0000 + 0 1101 = 0 1101 | A=0 1101, Y=1011 | A,Y整体右移:A=0 0110, Y=1101 (A末位1移入Y最高位) |
| 2 | 1 | A = A + X | A=0 0110 | 0 0110 + 0 1101 = 1 0011 | A=1 0011, Y=1101 | 右移:A=0 1001, Y=1110 (A末位1移入) |
| 3 | 0 | (不加) | A=0 1001 | - | A=0 1001, Y=1110 | 右移:A=0 0100, Y=1111 (A末位1移入) |
| 4 | 1 | A = A + X | A=0 0100 | 0 0100 + 0 1101 = 1 0001 | A=1 0001, Y=1111 | 右移:A=0 1000, Y=1111 (A末位1移入) |
循环结束:
- 最终 A (高4位) = 1000 (二进制) = 8 (十进制)
- 最终 Y (低4位) = 1111 (二进制) = 15 (十进制)
- 乘积 = A与Y拼接:
1000 1111(二进制) = 143 (十进制)。结果正确。
这个修正后的流程清晰地展示了硬件是如何工作的:加法在扩展位宽的A中进行,右移操作是A和Y作为一个整体进行的算术右移(对于原码乘法,高位补0),Y的最低位移出丢弃,A的最低位补充到Y的最高位。
3.3 为什么是“一位乘”和“原码”
- “一位乘”:每个时钟周期(或每次循环)只处理乘数的一位。与之相对的是“两位乘”或“阵列乘法器”,它们在一个周期内能处理更多位,速度更快,但电路更复杂。
- “原码”:意味着我们直接对数的绝对值(即原码的数值部分)进行操作。符号位单独用异或运算得出:
符号位 = X_sign ⊕ Y_sign。最终结果的符号位拼接上数值部分得到的乘积绝对值即可。
4. 从理论到“感觉”:实操中的关键点与边界
理解了标准步骤,在实际学习、做题甚至用HDL(硬件描述语言)模拟时,你还会遇到一些典型问题。我一般会建议按以下顺序来建立直觉和排查错误。
4.1 环境准备与验证思路
你不是在跑一个软件,而是在模拟一个硬件算法。你的“环境”就是笔、纸、或者一个文本编辑器/代码编辑器。
- 准备清晰的表格:像上面那样画一个表格,列包括:循环次数、判断位、操作前A、操作前Y、加法操作、操作后A、操作后Y、右移后A、右移后Y。这是最有效的防错手段。
- 确定位宽:明确被乘数、乘数、部分积、乘积的位宽。这是所有错误的根源。两个4位数相乘,乘积是8位,那么部分积寄存器A的位宽至少要为4位(考虑进位则需5位),乘数寄存器Y为4位。
- 先算十进制验证:在开始二进制演算前,先算出十进制结果。这样在最后拼接出二进制结果后,可以转换回十进制验证。
4.2 分步实操与常见“坑点”
按照步骤操作时,重点关注这几个地方:
第一步(判断)的坑:
- 判断的是Y的当前最低位,这个“当前”是随着右移不断变化的。每次右移后,Y的最低位都是新的待判断位。
- 不要提前把整个乘数扫描完再做决定,一定是“判断一位,处理一位,右移一位”的串行流程。
第二步(加法)的坑:
- 溢出处理:加法结果可能超出A寄存器当前位宽表示的范围。在硬件中,A寄存器需要有足够的位宽来容纳加法可能产生的进位。在我们的修正例子中,我们使用了5位的A(1位隐含高位+4位数值)来清晰展示进位。在实际简单的4位设计中,可能会有一个单独的进位触发器(C位)来存储这个溢出位,在右移时,C位会移入A的最高位。这是最容易出错的地方,务必在演算时考虑进位。
- 被加数:加的是被乘数寄存器X的值,不是Y的值。
第三步(右移)的坑:
- 联合右移:A和Y是作为一个整体向右移动一位。可以想象它们连接成一个长寄存器
[A, Y]。 - 移位方向:是算术右移。对于原码(正数),高位补0。移出的Y的最低位被丢弃。A的最低位(LSB)移入Y的最高位(MSB)。
- 进位位的处理:如果使用了进位位C,那么右移时是
[C, A, Y]整体右移,C移入A的最高位,A的最低位移入Y的最高位。
第四步(循环)的坑:
- 循环次数:严格等于乘数数值部分的位数n。4位乘数就循环4次,不要多也不要少。
- 结束状态:循环结束后,乘积的高n位在A寄存器中,低n位在Y寄存器中。注意,此时的A和Y是经历了最后一次右移之后的状态。有些教材的步骤是“先判断、再加、再右移”,循环n次后得到的结果就是最终乘积,无需额外操作。
4.3 当结果不对时,你的排查清单
如果手动演算或代码模拟的结果与预期不符,按这个顺序查:
- 检查初始值:A是否初始化为0?X和Y是否是正确的二进制原码数值部分(正数直接转,负数取绝对值)?
- 检查位宽:你的A寄存器位宽是否足够?加法后进位是否被正确保存了?在纸上演算时,建议用比数值位宽多1位的A来画图,避免进位丢失。
- 逐步核对表格:这是最有效的方法。每完成一行,就计算一下当前
[A, Y]组合代表的数值是多少,与理论中间结果对比。可以写一个简单的Python脚本辅助验证每一步。 - 检查右移逻辑:确认是A和Y联合右移,并且移位的来源和目的地是正确的。最容易乱的是“A的最低位移入Y的最高位”这个操作。
- 检查循环次数:是否做了n次?是否在最后一次右移后结束?
- 验证符号位:如果题目涉及负数,确认符号位是单独异或计算后,再拼接到乘积数值部分的前面。
4.4 相关概念延伸:它和“高精度乘法”、“浮点数乘法”有什么关系?
看到热搜词里的“高精度乘法”和“浮点数乘法”,这里简单建立一下联系:
- 高精度乘法:当数字非常大,超出CPU单条指令处理范围时(比如计算1000位的整数乘法),就需要用软件算法实现。原码一位乘的思想——将乘法分解为“加法”和“移位”——正是许多高精度乘法算法(如最基础的竖式模拟算法)的核心。只不过在高精度实现中,“位”变成了“十进制位”或“大数基数的位”,加法是更复杂的大数加法,但分解思路一脉相承。
- 浮点数乘法:IEEE 754浮点数乘法的核心步骤之一是尾数相乘。对于规格化的二进制尾数,这个乘法操作本质上就是两个定点小数的乘法。虽然现代CPU使用高度优化的硬件乘法器(如Booth算法、Wallace树等)来加速,但其基本运算单元仍然建立在加法和移位之上。理解原码一位乘有助于你理解浮点数乘法中尾数相乘这一步骤的硬件基础。而“shell里如何乘法”则是完全不同的层面,那是Shell解释器调用底层硬件乘法指令,和这个底层算法无关。
5. 总结:如何真正掌握并应用这个知识
原码一位乘不是一个你每天会直接用的工具,但它是一个重要的思维模型。要掌握它,我建议按以下路径:
- 死记步骤不如理解动机:记住“判断-加-移位”的循环,但更要理解为什么这么做——它是在模拟我们手算乘法时“逐位相乘、错位相加”的过程。
- 亲手画两遍表格:找两个简单的4位二进制数(如
0111 * 0011),严格按照位宽和进位规则,在纸上画表演算两遍。这是将知识从“看懂”变成“会用”的关键一步。 - 尝试用代码描述:用你熟悉的语言(Python、C、Verilog/VHDL)写一个模拟程序。不追求性能,只追求严格按步骤实现。这个过程会强迫你理清所有细节,尤其是寄存器的位宽和移位操作。
- 对比其他算法:了解还有“原码两位乘”、“补码一位乘(Booth算法)”、“阵列乘法器”等。知道原码一位乘是其中最基础、最慢但也是最直观的一种,这样你就知道了它在知识图谱中的位置。
最后,当你再遇到“变夸导模拟乘法电路”这类概念时,你就会明白,它很可能就是在用基本的与门、或门、非门、加法器和移位寄存器,来搭建实现我们上面一步步演算的逻辑电路。把抽象的算法步骤映射到具体的硬件信号流转,这才是学习计算机组成原理最有价值的部分。