什么是ALU?
ALU要完成加减运算,比较运算,还要完成移位运算,逻辑运算。那么它的电路应该如何设计呢?分析可知,ALU内部包含做加减操作、比较操作、移位操作和逻辑操作的逻辑,ALU的输人同时传输给这些逻辑,各逻辑同时运算,最后通过一个多路选择电路将所需的结果选择出来作为ALU的输出。整体电路结构图如下所示。
两个输入信号为两个32位操作码,一个控制信号用于选择进行哪种逻辑运算。
为什么输入时两个信号源?
根据下图可以看到,常用的指令的大部分是对两个寄存器中的数值进行操作,最终输出一个
控制信号alu_control是多少位?
因为cpu设计中对于选择信号一般使用独热码进行编码,alu_control的位数取决于alu内部有多少种运算。在本文中实现的简单MISP CPU的ALU中有基本的加减,比较,逻辑操作和运算共12种,故一共12位。据此可以确定输入输出信号的代码:
module MIPS_ALU (
input [31:0] alu_src1,
input [31:0] alu_src2,
input [11:0] alu_control,
output [31:0]alu_result
);
接下来对选择信号和输出信号进行细分,因为肯定不可能直接对alu_control和alu_result进行操作
alu_control细分为每个操作的选择:
wire op_add; //加
wire op_sub; //减
wire op_slt; //有符号比较
wire op_sltu; //无符号比较
wire op_and; //与
wire op_or; //或
wire op_nor; //或非
wire op_xor; //异或
wire op_sll; //逻辑左移
wire op_srl; //逻辑右移
wire op_ora; //算数右移
wire op_lui; //高位加载
assign op_add = alu_control[0];
assign op_sub = alu_control[1];
assign op_slt = alu_control[2];
assign op_slt = alu_control[3];
assign op_and = alu_control[4];
assign op_or = alu_control[5];
assign op_nor = alu_control[6];
assign op_xor = alu_control[7];
assign op_sll = alu_control[8];
assign op_srl = alu_control[9];
assign op_ora = alu_control[10];
assign op_lui = alu_control[11];
alu_result细分为每个操作的输出结果(32位):
//拆分输出信号alu_result
wire [31:0] add_result;
wire [31:0] sub_result;
wire [31:0] slt_result;
wire [31:0] sltu_result;
wire [31:0] and_result;
wire [31:0] or_result;
wire [31:0] nor_result;
wire [31:0] xor_result;
wire [31:0] sll_result;
wire [31:0] srl_result;
wire [31:0] ora_result;
wire [31:0] lui_result;
加减法可以用一个操作来表示,具体实现和原理后面再说,修改一下:
//拆分输出信号alu_result
wire [31:0] add_sub_result;
wire [31:0] slt_result;
wire [31:0] sltu_result;
wire [31:0] and_result;
wire [31:0] or_result;
wire [31:0] nor_result;
wire [31:0] xor_result;
wire [31:0] sll_result;
wire [31:0] srl_result;
wire [31:0] ora_result;
wire [31:0] lui_result;
alu的输出就是根据选择信号进行对应输出,现在每个选择信号和输出信号都有了,可以先写最终的输出代码了,至于具体每个alu_result后面再写:
assign alu_result = ({{32{op_add | op_sub}} & add_sub_result}) |
({{32{op_slt }} & slt_result }) |
({{32{op_sltu }} & sltu_result }) |
({{32{op_and }} & and_result }) |
({{32{op_or }} & or_result }) |
({{32{op_nor }} & nor_result }) |
({{32{op_xor }} & xor_result }) |
({{32{op_sll }} & sll_result }) |
({{32{op_srl }} & srl_result }) |
({{32{op_ora }} & ora_result }) |
({{32{op_lui }} & lui_result }) ;
这个代码是如何写出来的?
整体来看每个()之间的|可以当作选择器的作用,然后我们拿出其中一个来看,比如
({{32{op_slt }} & slt_result })
假如我们现在就是需要执行op_slt这个操作,那么输入选择信号alu_control为001000_000000(之前说了它是独热码,这也是为什么用独热码编码的原因),此时op_slt的值为1,那么此时整个的输出就是slt_result;
假如我们现在就是需要执行不是op_slt,那么根据独热码的原理,是op_slt的值为0,那么此时整个的输出就是0;而又因为每个()之间使用或逻辑,所以对整体的输出没有影响。
这就巧妙的符合我们想要的操作,并且使用assign语句而不是case语句,这将减少消耗和资源并且优化了代码结构,后面的cpu设计中经常会参考此模版,需要好好理解。
那么接下来就是每一个result是怎么实现的?
我们需要了解每种运算的原理。
最简单的逻辑运算与或非等
assign and_result = alu_src1 & alu_src2;
assign or_result = alu_src1 | alu_src2;
assign nor_result = ~or_result;
assign xor_result = alu_src1 ^ alu_src2;
然后是LIU指令即Load Upper Immediate(LUI),它所做的操作是把16位的立即数加载到寄存器的高位,寄存器的低位用0补位。因此实现起来也简单:
assign lui_result = {alu_src2[15:0],16'b0};
然后逻辑左移右移和算数右移也比较好写先写上:
assign sll_result = alu_src2 << alu_src1[4:0];
assign srl_result = alu_src2 >> alu_src1[4:0];
assign sra_result = ($signed(alu_src2)) >>> alu_src1[4:0];
由于>>只能实现逻辑右移所以需要加上内置函数以及>>>才能实现算术右移
接下来实现加法减法器,我们通过一些原理可以同时实现加法减法的运算
补码加法器有如下性质:
A的补码-B的补码=A的补码+B的补码取反+1
据此可用加法实现减法,只需要对源输入进行一定处理。如果进行减法操作就把B输入进行取反并且进位加一。
那么如何判断进行的是加法还是减法操作呢?
可以根据选择信号来判断,如果进行减或者比较操作的话就需要进行减法运算,因为比较操作也可以用加法操作来实现,因此可写代码如下
wire [31:0] adder_a;
wire [31:0] adder_b;
wire adder_cin;
wire [31:0] adder_result;//加减的输出
wire adder_cout;//进位输出用于作比较
assign adder_a = alu_src1;
assign adder_b = (op_sub | op_slt | op_sltu) ? ~alu_src2 : alu_src2;
assign adder_cin = (op_sub | op_slt | op_sltu) ? 1 : 0;
assign {adder_cout , adder_result}= adder_a + adder_b + adder_cin;
assign add_sub_result = adder_result;
比较操作可以用减法操作来实现,首先了解下slt指令的含义:
SLT是 MIPS 指令集中的一条指令,用于比较两个寄存器的内容,如果第一个寄存器的值小于第二个寄存器的值,则将目标寄存器的值设置为 1,否则设置为 0。
可以首先判断两个值的正负(符号位0正1负),如果一正一负那么肯定正大负小,如果都是正载做减法比较,其余位补充0;
//比较指令
//slt_result的逻辑表达式求解
/*
src1 src2 adder_result slt_result 表达式
1 0 1 src1 & ~src2
0 1 0
1 1 0 1 ?
1 1 1 0 ?
0 0 0 0
0 0 1 1 ( ~src1^src2 )&adder_result
src1 src2 adder_result adder_cout sltu_result 表达式
0 0 1 0 1
0 0 0 1 0
*/
assign slt_result[31:1] = 0;
assign sltu_result[31:1] = 0;
assign slt_result[0] = (alu_src1[31] & (~alu_src2[31]))|
((~(alu_src1[31]^alu_src2[31])) & adder_result[31]);
assign sltu_result[0] = ~adder_cout;
至于最终的逻辑表达式怎么写,则要利用逻辑代数关系去求解。根据数字电路的知识,只关注另结果等于真的逻辑关系就可以了。
下一节对此alu进行仿真和测试