修改了一下lab9的语言表达

This commit is contained in:
Liphen 2024-01-24 14:55:48 +08:00
parent 45fa1e0005
commit 511db78ac9
7 changed files with 254 additions and 57 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -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型指令格式"
)<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
)<RISC-V指令格式>
#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型运算指令"
)<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
)<R型运算指令>
]
#fakepar
R型运算指令都是三地址指令每条指令都有两个源操作数以及一个目的操作数。我们记源操作数1为src1源操作数2为src2。因此译码器中要产生的控制信号如下
R型运算指令都是三地址指令每条指令都有两个源操作数以及一个目的操作数。记源操作数1为src1源操作数2为src2。因此译码器中要产生的控制信号如下
- src1_rensrc1 是否需要读通用寄存器堆
- src1_raddrsrc1 的通用寄存器堆读地址
@ -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指令定义"
)<ADD指令定义>
#fakepar
ADD指令的源操作数都来自通用寄存器堆因此src1_ren和src2_ren都为1src1_raddr对应指令的19至15位src2_raddr对应指令的24至20位。ADD指令需要写回通用寄存器堆因此reg_wen为1reg_waddr对应指令的11至7位。因为目前我们要实现的指令都在执行单元的ALU中进行运算因此只需要将op设置正确就能完成指令的区分。op的设置有多种方法下面介绍两种
ADD指令的源操作数都来自通用寄存器堆因此src1_ren和src2_ren都为1src1_raddr对应指令的19至15位src2_raddr对应指令的24至20位。ADD指令需要写回通用寄存器堆因此reg_wen为1reg_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设计对于FUFunctionUnit功能部件内部的解码会有一定的影响我们等下在ALU的设计中会进行介绍
不同的op设计对于FUFunctionUnit功能部件内部的解码会有一定的影响好的编码设计可以大大提升硬件性能
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 ChipSoC。这个SoC芯片通过引脚连接电路板上的时钟晶振、复位电路以及LED灯、数码管、按键等外设接口设备。SoC芯片内部也是一个小系统其顶层为SoC_Lite内部结构如@基于myCPU的简单Soc结构 所示对应的RTL代码均位于mycpu_verify/rtl/目录下。我们重点关注SoC_Lite这个小系统。可以看到SoC_Lite的核心是我们将要实现的CPU——myCPU。这个CPU与指令RAMInst Sram和数据RAMData Sram进行交互完成取指和访存的功能。除此之外这个小系统中还包含PLL、Peripherals等模块。
在引入AXI总线接口设计之前实验将采用一个简单的计算机硬件系统。这个硬件系统将通过FPGA开发板实现。其核心是在FPGA芯片上实现的一个片上系统System On ChipSoC。这个SoC芯片通过引脚连接电路板上的时钟晶振、复位电路以及LED灯、数码管、按键等外设接口设备。SoC芯片内部也是一个小系统其顶层为SoC_Lite内部结构如@基于myCPU的简单Soc结构 所示对应的RTL代码均位于mycpu_verify/rtl/目录下。只需重点关注SoC_Lite这个小系统。可以看到SoC_Lite的核心是实验将要实现的CPU——myCPU。这个CPU与指令RAMInst Sram和数据RAMData 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的输出结果是否和预期一致。这个过程很类似与程序编程里的OJOnline Judge测试通过输入不同的测试数据得到不同的运行结果我们再比对运行结果进行判断程序的正确性。
在这个基本框架中给待验证电路DUT一些特定的输入激励然后观察DUT的输出结果是否和预期一致。这个过程很类似与程序编程里的OJOnline 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信号就够了吗假如有的指令不写回通用寄存器堆呢这时框架又该如何发现问题即使是跳转指令或是访存指令当时未写回寄存器堆但仍然会影响之后的指令执行结果只是我们发现问题的时间晚了一些但不影响问题的发现
+ 当前处理器采用的是哈佛结构还是冯诺依曼结构?
+ 谈谈你在实验中碰到了哪些问题?又是如何解决的?

View File

@ -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 都为 1src1_raddr 对应指令的 19 至 15 位src2_raddr 对应指令的 24 至 20 位。