查看: 828|回复: 1
收起左侧

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

[复制链接]

  离线 

  • TA的每日心情
    拍拍
    2022-10-3 08:50
  • 签到天数: 1 天

    [LV.1]

    发表于 2023-2-21 14:27:36 | 显示全部楼层 |阅读模式

    有人预言,RISC-V或将是继Intel和Arm之后的第三大主流处理器体系。欢迎访问全球首家只专注于RISC-V单片机行业应用的中文网站

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    本帖最后由 魏定国 于 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工作原理比较熟悉,并且能够自行构思其完整工作过程

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

    国内芯片技术交流-用Verilog搭出RISC-V架构单周期CPUrisc-v单片机中文社区(1)
    • 本设计基于开发板为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的正确值。
    1. module NPC(
    2.     input   [31:0]  imm     ,//符号扩展结果
    3.     input   [31:0]  rD1     ,//寄存器的第一输出
    4.     input           pc_sel  ,//来自CU单元,用于分支和跳转指令
    5.     input           npc_op  ,//同pc_sel来源相同
    6.     input   [31:0]  pc      ,
    7.     output  [31:0]  npc
    8.     );
    9.     //npc_op为0时pc+4,为1时有跳转(pc_sel为0时pc + imm,为1时pc = rD1 + imm)
    10.     assign npc = npc_op ? (pc_sel ? rD1 + imm : pc + imm) : pc + 4;
    11. 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模块
    1. module PC(
    2.     input                   clk     ,//CPU时钟
    3.     input                   rst     ,//复位信号,高电平有效
    4.     input           [31:0]  npc     ,//NPC模块的npc值
    5.     output  reg     [31:0]  pc                 //PC模块的输出值pc
    6.     );
    7.     always @(posedge clk, posedge rst)
    8.     begin
    9.         if(rst)     pc <= 32'h0ffff_fffc;//方便复位之后的第一个pc将为全0
    10.         else        pc <= npc;                         //不复位时将npc作为pc的新值
    11.     end
    12. endmodule
    复制代码
      5.1.2 NPC模块
    1. module NPC(
    2.     input   [31:0]  imm     ,//符号扩展结果
    3.     input   [31:0]  rD1     ,//寄存器的第一输出
    4.     input           pc_sel  ,//来自CU单元,用于分支和跳转指令
    5.     input           npc_op  ,//同pc_sel来源相同
    6.     input   [31:0]  pc      ,
    7.     output  [31:0]  npc
    8.     );
    9.     //npc_op为0时pc+4,为1时有跳转(pc_sel为0时pc + imm,为1时pc = rD1 + imm)
    10.     assign npc = npc_op ? (pc_sel ? rD1 + imm : pc + imm) : pc + 4;
    11. endmodule
    复制代码
      5.1.3 IROM模块
    1. module inst_mem(
    2.     input   [15:2]  addr,//是pc信号的部分截取,可以思考一下为什么只需要2~15bit位?
    3.     output  [31:0]  inst //32位risv-v架构的指令
    4.     );
    5.     //program为vivado对应IP核的实例化,大小为16384*32bit
    6.     program U0_irom
    7.     (
    8.         .a  (addr[15:2]),
    9.         .spo(inst      )
    10.     );
    复制代码
    5.2译码(ID)
      5.2.1 CU模块(仅列出R型指令部分)


    1. module CU(
    2.     input               fun7    ,//接口是inst[30]
    3.     input       [ 2:0]  fun3    ,//inst[14:12]
    4.     input       [ 6:0]  opcode  ,//inst[6:0]
    5.     input       [ 1:0]  b_flag  ,//branch的大小判断结果
    6.     //output  reg         debug_wb_ena,//debug信号
    7.     output  reg         npc_op  ,//用于NPC模块确认npc值
    8.     output  reg         pc_sel  ,//同样用于确认npc值
    9.     output  reg         rf_we   ,//寄存器堆的写使能
    10.     output  reg [ 1:0]  wd_sel  ,//写回模块的选择信号
    11.     output  reg [ 2:0]  sext_op ,//符号扩展单元的操作选择信号
    12.     output  reg [ 3:0]  alu_op  ,//ALU单元的操作选择信号
    13.     output  reg         alua_sel,//ALU单元的第一操作数选择信号
    14.     output  reg         alub_sel,//ALU单元的第二操作数选择信号
    15.     output  reg         branch  ,//分支信号,分支、跳转指令的标志
    16.     output  reg         dram_wr  //主存的写使能
    17.     );
    18.     //以下带`的如`R,`DFT均为宏定义的常量
    19.     always @(*)
    20.     begin
    21.         case(opcode)
    22.         `R      ://R-type
    23.         begin
    24.             //debug_wb_ena = 1;
    25.             npc_op   = 0     ;
    26.             pc_sel   = 0     ;
    27.             sext_op  = `DFT  ;
    28.             rf_we    = 1     ;
    29.             wd_sel   = 2'b00 ;
    30.             alua_sel = 0     ;
    31.             alub_sel = 0     ;   
    32.             branch   = 0     ;
    33.             dram_wr  = 0     ;
    34.             case(fun3)
    35.             3'b000:
    36.                 case(fun7)
    37.                 1'b0:   alu_op = `ADD   ;//add
    38.                 1'b1:   alu_op = `SUB   ;//sub
    39.                 default:alu_op = `DFT   ;
    40.                 endcase
    41.             3'b001:     alu_op = `LSHIFT ;//sll
    42.             3'b010:     alu_op = `CMP    ;//slt
    43.             3'b011:     alu_op = `UCMP   ;//sltu
    44.             3'b100:     alu_op = `XOR    ;//xor
    45.             3'b101:
    46.                 case(fun7)
    47.                 1'b0:   alu_op = `RSHIFT ;//srl
    48.                 1'b1:   alu_op = `ARSHIFT;//sra
    49.                 default:alu_op = `DFT    ;
    50.                 endcase
    51.             3'b110:     alu_op = `OR     ;//or
    52.             3'b111:     alu_op = `AND    ;//and
    53.             default:    alu_op = `DFT   ;
    54.             endcase
    55.         end
    56.         
    57.         default://unknown-type
    58.         begin
    59.             /*...*/
    60.         end
    61.         endcase
    62.     end
    63. endmodule

    复制代码
      5.2.2 RF模块

    1. module RF(
    2.     input               clk     ,
    3.     input               rst     ,
    4.     input       [ 4:0]  rR1_addr,//寄存器堆第一输出的地址
    5.     input       [ 4:0]  rR2_addr,//寄存器堆第二输出的地址
    6.     input       [ 4:0]  wR      ,//写寄存器堆的寄存器号
    7.     input               rf_we   ,
    8.     input       [31:0]  wD      ,//写寄存器堆的写回数据
    9.     output  reg [31:0]  rD1     ,
    10.     output  reg [31:0]  rD2
    11.     );
    12.     reg     [31:0]  rf [1:31];        //用二维信号定义了寄存器堆,实际上只有31个,x0恒为0不需要定义
    13.     always @(*)
    14.     begin
    15.         rD1 <= rR1_addr ? rf[rR1_addr] : 32'h0;
    16.         rD2 <= rR2_addr ? rf[rR2_addr] : 32'h0;
    17.     end
    18.     //写
    19.     always @(posedge clk, posedge rst)
    20.     begin
    21.         if(rst)
    22.         begin
    23.             /*...全部置零操作*/
    24.         end
    25.         else if(rf_we)
    26.         begin
    27.             rf[wR] <= wR ? wD : 32'h0;
    28.         end
    29.         else;
    30.     end
    31. endmodule

    复制代码
      5.2.3 SEXT模块(仅列出I型指令立即数的符号扩展)
    1. module SEXT(
    2.     input        [31:7] inst    ,
    3.     input        [ 2:0] sext_op ,
    4.     output  reg [31:0]  imm
    5.     );
    6.     always @(*)
    7.     begin
    8.             case(sext_op)
    9.             `I_ext  ://I-type      
    10.                 if(inst[31]) imm = {20'h0fffff, inst[31:20]};
    11.                 else         imm = {20'h000000, inst[31:20]};
    12.             default:         imm = 32'b0;
    13.             endcase
    14.     end
    15. endmodule

    复制代码

    5.3执行(EXE)
      5.3.1 ALU模块(仅列出一种操作)

    1. module ALU(
    2.     input       [31:0]  rD1     ,
    3.     input       [31:0]  pc      ,
    4.     input       [31:0]  rD2     ,
    5.     input       [31:0]  imm     ,
    6.     input       [ 3:0]  alu_op  ,
    7.     input               alub_sel,
    8.     input               alua_sel,
    9.     input               branch  ,
    10.     output  reg [ 1:0]  b_flag  ,
    11.     output  reg [31:0]  alu_c        //ALU单元的运算结果
    12.     );
    13.     wire    [31:0]  alu_a;                //第一操作数
    14.     wire    [31:0]  alu_b;                //第二操作数
    15.     reg     [31:0]  compare;        //比较计算结果
    16.     assign  alu_a = alua_sel ? pc : rD1; //1:pc,0:rD1
    17.     assign  alu_b = alub_sel ? imm : rD2;//1:imm,0:rD2
    18.     always @(*)
    19.     begin
    20.         if(branch)
    21.         begin//RISC-V参与运算的数均为补码形式,通过减法结果符号位可判断大小
    22.             case(alu_op)
    23.             `SUB://此情况均为有符号数比较
    24.             begin
    25.                 compare = alu_a  +  ~alu_b + 1;//用a - b来判断大小关系
    26.                 if(compare[31])    b_flag = `LESS   ;//a < b
    27.                 else
    28.                     if(compare)    b_flag = `MORE   ;//a > b
    29.                     else           b_flag = `EQUAL  ;//a = b
    30.             end
    31.             /*...*/
    32.             default:    b_flag = `DFT   ;
    33.             endcase
    34.         end
    35.         else
    36.         case(alu_op)//至少9种操作
    37.         /*...*/
    38.         endcase
    39.     end
    40. endmodule
    复制代码
    5.4访存(MEM)
      5.4.1 DRAM模块


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

    复制代码
    5.5写回(WB)
      5.5.1 WB模块

    1. module WB(
    2.     input   [31:0]  alu_c   ,
    3.     input   [31:0]  pc      ,
    4.     input   [31:0]  ramdout ,
    5.     input   [31:0]  imm     ,
    6.     input   [ 1:0]  wd_sel  ,
    7.     output  reg [31:0]  wD
    8.     );
    9.   
    10.     always @(*)  
    11.     begin
    12.         case(wd_sel)
    13.         2'b00:  wD = alu_c  ;
    14.         2'b01:  wD = ramdout;
    15.         2'b10:  wD = pc+4   ;
    16.         2'b11:  wD = imm    ;
    17.         default:wD = 32'h0  ;
    18.         endcase
    19.     end
    20. endmodule

    复制代码
    5.6显示(DISPLAY)(略)

    六、实际运行结果

    测试汇编代码:(包含算数逻辑运算,访存,跳转)
    国内芯片技术交流-用Verilog搭出RISC-V架构单周期CPUrisc-v单片机中文社区(2)
    图1.3.1
    国内芯片技术交流-用Verilog搭出RISC-V架构单周期CPUrisc-v单片机中文社区(3)
    图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,进行跳转;目前为止所有指令均按照预期进行。

    国内芯片技术交流-用Verilog搭出RISC-V架构单周期CPUrisc-v单片机中文社区(4)
    图1.3.3

    图1.3.3展示从sw x3,0(x0)到lw x4,0(x0)部分,可见在执行完xor指令后,x1, x2, x3寄存器的值全部清零,ramdout的值变为x3的值,表示指令sw执行成功
    国内芯片技术交流-用Verilog搭出RISC-V架构单周期CPUrisc-v单片机中文社区(5)
    图1.3.4

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

    七、结语

      构思这项工程时苦于自己近乎弱智的资料检索能力以及本届要求进一步严格化,只能“白手起家”从零开始搭建框架、实现模块、拼接连线以及逐步Debug,实际上板更是在若干次修改代码,写bitstream的噩梦循环中度过的,但总而言之对于设计硬件能力而言,确实有了长足的进步和质的提升。

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





    上一篇:openKylin已布局RISC-V架构OS办公场景生态
    下一篇:缪斯实验室推出十元级 RISC-V 开发板 nanoCH32V003
    RISCV作者优文
    全球首家只专注于RISC-V单片机行业应用的中文网站
    回复

    使用道具 举报

      离线 

  • TA的每日心情
    开心
    2023-1-20 22:24
  • 签到天数: 54 天

    [LV.5]

    发表于 2023-3-1 17:13:09 | 显示全部楼层
    给力
    有事请在RISC-V单片机中文社区内留言,或请发邮件至admin@risc-v1.com,我们会统一尽快回复。
    【本人基本每天不定时在线,欢迎加好友,欢迎发信息,欢迎互相骚扰!!!】
    点评回复

    使用道具 举报

    高级模式
    B Color Image Link Quote Code Smilies

    本版积分规则

    关闭

    RISC-V单片机中文网上一条 /2 下一条



    版权及免责声明|RISC-V单片机中文网 |网站地图

    GMT+8, 2024-4-20 03:02 , Processed in 1.151133 second(s), 52 queries .

    快速回复 返回顶部 返回列表