diff --git a/doc/计算机结构设计实验/lab09/image/image-20240107213027949.png b/doc/计算机结构设计实验/lab09/image/image-20240107213027949.png deleted file mode 100644 index 30bbb08..0000000 Binary files a/doc/计算机结构设计实验/lab09/image/image-20240107213027949.png and /dev/null differ diff --git a/doc/计算机结构设计实验/lab09/image/image-20240109144722861.png b/doc/计算机结构设计实验/lab09/image/image-20240109144722861.png deleted file mode 100644 index be809e9..0000000 Binary files a/doc/计算机结构设计实验/lab09/image/image-20240109144722861.png and /dev/null differ diff --git a/doc/计算机结构设计实验/lab09/image/image-20240109153542526.png b/doc/计算机结构设计实验/lab09/image/image-20240109153542526.png deleted file mode 100644 index 81addf5..0000000 Binary files a/doc/计算机结构设计实验/lab09/image/image-20240109153542526.png and /dev/null differ diff --git a/doc/计算机结构设计实验/lab09/image/image-20240124134556170.png b/doc/计算机结构设计实验/lab09/image/image-20240124134556170.png new file mode 100644 index 0000000..e62b565 Binary files /dev/null and b/doc/计算机结构设计实验/lab09/image/image-20240124134556170.png differ diff --git a/doc/计算机结构设计实验/lab09/image/image-20240124141127010.png b/doc/计算机结构设计实验/lab09/image/image-20240124141127010.png new file mode 100644 index 0000000..39dfb91 Binary files /dev/null and b/doc/计算机结构设计实验/lab09/image/image-20240124141127010.png differ diff --git a/doc/计算机结构设计实验/lab09/main.typ b/doc/计算机结构设计实验/lab09/main.typ index d55d932..279fec4 100644 --- a/doc/计算机结构设计实验/lab09/main.typ +++ b/doc/计算机结构设计实验/lab09/main.typ @@ -98,7 +98,7 @@ RISC-V单周期CPU设计实现简单,控制器部分是纯组合逻辑电路 === 设计的基本方法 -在计算机结构设计实验中,需要设计实现的CPU也是一个数字逻辑电路,其设计应该遵循数字逻辑电路设计的一般性方法。CPU不但要完成运算,也要维持自身的状态,所以CPU这个数字逻辑电路一定是既有组合逻辑电路又有时序逻辑电路的。CPU输入的、运算的、存储的、输出的数据都在组合逻辑电路和时序逻辑电路上流转,这些逻辑电路被称为数据通路(Datapath)。因此,要设计CPU这个数字逻辑电路,首要的工作就是设计数据通路。同时,因为数据通路中会有多路选择器、时序逻辑器件,所以还要有相应的控制信号,产生这些控制信号的逻辑被称为控制逻辑。所以,从宏观的视角来看,设计一个CPU就是设计“数据通路+控制逻辑”。 +在计算机结构设计实验中,需要设计实现的CPU也是一个数字逻辑电路,其设计应该遵循数字逻辑电路设计的一般性方法。CPU不但要完成运算,也要维持自身的状态,所以CPU一定是既有组合逻辑电路又有时序逻辑电路的。CPU输入的、运算的、存储的、输出的数据都在组合逻辑电路和时序逻辑电路上流转,这些逻辑电路被称为数据通路(Datapath)。因此,要设计CPU这个数字逻辑电路,首要的工作就是设计数据通路。同时,因为数据通路中会有多路选择器、时序逻辑器件,所以还要有相应的控制信号,产生这些控制信号的逻辑被称为控制逻辑。所以,从宏观的视角来看,设计一个CPU就是设计“数据通路+控制逻辑”。 #fakepar #figure( @@ -107,13 +107,13 @@ RISC-V单周期CPU设计实现简单,控制器部分是纯组合逻辑电路 )<理想的五级流水线CPU数据与控制信号传递图> #fakepar -根据指令系统规范中的定义设计出“数据通路+控制逻辑”的基本方法是:对指令系统中定义的指令逐条进行功能分解,得到一系列操作和操作的对象。显然,这些操作和操作的对象必然对应其各自的数据通路,又因为指令间存在一些相同或相近的操作和操作对象,所以我们可以只设计一套数据通路供多个指令公用。对于确实存在差异无法共享数据通路的情况,只能各自设计一套,再用多路选择器从中选择出所需的结果。接下来,我们将遵循这个一般性方法,具体介绍如何分析指令的功能以及如何设计出数据通路。@理想的五级流水线CPU数据与控制信号传递图 展示了RISC-V理想的五级流水线CPU数据与控制信号传递图。 +根据指令系统规范中的定义设计出“数据通路+控制逻辑”的基本方法是:对指令系统中定义的指令逐条进行功能分解,得到一系列操作和操作的对象。显然,这些操作和操作的对象必然对应其各自的数据通路,又因为指令间存在一些相同或相近的操作和操作对象,所以可以只设计一套数据通路供多个指令公用。对于确实存在差异无法共享数据通路的情况,只能各自设计一套,再用多路选择器从中选择出所需的结果。遵循这个一般性方法,下面具体介绍如何分析指令的功能以及如何设计出数据通路。@理想的五级流水线CPU数据与控制信号传递图 展示了RISC-V理想的五级流水线CPU数据与控制信号传递图。 === 以ADD指令为例 -我们来分析一下ADD指令需要哪些数据通路部件。 +以ADD指令为例,分析一下该指令需要哪些数据通路部件。 -首先,我们需要得到ADD这条指令:需要使用这条指令对应的PC作为虚拟地址进行虚实地址转换,得到访问指令SRAM的物理地址。这意味着需要的数据通路部件有:取指单元、虚实地址转换部件和指令SRAM。我们先对这部分的数据通路进行实现。 +首先,执行指令的前提是需要得到ADD这条指令,需要使用这条指令对应的PC作为虚拟地址进行虚实地址转换,得到访问指令SRAM的物理地址。这意味着需要的数据通路部件有:取指单元、虚实地址转换部件和指令SRAM。下面先对这部分的数据通路进行实现。 #noindent #strong(text(12pt, red)[前端]) @@ -125,7 +125,7 @@ RISC-V单周期CPU设计实现简单,控制器部分是纯组合逻辑电路 #noindent #text(fill: blue)[(#unitcnt_inc)取指单元] -因为实现的是一个64位的处理器,所以PC的指令宽度是64比特。我们用一组64位的触发器来存放PC。(后面为了行文简洁,在不会导致混淆的情况下,我们用pc代表这组用于存放PC的64位触发器。)由于我们的处理器使用到了SRAM进行数据的存取,而SRAM的特性是一次读数操作需要跨越两个时钟周期,第一个时钟周期向RAM发出读使能和读地址,第二个时钟周期RAM才能返回读结果。因此我们发送给指令SRAM的地址应该是下一拍的PC,也就是pc_next,而目前的实验设计pc_next的大小将一直等于pc+4(这里的4代表寻址4个字节,即一条指令的宽度),因此pc_next和pc之间只是组合逻辑的关系。 +因为实现的是一个64位的处理器,所以PC的指令宽度是64比特。用一组64位的触发器来存放PC。(后面为了行文简洁,在不会导致混淆的情况下,用pc代表这组用于存放PC的64位触发器。)由于处理器使用到了SRAM进行数据的存取,而SRAM的特性是一次读数操作需要跨越两个时钟周期,第一个时钟周期向RAM发出读使能和读地址,第二个时钟周期RAM才能返回读结果。因此发送给指令SRAM的地址应该是下一条指令的PC,也就是pc_next,而目前的实验设计pc_next的大小将一直等于pc+4(这里的4代表寻址4个字节,即一条指令的宽度),因此pc_next和pc之间只是组合逻辑的关系。 #fakepar #figure( @@ -134,38 +134,40 @@ RISC-V单周期CPU设计实现简单,控制器部分是纯组合逻辑电路 )<取指单元及指令RAM> #fakepar -pc的输出将送到指令SRAM中用于获取指令,由于我们的指令SRAM的地址宽度只有#paddr_wid 位,因此只有pc的低#paddr_wid 会被使用。目前来看,PC的输入有两个,一个是复位值0x80000000(由于发送给指令SRAM的是pc_next,所以pc的真正复位值其实是0x80000000-0x4),一个是复位撤销之后pc_next的值。 +pc的输出将送到指令SRAM中用于获取指令,由于指令SRAM的地址宽度只有#paddr_wid 位,因此只有pc的低#paddr_wid 会被使用。目前来看,PC的输入有两个,一个是复位值0x80000000(由于发送给指令SRAM的是pc_next,所以pc的真正复位值其实是0x80000000-0x4),一个是复位撤销之后pc_next的值。 因为取指单元只会对内存进行读操作,因此inst_sram_en只要在reset无效时使能即可,而inst_sram_wen应该恒为低电平。@取指单元及指令RAM 展示了取指单元的结构。 #noindent #text(fill: blue)[(#unitcnt_inc)虚实地址转换] -任何时候CPU上运行的程序中出现的地址都是虚地址,而CPU本身访问内存、I/O所用的地址都是物理地址,因此我们需要对CPU发出的虚拟地址进行转换,使用物理地址进行访存。在实现RISC-V的S模式之前,目前我们实现的CPU的虚拟地址与物理地址之间使用直接映射的方式,即物理地址的值等于虚拟地址的值。因此虚实地址转换部件目前可以先省略。 +任何时候CPU上运行的程序中出现的地址都是虚地址,而CPU本身访问内存、I/O所用的地址都是物理地址,因此需要对CPU发出的虚拟地址进行转换,使用物理地址进行访存。在实现RISC-V的S模式之前,目前实现的CPU的虚拟地址与物理地址之间使用直接映射的方式,即物理地址的值等于虚拟地址的值。因此虚实地址转换部件目前可以先省略。 #noindent #text(fill: blue)[(#unitcnt_inc)指令RAM] -得到取指所需的物理地址后,接下来就要将该地址送往内存。我们采用片上的RAM作为内存,并且将RAM进一步分拆为指令RAM和数据RAM两块物理上独立的RAM以简化设计。 +得到取指所需的物理地址后,接下来需要将该地址送往内存。实验采用片上的RAM作为内存,并且将RAM进一步分拆为指令RAM和数据RAM两块物理上独立的RAM以简化设计。 -指令RAM输出的#inst_ram_date_wid 位数据就是指令码。本书中我们实现的CPU采用小尾端的寻址,所以指令RAM输出的#inst_ram_date_wid 位数据与指令系统规范中的定义的字节顺序是一致的,不需要做任何字节序调整。 +指令RAM输出的#inst_ram_date_wid 位数据就是指令码。实验实现的CPU采用小尾端的寻址,所以指令RAM输出的#inst_ram_date_wid 位数据与指令系统规范中的定义的字节顺序是一致的,不需要做任何字节序调整。 @取指单元及指令RAM 展示了指令RAM的结构。指令RAM保留了写接口,这样的接口设计,是为了和之后的AXI的设计保持一致性。 #noindent #text(fill: blue)[(#unitcnt_inc)指令队列] -我们将取指单元/译码单元之间的流水线缓存称为指令队列。我们将指令队列之前的阶段称为前端,将指令队列之后的阶段称为后端。当取指单元一次取指的数量大于译码单元可以解码的数量时,又或是后端流水线发生暂停时,取指单元可以继续取指,多余的指令可以在指令队列中排队等待,而不用暂停取指。通过指令队列这个部件可以解耦前后端。 +取指单元/译码单元之间的流水线缓存称为指令队列。指令队列之前的阶段称为前端,指令队列之后的阶段称为后端。 + +当取指单元一次取指的数量大于译码单元可以解码的数量时,又或是后端流水线发生暂停时,取指单元可以继续取指,多余的指令可以在指令队列中排队等待,而不用暂停取指。因此指令队列部件的实现可以解耦前后端。 #fakepar #figure( - image("image/image-20240109153542526.png"), + image("image/image-20240124134556170.png"), caption: "指令队列" )<指令队列> #fakepar -如@指令队列 所示,指令队列的实现是一个深度为depth的寄存器组,每个寄存器中保存一个叫做data的数据包(目前我们需要保存指令的内容以及指令的PC这两个数据),宽度应该和data的宽度一致。出队指针和入队指针都是一个宽度为$log_2 lr(("depth"), size: #50%)$的寄存器。我们使用出队指针指示队列的头部,入队指针指示队列的尾部。由取指单元发送的数据存入入队指针指示的寄存器;出队指针指示的寄存器保存的数据发送到译码单元中。目前我们实现的是理想流水线,因此每一个clock我们的入队指针和出队指针都应该加1,发生reset时,两个指针都应该置为0。 +如@指令队列 所示,指令队列的实现是一个深度为depth的寄存器组,每个寄存器中保存一个叫做data的数据包(目前需要保存指令的内容以及指令的PC这两个数据),宽度应该和data的宽度一致。出队指针和入队指针都是一个宽度为$log_2 lr(("depth"), size: #50%)$的寄存器。使用出队指针指示队列的头部,入队指针指示队列的尾部。由取指单元发送的数据存入入队指针指示的寄存器;出队指针指示的寄存器保存的数据发送到译码单元中。目前实现的是理想流水线,因此每一个clock的上跳沿来临时入队指针和出队指针都应该加1,发生reset时,两个指针都应该置为0。 #noindent #strong(text(12pt, fill: red)[后端]) -前端部分我们已经成功取得指令,接下来我们需要通过译码识别出这条指令为ADD指令,并产生相应的控制信号。 +前端部分已经成功取得指令,接下来需要通过译码识别出这条指令为ADD指令,并产生相应的控制信号。 #noindent #text(fill: blue)[(#unitcnt_inc)译码单元] @@ -173,25 +175,224 @@ pc的输出将送到指令SRAM中用于获取指令,由于我们的指令SRAM (a)译码器 -首先我们要明白译码器是如何解码不同指令的。RISC-V有6种指令格式,如@R型指令格式 所示。译码器根据指令的opcode段识别出指令的格式,再进行下一步的译码。 +首先需要明白译码器是如何解码不同指令的。RISC-V有6种指令格式,如@RISC-V指令格式 所示。译码器根据指令的opcode段识别出指令的格式,再进行下一步的译码。 #fakepar #figure( - image("image/image-20240109144722861.png"), - caption: "R型指令格式" -) + tablex( + columns: 33, + align: center + horizon, + repeat-header: true, + header-rows: 1, + auto-lines: false, + + vlinex(x:0, start:1), vlinex(x:7, start:1), vlinex(x:12, start:1), vlinex(x:17, start:1), vlinex(x:20, start:1), vlinex(x:25, start:1), vlinex(x:32, start:1), + + cellx(align: left, colspan: 6)[31], cellx(align: right)[25], cellx(align: left, colspan: 4)[24], cellx(align: right)[20], cellx(align: left, colspan: 4)[19], cellx(align: right)[15], cellx(align: left, colspan: 2)[14], cellx(align: right)[12], cellx(align: left, colspan: 4)[11], cellx(align: right)[7], cellx(align: left, colspan: 6)[6], cellx(align: right)[0], [], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[funct7], colspanx(5, inset:(x: 2em))[rs2], colspanx(5, inset:(x: 2em))[rs1], colspanx(3)[funct3], colspanx(5, inset:(x: 2em))[rd], colspanx(7, inset:(x: 1.5em))[opcode], [R-type], + + hlinex(end:32), + + colspanx(12)[imm[11:0]], colspanx(5)[rs1], colspanx(3)[funct3], colspanx(5)[rd], colspanx(7)[opcode], [I-type], + + hlinex(end:32), + + colspanx(7)[imm[15:5]], colspanx(5)[rs2], colspanx(5)[rs1], colspanx(3)[funct3], colspanx(5)[imm[4:0]], colspanx(7)[opcode], [S-type], + + hlinex(end:32), + + colspanx(7)[imm[12|10:5]], colspanx(5)[rs2], colspanx(5)[rs1], colspanx(3)[funct3], colspanx(5)[imm[4:1|11]], colspanx(7)[opcode], [B-type], + + hlinex(end:32), + + colspanx(20)[imm[31:12]], colspanx(5)[rd], colspanx(7)[opcode], [U-type], + + hlinex(end:32), + + colspanx(20)[imm[20|10:1|11|19:12]], colspanx(5)[rd], colspanx(7)[opcode], [J-type], + + hlinex(end:32), + ), + caption: "RISC-V指令格式", + kind: table +) #fakepar -在本实验中,我们只需要实现R型的运算指令。@R型运算指令 展示了RV64中所有R型运算指令。我们先分析非字指令,不难发现,R型运算指令的opcode是0110011,再通过func3区别各指令的运算类型,其中ADD和SUB、SRL和SRA的func3一致,再由func7的第6位进行区分。而字指令的分析也和非字指令的分析一致。 +本实验只需要实现R型的运算指令。@R型运算指令 展示了RV64中所有R型运算指令。先分析非字指令,不难发现,R型运算指令的opcode是0110011,再通过func3区别各指令的运算类型,其中ADD和SUB、SRL和SRA的func3一致,再由func7的第6位进行区分。而字指令的分析也和非字指令的分析一致。 #fakepar -#figure( - image("image/image-20240109161753616.png", width: 95%), - caption: "R型运算指令" -) +#[ + #show figure: set block(breakable: true) + #figure( + tablex( + columns: 33, + align: center + horizon, + repeat-header: true, + header-rows: 1, + auto-lines: false, + + map-cells: cell => { + if (cell.x == 32) { + cell.content = { + set align(left) + cell.content + } + } + cell + }, + + vlinex(x:0, start:1), vlinex(x:6, start:1), vlinex(x:7, start:1), vlinex(x:12, start:1), vlinex(x:17, start:1), vlinex(x:20, start:1), vlinex(x:25, start:1), vlinex(x:32, start:1), + + cellx(align: left, colspan: 6)[31], cellx(align: right)[25], cellx(align: left, colspan: 4)[24], cellx(align: right)[20], cellx(align: left, colspan: 4)[19], cellx(align: right)[15], cellx(align: left, colspan: 2)[14], cellx(align: right)[12], cellx(align: left, colspan: 4)[11], cellx(align: right)[7], cellx(align: left, colspan: 6)[6], cellx(align: right)[0], [], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[000], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0110011], [ADD], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0100000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[000], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0110011], [SUB], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[001], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0110011], [SLL], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[010], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0110011], [SLT], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[011], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0110011], [SLTU], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[100], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0110011], [XOR], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[101], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0110011], [SRL], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0100000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[101], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0110011], [SRA], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[110], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0110011], [OR], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[111], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0110011], [AND], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[000], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0111011], [ADDW], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0100000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[000], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0111011], [ADDW], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[001], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0111011], [SLLW], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0000000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[101], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0111011], [SRLW], + + hlinex(end:32), + + colspanx(7, inset:(x: 5em))[0100000], + colspanx(5, inset:(x: 2em))[rs2], + colspanx(5, inset:(x: 2em))[rs1], + colspanx(3)[101], + colspanx(5, inset:(x: 2em))[rd], + colspanx(7, inset:(x: 1.5em))[0111011], [SRAW], + + hlinex(end:32), + + ), + caption: "R型运算指令", + kind: table + ) +] #fakepar -R型运算指令都是三地址指令,每条指令都有两个源操作数以及一个目的操作数。我们记源操作数1为src1,源操作数2为src2。因此译码器中要产生的控制信号如下: +R型运算指令都是三地址指令,每条指令都有两个源操作数以及一个目的操作数。记源操作数1为src1,源操作数2为src2。因此译码器中要产生的控制信号如下: - src1_ren:src1 是否需要读通用寄存器堆 - src1_raddr:src1 的通用寄存器堆读地址 @@ -201,16 +402,15 @@ R型运算指令都是三地址指令,每条指令都有两个源操作数以 - reg_wen:是否需要写回通用寄存器堆 - reg_waddr:通用寄存器堆的写地址 -#indent 接下来我们应该阅读手册,根据手册对指令功能的定义对各控制信号进行赋值。@ADD指令定义 展示了ADD指令的定义。 +#indent 接下来阅读手册,根据手册对指令功能的定义对各控制信号进行赋值。@ADD指令定义 展示了ADD指令的定义。 -#fakepar #figure( - image("image/image-20240107213027949.png"), + image("image/image-20240124141127010.png"), caption: "ADD指令定义" ) #fakepar -ADD指令的源操作数都来自通用寄存器堆,因此src1_ren和src2_ren都为1,src1_raddr对应指令的19至15位,src2_raddr对应指令的24至20位。ADD指令需要写回通用寄存器堆,因此reg_wen为1,reg_waddr对应指令的11至7位。因为目前我们要实现的指令都在执行单元的ALU中进行运算,因此只需要将op设置正确就能完成指令的区分。op的设置有多种方法,下面介绍两种: +ADD指令的源操作数都来自通用寄存器堆,因此src1_ren和src2_ren都为1,src1_raddr对应指令的19至15位,src2_raddr对应指令的24至20位。ADD指令需要写回通用寄存器堆,因此reg_wen为1,reg_waddr对应指令的11至7位。因为目前要实现的指令都在执行单元的ALU中进行运算,因此只需要将op设置正确就能完成指令的区分。op的设置有多种方法,下面介绍两种: 1. 简单的方法: @@ -218,15 +418,15 @@ ADD指令的源操作数都来自通用寄存器堆,因此src1_ren和src2_ren 2. 稍微复杂一些的方法: -#indent 观察指令格式进行op的设计,比如非字指令的opcode一致,仅func3以及func7的第6位有区别,那么我们可以将这几位进行拼接,由于ADD又有ADD和ADDW的区别,因此我们可以再加一位进行字指令的区分,因此ADD的op可以设计为00000。按这种思路,SRAW的op就是11101。 +#indent 观察指令格式进行op的设计,比如非字指令的opcode一致,仅func3以及func7的第6位有区别,那么可以将这几位进行拼接,由于ADD又有ADD和ADDW的区别,因此可以再加一位进行字指令的区分,因此ADD的op可以设计为00000。按这种思路,SRAW的op就是11101。 -不同的op设计对于FU(FunctionUnit,功能部件)内部的解码会有一定的影响,我们等下在ALU的设计中会进行介绍。 +不同的op设计对于FU(FunctionUnit,功能部件)内部的解码会有一定的影响,好的编码设计可以大大提升硬件性能。 (b)通用寄存器堆 -完成了控制信号的生成,接下来我们需要准备源操作数,也就是访问通用寄存器堆,相比SRAM这种存储类型,通用寄存器堆的访问都是当拍完成。通用寄存器堆可以实现在译码单元内部,也可以直接实现在CPU内部作为一个独立模块,两者没有什么太大的区别。 +完成了控制信号的生成,接下来需要准备源操作数,也就是访问通用寄存器堆,相比SRAM这种存储类型,通用寄存器堆的访问都是当拍完成。通用寄存器堆可以实现在译码单元内部,也可以直接实现在CPU内部作为一个独立模块,两者没有什么太大的区别。 -@译码单元 展示了译码单元的结构,译码器将从指令队列获得的指令进行译码,产生了相关的控制信号,我们将所有的控制信号打包成info数据包与寄存器堆读回的源操作数组成的src_info数据包以及指令队列获得的pc(传递pc的原因会在后面进行解释)一起打包成一个data数据包发送至下一级流水线缓存。 +@译码单元 展示了译码单元的结构,译码器将从指令队列获得的指令进行译码,产生了相关的控制信号,将所有的控制信号打包成info数据包与寄存器堆读回的源操作数组成的src_info数据包以及指令队列获得的pc一起打包成一个data数据包发送至下一级流水线缓存。 #fakepar #figure( @@ -237,7 +437,7 @@ ADD指令的源操作数都来自通用寄存器堆,因此src1_ren和src2_ren #noindent #text(fill: blue)[(#unitcnt_inc)执行级缓存] -我们将译码单元/执行单元这两级间的缓存称为执行级缓存,将缓存的名称归到它的输出对应的那一个流水阶段。这种命名风格来自我们进行仿真波形调试时的观察习惯,即除了触发器时钟采样边沿附近那段建立保持时间(当采用零延迟仿真时,这段时间可以看作瞬时、无穷小)外,触发器中存储的内容都是与它输出那一级流水的组合逻辑相关联的。后面也将统一采用这种单级流水线标识的方式。 +译码单元/执行单元这两级间的缓存称为执行级缓存,将缓存的名称归到它的输出对应的那一个流水阶段。这样子命名可以与仿真波形调试时的观察习惯保持一致,即除了触发器时钟采样边沿附近那段建立保持时间(当采用零延迟仿真时,这段时间可以看作瞬时、无穷小)外,触发器中存储的内容都是与它输出那一级流水的组合逻辑相关联的,如执行单元中运行的指令实际上是译码单元/执行单元这两级缓存中存储的数据。后面流水线级间缓存也将统一采用这种标识的方式。 #fakepar #figure( @@ -246,7 +446,7 @@ ADD指令的源操作数都来自通用寄存器堆,因此src1_ren和src2_ren )<执行级缓存> #fakepar -@执行级缓存 展示了执行级缓存的结构。执行级缓存内部有一个用于保存上一级传来的data的寄存器,在每个clock的上跳沿更新寄存器内容。寄存器保存的内容直接传往下一级。 +@执行级缓存 展示了执行级缓存的结构。执行级缓存内部有一个用于保存上一级传来的data数据包的寄存器,在每个clock的上跳沿更新寄存器内容。寄存器保存的内容直接传往下一级。 #noindent #text(fill: blue)[(#unitcnt_inc)执行单元] @@ -259,7 +459,7 @@ ADD指令的源操作数都来自通用寄存器堆,因此src1_ren和src2_ren )<执行单元> #fakepar -@执行单元 展示了执行单元的结构,R型运算指令只需要使用ALU这一个部件即可(ALU部件已经在数字电路实验中学习过)。我们将执行级缓存传来的data数据包(包内包括info数据包和src_info数据包)发送至ALU中。ALU可以通过info数据包内op的区别进行不同的计算操作,而源操作数在src_info数据包中。ALU将运算结果reg_wdata打包到rd_info数据包中和info数据包内的reg_wen、reg_waddr一块打包成新的data数据包发送至访存级缓存。 +@执行单元 展示了执行单元的结构,R型运算指令只需要使用ALU这一个部件即可(ALU部件已经在数字电路实验中学习过)。将执行级缓存传来的data数据包(包内包括info数据包和src_info数据包)发送至ALU中。ALU可以通过info数据包内op的区别进行不同的计算操作,而源操作数在src_info数据包中。ALU将运算结果reg_wdata打包到rd_info数据包中和info数据包内的reg_wen、reg_waddr一块打包成新的data数据包发送至访存级缓存。 #noindent #text(fill: blue)[(#unitcnt_inc)访存级缓存] @@ -281,7 +481,7 @@ ADD指令的源操作数都来自通用寄存器堆,因此src1_ren和src2_ren )<访存单元> #fakepar -ADD指令并不需要访问内存,因此在该流水级什么也不做,只需要将上一级缓存内的data数据包传到下一级缓存中即可。@访存单元 展示了访存单元的结构。我们将data_sram_en和data_sram_wen置为0,对于data_sram_addr、data_sram_wdata以及data_sram_rdata这三个信号就不需要理会了。 +ADD指令并不需要访问内存,因此在该流水级什么也不做,只需要将上一级缓存内的data数据包传到下一级缓存中即可。@访存单元 展示了访存单元的结构。将data_sram_en和data_sram_wen置为0,对于data_sram_addr、data_sram_wdata以及data_sram_rdata这三个信号就不需要理会了(在chisel语言中可以使用DontCare对data_sram_addr和data_sram_wdata进行赋值)。 #noindent #text(fill: blue)[(#unitcnt_inc)写回级缓存] @@ -296,7 +496,7 @@ ADD指令并不需要访问内存,因此在该流水级什么也不做,只 #noindent #text(fill: blue)[(#unitcnt_inc)写回单元] -ADD指令需要写回通用寄存器堆,因此我们需要在写回级访问通用寄存器堆。我们将data数据包解包,将info数据包内的reg_wen、reg_waddr和reg_wdata发送至通用寄存器堆,同时这些信号还需要与pc一起发往CPU的外部作为debug信号。 +ADD指令需要写回通用寄存器堆,因此需要在写回级访问通用寄存器堆。将data数据包解包,将info数据包内的reg_wen、reg_waddr和reg_wdata发送至通用寄存器堆,同时这些信号还需要与pc一起发往CPU的外部作为debug信号。 #fakepar #figure( @@ -307,7 +507,7 @@ ADD指令需要写回通用寄存器堆,因此我们需要在写回级访问 == 开发环境的组织与结构 -整个CPU设计开发环境(#dir_name)的目录结构及各主要部分的功能如下所示。其中只有标黑色的部分是需要大家自行开发的,其余部分都已经设计好了。 +整个CPU设计开发环境(#dir_name)的目录结构及各主要部分的功能如下所示。其中只有标黑色的部分是需要自行开发的,其余部分均为开发环境框架代码已经设计完毕。 // #fakepar // #figure( @@ -318,9 +518,9 @@ TODO:增加目录结构图 === 验证所用的计算机硬件系统 -单纯实现一个CPU没有什么实用价值,通常我们需要基于CPU搭建一个计算机硬件系统。在本书中,我们也是基于CPU搭建一个计算机硬件系统,然后通过在这个计算机硬件系统上运行测试程序来完成CPU的功能验证。 +单纯实现一个CPU没有什么实用价值,通常需要基于CPU搭建一个计算机硬件系统。本实验也是基于CPU搭建一个计算机硬件系统,然后通过在这个计算机硬件系统上运行测试程序来完成CPU的功能验证。 -在引入AXI总线接口设计之前,我们将采用一个简单的计算机硬件系统。这个硬件系统将通过FPGA开发板实现。其核心是在FPGA芯片上实现的一个片上系统(System On Chip,SoC)。这个SoC芯片通过引脚连接电路板上的时钟晶振、复位电路,以及LED灯、数码管、按键等外设接口设备。SoC芯片内部也是一个小系统,其顶层为SoC_Lite,内部结构如@基于myCPU的简单Soc结构 所示,对应的RTL代码均位于mycpu_verify/rtl/目录下。我们重点关注SoC_Lite这个小系统。可以看到,SoC_Lite的核心是我们将要实现的CPU——myCPU。这个CPU与指令RAM(Inst Sram)和数据RAM(Data Sram)进行交互,完成取指和访存的功能。除此之外,这个小系统中还包含PLL、Peripherals等模块。 +在引入AXI总线接口设计之前,实验将采用一个简单的计算机硬件系统。这个硬件系统将通过FPGA开发板实现。其核心是在FPGA芯片上实现的一个片上系统(System On Chip,SoC)。这个SoC芯片通过引脚连接电路板上的时钟晶振、复位电路,以及LED灯、数码管、按键等外设接口设备。SoC芯片内部也是一个小系统,其顶层为SoC_Lite,内部结构如@基于myCPU的简单Soc结构 所示,对应的RTL代码均位于mycpu_verify/rtl/目录下。只需重点关注SoC_Lite这个小系统。可以看到,SoC_Lite的核心是实验将要实现的CPU——myCPU。这个CPU与指令RAM(Inst Sram)和数据RAM(Data Sram)进行交互,完成取指和访存的功能。除此之外,这个小系统中还包含PLL、Peripherals等模块。 #fakepar #figure( @@ -329,25 +529,25 @@ TODO:增加目录结构图 )<基于myCPU的简单Soc结构> #fakepar -指令RAM和数据RAM与CPU之间的关系大家已经了解了。这里简单解释一下PLL、Peripherals以及myCPU与Data Sram、Peripherals之间的二选一功能。 +在数据通路分析中已经阐述了指令RAM和数据RAM与CPU之间的关系。这里简单解释一下PLL、Peripherals以及myCPU与Data Sram、Peripherals之间的二选一功能。 -我们在开发板上给FPGA芯片提供的时钟(来自时钟晶振)主频是#soc_freq。如果直接使用这个时钟作为SoC_Lite中各个模块的时钟,则意味着myCPU的主频至少要能达到#soc_freq。对于初学者来说,这可能是个比较严格的要求,因此我们添加了一个PLL IP,将其输出时钟作为myCPU的时钟输入。这个PLL以#soc_freq 输入时钟作为参考时钟,输出时钟频率可以配置为低于#soc_freq。 +开发板上给FPGA芯片提供的时钟(来自时钟晶振)主频是#soc_freq。如果直接使用这个时钟作为SoC_Lite中各个模块的时钟,则意味着myCPU的主频至少要能达到#soc_freq。对于初学者来说,这可能是个比较严格的要求,因此实验添加了一个PLL IP,将其输出时钟作为myCPU的时钟输入。这个PLL以#soc_freq 输入时钟作为参考时钟,输出时钟频率可以配置为低于#soc_freq。 myCPU通过访问Peripherals部件来驱动板上的LED灯、数码管,接收外部按键的输入。其操控的原理如下:外部的LED灯、数码管以及按键都是通过导线直接连接到FPGA的引脚上的,通过控制FPGA输出引脚上的电平的高、低就可以控制LED灯和数码管。同样,也可以通过观察FPGA输入引脚上电平的变化来判断一个按键是否按下。这些FPGA引脚又进一步连接到Peripherals部件中某些寄存器的某些位上,所以myCPU可以通过写Peripherals部件寄存器控制输出引脚的电平来控制LED灯和数码管,也可以通过读Peripherals部件寄存器来知晓连接到按键的引脚是高电平还是低电平。 -myCPU和dram、Peripherals之间有一个“一分二”部件。这是因为在RISC-V指令系统架构下,所有I/O设备的寄存器都是采用Memory Mapped方式访问的。我们这里实现的Peripherals也不例外。MemoryMapped访问方式意味I/O设备中的寄存器各自有一个唯一内存编址,所以CPU可以通过load、store 指令对其进行访问。不过,dram作为内存也是通过load、store指令进行访问的。对于一条load或store指令来说,如何知晓它访问的是Peripherals还是dram?我们在设计SoC的时候可以用地址对其进行区分。因此在设计SoC的数据通路时就需要在这里引入一个“一分二”部件,它的选择控制信号是通过对访存的地址范围进行判断而得到的。 +myCPU和dram、Peripherals之间有一个“一分二”部件。这是因为在RISC-V指令系统架构下,所有I/O设备的寄存器都是采用Memory Mapped方式访问的,这里实现的Peripherals也不例外。MemoryMapped访问方式意味I/O设备中的寄存器各自有一个唯一内存编址,所以CPU可以通过load、store 指令对其进行访问。不过,dram作为内存也是通过load、store指令进行访问的。对于一条load或store指令来说,如何知晓它访问的是Peripherals还是dram?在设计SoC的时候可以用地址对其进行区分(相当于存在一个数据选择器,其根据地址选择数据)。因此在设计SoC的数据通路时就需要在这里引入一个“一分二”部件,它的选择控制信号是通过对访存的地址范围进行判断而得到的。 -这里要提醒大家的是,因为整个SoC_Lite的设计都要实现到FPGA芯片中,所以在进行综合实现的时候,你选择的顶层应该是SoC_Lite,而不是你自己写的myCPU。 +这里要提醒是,因为整个SoC_Lite的设计都要实现到FPGA芯片中,所以在进行综合实现的时候,所需选择的顶层应该是SoC_Lite,而不是设计实现的myCPU。 === 验证所用的计算机仿真系统 -由于上板的限制条件很多,这里我们再介绍我们实现的软件仿真方法,其运行效果与上板仿真几乎无异,但效率更高且更加方便。 +由于上板的限制条件很多,这里再介绍软件的仿真方法,其运行效果与上板仿真几乎无异,但效率更高且更加方便。 TODO:仿真结构 === myCPU的顶层接口 -为了让大家设计的CPU能够直接集成到本平台所提供的CPU实验环境中,我们要对CPU的顶层接口做出明确的规定。myCPU顶层接口信号的详细定义如@差分测试框架接口 以及@myCPU顶层接口信号的描述 所示。只要大家设计的CPU符合这样的顶层接口就可以接入我们的差分测试框架中使用。 +为了使设计的CPU能够直接集成到本平台所提供的CPU实验环境中,这里要对CPU的顶层接口做出明确的规定。myCPU顶层接口信号的详细定义如@差分测试框架接口 以及@myCPU顶层接口信号的描述 所示。只要设计的CPU符合这样的顶层接口就可以接入的差分测试框架中使用。 #fakepar #figure( @@ -406,7 +606,7 @@ TODO:仿真结构 == 差分测试 -可能你会觉得疑惑,为什么在写回级将pc以及通用寄存器的写回信号发往CPU外部,其实这些信号都是为了进行差分测试服务的。在介绍如何对CPU进行差分测试前,我们先来了解下如何对数字逻辑电路的进行功能验证。 +在介绍如何对CPU进行差分测试前,先了解下如何对数字逻辑电路的进行功能验证。 #fakepar #figure( @@ -415,20 +615,16 @@ TODO:仿真结构 )<功能验证框架> #fakepar -数字逻辑电路的功能验证的作用是检查所设计的数字逻辑电路在功能上是否符合设计目标。简单来说,就是检查设计的电路功能是否正确。我们这里所说的功能验证与软件开发里的功能测试的意图是一样的。但是,要注意,我们使用了“验证”(Verification)这个词,这是为了避免和本领域另一个概念“测试”(Test)相混淆。在集成电路设计领域,测试通常指检查生产出的电路没有物理上的缺陷和偏 +数字逻辑电路的功能验证的作用是检查所设计的数字逻辑电路在功能上是否符合设计目标。简单来说,就是检查设计的电路功能是否正确。这里所说的功能验证与软件开发里的功能测试的意图是一样的。但是,要注意,这里使用了“验证”(Verification)这个词,这是为了避免和本领域另一个概念“测试”(Test)相混淆。在集成电路设计领域,测试通常指检查生产出的电路没有物理上的缺陷和偏 差,能够正常体现设计所期望的电路行为和电气特性。 所谓数字电路的功能仿真验证,就是用(软件模拟)仿真的方式而非电路实测的方式进行电路的功能验证。@功能验证框架 给出了数字电路功能仿真验证的基本框架。 -在这个基本框架中,我们给待验证电路(DUT)一些特定的输入激励,然后观察DUT的输出结果是否和预期一致。这个过程很类似与程序编程里的OJ(Online Judge)测试,通过输入不同的测试数据得到不同的运行结果,我们再比对运行结果进行判断程序的正确性。 +在这个基本框架中,给待验证电路(DUT)一些特定的输入激励,然后观察DUT的输出结果是否和预期一致。这个过程很类似与程序编程里的OJ(Online Judge)测试,通过输入不同的测试数据得到不同的运行结果,比对运行结果进行判断程序的正确性。 -我们对CPU设计进行功能仿真验证时,沿用的依然是上面的思路,但是在输入激励和输出结果检查方面的具体处理方式与简单的数字逻辑电路设计有区别。对简单数字逻辑电路进行功能仿真验证时,通常是产生一系列变化的激励信号,输入到被验证电路的输入端口上,然后观察电路输出端口的信号,以判断结果是否符合预期。对于CPU来说,其输入/输出端口只有时钟、复位和I/O,采用这种直接驱动和观察输入/输出端口的方式,验证效率太低。 +对CPU设计进行功能仿真验证时,沿用的依然是上面的思路,但是在输入激励和输出结果检查方面的具体处理方式与简单的数字逻辑电路设计有区别。对简单数字逻辑电路进行功能仿真验证时,通常是产生一系列变化的激励信号,输入到被验证电路的输入端口上,然后观察电路输出端口的信号,以判断结果是否符合预期。对于CPU来说,其输入/输出端口只有时钟、复位和I/O,采用这种直接驱动和观察输入/输出端口的方式,验证效率太低。 -我们采用测试程序作为CPU功能验证的激励,即输入激励是一段测试指令序列,这个指令序列通常是用汇编语言或C语言编写、用编译器编译出来的机器代码。我们通过观察测试程序的执行结果是否符合预期来判断CPU功能是否正确。这样做可以大幅度提高验证的效率,但是验证过程中出错后定位出错点的调试难度也相应提升了。考虑到初学者尚不具备强大的调试能力,我们提供了一套基于Trace比对的调试辅助手段,以帮助大家在调试过程中更加快速地定位错误。 - -大家在调试C程序的时候应该都用过单步调试这种调试手段。在通过单步调试这样的“慢动作”来运行程序的每一行代码的情况下,能够看到每一行代码的运行行为是否符合预期,从而及时定位到出错点。我们在本书开发环境中提供给大家的这套基于Trace比对的调试辅助手段,借鉴的就是这种单步调试的策略。 - -具体实现方式是:在myCPU每条指令写寄存器的时候,将myCPU中的PC和写寄存器的信息同模拟器的PC以及写寄存器信号进行比对,如果不一样,那么立刻报错并停止仿真。 +框架采用测试程序作为CPU功能验证的激励,即输入激励是一段测试指令序列,这个指令序列通常是用汇编语言或C语言编写、用编译器编译出来的机器代码。通过观察测试程序的执行结果是否符合预期来判断CPU功能是否正确。这样做可以大幅度提高验证的效率,但是验证过程中出错后定位出错点的调试难度也相应提升了。为了解决这个问题,框架在myCPU每条指令写寄存器的时候,将myCPU中的PC和写寄存器的信息同实现之前的模拟器的PC以及写寄存器信号进行比对,如果不一样,那么立刻报错并停止仿真。 = 实验要求 @@ -460,4 +656,5 @@ TODO:仿真结构 + RISC-V 的运算指令有进行运算结果的溢出判断吗,为什么要这样设计?可以对比 MIPS 指令集进行说明(无溢出判断,相比 MIPS 少了 ADDU 等不判断溢出的指令,应该是为了节省指令编码空间,况且溢出判断可以用软件实现) + 为什么并不是所有的R型计算指令都有对应的字指令? + 请问差分测试框架只有这些debug信号就够了吗?假如有的指令不写回通用寄存器堆呢,这时框架又该如何发现问题?(即使是跳转指令或是访存指令当时未写回寄存器堆,但仍然会影响之后的指令执行结果,只是我们发现问题的时间晚了一些,但不影响问题的发现) ++ 当前处理器采用的是哈佛结构还是冯诺依曼结构? + 谈谈你在实验中碰到了哪些问题?又是如何解决的? \ No newline at end of file diff --git a/doc/计算机结构设计实验/lab09/实现 R 型运算类指令的理想流水线设计实验.md b/doc/计算机结构设计实验/lab09/实现 R 型运算类指令的理想流水线设计实验.md index bd242a1..6d9f711 100644 --- a/doc/计算机结构设计实验/lab09/实现 R 型运算类指令的理想流水线设计实验.md +++ b/doc/计算机结构设计实验/lab09/实现 R 型运算类指令的理想流水线设计实验.md @@ -88,7 +88,7 @@ pc 的输出将送到指令 SRAM 中用于获取指令,由于我们的指令 S 我们将 Fetch/Decode 之间的流水线缓存称为指令队列。我们将指令队列之前的阶段称为前端,将指令队列之后的阶段称为后端。当取指单元一次取指的数量大于译码单元可以解码的数量时,又或是后端流水线发生暂停时,取指单元可以继续取指,多余的指令可以在指令队列中排队等待,而不用暂停取指。通过指令队列这个部件可以解耦前后端。 -![图 9.7 指令队列](image/image-20240109153542526.png) +![image-20240124134556170](image/image-20240124134556170.png) 如图 9.7 所示,指令队列的实现是一个深度为 depth 的寄存器组,每个寄存器中保存一个叫做 data 的数据包(目前我们需要保存指令的内容以及指令的 PC 这两个数据),宽度应该和 data 的宽度一致。出队指针和入队指针都是一个宽度为 $\rm log_2(depth)$的寄存器。我们使用出队指针指示队列的头部,入队指针指示队列的尾部。由取指单元发送的数据存入入队指针指示的寄存器;出队指针指示的寄存器保存的数据发送到译码单元中。目前我们实现的是理想流水线,因此每一个 clock 我们的入队指针和出队指针都应该加 1,发生 reset 时,两个指针都应该置为 0。 @@ -104,7 +104,7 @@ a)译码器 首先我们要明白译码器是如何解码不同指令的。RISC-V 有 6 种指令格式,如图 9.8 所示。译码器根据指令的 opcode 段识别出指令的格式,再进行下一步的译码。 -![图 9.8 R型指令格式](image/image-20240109144722861.png) + 在本实验中,我们只需要实现 R 型的运算指令。图 9.9 展示了 RV64 中所有 R 型运算指令。我们先分析非字指令,不难发现,R 型运算指令的 opcode 是 0110011,再通过 func3 区别各指令的运算类型,其中 ADD 和 SUB、SRL 和 SRA 的 func3 一致,再由 func7 的第 6 位进行区分。而字指令的分析也和非字指令的分析一致。 @@ -124,7 +124,7 @@ R 型运算指令都是三地址指令,每条指令都有两个源操作数以 接下来我们应该阅读手册,根据手册对指令功能的定义对各控制信号进行赋值。图 9.10 展示了 ADD 指令的定义。 -![图 9.10 ADD指令的定义](image/image-20240107213027949.png) +![image-20240124141127010](image/image-20240124141127010.png) ADD 指令的源操作数都来自通用寄存器堆,因此 src1_ren 和 src2_ren 都为 1,src1_raddr 对应指令的 19 至 15 位,src2_raddr 对应指令的 24 至 20 位。