课程project总结 - CPU

这个作业是在大二下学期编写的CPU五级流水线代码。首先看看整体结构:
一、整体架构(五级流水线)
五级流水线架构
CPU整体架构(包括CPU内核、指令内存、数据内存)
1、IF阶段:
  这个阶段的工作就是控制PC(指令地址)的增长或变化。一般情况下,CPU执行的指令地址每次都加一,然后从指令内存中读取对应的指令来执行,除非执行了跳转指令,指定运行某一条指令。
2、ID阶段:
  主要是赋值操作。大部分指令都需要进行计算,然后利用计算结果进行下一步操作。具体每一个指令进行的操作如下:
这里写图片描述
这里写图片描述
这里写图片描述
  比如LOAD指令,表示从内存中读取数据,然后存入寄存器中。所以如果把LOAD表示成一个函数,它就有三个参数:要被赋值的寄存器地址(gr[?])、读取数值的寄存器下标(r2)、立即数(val3),其中后两个参数相加的值就是从内存读取数据的地址。所以LOAD指令需要进行加法。那么,就需要把后两个参数赋值给两个中间寄存器:reg_A和reg_B,然后将它们相加之后的结果传入内存,再将内存读取的数据返回给寄存器gr[?]。
  不过不只有简单的加法和减法计算,还有位运算。首先是AND、OR、XOR,这三个运算方式和C++代码一样。
  然后是SLL、SRL、SLA、SRA。分别表示逻辑左移、逻辑右移、算数左移、算数右移。算数左移和逻辑左移的结果是一样的,但是逻辑右移和算数右移有不同:算数右移会保留符号位,比如1000右移两位,会变成1110,而逻辑右移则不会,变成0010。
  总结一下,ID部分计算的结果要么是赋给寄存器的值(ADD、ADDI、ADDC、SUB、SUBI、SUBC、AND、OR、XOR、SLL、SRL、SLA、SRA),要么是从内存中取值或赋值的地址(LOAD、STORE),要么是PC(JUMP、JMPR、BZ、BNZ、BN、BNN、BC、BNC,也就是跳转指令)或者其他。其中跳转指令涉及到一些特殊意义的寄存器,稍后会讲到。
3、EX阶段
  这个阶段的工作非常简单,就是将之前运算得到的结果赋给新的变量,只是有对STORE指令的特别处理:因为这个指令需要从内存中取值,然后赋给寄存器,而赋值给寄存器在MEM阶段,也就是下一个阶段实现,所以在这个阶段需要先把地址赋值给一个变量,才能保证CPU的下一个时钟到来的时候,内存中的值也到达了。
  不过这一点也不是很特别的地方。总体来说这一阶段还是很简单的。
4、MEM阶段
  顾名思义,这个阶段的任务就是接收从内存中返回的数据。
5、WB阶段
  WB的意思是Write Back,意思是将计算结果或者是从内存中得到的数据存入寄存器中。

二、代码具体实现
1、顶层代码(CPU_top.v)
  这个代码的作用就是连接三个部分:pcpu、指令内存、数据内存。另外还负责控制板上液晶屏的显示。
① 模块之间的连接
  其实这个连接就是变量的连接。比如PC,它代表的是指令的地址,所以它控制着pcpu和instr_mem的连接。当然这两个模块之间不止这一个变量,下面看看代码:
这里写图片描述
  不对每个参数都解释了,这里只说count参数:它是由板上的时钟控制的(0.2ms一个周期):
这里写图片描述
  也就是说count每0.2ms会加一,但是count不能直接作为CPU或者内存的时钟周期,原因有二:首先,CPU的时钟周期要一定程度大于内存的时钟周期,因为内存中的数据是要经常刷新的,如果内存更新的速度还不如CPU运行的速度的话,就会导致CPU读取/存储数据产生延迟。其次,如果周期太短,在板上查看运行效果的时候,速度就会太快,以至于观察不到寄存器数值的变化。
