cpu设计实战-verilog实现简单MIPS中CPU的ALU(1)

本文详细解释了ALU(算术逻辑单元)的电路设计,包括如何通过输入信号进行加减、比较、移位和逻辑运算,以及使用独热码编码的alu_control信号如何选择不同的运算。介绍了MIPSCPU中ALU的实现细节,如alu_result的细分和选择逻辑操作的编码方式。
摘要由CSDN通过智能技术生成

什么是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进行仿真和测试

  • 31
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值