[RISC-V MCU 创新应用比赛] 从零开始设计RISC-V处理器——单周期处理器的设计

[复制链接]
 楼主| 9dome猫 发表于 2022-6-28 10:29 | 显示全部楼层 |阅读模式
一、CPU如何执行指令?

CPU包括控制器和数据通路,数据通路从字面意思理解,就是指处理器中数据流通的路径。不同类型的指令的数据通路不一样,具体由控制器产生的控制信号决定。
下面以一个简单的数据通路为例,介绍以下处理器执行指令的过程。

4655062ba676673ae8.png

1.CPU执行指令的过程如下:

(1)pc做为地址,输入到指令存储器,读取一条指令出来。这条指令是一个32bit的二进制数,里面包含了将要执行的指令的所有信息,包括指令类型(opcode),指令执行的功能(func3,func7),立即数部分(imme),源寄存器(rs1,rs2),目的寄存器(rd)。


(2)将读出来的指令进行译码,输入到不同的部件。比如,rs1,rs2,rd输入到寄存器堆,将立即数部分进行立即数扩展,将opcode输入到控制部件,产生控制信号。将func3,func7输入到子控制模块,产生ALU的控制信号。

(3)译码出来的寄存器号,输入到寄存器堆,读出寄存器号对应的数据Read_data1和Read_data2。

对于R-type指令,进行的操作就是两个寄存器的数据进行运算。

对于I-type指令,进行的操作是一个寄存器的数据与立即数进行运算。

因此这里要产生立即数,并且需要一个二选一选择器进行数据的选择。

(4)ALU即是运算单元,对输入的两个数据进行运算操作并且将运算结果输出,具体进行什么运算,由ALU_control产生的控制信号决定。

(5)将计算结果写回寄存器,写入到目标寄存器号对应的寄存器中。

(6)对于访存指令,如lb,lh,lw,sb,sh,sw,指令,还需要与数据存储器进行数据交换。

对于load指令,执行的操作是将数据从数据存储器的某个地址读出数据并讲这个数据写回到寄存器。

对于store指令,执行的操作是将寄存器的数据写入到数据存储器的某个地址内。

这个数据存储器的地址怎么产生呢?

地址以基地址加偏移的形式给出,基地址从寄存器中取得,偏移量从立即数中得到,因此ALU进行加法运算的结果便是数据存储器的地址。

因此ALU的运算结果有两个去向,一是写回寄存器,二是作为数据存储器的地址。

(7)对于load指令,是将数据存储器的数据写回到寄存器。

因此寄存器的数据来源有两个,一是ALU的运算结果,二是从数据存储器读出的数据。

(8)对于不进行跳转的指令,每个周期pc+4,指令按顺序执行,因此需要一个加法器。为什么是加4呢?

一条指令有32bit,CPU的内存空间按字节编址,所以一条指令4个字节实际上占了4个地址空间。

(9)对于条件跳转指令,如BEQ,BNE等,满足跳转的条件时,跳转的新地址由pc加左移一位的立即数产生,又需要一个加法器。因此,pc的来源有两个,一是顺序执行,pc+4,二是条件跳转,pc+(imme2)。

(10)对于无条件跳转指令,如jal,pc+4写回寄存器,将pc+(imme2)作为新地址。对于jalr,将pc+4写回寄存器,将源寄存器的数据与立即数相加并将最低位置0,作为下一个pc。

因此,写入寄存器的数据来源变成3个:ALU的运算结果,数据存储器读出的数据,pc+4。

pc的来源也变成3个:pc+4,pc+imme,Read_data1+imme。

(11)再考虑U-type,即lui,auipc。对于lui,将立即数进行处理,写入寄存器。对于auipc,将pc+imme写入寄存器。

因此,写入寄存器的数据来源变成5个,分别是:ALU的运算结果,数据存储器读出的数据,pc+4,lui的立即数 ,auipc的pc+imme。

上图所示的数据通路并不完整,如果想实现我们提到的37条指令,必须逐条分析指令,在上图的基础上添加相应的部件。

————————————————

版权声明:本文为CSDN博主「不学无术的小胖子.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/qq_45677520/article/details/122386632




 楼主| 9dome猫 发表于 2022-6-28 10:30 | 显示全部楼层
完整的数据通路如下所示(图中不包含控制器):

9819662ba67b078f49.png

2.从以上的分析中,可以将CPU的关键部件总结如下:
(1)指令存储器
(2)译码单元
(3)寄存器堆
(4)ALU
(5)数据存储器
(6)控制器
其中指令存储器和数据存储器为CPU外部的存储器,CPU的核心为数据通路+控制器。
 楼主| 9dome猫 发表于 2022-6-28 10:32 | 显示全部楼层
因此,我们设计完的顶层模块应该是这样的:
8429062ba67cceffcb.png
下面来进行具体设计。
 楼主| 9dome猫 发表于 2022-6-28 10:33 | 显示全部楼层