② 显示寄存器数值
  首先CPU模块有一个输入为sw,这个变量的值就是要显示的寄存器的地址,它的值有板上的4个开关控制。有一个输出是gr_y,它的值就是对应寄存器的值。关于它的控制,在接下来讲解CPU模块的时候可以看到。
  那么有人会说,既然gr_y就是寄存器的值,是不是直接将gr_y显示出来就行了呢?确实是要将gr_y显示出来,但是gr_y是16位的,而板上的液晶显示屏上有4个数字,每个数字的值从0到f取值,也就是4位二进制数,所以需要把gr_y分成四部分,每部分有4位,然后通过较短的时钟(肉眼不可分辨它的变化)控制,依次显示这四个部分。
这里写图片描述
  data_y就是要显示出来的数字了,再根据液晶屏的结构,控制data_y取不同数值的时候,a_to_g的变化:
这里写图片描述
③ 与板上接口的连接
  顶层代码里面很多变量都需要通过板上的按钮,或者时钟来控制取值,CPU_top.ucf文件就控制着这些变量和接口的连接。

net "clk" loc = "V10";
net "enable" loc = "T9";
net "reset" loc = "B8";
net "start" loc = "T10";
net "sw[3]" loc = "T5";
net "sw[2]" loc = "V8";
net "sw[1]" loc = "U8";
net "sw[0]" loc = "N8";

net "a_to_g[6]" loc = "T17";
net "a_to_g[5]" loc = "T18";
net "a_to_g[4]" loc = "U17";
net "a_to_g[3]" loc = "U18";
net "a_to_g[2]" loc = "M14";
net "a_to_g[1]" loc = "N14";
net "a_to_g[0]" loc = "L14";

net "en[0]" loc = "N16";
net "en[1]" loc = "N15";
net "en[2]" loc = "P18";
net "en[3]" loc = "P17";

  clk的意思是时钟,所以它连接的是板上的时钟。a_to_g和en连接的是液晶显示器,控制着四个液晶屏中哪个亮,以及怎么亮。其他连接的都是开关。
  如果还对液晶屏的输出有疑问,请看下图(来源:Nexys3_rm用户手册)
这里写图片描述
2、内存(instr_mem.v、data_mem.v)
  内存有三个关键部分:第一是初始化,第二是输出数据,第三是将传入的数据存入内存中。
  对于指令内存来说,指令被初始化之后是不会更新的,所以初始化的指令就是接下来CPU要执行的指令(注意:并不是每一条指令都会执行,因为还有跳转指令)
这里写图片描述
  这是初始化的一种方式:由于内存的时钟是远小于CPU的时钟的,所以用内存的时钟来更新i_mem的值,不用担心CPU会取不到对应addr的值。r_data就是输出的指令。
  接下来看data_mem是怎么初始化的,用的是另一种方式:
这里写图片描述
  其实用指令内存初始化的方法也可以,只不过数据内存还可能被赋值(当dwe为真),所以分开两部分写,逻辑不会那么复杂。其他的传输数据方面的逻辑和指令内存就大同小异了。
3、CPU
  CPU实现的逻辑就是在第一部分讲到的五级流水线架构。只要跟着这几个部分实现的功能来理解就比较容易看懂CPU实现的代码,我写的时候也是根据这些逻辑来实现的。
  不过,在开始五级流水线之前,还需要一些准备工作:
① 定义常量
  mips指令是16位的,因此在判断指令的部分如果每次都要写16位的指令会非常麻烦。包括写寄存器的地址。因此,我们需要把这些有特殊意义的状态、地址和指令定义为常量:

