ARTICLE DETAIL

资讯详情

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

ALU设计实战:从超前进位加法器到标志位生成

ALU设计实战:从超前进位加法器到标志位生成 简介计算机组成原理课程中的ALU设计实验资料面向计算机专业学生及数字逻辑硬件设计入门者。内容围绕算术逻辑单元的功能定义、Quartus 2工程实现与仿真验证展开可帮助读者理解ALU如何通过门电路与运算选择信号完成加减、与或非、异或等基本操作以及零标志、溢出标志等状态位的生成逻辑。压缩包共53个文件以Quartus工程文件为主包括bdf原理图设计文件、vwf仿真波形文件以及rpt、qmsg、cdb、hdb等综合、仿真与报告文件完整呈现从设计输入、编译综合到仿真验证的工程流程包体仅112KB下载查阅方便。已有2192人学习使用。借助这份资料读者可以掌握ALU模块的顶层设计方法、控制信号与数据通路的连接方式并可直接在Quartus中打开工程结构进行二次扩展是计算机组成原理实验环节非常实用的参考资料。1. 先想清楚ALU到底要做什么再动手写代码做过几次CPU设计课设或者RISC-V小车项目的同学应该都有同感拿到设计一个ALU这个题目时第一反应是这有什么难的不就是一个加法器加几个逻辑门嘛。可真正动起手来尤其是当ALU被塞进三级流水线的CPU里、还要处理溢出、零标志、移位位数不够用这些情况时你就会发现事情远没有想的那么简单。我先说说ALU在整个CPU里的位置。不管是MIPS、RISC-V还是x86CPU的指令最终都可以拆成两类操作一类是访存和跳转一类是算术逻辑运算。后者几乎全部落在ALU头上。加减乘除、与或非异或、左移右移、大小比较、置零清零……这些指令在硬件层面的归宿都是ALU的那一堆门电路和加法器。所以ALU设计的质量直接决定了CPU的运算速度和指令吞吐量。北航、哈工大、合工大等好多学校的计算机组成原理课设都有一道经典题目基于MIPS指令集设计一个32位ALU。清华的同学们甚至还要在预项目阶段就把它在MARS和FPGA上跑通。这里最关键的一步不是写代码而是先列功能规格。你打算支持多少种运算要做哪些标志位是组合逻辑输出还是时序逻辑输出这些不敲定后面返工的代价非常高。我的建议是先用一张表把自己的ALU指令集梳理清楚指令类型具体操作编码方式算术运算add, sub, addi, slt3位功能码逻辑运算and, or, xor, nor3位功能码移位操作sll, srl立即数低5位比较操作slt, sltu无符号比较把这张表画完你的ALU边界就有了输入是两个32位数A和B外加一个功能选择信号ALUOp输出是32位结果和若干个标志位。这是一个典型的纯组合逻辑模块也是绝大多数课设的正确打开方式。2. 算术单元加法器选型决定了ALU的速度上限2.1 从半加器到全加器进位链是怎么形成的算术部分是ALU的心脏而心脏的核心就是加法器。我们先从最基本的单元讲起。一个半加器只处理两个加数不考虑低位进位所以它有两个输出和S与进位C。半加器的逻辑表达式是S A XOR BC A AND B。但多位加法时每一位都需要处理来自低位的进位所以必须用全加器。全加器多了一个输入Cin逻辑表达式变成S A XOR B XOR Cin Cout (A AND B) OR (Cin AND (A XOR B))把32个全加器串起来让每一级的Cout接到下一级的Cin就得到了最简单的行波进位加法器。这种结构逻辑简单、容易理解但有一个致命问题进位是一级一级波过去的。第31位的和要等第30位进位到达第30位要等第29位……最坏情况下进位信号要穿过全部32级延迟这在时钟频率上是非常糟糕的。我给大家算一笔账如果每个全加器平均延迟是2个门延迟32位行波进位加法器的最长路径就是64个门延迟。假设一个门延迟是0.1ns大约对应2GHz工艺下的典型值那么这一条路径就需要6.4ns对应的ALU最高频率只有156MHz。这个数字在现代CPU动辄3GHz以上的今天完全不够看。2.2 超前进位把依赖关系打散既然进位链是串行依赖的罪魁祸首那能不能提前把进位算出来当然可以。这就是超前进位加法器的思路。我们重新定义两个中间信号G_i A_i AND B_i生成信号这一位一定产生进位 P_i A_i XOR B_i传播信号这一位会传递进位那么每一位的进位是C_{i1} G_i OR (P_i AND C_i)把这个公式展开我们得到C_1 G_0 OR (P_0 AND C_0) C_2 G_1 OR (P_1 AND G_0) OR (P_1 AND P_0 AND C_0) C_3 G_2 OR (P_2 AND G_1) OR (P_2 AND P_1 AND G_0) OR (P_2 AND P_1 AND P_0 AND C_0)注意每一位的进位都只依赖输入的C0不依赖中间进位。这意味着所有位的进位可以并行计算整个加法器的关键路径从串行链变成了两级或三级门。但事情没有这么完美。上面是4位超前进位的表达式32位直接展开的话每个进位表达式会有几十个乘积项门的扇入系数门禁根本撑不住。所以工程上普遍采用分层超前进位第一层做4位超前进位第二层把这4个4位组再组合成16位甚至可以再做一层扩展到64位。这就是你在《计算机组成原理》教材里看到的组间串行进位和组内并行、组间并行的由来。看完对比你应该明白为什么课设板上用4位CLA组拼32位ALU是最常见的方案——它把延迟压缩到了大约4到6个门层次而且代码量不过几十行。如果你用的是Xilinx的FPGA直接调用IP核里的加法器也能获得类似效果Xilinx和Altera的综合器会对加法运算自动做进位链优化这一点后面讲RTL编码时再详细展开。2.3 减法就是取反加一不用单独设计有一个经验很多同学要到综合时才会领悟别为减法单独设计一套电路。减法A - B等价于A (~B) 1所以只需要在B的输入端加一组异或门做取反再把Cin直接置成1加法器就变成了减法器。ALU里Sub信号一路送到求和核的Cin和B的翻转开关上结构干净又省钱。这也解释了为什么在MIPS指令里sub和add共用同一个加法器——本质上它们用同一套硬件。3. 用Verilog把ALU写出来模块化设计最关键3.1 顶层接口定义下面这份是我在课程设计里实际用过的Verilog代码32位ALU支持9种运算你可以直接抄作业但我建议你先理解每一段的意图。module alu_32( input wire [31:0] A, input wire [31:0] B, input wire [3:0] alu_op, // 功能选择 output reg [31:0] result, output reg zero, // 零标志 output reg carry, // 进位/借位 output reg overflow, // 有符号溢出 output reg sign // 符号位即结果最高位 );功能码我采用以下约定4b0000是AND4b0001是OR4b0010是XOR4b0011是NOR4b0100是ADD4b0101是SUB4b0110是SLT带符号置位4b0111是SLL左移4b1000是SRL右移。为什么用4位功能码而不是3位3位最多只能编码8种运算一旦你还需要支持sltu无符号比较、sra算术右移、lui高位加载就已经超了。从设计的角度看用4位给未来留余量综合成本几乎为零。3.2 RTL代码实现的三种方案取舍方案一是行为级描述直接甩给综合器让工具自己发挥always (*) begin case (alu_op) 4b0100: result A B; 4b0101: result A - B; // ... endcase end这写法的优点是快缺点是进位和溢出标志位的计算不直观你还是得额外判断而且综合器可能推断出和你预期不一致的结构。方案二是结构级描述自己调用加法器模块wire carry_out; wire [31:0] sum; wire [31:0] B_mux; assign B_mux alu_op[0] ? ~B : B; // alu_op[0]1做减法 cla_32 u_cla( .a(A), .b(B_mux), .cin(alu_op[0]), .s(sum), .cout(carry_out) );这种写法把加法器的结构显式地表达出来时钟频率更可预期标志位的生成也更直接。方案三是调用厂商IP核例如Vivado里Fabric乘加器IP核。这个方案在FPGA上综合出来的面积和性能最优但移植性差课设答辩时如果你说不清IP核内部原理老师通常会接着追问容易露馅。我个人的经验是课设阶段用方案二仿真阶段引入方案三做对照实验写报告的时候再对比三者的资源占用和时序报告。这样既掌握了原理又展示了对工程实践的敏感度。3.3 移位操作在ALU里的实现细节注意移位操作在纯组合逻辑ALU里有两种实现路线。一种是用桶形移位器一次就能根据移位位数把结果选出来所以硬件面积很大32位输入对应了32根输出多路选择。另一种是逐级移位用5级每级移动1、2、4、8、16位通过移位位数对应位上的控制信号来选择是否经过该级。后者更省逻辑延迟也更稳定是RISC-V和MIPS设计中常见的做法。按我自己的经验课程设计里除非老师明确要求桶形移位器否则体量不需要那么大。用Verilog里的和运算符确实可以让你在一行内完成移位但这样综合器推出来的结构你无法掌控仿真和上板结果可能有出入。所以我在RTL里是手写了一个5级桶形移位器的展开结构这样逻辑清晰答辩时也有内容可讲。4. 标志位不是顺手加的Zero、Overflow、Carry背后的门电路逻辑这一节是很多初学者最忽略、而考试和课设答辩最常被问的部分。标志位看起来只是结果旁边多输出几位实际上每个都有独立的生成逻辑而且最容易弄混的是无符号进位和有符号溢出。4.1 进位和溢出是两码事无符号加法的进位Carry表示结果超出了32位能表示的范围。它的生成最简单就是加法器最高位的Cout直接连出来。当你把ALU用于地址计算、无符号加法时Carry才是有意义的标志。有符号溢出Overflow则是当把A和B当作补码数看待时结果超出了-2^31到2^31-1的范围。判断逻辑有两种做法第一种做法是看符号位变化正加正得负或负加负得正说明溢出了。逻辑表达式是overflow (~A[31]) (~B[31]) S[31] | A[31] B[31] (~S[31])第二种做法是看最高位进位和次高位进位是否不同换个说法就是C31 XOR C30。如果是1就溢出了。这个做法在加法器结构里最容易实现。我见过很多同学的ALU设计里把carry和overflow画了等号这是最典型的错误。它们的区别用一句话说Carry管的是无符号视角下的结果超范围Overflow管的是有符号视角下的结果超范围。两个标志位可以同时为1也可以只有一个为1完全取决于你怎么解释那两位二进制。4.2 SLT指令的实现用一个减法器搞定比较MIPS里的sltSet Less Than指令比较两个有符号数A B时结果置1否则置0。硬件上不需要额外的比较器直接在减法器上做文章。A B的检测逻辑是如果A和B同符号看减法结果sign位是1则为AB如果A和B异号那B是正数A是负数时一定小于。综合算式slt_result (A[31] ~B[31]) | (~(A[31] ^ B[31]) result[31])其中result是A和B相减的结果。这样一个公式同时处理了同号和异号的情况。如果你要做无符号比较sltu那更简单取减法结果借位的反——在减法中Carry_out就等于借位取反。4.3 Zero标志别用单等号判断Zero标志的逻辑看起来无比简单result 32b0。但实现上要注意一个问题——在超前进位加法器里判断每一位是否为0也可以通过一个大的NOR门把所有位的值收进来但这样做延迟会非常大。工程上更常见的做法是用多级OR树来减少扇入门负载4输入或8输入一组先归约再往上合并。虽然综合工具通常会自动优化这个结构但你在写RTL时用行为表达式(result 0)是最稳妥的交给工具去优化就够了不需要自己造轮子。5. 仿真与调试避免“仿真通过、上板就崩”的坑5.1 没写testbench就开发等于把命运交给运气ALU这种组合逻辑模块仿真手段其实非常简单直接但要覆盖全面需要刻意设计用例。我的测试策略分三个层次第一层基础正确性测试把9种运算的典型输入都跑一遍拿计算器手算对照。比如A 0x7FFFFFFF、B 1做加法期望结果是0x80000000、溢出为1、进位为0。再比如A 0x80000000、B 0x80000000做减法期望结果是0、借位为0、溢出为0。第二层边界数据测试专挑A和B全为0、全为1以及符号位翻转的输入。例如A 0x7FFFFFFF, B 0x7FFFFFFF做加法结果应该是0xFFFFFFFE、无进位但有溢出因为两个最大的正数相加超出上限了。这一类测试能一次性暴露你标志位逻辑的多个隐藏问题。第三层随机测试。用你熟悉的语言我用的是Python也可以写SystemVerilog的randomize随机生成几万组A、B和alu_op把RTL仿真的输出跟一个参考模型就是你自己用高级语言写的同一个运算比对。这个过程一旦跑通你对ALU正确性的信心会提升一个档次。5.2 常见Bug我以为我写了加法器结果工具实现了另一个我踩过最深的坑是仿真通过、上板必挂的经典问题在组合逻辑always块里用了不完整的敏感列表导致仿真时得出正确结果但综合后出现锁存器。问题的本质是这样的如果你用always块描述组合逻辑但没有列出所有输入信号或者在某些分支下没有给result赋值综合器会认为你需要保持上一个值于是悄悄生成一个锁存器。锁存器会让你的ALU在某些输入转换时出现毛刺时序完全不可预测。这个坑在ICC2和Vivado的综合报告里会标记成warning但不仔细看很容易忽略。排查方法很简单写always (*)让工具自动推断敏感列表并且确保case语句覆盖所有可能的分支最后用default兜底。每次综合后打开报告搜索inferred latch字样确保零命中。5.3 上板测试还需要检查模拟开关噪声如果你的ALU最终要用FPGA开发板跑起来还需要留意JTAG下载之后观察LED或者数码管的防抖问题。当输入是通过拨码开关给出时拨码开关在物理拨动的瞬间会有几毫秒的抖动这个抖动在组合逻辑上表现为毛刺数码管上看到的是乱码。解决办法是在输入级加两级D触发器同步加去抖而不是去改ALU内部逻辑。6. 把ALU塞进CPU流水线控制信号与关键路径优化6.1 ALUOp是怎么被译码出来的单独测试ALU很顺利但放进流水线CPU后你会发现一个新的挑战控制信号ALUOp的生成。MIPS指令中R型指令的低6位Funct字段才是真正决定运算类型的而opcode在R型指令里固定是0。所以主控单元给出的ALUOp通常是2位概略信号00表示lw/sw01表示beq10表示R型然后由ALU控制单元结合Funct译码出4位具体的alu_op。配一张我当年上课时整理的真值表ALUOp[1:0]Funct[5:0]实际操作alu_op00lw/sw加地址忽略A B010001beq比较忽略A - B010110R型100000 (add)A B010010R型100010 (sub)A - B010110R型100100 (and)A B000010R型101010 (slt)A B0110这个译码逻辑用case语句就写完了唯一的坑是case必须完整别漏掉funct的非法值。非法funct出现时建议默认给add保证流水线不挂但这只是权宜之计正规的做法是发一个异常信号给控制单元。6.2 关键路径把时钟频率卡死了怎么办ALU在流水线中的关键路径通常是从寄存器堆读出端口经过ALU再到写回端口。32位超前进位加法器本身的延迟已经大幅降低了但别忘了还有B输入端的异或取反门、结果端的标志位生成这些都会叠加进延迟。我在课程设计里曾经把时钟频率从100MHz降到50MHz才让CPU稳定跑起来。问题定位后发现不是电路错误而是ALU结果在标志位判断上多了一级比较逻辑这一级恰好落在了关键路径上。优化方案也很简单把标志位的生成逻辑调整到加法器的同一拍内并行计算而不是等加法结果出来再算。这需要你重新画一遍数据路径图确认所有信号路径的层次级别。在Vivado里可以打开时序报告看关键路径布线然后针对它做优化。6.3 流水线里数据冒险和ALU的配合最后提一点很多人会忽略的ALU结果作为转发路径的数据源必须能在一拍内输出否则流水线停顿设计会变得非常复杂。具体来说EX/MEM寄存器在时钟上升沿捕获ALU输出然后反馈给MUX供下一级使用。如果ALU延迟太大超过了时钟周期减去寄存器建立时间的预算那流水线会在EX段出问题表现为部分指令结果偶发错误。这也是为什么高主频CPU的ALU设计是核心难点。Intel和AMD的工程师们会花大量精力在进位链优化、门级电路定制、甚至堆料上为的就是把这一条数据路径延迟压下去。你在课设阶段如果能通过时序分析报告发现自己ALU的关键路径是哪里并且给出合理的优化方案答辩基本就稳了。最后再分享一个个人习惯我给ALU写完代码之后会习惯性把它打印出来贴在显示器边框上。不是因为代码优美而是因为后面验证流水线CPU时90%的bug最终都能追溯回ALU的某个边界条件处理。有一份清晰的接口图和真值表在旁边排查问题会快很多。本文还有配套的精品资源点击获取
返回列表