用Verilog搭出RISC-V架构单周期CPU

一、前言(一些废话)

  封面与内容无关!侵权必删!
  这是笔者学校暑期学期的必修课,难度并不高(仅追求及格的话),但相当耗时,对于在硬件设计方面完全不感冒的同学更是一场避不开的折磨,所以笔者想通过自己的成果和被折磨经验来帮助有此方面需求的同学,或是对于CPU设计有浓厚兴趣的人。


这是笔者在CSDN上第一篇博客,若有不足请多海涵
若有源码需求或对本文章有所见解者,请不吝赐教

二、知识预备

  如果在编写Verilog这类硬件编程语言时出现没有思路等现象时,可尝试反思对于设计所要求的理论知识是否牢固。
编写CPU的预备知识如下:

  • 对数字逻辑设计有一定了解或更深层次的学习
  • 对硬件原理有较好的基础并且能加以运用
  • 对《计算机组成原理》理论至少要有所涉猎
  • 对《计算机组成原理》教材(国内外皆可)中的CPU工作原理比较熟悉,并且能够自行构思其完整工作过程

三、整体构造图及开发板型号

手绘的单周期CPU整体框图

  • 本设计基于开发板为Xilinx的xc7a100tfgg484系列

四、将CPU工作分解

 4.1取指(IF)

  4.1.1 PC模块

   我们知道CPU要取出指令,所依靠的计数器就是PC,通过PC的值pc,CPU可确定取指令的位置,所以此模块负责传输pc值。

  4.1.2 NPC模块

   在单周期模型中,PC需要在每个时钟上升沿到来之际进行相应更新,我们可以把这个逻辑单元单独拿出来,向PC模块传输下一个pc的值npc,同时此模块也接受一些后序模块数据,因为这些数据可能影响到如何选择npc的正确值。

  4.1.3 IROM模块

   该模块使用vivado平台提供的IP核实现,通过预先存入16进制指令序列,并且接受PC模块的pc值作为地址寻址取指令,并将取出的指令inst传送至后序模块(inst可以说是CPU最重要的变量了,没有inst一切工作都不可能进行)。

 4.2译码(ID)

  4.2.1 CU模块

   控制单元(control unit)的简称,CU是CPU的“大脑”,负责解析从IROM模块传来的inst指令,并且生成各类控制信号,用于调遣其余部件进行工作。

  4.2.2 RF模块

   寄存器堆(register file)的简称,寄存器堆是CPU负责高速存储运算结果以及一些常用数据的存储设备,有读\写两种功能,为正确读写数据并且尽可能使CPU实际性能提示,采用组合逻辑读,时序逻辑写操作。

  4.2.3 SEXT模块

   符号扩展单元(sign extend),负责对risv-v指令的“可能”立即数位置所表示的立即数进行符号扩展为32位宽的数据,该行为受CU单元的sext_op信号所控制。

 4.3执行(EXE)

  4.3.1 ALU模块

   ALU是算数逻辑单元,负责CPU内部绝大部分运算任务(所以主频的提高关键跟ALU性能有着偌大的关系),接受前面IF阶段和ID阶段的各类信号以及数据输入,通过CU单元的控制信号选择第一、第二运算数以及运算类型。

 4.4访存(MEM)

  4.4.1 DRAM模块

   DRAM模块是使用vivado平台提供的IP核搭建的模拟主存,读写接口会有所提示。

 4.5写回(WB)

  4.5.1 WB模块

   其实单周期CPU中的写回模块就是一个多路选择器,通过CU单元的控制信号wd_sel决定写回数据的选择,备选的数据有:ALU运算结果,pc值,符号扩展结果imm,主存读取输出ramdout。

 4.6显示(DISPLAY)

  4.6.1 display模块

   此模块是为了实际下板而设立的,主要负责控制开发板上的LED灯,拨码开关和数码管。

 4.7各模块逻辑类型总结

  • 组合逻辑:NPC, IROM, SEXT, CU, ALU, WB
  • 时序逻辑:PC, DRAM, DISPLAY
  • 混合逻辑:RF(逻辑读,时序写)

五、各模块关键代码

 5.1取指(IF)

  5.1.1 PC模块

module PC(
    input                   clk     ,//CPU时钟
    input                   rst     ,//复位信号,高电平有效
    input           [31:0]  npc     ,//NPC模块的npc值
    output  reg     [31:0]  pc		 //PC模块的输出值pc
    );
    always @(posedge clk, posedge rst)
    begin
        if(rst)     pc <= 32'h0ffff_fffc;//方便复位之后的第一个pc将为全0
        else        pc <= npc;			 //不复位时将npc作为pc的新值
    end
endmodule

  4.1.2 NPC模块

module NPC(
    input   [31:0]  imm     ,//符号扩展结果
    input   [31:0]  rD1     ,//寄存器的第一输出
    input           pc_sel  ,//来自CU单元,用于分支和跳转指令
    input           npc_op  ,//同pc_sel来源相同
    input   [31:0]  pc      ,
    output  [31:0]  npc
    );
    //npc_op为0时pc+4,为1时有跳转(pc_sel为0时pc + imm,为1时pc = rD1 + imm)
    assign npc = npc_op ? (pc_sel ? rD1 + imm : pc + imm) : pc + 4;