`define exec 1'b0
`define idle 1'b1
`define NOP 5'b00000
`define HALT 5'b00001
`define LOAD 5'b00010
`define STORE 5'b00011
`define LDIH 5'b10000
`define ADD 5'b01000
`define ADDI 5'b01001
`define ADDC 5'b10001
`define SUB 5'b10010
`define SUBI 5'b10011
`define SUBC 5'b10111
`define CMP 5'b01100
`define AND 5'b01101
`define OR 5'b01110
`define XOR 5'b01111
`define SLL 5'b00100
`define SRL 5'b00110
`define SLA 5'b00101
`define SRA 5'b00111
`define JUMP 5'b11000
`define JMPR 5'b11001
`define BZ 5'b11010
`define BNZ 5'b11011
`define BN 5'b11100
`define BNN 5'b11101
`define BC 5'b11110
`define BNC 5'b11111
`define gr0 3'b000
`define gr1 3'b001
`define gr2 3'b010
`define gr3 3'b011
`define gr4 3'b100
`define gr5 3'b101
`define gr6 3'b110
`define gr7 3'b111

  其中exec和idle分别表示CPU的运行/终止状态,gr0~gr7分别表示8个寄存器的地址,其他都是指令。
  注意:引用这些常量的时候,必须在常量名前面加上'`'。
② 定义变量

module CPU(
clock, enable, reset, start, select_y, i_datain, d_datain,
d_addr, d_dataout, d_we, i_addr, y
    );

input wire clock;
input wire enable;
input wire reset;
input wire start;
input wire [15:0]i_datain;
input wire [15:0]d_datain;
input wire [3:0]select_y;
output wire [7:0]d_addr;
output wire [15:0]d_dataout;
output wire d_we;
output wire [7:0]i_addr;
output reg [15:0]y;

//************************************************//
//* You need to complete the design below        *//
//* by yourself according to your operation set  *//
//************************************************//

reg [15:0]gr[7:0]; 

reg nf;
reg zf;
reg cf;

reg state;

reg next_state;

reg [15:0]id_ir;
// 以下的信号由于在后面只是赋值不是取值,所以只需传命令和要写回的寄存器的那五位
reg [7:0]wb_ir;
reg [7:0]mem_ir;
reg [7:0]ex_ir;

reg [7:0]pc;
reg [15:0]reg_A;
reg [15:0]reg_B;
reg [15:0]smdr;
reg [15:0]smdr1;
reg [15:0]reg_C;
reg [15:0]ALUo;
reg dw;
reg [15:0]reg_C1;

// 为了不破坏时序,要用组合逻辑对输出进行赋值
assign d_addr = reg_C[7:0]; // 记录地址
assign d_we = dw;
assign d_dataout = smdr1; // smdr1在EX的时候已经决定要不要取r1了,所以在这里直接赋值
assign i_addr = pc;

③ 控制要显示的寄存器值输出