二、存储器
1.指令存储器
指令存储器属于只读存储器(rom),可读不可写。
实现方式有两种:
(1)用硬件描述性语言设计
(2)用FPGA工具调用专用的rom IP核。
此处为了提高代码的通用性,采用第一种方式设计。
模块的输入输出端口定义如下:
2472362ba683a4203f.png
 楼主| 9dome猫 发表于 2022-6-28 10:34 | 显示全部楼层
对rom进行初始化,只需要将二进制指令存到文件“rom_binary_file.txt”中。
如下所示:
7237362ba68b4a7455.png
 楼主| 9dome猫 发表于 2022-6-28 10:40 | 显示全部楼层
代码如下:
  1. module instr_memory(
  2.         addr,
  3.         instr
  4.     );
  5.         input [7:0]addr;
  6.         output [31:0]instr;
  7.        
  8.         reg[31:0] rom[255:0];
  9.        
  10.     //rom进行初始化
  11.     initial begin
  12.         $readmemb("./rom_binary_file.txt", rom);
  13.         //$readmemh("rom_hex_file.txt", rom);
  14.     end
  15.        
  16.     assign instr = rom[addr];

  17. endmodule
 楼主| 9dome猫 发表于 2022-6-28 10:40 | 显示全部楼层
2.数据存储器
数据存储器属于随机读写存储器(ram),可读可写,同样是两种实现方式。
这里用硬件描述语言设计。
 楼主| 9dome猫 发表于 2022-6-28 10:41 | 显示全部楼层
模块的输入输出端口定义如下:
9458162ba6a454f89c.png
 楼主| 9dome猫 发表于 2022-6-28 10:43 | 显示全部楼层
由于指令集中包含多位宽的读写指令,所以这里数据存储器的设计也应该支持多位宽读写。
根据输入的RW_type信号来确定读写类型。
 楼主| 9dome猫 发表于 2022-6-28 10:43 | 显示全部楼层
这里的RW_type信号,其实就是指令中的func3信号。
6368062ba6acc229e4.png
 楼主| 9dome猫 发表于 2022-6-28 10:44 | 显示全部楼层
