Logisim 的计组实验做到乘法器这一站,基本就意味着课程设计要开始动真格了。我是在捋西电计组课设基本模型机之前,先被这个实验卡了将近两天,调试过程中把移位加法、寄存器使能、状态机时钟这些概念重新过了一遍才真正跑通。这篇把乘法器背后的设计思路、Logisim 里的连线要点、测试用例和排错经验一次说清楚,希望能让正在做这个实验的同学少走一晚上弯路。
这个实验表面上是“用 Logisim 搭一个乘法器”,实际上考察的是你对数据通路和控制逻辑的整体理解。乘法器不是孤立的组合电路,它里面有寄存器、加法器、移位逻辑、计数器、状态判断,几乎把前面所有实验的基础模块都串起来了。无论你之后是继续做单总线 CPU 设计,还是应付考研 408 里的计组题,把这个实验吃透都特别值。
1. 这个实验到底在训练什么
1.1 乘法器不是“一个电路”,是一台小机器
很多人第一次听说乘法器实验,第一反应是“乘法不就是一堆加法?”话是没错,但计组实验里的乘法器,重点不在“怎么算出乘法”,而在“怎么用寄存器时序把乘法算出来”。
你可以把乘法器理解成一台迷你计算机:被乘数和乘数要先存进寄存器,然后控制器根据乘数的某一位决定“加不加被乘数”,加完再移位,如此反复。整个过程里,数据是在时钟边沿下一拍一拍流动的。这和你在 Logisim 里搭一个单纯的全加器阵列完全不是一回事。
计组课安排乘法器实验,目的就是让你把前面学过的寄存器、ALU、移位、控制状态机这些东西,放到一个真实的问题里组合起来。等你把乘法器调通,再去碰单总线 CPU 设计里的取指、译码、执行周期,你会觉得那些控制信号的写法非常眼熟。
1.2 Logisim 适合做什么,不适合做什么
Logisim 是一个教学用的数字逻辑仿真工具,它的最大优点是“看得见”。每个寄存器的值可以点开看,每根线的电平可以用探针量,时钟可以手动一步步打。调试乘法器这种带反馈的数据通路,Logisim 比写 Verilog 直观太多。
但 Logisim 也有自己的脾气,它对组合逻辑环路的检查很严格,有时候你只是想让某根线“暂时不动”,它就会给你报错。还有就是时钟问题,如果多个寄存器共用一个时钟源,仿真时容易出现“看起来全都在变,但结果就是不对”的情况。后面我会专门讲这些坑。
2. 乘法器的几种实现思路,以及为什么实验常用移位相加
2.1 回到手算竖式:一位一位乘过去
十进制的竖式乘法大家都会:把乘数每一位分别和被乘数相乘,得到部分积,再把所有部分积按位对齐相加。二进制也一样,只是每一位只有 0 和 1,所以部分积要么是被乘数本身,要么是 0。
问题是,如果直接按竖式搭电路,n 位乘 n 位需要 n 个 n 位加法器同时工作,这就是组合逻辑阵列乘法器。它速度非常快,但电路规模很大。对计组实验来说,更经典的做法是“移位相加”:串行地一位一位处理乘数,用一个加法器反复累加,把部分积逐步移位累积起来。
移位相加乘法器本质上是拿时间换面积。它需要的硬件很少,一个加法器、几个寄存器、一个移位器就够了,但需要 n 个时钟周期才能算完。这个思路和 CPU 里“用有限硬件资源完成复杂运算”的思想完全一致,所以教材和实验课普遍采用它。
2.2 原码一位乘、补码一位乘和 Booth 算法
如果乘数和被乘数都是无符号数,直接按“最低位判断、加被乘数、右移”的流程做就行,这叫原码一位乘。原码一位乘的逻辑很简单:先把符号位单独拎出去异或,数值部分按无符号数相乘,最后再拼上符号位。
如果要计算补码数,事情就麻烦一点。补码的最高位是符号位,不能当普通数值位处理,所以出现了补码一位乘,也就是 Booth 算法。Booth 算法的核心是引入一个额外的附加位 Q(-1),每次根据乘数最低位和附加位的差值来决定“加被乘数”“减被乘数”还是“不变”,然后做算术右移。
408 统考里经常考 Booth 算法的计算过程。比如让你算两个补码相乘的中间步骤,或者问你“Booth 算法一次判断几位乘数决定一次操作”。实验时候如果只要求无符号乘法,可以用原码一位乘;如果要求支持负数,就必须上 Booth。
2.3 阵列乘法器:到底什么时候用
阵列乘法器是纯组合逻辑,一旦输入稳定,经过一段传播延迟后直接输出结果,不需要时钟。它的优点是吞吐率高,缺点是硬件多。n 位阵列乘法器大约需要 n 的平方个全加器,位数一上去面积就很恐怖。
在实验里,我建议不要把阵列乘法器作为首选。因为纯组合逻辑电路在 Logisim 里看起来简单,但连线极多,查错很痛苦。而且计组实验的隐藏要求是“用可控时序实现运算”,你要的是一台能由外部控制何时开始、何时结束的乘法器,而不是一个黑盒。
当然,如果你想理解 408 里“并行乘法器为什么快”,可以在移位相加乘法器做完之后,额外搭一个 2 位或 4 位的阵列乘法器感受一下。但作为实验八的主体,老老实实做移位相加更符合课程目标。
3. Logisim 实操:搭一个可复现的 4 位无符号乘法器
3.1 顶层端口规划:先想清楚谁进谁出
打开 Logisim 新建电路之前,先把端口定下来。我做的是一个 4 位无符号数乘法器,输入两个 4 位数,输出一个 8 位乘积。顶层端口如下:
| 端口名 | 方向 | 位宽 | 作用 |
|---|---|---|---|
| X | 输入 | 4 位 | 被乘数 |
| Y | 输入 | 4 位 | 乘数 |
| Start | 输入 | 1 位 | 开始信号,高电平有效 |
| CLK | 输入 | 1 位 | 时钟,控制所有寄存器同步更新 |
| Reset | 输入 | 1 位 | 异步清零,复位所有状态 |
| Product | 输出 | 8 位 | 最终乘积 |
| Done | 输出 | 1 位 | 计算完成标志,高电平有效 |
端口定好后,再去定内部寄存器。我用了这几个:
- M 寄存器:4 位,存被乘数 X,整个计算过程中不变。
- Q 寄存器:4 位,初始存乘数 Y,之后每周期右移一位,最低位 Q0 决定当前要不要加被乘数。
- A 寄存器:5 位,存部分积的高位,注意这里比乘数多一位,用来存中间进位。
- 计数器:2 位就够了,但用 3 位更保险,用来记录循环次数。
A 寄存器为什么要 5 位?因为每次加法可能是“当前部分积 + 被乘数”,两个 4 位数相加最多产生 5 位结果。如果 A 只有 4 位,最高位的进位就直接丢了,结果必错。
3.2 数据通路连线要点:先画数据,再画控制
连线之前,在纸上把数据通路画出来,这一步绝对不能省。我最后在 Logisim 里的连线方式是这样的:
加法器用 5 位加法器,两个输入分别是 A 寄存器当前值和被乘数选择器的输出。选择器的作用是:当 Q0 为 1 时,把 M 寄存器扩展成 5 位送给加法器;当 Q0 为 0 时,送 0。
加法器的输出记为 S。注意,S 是“加完之后的完整部分积”,但乘法器每个周期要求“加完再右移”,所以不能直接把 S 送回 A,要先把 S 逻辑右移一位再送回 A。具体接法:A 寄存器的输入取 S 的高 4 位,即 S[4:1],最高位接 0。这样 A_next 就等于 S >> 1。
Q 寄存器的输入接法有点绕,但理解了就很简单:Q 要右移,Q 的最高位要接收从 A 那边移过来的位。在“加完再右移”的模型里,移入 Q 最高位的是加法器输出 S 的最低位,也就是 S[0]。所以 Q 寄存器的输入 = {S[0], Q[3:1]}。最高位是 S[0],低三位是原来 Q 的高三位。
M 寄存器只在 Start 信号有效时载入 X。Q 寄存器在 Start 有效时载入 Y,其他时间接收右移拼接结果。A 寄存器在 Start 有效时清零,其他时间接收 S >> 1。计数器在 Start 有效时清零,其他时间每周期加一。
用 Logisim 里的话说,就是给 A、Q、计数器都接一个 2 选 1 多路选择器:一侧是“初始化值”,另一侧是“运行周期值”,选择端由 Start 控制。M 寄存器的使能端也要接 Start,保证运行期间 M 不动。
3.3 控制器怎么设计:计数器和状态位就够了
很多同学卡在“什么时候算结束”上。其实 4 位无符号乘法只需要循环 4 次,所以一个计数器就够。我用的方案是:Start 有效时计数器清零,之后每个 CLK 上升沿计数器加一。当计数器计到 4 时,输出 Done 为 1,Q 和 A 里的最终结果保持不变。
但这里有个细节:如果计数器一直加下去,到 5、6、7 之后结果会不会被破坏?会。所以要把计数器的使能控制住。最简单的方法是加一个 D 触发器作为“完成标志”。当计数器等于 4 时,完成标志置 1,这个完成标志反过来禁止计数器继续加。下次 Reset 或 Start 时再清掉。
如果你想让乘法器看起来更像一个状态机,可以分成三个状态:IDLE、WORK、DONE。IDLE 状态等 Start,WORK 状态循环 n 次,DONE 状态置 Done 信号。但在 Logisim 实验里,用计数器加 D 触发器已经能完成任务,代码量更少,也不容易错。状态机的好处是逻辑更清晰,适合后面做单总线 CPU 时复用,这个我放到第五节说。
3.4 单步运行观察寄存器变化
搭完电路别急着看结果,先用一个简单例子手工验证。我用的是 X=3,Y=5,也就是 0011 × 0101,期望结果是 00001111,十进制 15。
在 Logisim 里把时钟设为手动步进,每按一次 CLK,记一次 A、Q、S 的值:
| 周期 | 旧 A | 旧 Q | Q0 | 加法器输出 S | A 新值 | Q 新值 |
|---|---|---|---|---|---|---|
| 初始 | 00000 | 0101 | 1 | - | - | - |
| 1 | 00000 | 0101 | 1 | 00011 | 00001 | 1010 |
| 2 | 00001 | 1010 | 0 | 00001 | 00000 | 1101 |
| 3 | 00000 | 1101 | 1 | 00011 | 00001 | 1110 |
| 4 | 00001 | 1110 | 0 | 00001 | 00000 | 1111 |
第 4 个周期结束后,A = 00000,Q = 1111,拼接成 00001111,正好是 15。看到这个表,你就明白“加完再右移”到底是怎么在寄存器之间流动的了。
最终输出 Product 只需要把 A 的低 4 位和 Q 拼起来。因为 A 的最高位在这个算法里永远是 0,所以直接 Product = {A[3:0], Q} 就行。
4. 测试用例与常见问题排查
4.1 边界测试用例:把乘法器拷打一遍
电路跑通一个例子不代表正确。我每次调完都会跑一组边界用例,这些用例能覆盖绝大多数连线错误和位宽问题。
| 用例 | X | Y | 期望乘积 | 实际结果 | 说明 |
|---|---|---|---|---|---|
| 0×0 | 0000 | 0000 | 00000000 | 00000000 | 全零路径 |
| 0×15 | 0000 | 1111 | 00000000 | 00000000 | 被乘数为 0 |
| 1×15 | 0001 | 1111 | 00001111 | 00001111 | 倍乘为自身 |
| 15×15 | 1111 | 1111 | 11100001 | 11100001 | 最大数,检查进位 |
| 3×5 | 0011 | 0101 | 00001111 | 00001111 | 最常规用例 |
| 10×10 | 1010 | 1010 | 01100100 | 01100100 | 检查多位进位 |
如果 15×15 算出来不是 225,问题基本出在 A 寄存器位宽不够,或者加法器进位没接对。如果 1×15 都不对,那大概率是 Start 初始化逻辑出了问题,Q 寄存器没有在开始前正确载入 Y。
4.2 常见报错和现象速查表
Logisim 报错分两种,一种是编译时报错,一种是仿真现象不对。下面这些是我实际踩过的坑,直接整理成速查表:
| 现象或报错 | 大概率原因 | 排查方法 |
|---|---|---|
| 输出始终为 0 | Q 或 A 寄存器没接到 CLK;Start 初始化没生效 | 手动步进,看每个周期 A/Q 是否变化 |
| 结果只有低 4 位对,高 4 位不对 | A 寄存器位宽不够;Product 拼接顺序错 | 检查 A 是否为 5 位,检查拼接是不是 A[3:0] + Q |
| 组合回路报错,黄色波浪线 | 某根线从输出直接回到同一个逻辑的输入,中间没有寄存器 | 顺着报警路径找,确认 MUX 选择端是不是接到了组合输出 |
| 结果在几个值之间乱跳 | 多个寄存器使用了不同时钟源,或时钟步进时同时触发多级状态 | 所有寄存器统一接同一个 CLK,用 Reset 先清零 |
| Start 按了没反应 | Start 信号作为组合条件没有经过时钟同步,导致初始化只在瞬间有效 | 把 Start 当成寄存器输入的选择条件,而不是直接强置寄存器 |
| 手动步进一次后,A 和 Q 都变了但 Done 不变 | 计数器没有正确产生完成信号,或者完成标志没有锁存 | 单独拉一根线出来看计数器当前值 |
Logisim 里有一个特别好用的排查工具是探针。你可以在 A、Q、加法器输出 S、计数器输出上各拉一个探针,然后手动步进,观察每个周期数据怎么流动。如果某一个周期 S 的值和你手工算的不一样,问题就锁定在那一段组合逻辑上。
4.3 一个容易忽略的复位问题
寄存器清零分同步清零和异步清零。Logisim 里寄存器自带的 CLR 端是异步清零,只要 CLR 为 1,输出立刻变 0,不受时钟控制。这个特性在初始化时很有用,但也容易出问题。
如果你用 Reset 同时清零 A、Q、计数器,而 Reset 信号一直为 1,那么所有寄存器都会一直被清零,Start 按多少次都没用。正确做法是:Reset 是高电平有效,平时保持 0;点一下置 1 再恢复 0。或者用 Start 信号去控制“初始化数据的载入”,而不是用 Reset 一直卡住电路。
我在实验里习惯把 Start 和 Reset 分开:Reset 负责总复位,Start 负责开始计算。这样即使 Start 一直为 1,也只会在第一个时钟周期载入初值,之后的每个周期会正常执行移位累加,不会重复初始化。
5. 从乘法器到单总线 CPU 设计:这个实验的真正后劲
5.1 控制状态机的可复用性
做完乘法器,你会发现它的控制部分其实就是一个小的有限状态机:等待开始、进入循环、完成输出。这个套路和单总线 CPU 设计里的取指周期、执行周期非常像。单总线 CPU 设计 Logisim 里,你需要根据操作码和当前状态产生一系列控制信号,本质上和你根据 Q0 决定“加还是不加”是同一个思路。
所以我在做乘法器的时候,特意没有把控制逻辑写成一坨乱七八糟的门电路,而是用“状态 + 计数器”的方式组织。等后面做模型机时,这套控制逻辑可以直接扩展成微程序控制器或者硬布线控制器。很多同学到课设时觉得状态机难,是因为没有在前面乘法器实验里把状态编码、状态转移、输出译码这些基本功练熟。
5.2 扩展方向:负数、更大位宽和数码管显示
如果实验要求支持负数乘法,就得在原码一位乘的基础上引入符号位处理,或者直接用 Booth 算法。Booth 算法做起来比原码一位乘麻烦,但本质差别只在控制逻辑,数据通路基本还是那套寄存器和加法器。你只需要把“根据 Q0 判断”改成“根据 Q0 和 Q(-1) 的组合判断”,然后多一个附加位寄存器。
位数扩展也很简单,把 M、Q、加法器位宽都改成 8 位,A 改成 9 位,计数器循环次数改成 8,Product 改成 16 位。其余逻辑完全不需要动。这个扩展过程本身就是很好的练习,能检验你到底理解了原理,还是只会照着 4 位连线。
很多课设还要求把结果显示在七段数码管上。Logisim 里有现成的七段数码管组件,你只需要把 8 位乘积拆成高 4 位和低 4 位,分别接两个数码管的输入。这里的难点不是数码管,而是进制转换和位宽匹配。我做的时候习惯在电路里先加一个十六进制转七段数码管的子电路,方便以后复用。
5.3 一点个人体会
我调乘法器最大的收获,不是学会了“怎么算乘法”,而是真正理解了为什么 CPU 里要有一个“时钟”来同步所有操作。刚开始我的 A 寄存器和 Q 寄存器没有统一用一个时钟,结果每次步进,数据总是差一拍。后来我强制自己把所有需要同步更新的寄存器都接到同一个 CLK 上,问题立刻消失。
还有一点想提醒大家:Logisim 里看起来连对了,不等于电路真的对。一定要动手在纸上把状态表推一遍,尤其是“加法器输出 S 的最低位会移入 Q 的最高位”这个细节,不然你自己都会被仿真结果骗过去。把 3×5 这个例子用手推完,再去看仿真波形,很多疑问会瞬间解开。
乘法器实验做完后,我明显感觉到自己对寄存器传输级设计的理解上了一个台阶。后面再去看单总线 CPU 设计里的那些控制信号,不再觉得它们是一堆乱线,而是能看出“这个信号是为了让数据在哪里停一拍,那个信号是为了让哪个寄存器在哪个周期载入”。这种从“连电路”到“设计数据通路”的转变,大概就是计组实验真正想让你完成的事。