//************* the output y control *************//
always @ (*)
   begin
       case(select_y)
        0: y <= gr[0];
        1: y <= gr[1];
        2: y <= gr[2];
        3: y <= gr[3];
        4: y <= gr[4];
        5: y <= gr[5];
        6: y <= gr[6];
        7: y <= gr[7];
        8: y <= {8'b0000_0000, pc};
        default: y <= 16'b0;
       endcase
    end

  其中select_y和y分别对应顶层文件中的sw和gr_y。(好吧其实还输出了指令)
④ CPU的状态控制
  CPU的状态看起来很好控制,也就两种状态嘛:idle和exec,但它是由三个变量控制的:reset、enable和start。从idle状态变成exec状态,条件是enable和start同时为1;而从运行状态到停止状态,条件为enable为0,或者reset为1。所以,如果在运行状态下,你想让CPU暂停工作,然后在板上更方便查看寄存器的值,就可以令enable为0。再次让enable为1,CPU会从中断的指令继续运行。

//************* CPU control *************//
always @(posedge clock)
    begin
        if (reset)
            state <= `idle;
        else
            state <= next_state;
    end

always @(*)
    begin
        case (state)
            `idle : 
                if ((enable == 1'b1)
                && (start == 1'b1))
                    next_state <= `exec;
                else    
                    next_state <= `idle;
            `exec :
                if ((enable == 1'b0)
                || (wb_ir[7:3] == `HALT))
                    next_state <= `idle;
                else
                    next_state <= `exec;
        endcase
    end

  准备工作做完,接下来就是五级流水线阶段了。
④ IF
这里写图片描述
这里写图片描述
  逻辑是很好理解的,就是有关pc的赋值:当指令是跳转指令的时候,pc就变成计算出来的地址,否则就加一。
  这里再讲一下比较特殊的三个跳转指令:BC、BZ、BN。BC的意思是进位跳转:当往前第三条指令在EX中计算的结果发生了进位,导致cf为1,就进位。BZ则为计算结果是0,使得zf为1,BN为计算结果是负数,使得nf为1。注意:由于是往前第三条指令,所以带符号的跳转指令,前面都必须接上两条nop指令,才能保证cf、nf、zf在MEM阶段被成功赋值。BNC、BNZ、BNN和这三条指令的意思相反。
⑤ ID
  ID阶段是ALU进行计算的准备,也就是对进行加法、减法等运算变量的赋值。这部分的代码完全是根据前面提到的指令集的意义实现的。
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
  可以看到reg_A的取值分为r1、r2和0,前两个都是从寄存器中取值。而reg_B可以是立即数,也可以是寄存器(r3)。总之,reg_A和reg_B就是计算的时候,运算符两端的数。
  还有两点要注意的地方:一是为什么ex_ir只要id_ir的前八位呢?因为后面的三个阶段已经不涉及计算了,只要知道指令是什么,以及要写回的寄存器是什么。所以只要16位指令的前八位。这个比较现实的意义是可以减少CPU做出来的面积
  另外一个变量是smdr,这个参数只在STORE指令的时候有用,它存储了要准备写入内存的寄存器值。STORE指令还是需要利用reg_A和reg_B计算出写入内存的地址的,所以需要另外的变量:smdr来存寄存器值。
⑥ EX
  从五级流水线的结构上看,ALU是属于的EX的,但我更愿意把ALU放到ID阶段去,因为ALU是组合逻辑,而五个阶段都是时序逻辑。这两个逻辑的区别是:前者不需要时钟触发,而是一直在执行,所以ALU的运算可以理解为在ID之后立即执行,EX阶段之前,ALUo、cf等已经算出来了。(所以时序和组合逻辑复杂程度要分配好,组合逻辑的计算时间不能超过一个时钟周期)
  总之,要把ALU理解为EX之前执行好的,而不是同时执行的。
这里写图片描述
这里写图片描述
  要注意{cf, ALUo} <= reg_A + reg_B这种赋值方式。以及如何强制类型转换为符号数:$signed。还有算数位运算的符号:<<<和>>>。
  再来看看EX阶段:
这里写图片描述
这里写图片描述
  主要就是dw和cf、nf、zf的赋值。逻辑上都比较简单。dw的赋值时为了STORE指令接下来在内存中存储数据作准备,为1则要存入。smdr1则是从smdr来,存储了准备赋给内存的值。
⑦ MEM
这里写图片描述
  对LOAD指令的处理。
  
⑧ WB
这里写图片描述
  WB的逻辑也非常简单。
  
三、仿真、板级验证
  由于ISE的仿真出现了问题,所以我就只展示板级验证的结果:
这里写图片描述
  这个指令的意思是,首先从内存中读取地址为0的值(一般gr0就定义为0,不修改),然后存入gr1中(这里是8),接着将gr1逻辑右移两位的结果存入gr2,将gr1算数右移两位的结果存入gr3。所以最后gr2的值是0x2000,gr3的值为0xE000。
  接下来就是在板上执行程序了。
1、生成二进制文件
这里写图片描述
2、电脑连接NEXYS3
  打开软件Digilent Adept,然后将二进制文件烧入板中。
这里写图片描述
3、板上操作
  依次查看gr1、gr2和gr3的值:
这里写图片描述
这里写图片描述
这里写图片描述
四、资源下载网址
代码:http://download.csdn.net/detail/xiaoliizi/9466825
Nexys3_rm(用户手册):http://download.csdn.net/detail/xiaoliizi/9467879

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值