代码如下:
  1. `include "define.v"
  2. module data_memory(
  3.         clk,
  4.         rst_n,
  5.         W_en,
  6.         R_en,
  7.         addr,
  8.         RW_type,
  9.         din,
  10.         dout
  11.     );
  12.        
  13.        
  14.         input clk;
  15.         input rst_n;
  16.        
  17.         input W_en;
  18.         input R_en;
  19.        
  20.         input [31:0]addr;
  21.         input [2:0]RW_type;

  22.         input [31:0]din;
  23.         output [31:0]dout;


  24.         reg [31:0]ram[255:0];
  25.        
  26.         wire [31:0]Rd_data;reference
  27.        
  28. //
  29.        
  30.        
  31.         reg [31:0]Wr_data_B;//字节拼接
  32.         wire [31:0]Wr_data_H;//半字拼接
  33.        
  34.         wire [31:0]Wr_data;
  35.        
  36.        
  37.         assign Rd_data=ram[addr[31:2]];/读基准

  38. always@(*)
  39.         begin
  40.                 case(addr[1:0])
  41.                         2'b00:Wr_data_B={Rd_data[31:8],din[7:0]};
  42.                         2'b01:Wr_data_B={Rd_data[31:16],din[7:0],Rd_data[7:0]};
  43.                         2'b10:Wr_data_B={Rd_data[31:24],din[7:0],Rd_data[15:0]};
  44.                         2'b11:Wr_data_B={din[7:0],Rd_data[23:0]};
  45.                 endcase
  46.         end

  47. assign Wr_data_H=(addr[1]) ? {din[15:0],Rd_data[15:0]} : {Rd_data[31:16],din[15:0]} ;
  48.        
  49. ///根据写类型,选择写入的数捿

  50. assign Wr_data=(RW_type[1:0]==2'b00) ? Wr_data_B :( (RW_type[1:0]==2'b01) ? Wr_data_H : din   );

  51. ///上升沿写入数捿

  52. always@(posedge clk)
  53. begin
  54.         if(W_en)
  55.                 ram[addr[9:2]]<=Wr_data;
  56. end


  57. //读部刿


  58. reg [7:0]Rd_data_B;
  59. wire [15:0]Rd_data_H;

  60. wire [31:0] Rd_data_B_ext;
  61. wire [31:0] Rd_data_H_ext;

  62. 根据写地坿,实现截叿
  63. always@(*)
  64. begin
  65.         case(addr[1:0])
  66.                 2'b00:Rd_data_B=Rd_data[7:0];
  67.                 2'b01:Rd_data_B=Rd_data[15:8];
  68.                 2'b10:Rd_data_B=Rd_data[23:16];
  69.                 2'b11:Rd_data_B=Rd_data[31:24];
  70.         endcase
  71. end
  72.                
  73. assign Rd_data_H=(addr[1])? Rd_data[31:16]:Rd_data[15:0];

  74. ///扩展丿32使
  75. assign Rd_data_B_ext=(RW_type[2]) ? {24'd0,Rd_data_B} : {{24{Rd_data_B[7]}},Rd_data_B};

  76. assign Rd_data_H_ext=(RW_type[2]) ? {16'd0,Rd_data_H} : {{16{Rd_data_H[15]}},Rd_data_H};


  77. /
  78. assign dout=(RW_type[1:0]==2'b00) ? Rd_data_B_ext : ((RW_type[1:0]==2'b01) ? Rd_data_H_ext : Rd_data );


  79. endmodule
 楼主| 9dome猫 发表于 2022-6-28 10:46 | 显示全部楼层
上面代码中出现的define.v文件里面定义了一些参数,该文件展示如下:
 楼主| 9dome猫 发表于 2022-6-28 11:01 | 显示全部楼层
  1. `define                zero_word                32'd0

  2. `define                lui                                7'b0110111
  3. `define                auipc                        7'b0010111
  4. `define                jal                                7'b1101111
  5. `define                jalr                        7'b1100111
  6. `define                B_type                        7'b1100011
  7. `define                load                        7'b0000011
  8. `define                store                        7'b0100011
  9. `define                I_type                        7'b0010011
  10. `define                R_type                        7'b0110011

  11. `define         ADD                          4'b0001
  12. `define         SUB                          4'b0011
  13. `define         SLL                          4'b1100
  14. `define         SLT                          4'b1001
  15. `define         SLTU                         4'b1000
  16. `define         XOR                          4'b0110
  17. `define         SRL                          4'b1101
  18. `define         SRA                          4'b1110
  19. `define         OR                           4'b0101
  20. `define         AND                          4'b0100
 楼主| 9dome猫 发表于 2022-6-28 11:02 | 显示全部楼层
三.数据通路
1.寄存器堆
寄存器堆由快速的静态随机读写存储器(sram)实现,这种ram可以多路并发访问不同的寄存器。
寄存器堆由32个寄存器阵列组成,其中0号寄存器的值恒为0,只读不写。
 楼主| 9dome猫 发表于 2022-6-28 11:04 | 显示全部楼层
模块的输入输出端口定义如下:
7465162ba6fa83df4f.png
 楼主| 9dome猫 发表于 2022-6-28 11:04 | 显示全部楼层
代码如下:
  1. `include "define.v"
  2. module registers(
  3.         clk,
  4.         W_en,
  5.         Rs1,
  6.         Rs2,
  7.         Rd,
  8.         Wr_data,
  9.         Rd_data1,
  10.         Rd_data2
  11.     );
  12.         input clk;
  13.         input W_en;
  14.         input [4:0]Rs1;
  15.         input [4:0]Rs2;
  16.         input [4:0]Rd;
  17.         input [31:0]Wr_data;
  18.        
  19.         output [31:0]Rd_data1;
  20.         output [31:0]Rd_data2;
  21.        
  22.         reg [31:0] regs [31:0];
  23.        
  24.        
  25. ///write
  26.         always@(posedge clk )
  27.                 begin
  28.                         if(W_en & (Rd!=0))
  29.                         regs[Rd]<=Wr_data;       
  30.                 end
  31. //read

  32.         assign Rd_data1=(Rs1==5'd0)?`zero_word: regs[Rs1];
  33.         assign Rd_data2=(Rs2==5'd0)?`zero_word: regs[Rs2];
  34.        
  35. endmodule

 楼主| 9dome猫 发表于 2022-6-28 11:05 | 显示全部楼层
2.译码模块
该模块对指令存储器输出的32位指令进行译码,得到opcode,Rs1,Rs2,Rd,imme,func3,func7等信息。
 楼主| 9dome猫 发表于 2022-6-28 11:06 | 显示全部楼层
按照以下格式进行译码:
386862ba7034369a2.png
 楼主| 9dome猫 发表于 2022-6-28 11:16 | 显示全部楼层
这里补充以下上一篇文中中提到的问题:
对于jal指令(UJ-type)和条件跳转指令(SB-type),他们的立即数表示方法是[20:1]或[12:1],这里其实就是把最低位默认成0了。
 楼主| 9dome猫 发表于 2022-6-28 11:19 | 显示全部楼层
如果按照最低位为0来扩展立即数,那么这个立即数直接与pc相加,即为跳转地址。如果最低位不进行补0,那么就如本篇刚开始提到的数据通路所示,必须将立即数左移一位之后与立即数相加。其最终效果是一样的。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

139

主题

1575

帖子

2

粉丝
快速回复 在线客服 返回列表 返回顶部

139

主题

1575

帖子

2

粉丝
快速回复 在线客服 返回列表 返回顶部