资讯动态

从加法器到乘除法:手把手设计一个完整ALU

发布时间:2026/10/17 3:28:15 来源:尧图企业网站定制
1. ALU到底在干什么从逻辑门到整数运算器的完整拼图刚接触计算机组成原理的同学第一次看到“ALU”这个缩写往往只记住了它的全称是 Arithmetic Logic Unit翻译过来叫“算术逻辑单元”。但如果你真的动手写过CPU或者做过一点底层硬件的Verilog仿真会发现这个名字其实有点误导——它最核心的工作恰恰是把“加法”用硬件电路的方式“算”出来然后基于加法扩展出减法、乘法、除法、比较、与或非移位等等一系列操作。我们常说的“四则运算”在高级语言里只是一个 、-、*、/ 符号后端到CPU里全部会被翻译成一条或多条指令最终送到ALU里面去执行。ALU的输入是一堆二进制操作数外加一个“控制信号”比如你是要做加法还是做按位与ALU的输出就是运算结果同时还要打出几个重要的状态标志进位、零、溢出、符号等这些标志直接决定了后面程序分支怎么走。这个模块为什么值得单独开一章因为它在整个CPU里承担的是“最脏最累的活”。你去看看任何一款RISC-V或者MIPS核的datapath图ALU永远是排在寄存器堆之后、数据通路中央的那个大块头。无论是取指令后计算地址还是执行普通算术指令或者做转移指令的跳转地址计算ALU都得掺和一脚。作为学习路径我的建议是先不要把ALU当成一个抽象的“黑盒”。你最好自己能动手哪怕是在Logisim里搭一个最简的加法器然后把它扩展成能算减法的补码加法器再去思考乘法器和除法器怎么复用手头的加法器资源。这一篇内容就是从这些最基础、最底层的细节出发把第三章上半部分涉及的东西全部捋清楚。2. 整数的加减法补码设计背后的那笔“精明的账”2.1 为什么减法非要转成加法补码的真正动机很多教材讲补码的时候喜欢用钟表来类比说“倒拨4个小时等于正拨8个小时”这个类比能帮你记住结论但真正的问题是硬件设计者为什么愿意接受补码这种看起来绕来绕去的编码方式答案非常简单粗暴为了把减法和加法统一起来让硬件只做加法从而大大简化电路设计。如果你用原码表示负数那做减法的时候电路先要比较两个数绝对值的大小然后决定谁减谁再决定结果符号位填什么这个比较器和纠符号的逻辑会消耗大量晶体管而且延迟明显变长。而如果采用补码一切都变得顺理成章A减去B在硬件上只需要把B取反码再加1即得到B的补码然后直接做加法。只要加法器的进位输出被正确处理掉拿到的结果就是符合数学预期的。负数的符号问题全部由最高位的进位和溢出逻辑来兜底不需要单独去“看符号”做判断。这就是补码设计真正的动机。我后面做模拟CPU的时候最深的感受是如果非要坚持用原码做四则运算整个控制器的状态机复杂度至少翻一倍。因为原码乘除法要处理符号位分离减法要先判大小加法的结果还要考虑是不是有进位影响到符号。相比之下补码方案下的运算单元全部是“无脑加法移位”控制逻辑清爽很多。2.2 加法器实现细节从半加器到全加器再到进位链如果你去看任何一篇讲加法器的文章都会提到半加器Half Adder和全加器Full Adder。半加器只管两个输入位的相加输出一个和位Sum一个进位Carry Out。全加器在此基础上多了一个进位输入Carry In用途是在多位加法里把上一级的进位传递下来。用逻辑表达式来说S A XOR B XOR CinCout (A AND B) OR (Cin AND (A XOR B))这两个式子不要死记你把它当成“按位加的规则”去理解就很自然了当前位的和就是三个输入做异或什么时候会产生进位呢两个原始位都是1或者输入有进位且当前两个位中恰有一个是1则进位为1。多位数的加法就是把这些全加器串起来第0位的Cout接到第1位的Cin以此类推。这就是“行波进位加法器”Ripple Carry Adder。它在教学里非常好用因为结构一眼就能看懂但实际工程里没人会用纯行波进位来做大位宽的加法器原因就一个字慢。每个位的进位都必须等前面一位的进位算完才能算32位加法最坏情况要走32级门延迟。2.3 超前进位加法器怎么把串行等位变成并行算要解决进位链太长的问题聪明的前辈们想出了“超前进位”Carry Lookahead的思路。核心逻辑是定义两个中间信号生成Generate和传播Propagate。Gi Ai AND Bi如果这一位两个输入都是1则不管Cin如何这一位一定会产生进位。Pi Ai XOR Bi如果这一位只有一个是1则当Cin为1时进位会被传递到下一级。于是这一级的进位输出可以写成Cout(i) Gi OR (Pi AND Cin(i))关键来了这个式子不需要等待前面一位完整算完的结果你可以把Cin(i)继续展开成由G(i-1)、P(i-1)、Cin(i-1)组成的表达式一层层展开直到全部都变成只跟初始输入A、B和最开始的进位Cin(0)有关的逻辑式。这样在理论上每一位的进位可以立刻由当前输入直接算出不用一级级等。展开后的电路虽然逻辑表达式变复杂了但它把“串联等待”变成了“并联计算”。32位超前进位加法器的关键路径大约只是几个门延迟比行波进位几十个门延迟要快好几倍。代价就是消耗更多的逻辑门特别是逻辑表达式展开到高位时项的复杂度会爆炸。所以工程实践里常用折中方案每4位或8位一组做超前进位组间再用超前进位或行波方式连接。我实测过在FPGA上写一个32位超前进位加法器和行波进位加法器从时序报告上看超前进位方案至少能把最大时钟频率拉高40%以上但逻辑量大约是行波方案的2.5倍。由此可见时间就是空间换来的没有免费的午餐。2.4 溢出判断的三种方法从高位进位到符号位比较整数的补码加法最让人头疼的问题就是溢出Overflow。溢出不是错误而是“结果超出了当前位宽所能表示的范围”。比如8位补码的范围是-128到127如果算的是120 20数学上是140已经超过1278位装不下这就是正溢出如果是-120 (-20) -140小于-128这是负溢出。判断溢出的方法有好几种考试和硬件实现各有所爱。我整理成一个表格判断方法具体做法适用场景双符号位法运算时把符号位扩展一位用两位符号位参加运算结果的高两位不同即溢出教学常用直观进位输入输出不同法最高有效位的进位输入Cin与进位输出Cout不同时说明符号位被“破坏”即溢出硬件实现最常用因为进位值本来就要计算操作数和结果符号法两个操作数同号但结果符号与操作数符号不同则溢出软件模拟时常用因为可以轻易做符号判断注意加法器的“进位输出”和“溢出标志”绝对不是一回事。Carry Out 是最高位的进位它代表“无符号数运算的越界”但 Overflow 是“有符号数运算的越界”。比如8位补码计算 127 1加法的结果二进制为 10000000此时最高位产生了进位吗没有因为结果的高位已经变成1这个1不是进位来的而是和的本身。所以Carry为0但结果解释为有符号数是-128明显溢出Overflow为1。这是初学者最常踩的坑把进位当溢出或者把溢出当进位。想要彻底搞明白你一定得记住同一个二进制加法器既支持无符号数的运算也支持有符号数的运算。它根本不在乎你“把它当成”什么它只是老老实实地加法而 Overflow 和 Carry 两种标志分别服务两种解释方式。硬件上判断溢出最简单的写法就是overflow Cout(msb) XOR Cout(msb-1)其中Cout(msb)是整个加法的最终进位Cout(msb-1)是最高有效数据位的进位。2.5 补码加减法的完整电路结构综合一个最简ALU现在把加法器和减法器合在一起设计一个最简的整数ALU。它的输入有操作数A和B均为n位一个控制信号Sub0表示加法1表示减法。减法怎么用加法器实现经典套路是把Sub信号接到每个位的B输入端的异或门上Sub0时异或门输出的就是B本身Sub1时输出B的反码。同时Sub信号也直接接到加法器的最低位进位Cin上。于是整个电路做的事情是A (B XOR {n{Sub}}) Sub。Sub1时就是A (~B) 1刚好就是A - B的补码运算。这个结构的精妙之处在于你只用一个加法器外加一排异或门就完成了两种运算而控制信号Sub本身就充当了功能选择线。放到真实ALU里这个思想被进一步扩展通过一个多位控制信号比如ALUOp在一个多路选择器MUX后面把加法、减法、与、或、异或、逻辑左移、逻辑右移的结果选出来。如果你在这个电路后面再接一个零检测器一个大的NOR门把结果的所有位或非起来输出Zero标志以及溢出的判断电路一个能支撑加减法、比较、跳转判断的ALU核心模块就算正式到位了。3. 整数乘法用移位和加法堆出高速乘法器3.1 乘法器的基本实现思路手工竖式计算的硬件化小学学乘法的时候做的是竖式把被乘数分别乘以乘数每一位然后错位相加。硬件做乘法本质上就是把这个竖式逻辑用二进制简化为“移位加”。举个例子8位二进制无符号乘法被乘数是1011十进制的11乘数是1101十进制的13乘数第0位是1部分积 被乘数 0乘数第1位是0跳过乘数第2位是1部分积 被乘数 2乘数第3位是1部分积 被乘数 3所有部分积加起来。因为二进制只有0和1所以乘数的每一比特决定了一个部分积是“被乘数左移若干位”还是“0”然后把所有部分积累加。最简单的硬件实现是重复加法的思想用一个累加器判断乘数当前最低位是1还是0如果是1就把被乘数加到累加器然后把被乘数左移一位、乘数右移一位重复n次。这样所需的硬件面积很小但需要n个时钟周期周期数随位宽线性增长属于典型的“时间换面积”。3.2 Booth算法的引入为什么无符号乘法之外还要学它如果只用“竖式移位加法”实现乘法器处理无符号数已经很稳了但遇到带符号的补码乘法直接竖式搞不定。原因是补码的符号位也参与数值运算按竖式思路展开时符号位会被错误地当作1来参与移位加法。Booth算法可以统一处理带符号数乘法核心手段是考察乘数相邻位。简单版本中假设Y是乘数我们添加一个隐含的”低位0“然后从低位到高位比较相邻两位如果 yi 是0y(i-1) 是1说明段结束要做减法即累加器减去被乘数如果 yi 是1y(i-1) 是0说明新段开始要做加法即累加器加上被乘数如果两位相同不需要操作只做移位。你可能觉得奇怪为什么还能出现“减去被乘数”别忘了一个数学事实例如01110在二进制里可以写成10000 - 00010。Booth算法本质上是在利用这种二进制重构方式减少部分积个数而且自然兼容补码符号位。硬件实现中Booth算法的好处是部分积的个数最多为n/2如果采用radix-4的Booth重编码而普通按位乘法部分积个数是n。部分积越少后面的压缩树Wallace Tree就越简单乘法器延迟越小。所以现代CPU里的高性能乘法器设计逻辑几乎都是基于某种Radix-4 Booth编码加上Wallace树/进位保存加法器CSA压缩来做的。3.3 阵列乘法器与Wallace树如何缩短关键路径如果你把所有部分积当成一个二维数组直接用一排全加器去累加那就是“阵列乘法器”。它的优点是布局非常规整适合做成流水线级缺点是进位链依然很长位宽增大时延迟增长明显。“进位保存加法器”Carry Save Adder的思路非常巧妙常规加法器需要把三对数A、B、Cin加成一对数结果。而进位保存加法器可以一次把三个输入压缩成两个输出一个“和位”阵列和一个“进位位”阵列。你可以并行地把一堆部分积不断压缩成越来越少的行最后只剩两行时再用一个常规的超前进位加法器做最终合并。Wallace树就是用这种思路来实现的将所有部分积按列分组每组用全加器压缩成两个中间数不断迭代把多行压缩成两行。因为每一层的压缩操作彼此独立所以整体延迟接近O(log n)级别比行波式的阵列乘法器的O(n)级别要快得多。如果你在FPGA上做乘法器我强烈建议你直接用乘法运算符或DSP单元综合工具会自动帮你生成最优结构自己手写Wallace树往往事倍功半。但在教学理解层面手写和仿真一下8位的Wallace树能让你彻底明白现代乘法器为什么延迟那么低。3.4 有符号与无符号乘法器的统一处理很多教科书会单独分开讲无符号乘法和补码乘法但真实芯片里为了节省面积往往希望同一套乘法硬件能同时服务无符号乘法和有符号乘法。一个经典技巧是“符号扩展”把两个n位数都扩展到2n位。无符号数高位补0有符号数高位做符号扩展然后直接按无符号乘法做。这样用一个乘法器阵列就可以同时处理两种类型只需控制最高位的扩展方式即可。在RISC-V指令集里MUL和MULHU的区别就在于此前者做有符号乘法的低位结果后者做无符号乘法的低位结果。只要一个乘法器内部能同时输出低n位和高n位软件上通过选择不同的读法就能得到所需的结果。硬件层面其实没有本质差别区别只体现在你如何解释“扩展方式”。4. 整数除法缓慢但无法绕过的恢复余数法4.1 除法的二进制长除与硬件实现差异整数除法在四则运算里是最特殊的它不可逆不满足交换律、结合律而且出现除数为0时硬件要触发异常。更麻烦的是除法根本没办法像乘法那样用部分积并行压缩的思路来加速——因为除法的每一位商都依赖前一步比较的反馈结果。想一想手工长除法是怎么做的每一步写下一个商位然后用余数减去除数判断够不够减如果够减则商位为1否则为0。硬件上的经典实现“恢复余数法”就是这个思想的直接翻译每次将被除数/余数左移一位加上一位新的被除数位然后减去除数如果减法结果不为负则商位为1如果为负说明不够减必须把被减上来的东西加回去即“恢复”原来的余数商位为0。恢复余数法每一步最多要执行一次减法和一次可能的加法时钟周期比较多但控制逻辑极其简单适合入门理解和小面积低功耗场景。另一种“不恢复余数法”的改进思路是发现不够减之后不需要“恢复”原余数而是下一次迭代时改做加法把错误累积到下一步。这样每一步只做一个加法或减法省掉了一个周期。4.2 无符号除法的边界条件与溢出保护无符号除法的硬件实现里最容易忽略的是“被除数比除数本身多一位”的情形。比如一个n位被除数除以n位除数如果不预先扩展结果可能需要n1位才能存下商直接截断就会出错。更危险的是除数为0任何数除以0在数学上无意义硬件上如果不做检查状态机会进入无限循环或者产生错误商。所以真实处理器的除法指令执行流程一般是检查除数是否为0为0则触发除零异常检查商是否溢出被除数的高位部分和除数的大小关系溢出则触发溢出异常初始化寄存器堆进入迭代迭代n次生成商和余数将结果写回寄存器堆RISC-V里的DIV指令规定除数为0时商为全1余数为被除数本身商溢出时商为最大负值。这些看似奇怪的语义其实是为了让硬件不用额外停下来等待处理器的异常处理流程而是直接给出一个确定的、可预测的结果软件层面再自行判断。4.3 除法在哪里过慢一种直观的性能感受做过软件优化的人都知道“除法很贵”但究竟贵在哪里以32位无符号除法为例恢复余数法大约需要32~64个时钟周期。相比之下32位加法器在现代处理器上1个周期就能完成乘法器用上BoothWallace一般也就是3~5个周期而延迟最高的除法器常常需要20个甚至更多周期。所以在编写低层代码时大家都想方设法回避除法用右移代替除以2的幂用乘法配合移位代替除以非2幂的常数或者干脆用查表法处理一小段取值区间。但在真正理解底层原理后你会明白这种优化不是“耍小聪明”而是因为除法器在物理上就是这样慢每一条除法指令在流水线上停留的时间足够执行好几条简单算术指令了。现代处理器很少为每个核都放一个硬件除法器更多是利用级联的移位-比较电路采用“每周期产生2位商”的基-4除法器换来延迟和面积的折中。这种技术原理上和不恢复余数法一脉相承只是每一步多算一位权重更高的商。5. 实战用Verilog实现一个支持加减乘除的最简ALU理论说了一大堆下面动手写一个支持加减乘除的简易整数ALU我选用Verilog来做因为综合工具对它的支持最成熟代码也比较贴近硬件实际。5.1 模块接口与整体架构设计先定义我们的ALU对外接口module alu_simple #( parameter DATA_WIDTH 32 )( input wire [DATA_WIDTH-1:0] a, input wire [DATA_WIDTH-1:0] b, input wire [2:0] op, // 000: add, 001: sub, 010: and, 011: or, 100: mul, 101: div output reg [DATA_WIDTH-1:0] result, output reg zero, output reg overflow, output reg carry_out );整体思路用组合逻辑或有限状态机实现加法减法用组合逻辑直接实现乘法因为延迟较大先用组合逻辑做综合工具会自己优化除法需要多周期因此这里做一个简化版本只演示恢复余数法的迭代结构。5.2 加减法部分让同一个加法器服务两种运算wire [DATA_WIDTH:0] sum; wire [DATA_WIDTH-1:0] b_eff op[0] ? ~b : b; assign sum {1b0, a} {1b0, b_eff} op[0]; always (*) begin case (op) 3b000: result sum[DATA_WIDTH-1:0]; 3b001: result sum[DATA_WIDTH-1:0]; ... endcase carry_out sum[DATA_WIDTH]; overflow (a[DATA_WIDTH-1] b_eff[DATA_WIDTH-1]) (sum[DATA_WIDTH-1] ! a[DATA_WIDTH-1]); end这里的核心点在于减法时直接取反加一而b_eff在做减法时已经是对b取反的结果所以最终加的1就是借位处理。overflow判断用的是“操作数符号相同但结果符号不同”法如果你在综合时发现时序吃紧可以换成只跟最高位进位有关的版本。5.3 乘法器实现用行为级数字拿到结果再说明如何优化在行为级建模里乘法可以简单写成wire [2*DATA_WIDTH-1:0] mul_result $signed(a) * $signed(b); assign result mul_result[DATA_WIDTH-1:0];这一行代码综合工具会替你选择FPGA上的DSP乘法器或查找表逻辑你根本不需要自己去搭Wallace树。但如果你想做性能优化可以约束使用DSP原语或者为了训练手动编写部分积压缩树genvar i; generate for (i0; iDATA_WIDTH; ii1) begin: stage wire [DATA_WIDTH-1:0] pp b[i] ? (a i) : {DATA_WIDTH{1b0}}; // 进位保存加法器堆积部分积 end endgenerate验证阶段两种实现的结果一致但综合后面积和时序会有差异。教学建议是先用简单的*调通功能再去考虑手工优化乘法器否则会陷入细节泥潭。5.4 除法器实现用状态机代替纯组合逻辑除法器用组合逻辑也可以做比如用恢复余数法把n次迭代展开成一个大组合逻辑块但位宽一大逻辑级数就爆炸所以这里我设计成一个简单的有限状态机。状态机有三个状态IDLE、CALC、DONE。reg [DATA_WIDTH-1:0] remainder_reg; reg [DATA_WIDTH-1:0] quotient_reg; reg [DATA_WIDTH-1:0] divisor_reg; reg [$clog2(DATA_WIDTH)-1:0] count; reg [1:0] state; always (posedge clk or posedge rst) begin if (rst) begin state IDLE; end else begin case (state) IDLE: begin if (start) begin quotient_reg 0; divisor_reg b; remainder_reg a; count DATA_WIDTH-1; state CALC; end end CALC: begin // 左移一位余数, 引入被除数新的一位 remainder_reg {remainder_reg[DATA_WIDTH-2:0], quotient_reg[DATA_WIDTH-1]}; // 试减 if (remainder_reg divisor_reg) begin remainder_reg remainder_reg - divisor_reg; quotient_reg {quotient_reg[DATA_WIDTH-2:0], 1b1}; end else begin quotient_reg {quotient_reg[DATA_WIDTH-2:0], 1b0}; end if (count 0) state DONE; else count count - 1; end DONE: begin result quotient_reg; state IDLE; end endcase end end上面是一个高度简化的骨架真正的除法器还需要处理余数寄存器宽度、被除数的移位同步、除数为0的提前检测。但这个骨架基本还原了“恢复余数法”的每一步流程你把它一步步仿真就能透彻理解教材里的算法图。5.5 仿真实操记录怎么验证ALU功能是对的写完成代码最要紧的是写一个完备的testbench。我的习惯是构造一个穷举小范围数据的测试比如4位ALU把所有可能的a、b组合跑一遍然后和Python或C语言算出来的标准结果对比。module tb_alu_simple(); reg [3:0] a, b; reg [2:0] op; wire [3:0] result; wire zero, overflow, carry_out; alu_simple #(.DATA_WIDTH(4)) u1 (.*); integer errors; initial begin errors 0; for (op0; op7; opop1) begin for (a0;a16;aa1) begin for (b0;b16;bb1) begin // 添加期望值检查逻辑, 一旦不匹配就打印 end end end $display(test done, errors%d, errors); $finish; end endmodule穷举法在4位ALU上瞬间出结果16位时可以随机采样一部分向量。我遇到的坑里比较典型的是减法溢出标志判断错误因为我忘了把符号位扩展后的最高位进位单独拎出来还有除法状态机在DONE状态没有把result锁存导致输出一直在变化下游寄存器采到错误值。这类问题光靠看RTL很难发现一定要跑仿真波形用信号名追踪。6. 常见问题与踩坑经验从表面错误看到本质原因6.1 减法运算结果总差一个1问题往往出在Cin上减法实现里最容易犯的错误是忘了把控制信号接到Cin上。如果你做的是“A加B反码”却不加1结果是A - B - 1。有些同学抄代码时漏了 op[0]这个项就会发现所有减法结果都比预期多了或少了1而且负数偏移正好是1。排查思路很简单把8位补码的减法随便挑两个数比如7减去3然后手算补码相加的过程再对着仿真波形看每一位的进位输入输出很快就能发现最低位的Cin是0而不是1。6.2 有符号乘法结果不对高位符号扩展忘做了用$signed(a) * $signed(b)只对操作数做了有符号解释但如果你把两个8位数据直接赋值给16位寄存器、再强制拼接出问题的概率极高。问题根因在于乘法结果需要2倍位宽来保存而把8位有符号数存进16位寄存器的时候高8位必须做符号扩展否则负数会变成很大的正数。有一个非常隐蔽的坑{8b11111111, 8b00000001}这样的拼接并不会自动做符号扩展它的高8位是固定值不是符号位的复制。硬件描述语言里必须写{{8{a[7]}}, a}才行。这算是我见过最多的“乘法结果诡异”来源。6.3 除法卡死状态机检查余数和被除数的位宽关系恢复余数法的迭代里余数寄存器初始化和被除数的位宽必须协调好。你想想看如果被除数是4位而你的余数寄存器也做成4位那在左移之后余数寄存器可能放不下新的高位和减法的中间结果直接溢出丢位最终商完全乱套。经验做法是余数寄存器的位宽至少应该是2倍的被除数位宽或者更准确地说是被除数位宽除数位宽。这样不管左移多少减法试减都留有余量。调试这类问题的大杀器是在状态机每一步打$display把余数、商、被除数当前值全部打出来脚本能自动对比期望值。6.4 综合后频率上不去拆开进位链和DSP资源如果你的ALU只是功能对但综合下来时序报告里带着巨大的负slack我给你的建议是分三个方向排查。第一加法器是不是行波进位结构如果是考虑改成超前进位或者直接使用综合工具的加法器原语/进位链结构。第二乘法器是不是被综合成了一大串小查找表的组合逻辑如果是检查代码里是否显式使用了无符号乘法加约束让工具走DSP原语。第三结果输出后面是不是直接连了一大串逻辑建议在ALU输出加一级流水寄存器把大组合逻辑切开。6.5 标志位使用指南CF、ZF、OF、SF怎么配合实现条件跳转ALU输出的标志位最终服务于分支指令。以x86风格的条件码为例无符号比较看CF和ZF有符号比较看OF和SF。一个很实用的记忆方法无符号数比较就是比大小A B 等价于“A减B没有产生借位且不为0”所以CF0且ZF0有符号数比较更复杂A B 等价于“减法结果非负且无溢出或结果为负且有溢出”但如果你用硬件实现了直接输出LT/GE等比较位就完全不需要操心这些。RISC-V的做法更干净通过SLT指令直接计算并写入寄存器标志位的负担少了很多。条件跳转电路的实现思路是把比较指令的运算结果放到ALU的Zero输出或单独的比较输出上分支控制逻辑取这些信号来生成跳转使能。最容易出错的地方是写CMP指令时用了无符号比较之后跳转指令却按有符号语义去解析标志位实际跑出来的分支结果完全不符合预期。7. 写在最后ALU设计中最容易忽略但绝对重要的三件事前后写了这么多最后想强调几个实践体会也算是我自己踩坑次数最多的地方。第一不要迷信高性能算法。在教学实验里超前进位、Wallace树、Booth重编码这些技术听起来很酷但如果你只是做一个简化的单周期MIPS或RISC-V教学核一个朴素的行波进位加法器配合一个简单的移位乘法器足以支撑全部指令跑通。高性能方案的复杂度是平方级上升的调试成本很高初学者容易在这里陷进去出不来。第二布线之前先设计好数据通路。ALU不是孤立存在的模块它后面接着寄存器堆、数据存储器和控制单元。我在做模拟项目X的时候最开始把ALU的输出直接接到主存地址总线结果发现地址计算结果和普通运算结果共用一个MUX每次取指都和访存抢资源最后不得不拆出两个单独的加法器。这个教训告诉我数据通路设计永远排在具体电路细节之前。第三测试向量一定要包含边界值。全0相乘、全1相乘、正数减负数、负数减正数、最大正数加最大正数、最小负数加最小负数、除数为1、除数为自身、除数为0这些边界条件每一个都不能漏。边界乱象才是ALU设计的试金石很多隐藏的bug只在边界情况下才会冒出来。如果你把这篇文章里提到的每一个模块都在Logisim或Verilog里亲手搭一遍、跑一遍仿真我相信你对ALU的理解不会停留在“背教材”层面而是真正知道一条加减乘除指令在硬件里是怎么一步步走完的。后面学流水线、乱序执行、多级缓存的时候你会持续受益于这个底层视角。最后一个小建议给ALU写testbench的时候不妨让随机测试向量覆盖到百万级然后用C语言实现同样的运算函数做交叉比对一旦发现不匹配就在波形上追移位。这是我调试硬件逻辑时用的最有效的手段也推荐给正在头疼的你。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