魏定国 发表于 2023-2-21 14:27:36

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

本帖最后由 魏定国 于 2023-2-21 14:27 编辑

单周期CPU设计目录
一、前言(一些废话)
二、知识预备
三、整体构造图及开发板型号
四、将CPU工作分解
 4.1取指(IF)
  4.1.1 PC模块
  4.1.2 NPC模块
  4.1.3 IROM模块
 4.2译码(ID)
  4.2.1 CU模块
  4.2.2 RF模块
  4.2.3 SEXT模块
 4.3执行(EXE)
  4.3.1 ALU模块
 4.4访存(MEM)
  4.4.1 DRAM模块
 4.5写回(WB)
  4.5.1 WB模块
 4.6显示(DISPLAY)
  4.6.1 display模块
 4.7各模块逻辑类型总结
五、各模块关键代码
 5.1取指(IF)
  5.1.1 PC模块
  4.1.2 NPC模块
  5.1.3 IROM模块
 5.2译码(ID)
  5.2.1 CU模块(仅列出R型指令部分)
  5.2.2 RF模块
  5.2.3 SEXT模块(仅列出I型指令立即数的符号扩展)
 5.3执行(EXE)
  5.3.1 ALU模块(仅列出一种操作)
 5.4访存(MEM)
  5.4.1 DRAM模块
 5.5写回(WB)
  5.5.1 WB模块
 5.6显示(DISPLAY)(略)
六、实际运行结果
七、结语

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

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

二、知识预备
  如果在编写Verilog这类硬件编程语言时出现没有思路等现象时,可尝试反思对于设计所要求的理论知识是否牢固。
编写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的正确值。
module NPC(
    input   imm   ,//符号扩展结果
    input   rD1   ,//寄存器的第一输出
    input         pc_sel,//来自CU单元,用于分支和跳转指令
    input         npc_op,//同pc_sel来源相同
    input   pc      ,
    outputnpc
    );
    //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  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         npc   ,//NPC模块的npc值
    outputreg   pc               //PC模块的输出值pc
    );
    always @(posedge clk, posedge rst)
    begin
      if(rst)   pc <= 32'h0ffff_fffc;//方便复位之后的第一个pc将为全0
      else      pc <= npc;                         //不复位时将npc作为pc的新值
    end
endmodule  5.1.2 NPC模块
module NPC(
    input   imm   ,//符号扩展结果
    input   rD1   ,//寄存器的第一输出
    input         pc_sel,//来自CU单元,用于分支和跳转指令
    input         npc_op,//同pc_sel来源相同
    input   pc      ,
    outputnpc
    );
    //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   addr,//是pc信号的部分截取,可以思考一下为什么只需要2~15bit位?
    outputinst //32位risv-v架构的指令
    );
    //program为vivado对应IP核的实例化,大小为16384*32bit
    program U0_irom
    (
      .a(addr),
      .spo(inst      )
    ); 5.2译码(ID)
  5.2.1 CU模块(仅列出R型指令部分)

module CU(
    input               fun7    ,//接口是inst
    input       [ 2:0]fun3    ,//inst
    input       [ 6:0]opcode,//inst
    input       [ 1:0]b_flag,//branch的大小判断结果
    //outputreg         debug_wb_ena,//debug信号
    outputreg         npc_op,//用于NPC模块确认npc值
    outputreg         pc_sel,//同样用于确认npc值
    outputreg         rf_we   ,//寄存器堆的写使能
    outputreg [ 1:0]wd_sel,//写回模块的选择信号
    outputreg [ 2:0]sext_op ,//符号扩展单元的操作选择信号
    outputreg [ 3:0]alu_op,//ALU单元的操作选择信号
    outputreg         alua_sel,//ALU单元的第一操作数选择信号
    outputreg         alub_sel,//ALU单元的第二操作数选择信号
    outputreg         branch,//分支信号,分支、跳转指令的标志
    outputreg         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       wD      ,//写寄存器堆的写回数据
    outputreg rD1   ,
    outputreg rD2
    );
    reg   rf ;      //用二维信号定义了寄存器堆,实际上只有31个,x0恒为0不需要定义
    always @(*)
    begin
      rD1 <= rR1_addr ? rf : 32'h0;
      rD2 <= rR2_addr ? rf : 32'h0;
    end
    //写
    always @(posedge clk, posedge rst)
    begin
      if(rst)
      begin
            /*...全部置零操作*/
      end
      else if(rf_we)
      begin
            rf <= wR ? wD : 32'h0;
      end
      else;
    end
endmodule

  5.2.3 SEXT模块(仅列出I型指令立即数的符号扩展)
module SEXT(
    input       inst    ,
    input      [ 2:0] sext_op ,
    outputreg imm
    );
    always @(*)
    begin
            case(sext_op)
            `I_ext://I-type      
                if(inst) imm = {20'h0fffff, inst};
                else         imm = {20'h000000, inst};
            default:         imm = 32'b0;
            endcase
    end
endmodule


 5.3执行(EXE)
  5.3.1 ALU模块(仅列出一种操作)
module ALU(
    input       rD1   ,
    input       pc      ,
    input       rD2   ,
    input       imm   ,
    input       [ 3:0]alu_op,
    input               alub_sel,
    input               alua_sel,
    input               branch,
    outputreg [ 1:0]b_flag,
    outputreg alu_c      //ALU单元的运算结果
    );
    wire    alu_a;                //第一操作数
    wire    alu_b;                //第二操作数
    reg   compare;      //比较计算结果
    assignalu_a = alua_sel ? pc : rD1; //1:pc,0:rD1
    assignalu_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)    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             addr    ,//alu单元运算结果就是地址
    input                   dram_wr ,
    input             din   ,//输入数据就是RF的输出信号rD2
    output            ramdout
    );
    wire    ram_clk = ~clk;// 因为芯片的固有延迟,DRAM的地址线来不及在时钟上升沿准备好,
    // 使得时钟上升沿数据读出有误。所以采用反相时钟,使得读出数据比地址准备好要晚大约半个时钟,
    // 从而能够获得正确的数据。
    wire    addr_tmp = addr - 16'h4000;
    dram U_dram
    (
      .clk    (ram_clk)                  ,   // input wire clka
      .a      (addr_tmp)         ,         // input wire addra
      .spo    (ramdout)                  ,   // output wire douta
      .we   (dram_wr)                  ,   // input wire wea
      .d      (din)                              // input wire dina
    );
endmodule

 5.5写回(WB)
  5.5.1 WB模块
module WB(
    input   alu_c   ,
    input   pc      ,
    input   ramdout ,
    input   imm   ,
    input   [ 1:0]wd_sel,
    outputreg 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的噩梦循环中度过的,但总而言之对于设计硬件能力而言,确实有了长足的进步和质的提升。

  最后附上一句激励我坚持完整个暑期学期的话语,伟人的言语总是充满力量与希望,在人心中种下信念的火种。
一步一步走下去
一点一滴做下去
世间事最怕的就是认真
      ——教员完

RISC-VMCU 发表于 2023-3-1 17:13:09

给力
页: [1]
查看完整版本: 用Verilog搭出RISC-V架构单周期CPU