endmodule

  5.1.3 IROM模块

module inst_mem(
    input   [15:2]  addr,//是pc信号的部分截取,可以思考一下为什么只需要2~15bit位?
    output  [31:0]  inst //32位risv-v架构的指令
    );
    //program为vivado对应IP核的实例化,大小为16384*32bit
    program U0_irom
    (
        .a  (addr[15:2]),
        .spo(inst      )
    );

 5.2译码(ID)

  5.2.1 CU模块(仅列出R型指令部分)


module CU(
    input               fun7    ,//接口是inst[30]
    input       [ 2:0]  fun3    ,//inst[14:12]
    input       [ 6:0]  opcode  ,//inst[6:0]
    input       [ 1:0]  b_flag  ,//branch的大小判断结果
    //output  reg         debug_wb_ena,//debug信号
    output  reg         npc_op  ,//用于NPC模块确认npc值
    output  reg         pc_sel  ,//同样用于确认npc值
    output  reg         rf_we   ,//寄存器堆的写使能
    output  reg [ 1:0]  wd_sel  ,//写回模块的选择信号
    output  reg [ 2:0]  sext_op ,//符号扩展单元的操作选择信号
    output  reg [ 3:0]  alu_op  ,//ALU单元的操作选择信号
    output  reg         alua_sel,//ALU单元的第一操作数选择信号
    output  reg         alub_sel,//ALU单元的第二操作数选择信号
    output  reg         branch  ,//分支信号,分支、跳转指令的标志
    output  reg         dram_wr  //主存的写使能
    );
    //以下带`的如`R,`DFT均为宏定义的常量
    always @(*)
    begin
        case(opcode)
        `R      ://R-type
        begin
            //debug_wb_ena = 1;
            npc_op   = 0     ;
            pc_sel   = 0     ;
            sext_op  = `DFT  ;
            rf_we    = 1     ;
            wd_sel   = 2'b00 ;
            alua_sel = 0     ;
            alub_sel = 0     ;   
            branch   = 0     ;
            dram_wr  = 0     ;
            case(fun3)
            3'b000:
                case(fun7)
                1'b0:   alu_op = `ADD   ;//add
                1'b1:   alu_op = `SUB   ;//sub
                default:alu_op = `DFT   ;
                endcase
            3'b001:     alu_op = `LSHIFT ;//sll
            3'b010:     alu_op = `CMP    ;//slt
            3'b011:     alu_op = `UCMP   ;//sltu
            3'b100:     alu_op = `XOR    ;//xor
            3'b101:
                case(fun7)
                1'b0:   alu_op = `RSHIFT ;//srl
                1'b1:   alu_op = `ARSHIFT;//sra
                default:alu_op = `DFT    ;
                endcase
            3'b110:     alu_op = `OR     ;//or
            3'b111:     alu_op = `AND    ;//and
            default:    alu_op = `DFT   ;
            endcase
        end
        
        default://unknown-type
        begin
            /*...*/
        end
        endcase
    end
endmodule

  5.2.2 RF模块


module RF(
    input               clk     ,
    input               rst     ,
    input       [ 4:0]  rR1_addr,//寄存器堆第一输出的地址
    input       [ 4:0]  rR2_addr,//寄存器堆第二输出的地址
    input       [ 4:0]  wR      ,//写寄存器堆的寄存器号
    input               rf_we   ,
    input       [31:0]  wD      ,//写寄存器堆的写回数据
    output  reg [31:0]  rD1     ,
    output  reg [31:0]  rD2
    );
    reg     [31:0]  rf [1:31];	//用二维信号定义了寄存器堆,实际上只有31个,x0恒为0不需要定义
    always @(*)
    begin
        rD1 <= rR1_addr ? rf[rR1_addr] : 32'h0;
        rD2 <= rR2_addr ? rf[rR2_addr] : 32'h0;
    end
    //写
    always @(posedge clk, posedge rst)
    begin
        if(rst)
        begin
            /*...全部置零操作*/
        end
        else if(rf_we)
        begin
            rf[wR] <= wR ? wD : 32'h0;
        end
        else;
    end
endmodule

  5.2.3 SEXT模块(仅列出I型指令立即数的符号扩展)

module SEXT(
    input        [31:7] inst    ,
    input        [ 2:0] sext_op ,
    output  reg [31:0]  imm
    );
    always @(*)
    begin
            case(sext_op)
            `I_ext  ://I-type      
                if(inst[31]) imm = {20'h0fffff, inst[31:20]};
                else         imm = {20'h000000, inst[31:20]};
            default:         imm = 32'b0;
            endcase
    end
endmodule

 5.3执行(EXE)

  5.3.1 ALU模块(仅列出一种操作)

module ALU(
    input       [31:0]  rD1     ,
    input       [31:0]  pc      ,
    input       [31:0]  rD2     ,
    input       [31:0]  imm     ,
    input       [ 3:0]  alu_op  ,
    input               alub_sel,
    input               alua_sel,
    input               branch  ,
    output  reg [ 1:0]  b_flag  ,
    output  reg [31:0]  alu_c	//ALU单元的运算结果
    );
    wire    [31:0]  alu_a;		//第一操作数
    wire    [31:0]  alu_b;		//第二操作数
    reg     [31:0]  compare;	//比较计算结果
    assign  alu_a = alua_sel ? pc : rD1; //1:pc,0:rD1
    assign  alu_b = alub_sel ? imm : rD2;//1:imm,0:rD2
    always @(*)
    begin
        if(branch)
        begin//risc-v参与运算的数均为补码形式,通过减法结果符号位可判断大小
            case(alu_op)
            `SUB://此情况均为有符号数比较
            begin
                compare = alu_a  +  ~alu_b + 1;//用a - b来判断大小关系
                if(compare[31])    b_flag = `LESS   ;//a < b
                else
                    if(compare)    b_flag = `MORE   ;//a > b
                    else           b_flag = `EQUAL  ;//a = b
            end
            /*...*/
            default:    b_flag = `DFT   ;
            endcase
        end
        else
        case(alu_op)//至少9种操作
        /*...*/
        endcase
    end
endmodule

 5.4访存(MEM)

  5.4.1 DRAM模块


module data_mem(
    input                   clk     ,
    input       [15:0]      addr    ,//alu单元运算结果就是地址
    input                   dram_wr ,
    input       [31:0]      din     ,//输入数据就是RF的输出信号rD2
    output      [31:0]      ramdout
    );
    wire    ram_clk = ~clk;// 因为芯片的固有延迟,DRAM的地址线来不及在时钟上升沿准备好,
    // 使得时钟上升沿数据读出有误。所以采用反相时钟,使得读出数据比地址准备好要晚大约半个时钟,
    // 从而能够获得正确的数据。
    wire    [31:0]  addr_tmp = addr - 16'h4000;
    dram U_dram 
    (
        .clk    (ram_clk)    		,   // input wire clka
        .a      (addr_tmp[15:2]) 	, 	// input wire [15:0] addra
        .spo    (ramdout)    		,   // output wire [31:0] douta
        .we     (dram_wr)    		,   // input wire [0:0] wea
        .d      (din)              		// input wire [31:0] dina
    );
endmodule

 5.5写回(WB)

  5.5.1 WB模块

module WB(
    input   [31:0]  alu_c   ,
    input   [31:0]  pc      ,
    input   [31:0]  ramdout ,
    input   [31:0]  imm     ,
    input   [ 1:0]  wd_sel  ,
    output  reg [31:0]  wD
    );
  
    always @(*)  
    begin
        case(wd_sel)
        2'b00:  wD = alu_c  ;
        2'b01:  wD = ramdout;
        2'b10:  wD = pc+4   ;
        2'b11:  wD = imm    ;
        default:wD = 32'h0  ;
        endcase
    end
endmodule

 5.6显示(DISPLAY)(略)

六、实际运行结果

测试汇编代码:(包含算数逻辑运算,访存,跳转)

在这里插入图片描述
                   图1.3.1

在这里插入图片描述

                   图1.3.2

图1.3.2中所执行的指令为lui x1,0x21212至jal start部分,其中bge的条件被满足会分支跳转,jal不会被执行,下一条指令将是sw x3,0(x0),寄存器x1在第一条指令后变为0x21212000,第二条指令后变为0x21212121;x2的值在第三条指令后值为0xffffffe1;x3在第四条指令后变为0x42424242,即x1内容左移1位的结果;x4在第五条指令后变为0x10909090,即为x1内容算术右移1位后的结果;同时第六条分支指令bge的b_flag结果为2,即表示x3>x2,进行跳转;目前为止所有指令均按照预期进行。

在这里插入图片描述

                   图1.3.3

图1.3.3展示从sw x3,0(x0)到lw x4,0(x0)部分,可见在执行完xor指令后,x1, x2, x3寄存器的值全部清零,ramdout的值变为x3的值,表示指令sw执行成功

在这里插入图片描述
                   图1.3.4
图1.3.4展示最后一条指令执行结果,x4在短暂清零后获得了存入内存的x3的值,至此,所有指令均成功执行。

七、结语

  构思这项工程时苦于自己近乎弱智的资料检索能力以及本届要求进一步严格化,只能“白手起家”从零开始搭建框架、实现模块、拼接连线以及逐步Debug,实际上板更是在若干次修改代码,写bitstream的噩梦循环中度过的,但总而言之对于设计硬件能力而言,确实有了长足的进步和质的提升。
  最后附上一句激励我坚持完整个暑期学期的话语,伟人的言语总是充满力量与希望,在人心中种下信念的火种。

一步一步走下去
一点一滴做下去
世间事最怕的就是认真
      ——教员

  • 37
    点赞
  • 249
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值