3. 算术逻辑单元(ALU)设计:加法器、减法器、逻辑运算、移位器、比较器的硬件实现
各位同学,今天我们来聊聊ALU。说白了,ALU就是CPU的“计算核心”,所有加减乘除、逻辑判断、位运算,最终都要落到它头上。我在做第一颗RISC-V处理器时,花在ALU上的调试时间是最长的——不是因为难,而是因为细节太多,稍不留神就翻车。
3.1 加法器:从半加器到超前进位
加法器是ALU的基础。你想想看,减法、乘法、甚至地址计算,底层都依赖加法。最简单的加法器是半加器,只处理两个1位二进制数相加,输出和与进位。
// 半加器 Verilog 实现
module half_adder (
input a, b,
output sum, carry
);
assign sum = a ^ b; // 异或
assign carry = a & b; // 与
endmodule
但实际项目中,我们很少用半加器。为什么?因为它不支持进位输入。全加器才是常态——多了一个进位输入cin,可以级联成多位加法器。
我记得第一次做32位加法器时,直接用了行波进位结构。结果仿真通过,但综合后时序惨不忍睹。32位进位链太长,延迟大到没法用。后来我换成了超前进位加法器(CLA),才把关键路径压下来。
核心要点:超前进位加法器通过并行计算进位,将加法延迟从O(n)降到O(log n)。对于32位RISC-V处理器,4位一组CLA是经典选择。
// 4位超前进位加法器核心逻辑
module cla_4bit (
input [3:0] a, b,
input cin,
output [3:0] sum,
output cout
);
wire [3:0] g = a & b; // 生成信号
wire [3:0] p = a ^ b; // 传播信号
wire [3:0] c;
assign c[0] = cin;
assign c[1] = g[0] | (p[0] & c[0]);
assign c[2] = g[1] | (p[1] & c[1]);
assign c[3] = g[2] | (p[2] & c[2]);
assign cout = g[3] | (p[3] & c[3]);
assign sum = p ^ c;
endmodule
我的经验:实际项目中,我习惯用generate语句生成多级CLA。这样代码可读性好,也方便调整位宽。另外,注意进位链的扇出——驱动太多加法器时,需要插入缓冲器。
3.2 减法器:补码的妙用
减法器其实不需要单独设计。你想想看,a - b 等价于 a + (~b + 1)。这就是补码运算的精髓。我们只需要在加法器的基础上,对b取反,再把进位输入置为1,就完成了减法。
// 可加减的ALU单元
module add_sub (
input [31:0] a, b,
input sub, // 1:减法, 0:加法
output [31:0] result,
output overflow
);
wire [31:0] b_mux = sub ? ~b : b;
wire [31:0] sum;
wire cout;
// 复用加法器,sub信号同时控制b取反和进位输入
adder_32bit u_adder (
.a (a),
.b (b_mux),
.cin (sub),
.sum (sum),
.cout(cout)
);
assign result = sum;
assign overflow = (a[31] == b_mux[31]) && (sum[31] != a[31]);
endmodule
这里有个坑——溢出检测。我曾经在项目中漏掉了符号溢出判断,结果有符号数减法算出来全是错的。检查了三天才发现。嗯,这里要注意:无符号溢出看进位cout,有符号溢出看符号位变化。
3.3 逻辑运算:与或非的硬件实现
逻辑运算在RISC-V中包括AND、OR、XOR、NOR等。实现起来很简单,就是按位操作。但有个细节——RISC-V的ANDI、ORI等指令是立即数操作,需要把立即数扩展到32位。
// 逻辑运算单元
module logic_unit (
input [31:0] a, b,
input [1:0] op, // 00:AND, 01:OR, 10:XOR, 11:NOR
output [31:0] result
);
reg [31:0] result_reg;
always @(*) begin
case (op)
2'b00: result_reg = a & b;
2'b01: result_reg = a | b;
2'b10: result_reg = a ^ b;
2'b11: result_reg = ~(a | b); // NOR
endcase
end
assign result = result_reg;
endmodule
注意:逻辑运算和算术运算的时序不同。逻辑运算没有进位链,延迟极低。我建议在ALU设计中,把逻辑运算和算术运算分开处理,最后用多路选择器合并结果。这样可以优化关键路径。
3.4 移位器:桶形移位器的艺术
移位操作在RISC-V中有SLL(逻辑左移)、SRL(逻辑右移)、SRA(算术右移)。硬件实现上,桶形移位器是最常用的结构。
我个人习惯用MUX树实现桶形移位器。比如32位移位器,用5级2选1MUX,每级移位2^i位。这样面积和延迟都比较均衡。
// 32位桶形移位器(逻辑左移)
module barrel_shifter (
input [31:0] data,
input [4:0] shamt, // 移位量
output [31:0] result
);
wire [31:0] stage1, stage2, stage3, stage4;
// 第1级:移位0或1位
assign stage1 = shamt[0] ? {data[30:0], 1'b0} : data;
// 第2级:移位0或2位
assign stage2 = shamt[1] ? {stage1[29:0], 2'b0} : stage1;
// 第3级:移位0或4位
assign stage3 = shamt[2] ? {stage2[27:0], 4'b0} : stage2;
// 第4级:移位0或8位
assign stage4 = shamt[3] ? {stage3[23:0], 8'b0} : stage3;
// 第5级:移位0或16位
assign result = shamt[4] ? {stage4[15:0], 16'b0} : stage4;
endmodule
避坑指南:我曾经在实现算术右移时,忘了把符号位扩展。结果负数右移后变成了正数。算术右移的关键是:高位补符号位,不是补0。对于RISC-V,SRA指令需要单独处理符号扩展。
3.5 比较器:不只是大于小于
比较器在RISC-V中用于SLT(有符号小于)、SLTU(无符号小于)等指令。实现方式有两种:
- 减法比较法:用减法器计算a-b,根据结果符号判断大小。优点是复用加法器,缺点是延迟稍大。
- 专用比较器:从高位到低位逐位比较。速度快,但面积大。
我建议在低功耗设计中用减法比较法,在高性能设计中用专用比较器。具体选哪个,看你的PPA目标。
// 有符号比较器(减法实现)
module signed_comparator (
input [31:0] a, b,
output lt // a < b 时输出1
);
wire [31:0] diff;
wire overflow;
// 复用减法器
add_sub u_sub (
.a (a),
.b (b),
.sub (1'b1),
.result (diff),
.overflow(overflow)
);
// 有符号小于:符号位不同时看溢出,相同时看差符号
assign lt = (a[31] != b[31]) ? a[31] : diff[31];
endmodule
3.6 ALU整体架构
好了,我们把各个模块拼起来。一个完整的RISC-V ALU包含:
- 算术单元:加法、减法、加立即数
- 逻辑单元:AND、OR、XOR、NOR
- 移位单元:SLL、SRL、SRA
- 比较单元:SLT、SLTU
- 结果选择器:根据操作码选择输出
下面这张图展示了ALU的内部数据流:
从图中可以看到,操作数B先经过MUX选择(来自寄存器或立即数),然后分四路进入不同的运算单元。控制单元根据指令操作码,决定哪个运算结果通过最终MUX输出。
设计要点总结:
- 加法器是ALU的核心,减法、比较都依赖它
- 逻辑运算延迟最小,可以和其他运算并行执行
- 移位器用桶形结构,面积和延迟要权衡
- 比较器注意有符号和无符号的区别
- 最终结果选择器是关键路径,注意优化
好了,ALU的设计就讲到这里。每个模块单独看都不复杂,但组合起来要考虑时序、面积、功耗的平衡。我在项目中吃过不少亏,希望你们能少走弯路。
公众号:蓝海资料掘金营,微信deep3321