ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Logisim原码一位乘法器设计:寄存器电路与数据通路详解

Logisim原码一位乘法器设计:寄存器电路与数据通路详解 上学期带数字逻辑课程设计我注意到一个现象很多同学在Logisim里搭ALU、搭寄存器都挺顺一到乘法器就卡住。卡住的原因倒不是看不懂算法而是不知道乘法算法里那些“右移一位”“判断最低位”的操作在硬件上要拆成哪几个寄存器、哪几条信号线、哪几步时钟动作。更别提有些参考教程只给了电路截图不解释为什么这么连抄完了还是一头雾水。所以我干脆把基于Logisim的原码一位乘法器从头到尾拆开讲一遍顺带把寄存器电路的选型、接法和时序控制单独拉出来说清楚。这篇东西适合正在学计算机组成原理、数字逻辑或者正在做课程设计被乘法器折磨的人。如果你只是想把电路跑通交作业这里也有可以直接照抄的接线方案和控制真值表如果你想弄明白“为什么乘一个数要右移这么多次”那我们把数学原理、数据通路、寄存器时序一层层剥开来看。1. 原码乘法的数学基础为什么“一位一位算”反而是硬件最容易实现的方案1.1 原码的表示规则与乘法拆解先明确一个前提原码是带符号数最直观的表示方式——最高位是符号位0代表正1代表负剩余的位表示绝对值的二进制。比如8位原码中00001101表示1310001101表示-13。两个原码数相乘规则很简单乘积的符号位等于两个乘数符号位做异或乘积的数值位等于两个绝对值做无符号乘法。这一步非常关键因为它把带符号乘法硬生生降级成了无符号乘法硬件上只需要处理非负数不需要考虑补码的符号扩展、借位这些麻烦事。无符号乘法怎么算回忆一下小学学过的竖式乘法把乘数每一位拆出来如果是1就把被乘数抄一遍并左移对应的位数最后把所有行加起来。二进制只有0和1所以“每一位拆出来”就更简单了——乘数某一位是1就加一个被乘数是0就不用加。这就是原码一位乘法的核心思想把“乘”这个动作拆成“判断最低位是否为1 加被乘数 移位”的循环。1.2 为什么要不断右移而不是左移你可能会有个疑问竖式乘法不是应该把被乘数左移吗为什么教科书和电路里都在右移左移方案存在一个硬件上的尴尬4位乘数要产生4个部分积每个部分积的位宽不一样要累加起来得有8位、12位甚至更宽的加法器还要逐位对齐。在纸上写字当然没问题但电路里这么搞加法器的位宽和走线数量会随乘数位数线性膨胀代价太高。右移方案的巧妙之处在于把加法器固定成n位外加1位进位每一轮只做一次n位加法然后把加出来的结果和乘数寄存器一起逻辑右移一位。这样做的效果等价于把部分积的低位“挪进”乘数寄存器里腾出高位的空间给下一轮加法。等于说乘数寄存器在循环过程中逐渐“让位”给低半部分积整个运算过程中只需要一个n位加法器硬件规模与位宽成线性增长这是工程上很容易接受的做法。1.3 一位乘法的完整执行规则假设被乘数B是n位乘数Q是n位结果P是2n位。硬件上需要三个n位空间A寄存器保存部分积的高位Q寄存器既保存乘数又保存部分积的低位B寄存器保存被乘数。另外还需要1位进位C因为加法可能产生溢出进位这个进位也必须参与移位。运算开始前A清零Q装入乘数B装入被乘数C清零。然后执行n轮同样的操作判断Q的当前最低位Q0。如果Q01做加法C:A ← A B这个加法是n1位的因为可能产生进位到C。如果Q00什么都不加。把(C, A, Q)三部分拼在一起整体逻辑右移一位。循环结束后(A, Q)合起来就是乘积的绝对值。因为移位了n次乘数原始值已经完全被“挤”出去了Q寄存器里装的全是乘积的低n位。下面用13×11来验证一下。B1101Q1011A0000C0n4。轮次操作前(C,A,Q)Q0是否加法加/保持后(C,A)右移后(C,A,Q)10,0000,10111加0,11010,0110,110120,0110,11011加1,00110,1001,111030,1001,11100保持0,10010,0100,111140,0100,11111加1,00010,1000,1111结果(A,Q)10001111也就是143和13×11完全一致。注意第二轮的加法后C变成了1这个进位如果不在下一步右移时“挤回”到A的最高位整个结果就会错。所以C这个寄存器不是可选项而是必须的。2. 硬件数据通路设计三个寄存器、一个加法器、一套移位逻辑2.1 数据通路整体架构理解了算法电路结构就是水到渠成的事。一个完整的原码一位乘法器数据通路包含以下部件B寄存器n位保存被乘数绝对值整个运算过程中保持不变。A寄存器n位保存部分积的高n位初始为0。Q寄存器n位初始保存乘数绝对值随后每轮右移逐步被部分积的低位填充。C触发器1位保存加法进位。加法器n位一个操作数来自B另一个操作数来自A受控加法用与门控制B是否通过。右移逻辑把(C, A, Q)当做一个2n1位的整体在每个时钟沿统一右移。控制器产生加载、移位、加法控制信号并计数n轮后输出结束信号。数据通路的信号流向可以这样描述加法器的两个输入一个是B寄存器输出另一个是A寄存器输出加法器输出的n位结果反馈到A寄存器的输入端口进位输出反馈到C触发器的输入端口。Q寄存器的串行输入接A寄存器的最低位输出A寄存器的串行输入接C触发器的输出。而Q寄存器的最低位移出后直接丢弃——那是判断过的乘数位已经没有利用价值了。2.2 控制器要干什么事控制器本质上是一个有限状态机。原码一位乘法器的状态迁移极简单S0初始寄存器加载信号有效A清零Q载入乘数B载入被乘数C清零。然后进入S1。S1判断与加法读Q0若为1则加法控制信号有效A更新为AB并更新C若为0则A保持不变C清零。S2右移发出移位信号A、Q、C统一右移一位循环计数器加1。判断计数器如果循环了n次进入结束状态输出完成信号否则回到S1。这里有个容易混淆的细节判断Q0和加法发生在右移之前但右移之后Q0会变成新的值来自A的最低位所以每轮判断的其实是“当前乘数最低位在右移前的值”。也就是说如果某轮Q01先加被乘数然后整体右移把加出来的低位移到Q里。这个顺序绝对不能反反了就是另一套算法了。2.3 为什么用逻辑右移而不是算术右移对无符号数做右移最高位补0就行这就是逻辑右移。这里有个隐藏好处由于A寄存器里存的是部分积最高位补进0正好为下一轮加法“腾出”空间也是把C里的进位挤进去。如果错用了带符号扩展的算术右移最高位会被复制而不是补0那部分积的正确性就彻底崩了。在Logisim里实现逻辑右移不需要单独的移位器组件——通过寄存器之间的交叉连线就能完成。A寄存器的最低位移给Q寄存器的串行输入C的输出移给A寄存器的串行输入每个时钟沿到来时所有寄存器同时动作天然形成一次整体右移。正因为这种同时性必须保证这些寄存器都由同一个时钟驱动任何异步的“手动右移”都会造成时序错乱。3. 寄存器电路深度拆解Logisim里的加载、保持与同步移位3.1 寄存器与锁存器的本质区别进入电路细节之前先把最核心的元件搞清楚。Logisim的组件库里有一个叫Register的元件这才是本设计的主角。外观上它是一个矩形框标着D、Q等引脚。与常见的D锁存器不同Register是边沿触发的也就是说它只在时钟信号的上升沿或下降沿取决于配置那一瞬间采集输入端的值其余时间输出保持不变。为什么要强调边沿触发因为数据通路上有个经典的问题如果A寄存器的输出直接接到加法器加法器的输出又接回A寄存器的输入当加法器产生结果并稳定后会不会又立刻被采集回去导致寄存器不断自加答案是在边沿触发下不会。因为在同一个时钟周期里时钟为高电平时寄存器锁存输出加法器产生的中间结果不会立即写入A只有在下一个时钟沿到来时A才会把加法器的当前输出采样进去。这个“先算后写、沿触发”的机制是寄存器能实现稳定状态流转的根本保证。如果在Logisim里误用了锁存器电路会陷入死循环或出现毛刺这是新手最容易踩的坑之一。3.2 Register组件的引脚与属性Logisim寄存器核心引脚有这几个D数据输入需要写入的数据位宽与寄存器位宽一致。Q数据输出当前保存的值。C时钟输入决定何时采样D的值。CLR异步清零高电平时立即把寄存器清零与时钟无关。EN使能高电平时允许时钟沿采样低电平时寄存器保持原值。在设计乘法器时这几个引脚要分工明确。A寄存器的CLR用于初始清零Q寄存器的D端口需要接一个选择器或与门来控制是加载乘数还是保持原值——因为Q在后续轮次中不能被外部数据覆盖它的“新值”应该来自本寄存器右移后的反馈。如果让Q寄存器一直处于可加载状态它就会在每轮时钟沿把D端口的输入写进去而D端口接的是外部乘数输入的话乘数值会被反复装载右移就白做了。所以必须要给Q加使能控制加载阶段EN1让乘数进入移位阶段EN0让Q保持或走移位反馈路径。这里提供两种实现思路方案一用多路选择器根据控制信号把“外部乘数输入”和“移位反馈输入”选一个接给D。方案二用使能端EN加载时EN1之后EN0同时把D固定接成移位反馈。两种方案都能工作但方案二更干净因为乘数只在最开始用一次后续不再需要外部输入路径只要EN一直为0Q反反复复走的都是反馈回路。3.3 三寄存器级联右移的连线方法现在来看最关键的移位连接这也是标题里“寄存器电路详解”的重心。整体右移要求把(C, A, Q)看成一个2n1位的连续寄存器组。在Logisim中可以用三个独立寄存器一条串行链实现C寄存器的输入接加法器进位输出输出接A寄存器的串行输入也就是A的D端口最高位但如果A的D端口是一整根总线就需要用分裂器把A的Q输出拆成单路信号再组装成新的D值。A寄存器的Q输出最低位接Q寄存器的串行输入同样要用分裂器处理取出单路信号再与Q的高n-1位拼成新的D输入。Q寄存器的Q输出最低位悬空不接让它自然丢弃。这里需要借用Logisim的Splitter组件。比如A寄存器位宽是4输出总线是4根线要做一个右移操作就需要拆成“最高位来自C中间三位来自A的高三位最低位送给Q”。实际操作是用Splitter把A的Q拆成Bit 0、Bit 1、Bit 2、Bit 3四根单线把C的输出接到Bit 3把A自己的Bit 3、Bit 2、Bit 1接回D的对应高位把Bit 0送给Q的串行输入。同理Q寄存器的D端口要把A的Bit 0接给Bit 3自己的Bit 3、Bit 2、Bit 1接给D的对应高位Bit 0丢弃。这样连完后每个时钟沿C的旧值进入A的最高位A的旧低位进入Q的高位整体右移一位。这个结构等价于移位寄存器没有用任何专门的移位芯片全靠连线完成。3.4 加法器与B寄存器的受控加法被乘数B寄存器需要有一个加载引脚初始阶段写入被乘数之后保持。它的输出是一整根总线直接接到加法器的一个输入端。加法器另一个输入端接A的输出但不能直连因为受控加法要求“Q01才加Q00不加”。实现受控加法有两种方式一种是“真值表控制方式”在加法器的另一个输入端前加一排与门每个与门的一端接A的对应位另一端统一接Q0控制信号。Q0为1时与门原样输出A的值等价于多加了BQ0为0时与门输出全0等价于不加。另一种是“多路选择器方式”用二选一MUX把A的输出和常量0分别接两个输入Q0作为选择信号。相比之下MUX更直观排线也更整洁推荐优先使用。加法器的进位输出就是前面说的C触发器的输入。Q0为0那一轮无需加法所以C应该被清零——这里同样可以用2选1MUX控制C的输入Q01时选加法器进位Q00时选常量0。这个细节很多人会漏结果就是进位C里残留着几轮之前的旧值右移时把脏数据挤进A最后结果莫名其妙多了一个1。4. Logisim中的搭建步骤与控制器实现4.1 全局参数与元件布局打开Logisim后先新建电路把位数统一设置为4位为了实验演示方便之后想扩展到8位或更多只需要改寄存器和加法器的位宽控制器逻辑完全不变。推荐的布局方式是从左到右一字排开最左侧B寄存器、被乘数输入引脚。中间偏上加法器、A寄存器。中间偏下Q寄存器。最下方C触发器。最右侧输出引脚显示A和Q拼接后的8位结果。这样做的好处是信号流方向一致排查连线时不会眼花。每个寄存器上都加上标签比如B_REG、A_REG、Q_REG、C_BIT否则连到一半自己都分不清谁是谁。4.2 数据通路的逐段搭建清单按下面的顺序接线可以最大程度减少返工放置4位常量0接到A寄存器数据输入的最初来源A的D初始接0或者利用CLR异步清零后先保持0。放置4位乘数输入引脚接Q寄存器的D输入端口放置4位被乘数输入引脚接B寄存器的D输入端口。用时钟组件Clock统一接到A、B、Q、C的时钟输入端。如果希望用按钮控制单步可以接一个手动时钟按钮方便观察每一步的变化。把A的Q输出用Splitter拆开接加法器一端B的Q输出接加法器另一端。加法器的进位输出接C的D输入和Q0控制信号做一个2选1MUXMUX输出再接C的D输入。加法器的4位结果接A的D输入这个D同样需要和右移逻辑做整合见下一步。做右移整合A的D端口把C当前输出接最高位加法器结果的高3位接高3位最低位不要了因为加完后会移位A的最低位移给Q。等一下这里要仔细移位是在加法之后发生的。如果A的D端口直接接了加法器输出那么时钟沿来临时是“加法结果直接写入A”而不是“加法结果右移后再写入A”。这就引出一个问题——加法结果和移位怎么合成一个动作这里我用的实际方案是加法器结果本身已经包含了“当前A加上被乘数”的全部n位而A的D端口需要接收的是“这个结果逻辑右移一位”后的值。也就是说A的新值 加法结果右移一位。这个右移不能靠寄存器完成必须在组合逻辑里先做掉用Splitter把加法器结果的高位、C的旧值重新排布再接到A的D端口。所以A的D端口接线应该是最高位C的输出次高位加法器结果第3位第三位加法器结果第2位最低位加法器结果第1位。加法器结果的最低位不接A的D改接到Q的串行输入。Q的D端口类似最高位加法器结果的第0位来自A旧最低位次高位Q旧最高位第三位Q旧次高位最低位Q旧第三位。当然这是在加载阶段之后。这样的组合逻辑移位方案在一个时钟沿上就完成了“加法结果右移并挤入C”的全部效果不需要额外的移位时钟。这是Logisim搭建中最重要的技巧理解和掌握了它整个乘法器就通了。4.3 控制器计数器ROM真值表方案控制器可以用状态机搭但对新手来说用“计数器ROM”的方案更不容易出错。思路是用一个计数器记录已经执行的轮数。用一个ROM或组合逻辑真值表根据当前状态输出各个控制信号。控制信号总共需要这几路状态动作输出控制信号S0装入数据、A清零LOAD_EN1, SHIFT_EN0, ADD_EN0S1(S2)判断Q0决定是否加法ADD_ENQ0, SHIFT_EN0S3右移一位SHIFT_EN1, ADD_EN0S3后若未满n轮回到S1否则结束DONE1在Logisim里实现时用两个D触发器组成2位计数器计数0~3其中0对应初始加载1对应加法2对应移位3是中间过渡。ROM存储根据计数器值输出的控制信号。计数器每到3就检查是否已循环n次用一个额外的4位计数器记录总轮数达到4轮后封锁时钟或置DONE。这部分的逻辑虽然不多但连线比较密建议先画好控制信号真值表再动手否则容易在布线时搞混哪根线控制哪个寄存器。4.4 关于Logisim新版与旧版的差异提醒Logisim有多个版本Evolution版和原版的元件库布局、引脚名称略有差异。比如有些版本寄存器只有D、Q、C没有单独的EN引脚有些版本Splitter的名字叫“Bit Extender”或“Bit Adder”。如果发现找不到我上面说的引脚先确认用的是不是Logisim Evolution再检查组件属性面板里的位宽和触发沿设置。中途更换版本会导致保存的文件打不开或引脚对不上建议从开始就固定一个版本。5. 仿真验证与被逼疯的重重陷阱5.1 测试用例设计电路搭完不代表能用必须跑完整测试矩阵。我建议至少测以下五类用例每个用例都要记录中间每一步的(C,A,Q)值用例被乘数乘数期望结果说明0乘任意数0000010100000000验证全0路径1乘任意数0001011000000110验证加法路径普通正乘正0011010100001111基本正确性大数相乘1111111111100001验证进位与连续加法含符号位1 00110 01011 00001111验证符号异或与数值分离仿真时如果结果不对先不要急着改电路而是把每一轮的寄存器值跟上文例子对比看是在哪一轮开始出现偏差。Logisim的时钟组件可以设为手动模式每按一次走一步配合探针Probe和标签观测各个寄存器输出比一直开着自动时钟更可控。5.2 踩坑一Q0的判断被移位提前消费很多同学把Q寄存器的初始值装好后第一轮判断Q0但下拉到某个地方时发现乘数的最低有效位已经丢了导致后续判断的全是移位进来的部分积低位。原因大多是移位逻辑写错了顺序或者把乘数装进了Q之后又立刻有一次多余的移位。排查方法是只重置一次电路观察Q在第一个时钟沿前后的变化。如果第一个时钟沿就让Q右移了说明加载和移位共用同一个时钟沿需要在加载阶段把移位信号屏蔽掉。5.3 踩坑二进位C在结果里凭空消失如果乘法结果误差正好是2的幂次倍大概率是C寄存器环节出了问题。常见的有两种一是加法进位根本没接进C的D端口导致超过n位的和溢出丢掉了二是C虽然存了进位但在右移时没有接到A的最高位导致进位永远停在原地不会进入结果。我调试时最喜欢用一组大数相乘来检测比如1111×1111乘法过程中必然会多次产生进位如果最终结果比正确值小若干个16就顺着C→A最高位这条线查十有八九是Splitter的位映射接反了。5.4 踩坑三Logisim总线的灰色线问题Logisim里不同位宽的总线连接时线会变成灰色且无法正常导通。最典型的场景是加法器输出是4位但Splitter把其中一根线拿出来接到1位的C寄存器时如果位宽配置不对整根线会变灰。解决办法是检查Splitter的“Bit Width”属性和“Fan Out”数量确保每一根分线的位宽都是1位并且目标引脚位宽匹配。另外还要注意Splitter的位序Bit 0对应的是最低位Bit n-1对应最高位连线时别把最高位和最低位颠倒了。5.5 踩坑四寄存器的触发沿导致的结果错位如果你使用的是Logisim的默认寄存器它默认在上升沿触发。当你把时钟接到寄存器和计数器上时如果计数器的触发沿与寄存器不一致就可能出现“计数器已经加1了但A还没更新”的毛刺问题。建议统一把所有组件的触发沿设置为同一类型或者在控制信号上增加与门来错开数据更新的时机。我在实践中更推荐的做法是把整个数据通路的时钟固定在上升沿控制器的状态转移也使用同一个时钟但控制信号的改变放在时钟下降沿后的组合逻辑阶段。当前Logisim版本对同源时钟的处理还算宽容但如果你在做更高位宽的乘法器时序问题会变得尖锐可以用Logisim的“Combinational Analysis”功能校验组合逻辑真值表把步伐放缓一点。6. 从乘法器到完整CPU后续扩展与现实意义6.1 把符号位纳入运算扩展成Booth乘法器原码一位乘法器只能处理原码符号位还要单独异或。虽然逻辑简单但有一些硬件设计场景下不希望额外做符号判断这时可以考虑Booth算法。Booth算法的核心思想是把补码乘法转换成“加、减、移位”循环通过观察乘数的相邻两位来决定是加、减、还是保持。不需要额外处理符号位硬件上可以和加减法统一。从原码一位乘法器扩展到Booth主要改动是加法器换成可加减的ALU、增加一个Q(-1)触发器用于记录上一位乘数值、控制真值表变成三位判断Q0和Q(-1)。如果你在课程设计里做的是“多周期MIPS CPU”乘法器模块一般会用Booth算法或者直接调用现成的乘法器IP核。原码一位乘法器更像是一个教学起点帮你看清“运算指令在硬件里到底是怎么一步步执行的”。6.2 集成到单周期或多周期CPULogisim里做CPU课程设计乘法指令往往是最高频的痛点因为单周期CPU里如果直接用“一个时钟周期完成乘法”需要把n轮循环全部展开成组合逻辑面积和时延都不可控。更合理的做法是参照多周期CPU的思路把乘法器作为一个独立的功能单元在数据通路中加入“忙等待”机制CPU发出MUL指令后乘法器开始运行在运行期间CPU的状态机进入等待循环直到乘法器的DONE信号拉高再把(A,Q)结果写回寄存器堆。这需要与CPU的主控制器进行握手而握手的本质就是一组状态转移——比单独做乘法器多不了多少工作量。如果你做的单周期CPU可以考虑在ALU外挂一个“组合逻辑乘法器”或“迭代乘法器”迭代乘法器可以把多周期信号周期化占用更少的门资源。不过这会引入时序待解决的问题需要你的CPU主频足够低或者额外插入等待周期。6.3 为什么寄存器电路是这一切的地基前面强调的寄存器加载、保持、移位三态控制不仅仅服务于乘法器。寄存器堆的写使能、流水线寄存器在气泡插入时的保持、移位指令的实现说到底都是同一套机制想清楚“什么数据在什么时机写入、什么时机保持、什么时机移位”。所以如果在这篇博文里只记住一个知识点我希望是任何时序电路先画出数据在元件间的走向再画出时钟沿在每一拍触发哪些写入最后才是连线和调参。顺序对了Logisim里的乘法器就只是重复劳动顺序反了调三天也查不出那根灰线到底错在哪。个人在做课程设计时还有一个习惯每搭建一个子模块就立刻新建一个电路单独测试不急着把乘法器和CPU主电路拼在一起。比如A寄存器右移逻辑可以先单独用一个常量和时钟验证Q寄存器的串行输入单独测试全部通过后再合并。否则整个电路几十根线一起亮出了错只能靠瞪眼找——那滋味谁试谁知道。
返回列表