体系结构的第一个实验,实际上是中科大的实验,GitHub直接搜ustc_ca可以搜出很多,我也是借鉴了几位大佬的。这个课是计科第一年开课的,所以以后的实验内容可能会有所调整。
这次实验的内容也很简单,用ripes跑一遍即可,这个软件还是很智能的,每一步骤干什么,用的哪个寄存器,以及数值是多少都能显示出来。
闲麻烦不想在GitHub上下载的,直接放到网盘了。
具体用法是先打开ripes.exe,左上角editor->setiing,然后
选择另一个文件夹里的编译器,然后就可以运行代码了。
一般这个实验都在期中前做的,所以一定要认认真真的跑一遍流水线,把一些基本指令的流程都弄懂,期中必考的。
还有一个坑就是bge指令的立即数那里,它那个是要左移一位的,最高位补的数字和次高位一样
参考提供为了更好的理解RISC-V,通过学习RV32I Core的设计图,理解每条指令的数据流和控制信号,为之后指令流水线及乱序发射实验打下基础。
各部分的部件的主要控制信号如下:
1.HarzardUnit
流水线冲突处理模块,基本手段:(1)插入气泡stall
,(2)定向路径(前递,转发)forward
,(3)冲刷流水段flush
,组合逻辑电路,信号说明:
输入:
CpuRst
: 外部信号,用来初始化CPU,当CpuRst1时CPU全局复位清零(所有段寄存器flush),Cpu_Rst0时cpu开始执行指令ICacheMiss, DCacheMiss
:为后续实验预留信号,暂时可以无视,用来处理cache missBranchE
,JalrE
,JalD
: 控制相关处理信号Rs1D
,Rs2D
,Rs1E
,Rs2E
,RdE
,RdM
,RdW
: 译码,执行,访存,写会阶段处理数据相关的信号,对应的源寄存器和目标寄存器号码。RegReadE
: 标记A1
和A2
对应的寄存器值是否被用到。MemToRegE
: 标志EX段从data mamory
加载数据到寄存器RegWriteM
,RegWriteW
: 标记MEM段和WB段是否有目标寄存器写入操作。输出:
StallF
,FlushF
: IF段插入气泡(维持状态不变)/冲刷(清零)StallD
,FlushD
: ID段插入气泡/冲刷StallE
,FlushE
: EX段插入气泡/冲刷StallM
,FlushM
: MEM段插入气泡/冲刷StallW
,FlushW
: WB段插入气泡/冲刷Forward1E
,Forward2E
: 定向路径控制信号2.ControlUnit
控制模块(译码器),根据指令的操作码部分Op
,func3部分Fn3
和func7部分Fn7
产生如下控制信号:
输入:
Op
:是指令的操作码部分Fn3
:是指令的func3部分Fn7
:是指令的func7部分输出:
JalD==1
: 标志Jal
指令到达指令ID译码阶段JalrD==1
: 标志Jalr
指令到达指令ID译码阶段RegWriteD
: 表示指令ID译码阶段的寄存器写入模式MemToRegD==1
: 标志ID阶段指令需要从data memory
读取数据到寄存器MemWriteD
: 共4bit,为1的部分有效,指示data memory
的四个字节中哪些需要写入LoadNpcD
: 标志将NextPC
输出到ResultM
RegReadD
: 标志两个源寄存器的使用情况,RegReadD[1]
== 1,表示A1
对应的寄存器值被使用到了,RegReadD[0]
== 1,表示A2
对应的寄存器值被使用到了,用于forward处理BranchTypeD
: 表示不同分支类型(参见BranchDecisionMaking
部分)AluContrlD
: 表示不同算数逻辑运算种类(参见ALU
部分)AluSrc2D
: Alu输入源Operand2
的选择AluSrc1D
: Alu输入源Operand1
的选择ImmType
: 立即数编码格式类型3.NPC_Generator
用来生成Next PC值的模块,根据不同的跳转信号选择不同的新PC值
输入:
PCF
:旧的PC值JalrTarget
:jalr指令的对应的跳转目标BranchTarget
:branch指令的对应的跳转目标JalTarget
:jal指令的对应的跳转目标BranchE==1
:Ex阶段的Branch指令确定跳转JalD==1
:ID阶段的Jal指令确定跳转JalrE==1
:Ex阶段的Jalr指令确定跳转输出:
PC_In
:NPC的值4.RegisterFile
上升沿写入,异步读的寄存器堆,0号寄存器值始终为32’b0
5.ImmOperandUnit
利用正在被译码的指令的部分编码值,生成不同类型的32bit立即数
输入:
IN
:是指令除了opcode以外的部分编码值Type
:表示立即数编码类型,全部类型定义在Parameters中输出:
OUT
:表示指令对应的立即数32bit实际值6.BranchDecisionMaking
跳转判断单元,根据控制信号BranchTypeE
指定的分支类型,对操作数Operand1
和Operand2
进行比较并决定是否跳转,将判断结果通过BranchE
输出。各分支类型对应的控制信号如下
7.ALU
算数逻辑运算单元,接受Operand1
和Operand2
两个操作数,按照控制信号AluContrl
执行对应的算术逻辑运算,将结果从AluOut
输出
8.DataExt
输入:
IN
:是从Data Memory中load的32bit字LoadedBytesSelect
:等价于AluOutM[1:0],是读Data Memory地址的低两位,因为DataMemory是按字(32bit)进行访问的,所以需要把字节地址转化为字地址传给DataMem,DataMem一次返回一个字,低两位地址用来从32bit字中挑选出我们需要的字节RegWriteW
:表示不同的寄存器写入模式,所有模式定义在Parameters中输出:
OUT
:表示要写入寄存器的最终值参考提供的RISC-V 32I的设计图,思考每条指令的数据通路,熟悉RISC-V电路图,并且为后续动态分支预测和Tomasulo实验打下基础。
具体步骤见https://github.com/mortbopet/Ripes
已提供riscv32gcc编译器的ubuntu版本和windows版本,其余版本下载参考:
https://github.com/mortbopet/Ripes/blob/master/docs/c_programming.md
下载后页面如下:
输入以下代码后生成对应汇编指令
void main()
{
int A[100];
int i;
for(i=0;i<100;i++)
A[i]=i;
for(i=1;i<100;i++)
A[i]=A[i-1]+1000;
}
生成的汇编指令如下:
00010144 ://主函数的开始
//x2的值减去432,然后存回x2。x2寄存器是栈指针,这条指令是为了分配栈空间
10144: e5010113 addi x2 x2 -432
//x8的值存储到x2加上428的内存地址。x8寄存器是帧指针,所以这条指令是为了保存帧指针。
10148: 1a812623 sw x8 428 x2
//x2寄存器的值加上432,然后存回x8寄存器。这条指令是为了更新帧指针
1014c: 1b010413 addi x8 x2 432
//x0寄存器的值存储到x8寄存器减去20的内存地址。这条指令初始化一个局部变量i为0
10150: fe042623 sw x0 -20 x8
//无条件跳转到当前地址加上40的位置,并将返回地址存储到x0寄存器。
//因为x0寄存器是零寄存器,所以返回地址会被丢弃。这条指令是为了跳过下面的循环体。
10154: 0280006f jal x0 40
//将x8寄存器减去20的内存地址的值加载到x15寄存器。这条指令是为了读取局部变量i的值
10158: fec42783 lw x15 -20 x8
//将x15寄存器的值左移两位,然后存回x15寄存器。这条指令相当于将i乘以4,因为每个整数占4个字节
1015c: 00279793 slli x15 x15 2
//将x8寄存器的值减去16,然后存回x14寄存器。这条指令是为了计算数组A在栈上的起始地址
10160: ff040713 addi x14 x8 -16
//将x14寄存器和x15寄存器的值相加,然后存回x15寄存器。这条指令是为了计算A[i]在栈上的地址
10164: 00f707b3 add x15 x14 x15
//将x8寄存器减去20的内存地址的值加载到x14寄存器。这条指令又一次读取局部变量i的值
10168: fec42703 lw x14 -20 x8
//将x14寄存器的值存储到x15寄存器减去404的内存地址。这条指令相当于执行A[i]=i
1016c: e6e7a623 sw x14 -404 x15
//将x8寄存器减去20的内存地址的值加载到x15寄存器。这条指令又一次读取局部变量i的值
10170: fec42783 lw x15 -20 x8
//将x15寄存器的值加上1,然后存回x15寄存器。这条指令相当于执行i++
10174: 00178793 addi x15 x15 1
//将x15寄存器的值存储到x8寄存器减去20的内存地址。这条指令相当于更新局部变量i的值
10178: fef42623 sw x15 -20 x8
//将x8寄存器减去20的内存地址的值加载到x14寄存器。这条指令又一次读取局部变量i的值
1017c: fec42703 lw x14 -20 x8
//将x0寄存器的值加上99,然后存回x15寄存器。这条指令相当于将99赋值给一个临时变量
10180: 06300793 addi x15 x0 99
//比较x15寄存器和x14寄存器的值,如果x15大于等于x14,就跳转到当前地址减去44的位置。
//这条指令相当于执行if(i<100) goto loop
10184: fce7dae3 bge x15 x14 -44
//将x0寄存器的值加上1,然后存回x15寄存器。这条指令相当于初始化一个局部变量j为1
10188: 00100793 addi x15 x0 1
//将x15寄存器的值存储到x8寄存器减去20的内存地址。这条指令相当于更新局部变量j的值
1018c: fef42623 sw x15 -20 x8
//无条件跳转到当前地址加上64的位置,并将返回地址存储到x0寄存器。
//因为x0寄存器是零寄存器,所以返回地址会被丢弃。这条指令是为了跳过下面的循环体。
10190: 0400006f jal x0 64
//将x8寄存器减去20的内存地址的值加载到x15寄存器。这条指令是为了读取局部变量j的值
10194: fec42783 lw x15 -20 x8
//将x15寄存器的值减去1,然后存回x15寄存器。这条指令相当于执行j–1
10198: fff78793 addi x15 x15 -1
//将x15的值左移两位,然后存回x15寄存器。这条指令相当于将j-1乘以4,因为每个整数占4个字节
1019c: 00279793 slli x15 x15 2
//将x8寄存器的值减去16,然后存回x14寄存器。这条指令是为了计算数组A在栈上的起始地址
101a0: ff040713 addi x14 x8 -16
//将x14寄存器和x15寄存器的值相加,然后存回x15寄存器。这条指令是为了计算A[j-1]在栈上的地址
*101a4: 00f707b3 add x15 x14 x15
//将x15寄存器减去404的内存地址的值加载到x15寄存器。这条指令相当于获得A[j-1]的值
101a8: e6c7a783 lw x15 -404 x15
//将x15寄存器的值加上1000,然后存回x14寄存器。这条指令相当于将A[j-1]+1000的值存在x14中
101ac: 3e878713 addi x14 x15 1000
//将x8寄存器减去20的内存地址的值加载到x15寄存器。这条指令又一次读取局部变量j的值
101b0: fec42783 lw x15 -20 x8
//将x15寄存器的值左移两位,然后存回x15寄存器。这条指令相当于将j乘以4,因为每个整数占4个字节
101b4: 00279793 slli x15 x15 2
//将x8寄存器的值减去16,然后存回x13寄存器。这条指令是为了计算数组A在栈上的起始地址
101b8: ff040693 addi x13 x8 -16
//将x13寄存器和x15寄存器的值相加,然后存回x15寄存器。这条指令是为了计算A[j]在栈上的地址
101bc: 00f687b3 add x15 x13 x15
//将x14寄存器的值存储到x15寄存器减去404的内存地址。这条指令相当于执行A[j]=A[j-1]+1000
101c0: e6e7a623 sw x14 -404 x15
//将x8寄存器减去20的内存地址的值加载到x15寄存器。这条指令又一次读取局部变量j的值
*101c4: fec42783 lw x15 -20 x8
//将x15寄存器的值加上1,然后存回x15寄存器。这条指令相当于执行j++
101c8: 00178793 addi x15 x15 1
//将x15寄存器的值存储到x8寄存器减去20的内存地址。这条指令相当于更新局部变量j的值
*101cc: fef42623 sw x15 -20 x8
//将x8寄存器减去20的内存地址的值加载到x14寄存器。这条指令又一次读取局部变量j的值
101d0: fec42703 lw x14 -20 x8
//将x0寄存器的值加上99,然后存回x15寄存器。这条指令相当于将99赋值给一个临时变量
101d4: 06300793 addi x15 x0 99
//比较x15寄存器和x14寄存器的值,如果x15大于等于x14,就跳转到当前地址减去68的位置。
//这条指令相当于执行if(j<100) goto loop;
*101d8: fae7dee3 bge x15 x14 -68
//将x0寄存器的值加上0,然后存回x0寄存器。这条指令没有实际作用,只是为了占位
101dc: 00000013 addi x0 x0 0
//将x2寄存器加上428的内存地址的值加载到x8寄存器。这条指令是为了恢复帧指针
101e0: 1ac12403 lw x8 428 x2
//将x2寄存器的值加上432,然后存回x2寄存器。这条指令是为了释放栈空间
101e4: 1b010113 addi x2 x2 432
//跳转到x1寄存器加上0的地址,并将返回地址存储到x0寄存器。
//因为x0寄存器是零寄存器,所以返回地址会被丢弃。这条指令是为了从main函数返回
101e8: 00008067 jalr x0 x1 0
找出循环A[i]=A[i-1]+1000;对应的汇编代码,思考以下问题:
add x15, x14, x15
问题描述:分析指令add x15, x14, x15
(x是指以x开头的通用寄存器),写出该指令在流水线五个阶段(IF、ID、EX、MEM和WB)关键的控制信号(参考RISC V电路设计图),并通过分析指出数据通路
答:这条指令的作用是将x14寄存器中的值加上x15寄存器中的值存到x15寄存器中,其中x14寄存器存入的是数组A的基址地址,x15存入的是偏移量,两者相加后获得A[j-1]的地址
R-型指令 | funct7 | rs2 | rs1 | funct3 | rd | opcode |
---|---|---|---|---|---|---|
add | 0000000 | 01111 | 01110 | 000 | 01111 | 0110011 |
这条指令是一条R类型的指令
IF阶段:
PCF
,从指令存储器中取出指令00000000111101110000011110110011
ID阶段:
IF/ID
寄存器中译码出指令的操作码(inst[6:0]
)
inst[24:20]
,inst[19:15]
)对应地址的值,送入A1
和A2
寄存器,送往ID/EX
寄存器inst[11:7]
) 送往ID/EX
寄存器IF/ID
中得到指令段,送往Control Unit
控制单元产生控制信号ImmType=3'd0
(代表是R类型指令,无需生成立即数)EX阶段:
A1
和A2
寄存器中取出两个源操作数,送入ALU
进行加法运算,得到结果,送入ALUOutE
,结果送入EX/MEM
寄存器。ID/EX
中得到Rd
(目的寄存器地址),送往EX/MEM
寄存器AluContrlE=3
,ALU
执行加法操作Forward1E=2'b00
,第一个操作数选择来自A1
寄存器的值Forward2E=2'b00
,第二个操作数选择来自A2
寄存器的值ALUSrc1E=1
,ALU
第一个操作数来自A1
寄存器ALUSrc2E=2'00
,ALU
第二个操作数来自A2
寄存器RegReadE[1]=1
,A1
对应的寄存器值被使用到了RegReadE[0]=1
,A2
对应的寄存器值被使用到了MEM阶段:
EX/MEM
寄存器中取出运算结果ALUOutM
,送入数据存储器和MemData
多路选择器,存入MEM/WB
寄存器中。EX/MEM
中得到Rd
(目的寄存器地址),送往MEM/WB
寄存器LoadNpcD=1
: 将下一条指令的地址存储到ResultM
中,以便在WB阶段更新PC
寄存器的值MemWriteM=4'0
,不写内存WB阶段:
数据通路:
MEM/WB
寄存器中取出写回数据,根据RegDst
信号选择目的寄存器地址,将数据写入寄存器文件。控制信号:
MemToRegW=1
,选择将ALU运算结果传入寄存器文件中RegWriteW=1
,写回目的寄存器bge x15 x14 -68
问题描述:分析指令bge x15, x14, -68,写出该指令在流水线五个阶段(IF、ID、EX、MEM和WB)关键的控制信号(参考RISC V电路设计图),并通过分析指出数据通路
答:这条指令的作用是比较x15寄存器和x14寄存器的值,如果x15大于等于x14,就跳转到当前地址减去68的位置。这条指令相当于执行if(j<100) goto loop。
指令输入SB-型指令,只会执行IF,ID和EX段。
PCF
,从指令存储器中取出指令11111010111001111101111011100011。PC
寄存器的值加4,更新为下一条指令的地址。A1
,A2
地址分别为rs1
,rs2
,读出待比较的值RegOut1D
和RegOut2D
ImmD
,左移一位后与PCD
相加得到JalNPC
,传入段寄存器Inst
传给Controller Decoder
,生成控制信号ImmD
,结果为-224Reg1
-Reg2
,如果结果大于等于0,则BrE信号应该被设置为1,表示应该跳转。PC + ImmE
,即-68<<2(PC是当前指令的地址,左移两位是因为指令存储器是四字节地址寻址的)Forward1E=2'b00
,第一个操作数选择来自A1
寄存器的值Forward2E=2'b00
,第二个操作数选择来自A2
寄存器的值ALUSrc1E=1
,reg1来自A1
寄存器ALUSrc2E=2'00
,reg2来自A2
寄存器lw x15, -20 x8
问题描述:分析指令lw x15, -20 x8,写出该指令在流水线五个阶段(IF、ID、EX、MEM和WB)关键的控制信号(参考RISC V电路设计图),并通过分析指出数据通路
答:指令的作用是将x8寄存器减去20的内存地址的值加载到x15寄存器。其中x8寄存器减去20的内存地址的值即为局部变量i的值,所以这里是将i的值赋给x15寄存器
I-型指令 | immediate | rs1 | funct3 | rd | opcode |
---|---|---|---|---|---|
lw | 111111101100 | 01000 | 010 | 01111 | 0000011 |
这是一条I-型指令:
PCF
,从指令存储器中取出指令11111110110001000010011110000011Inst
传给Controller Decoder
,生成控制信号inst[19:15]
),将读出的值RegOut1D
送入ID/EX
寄存器inst[11:7
])rd
送往ID/EX
寄存器Immediate Generate
生成立即数,将立即数扩展为32位,送入ImmD
,接着送入ID/EX
寄存器。ImmType=3'd1
,代表是I类型指令,需要生成立即数RegWriteW=1
,结果写回目的寄存器x15
ImmOperandUnit
中取出两个源操作数,送入ALU
进行加法运算,得到内存地址,送入ALUOutE
,结果送入EX/MEM
寄存器。ID/EX
中得到Rd
地址,送往EX/MEM
寄存器AluContrlE=4'b0011
,ALU
执行加法操作Forward1E=2'b00
,第一个操作数选择来自A1
寄存器的值Forward2E=2'b01
,第二个操作数选择来自立即数ALUSrc1E=1'b1
,ALU
第一个操作数来自A1
寄存器ALUSrc2E=2'b00
,ALU
第二个操作数来自Imm
寄存器RegReadE[1]=1
,A1
对应的寄存器值被使用到了EX/MEM
中得到ALU
运算结果,送往数据存储器取出对应地址内容,内容送往MEM/WB
寄存器EX/MEM
中得到Rd地址,送往MEM/WB
寄存器LoadNpcD=0
,将NextPC输出到ResultMMemWriteM=4'b0000
,不写内存MEM/WB
中得到存储器内容,送往DataExt
数据扩展单元扩展后送往寄存器堆MEM/WB
中得到Rd
写回地址,送往寄存器堆用于写回上方内容MemToRegW=0
,标志WB
阶段指令需要从内存读取数据到寄存器RegWriteW=1
,写回目的寄存器x15
LoadedBytesSelect=4
,相当于ResultW[3:0]
,即选取要取的4字节数据sw x15, -20 x8
问题描述:分析指令sw x15, -20 x8
,写出该指令在流水线五个阶段(IF、ID、EX、MEM和WB)关键的控制信号(参考RISC V电路设计图),并通过分析指出数据通路
答:这是一条S-型指令,它的作用是将寄存器x15中的值存储到地址为寄存器x8中的值减去20处的内存中。这条指令相当于更新局部变量i的值
PCF
,从指令存储器中取出指令11111110111101000010011000100011Inst
传给Controller Decoder
,生成控制信号inst[24:20]
),将读出的值RegOut2D
送入ID/EX
寄存器inst[19:15]
),将读出的值RegOut1D
送入ID/EX
寄存器Immediate Generate
生成立即数,将立即数扩展为32位,送入ImmD
,接着送入ID/EX
寄存器。ImmType
,S类型指令,需要生成立即数A1
寄存器和ImmOperandUnit
中取出两个源操作数,送入ALU
进行加法运算,得到要写入的内存地址,送入ALUOutE
,结果送入EX/MEM
寄存器。ID/EX
中得到A2寄存器读出的值RegOut2E
,送往EX/MEM
寄存器AluContrlE=3
,代表ALU
执行ADD
运算Forward1E=2'b00
,第一个操作数选择来自A1
寄存器的值Forward2E=2'b00
,将A2
寄存器读出的值RegOut2E
传递下去AluSrc1E=1'b1
,代表ALU
第一个操作数来自A1
寄存器AluSrc2E=2'b10
,代表ALU
第二个操作数来自Imm
寄存器RegReadE[1]=1
,A1
对应的寄存器值被使用到了EX/MEM
寄存器读出要写入内存的值,通过StoreDataM
写入Data Memory
的WD
接口EX/MEM
寄存器读出要写入的内存地址,AluOutM
写入A
接口LoadNpcD=1
,将NextPC输出到ResultMMemWriteM=1
,本条指令需要将数据写入内存MemToRegW=0
,不需要从内存读取数据到寄存器RegWriteW=0
,不需要写回目的寄存器BranchE信号是指令执行(EX)阶段的一个控制信号,用于判断是否需要进行分支跳转。在流水线的EX阶段
NPC Generator 中对于不同跳转 target 的选择有没有优先级?如果有,请举例并分析。如果没有,请解释原因
答:有。
JalrT、BrT 和 JalT 是 RISC-V CPU 中用于控制跳转指令的信号。
Jalr 指令的目标地址是通过寄存器的内容计算得到的,而 Jal 指令的目标地址则是在指令中直接给出的。
branch和jalr是EX段跳转,而jal是在ID段跳转。所以必须设置优先级,使得在后的指令先跳转。因此JalrT和BrT的优先级高于JalT
同时,若修改数据通路,使得br,jal,jalr均在EX段跳转,则不会有冲突,此时也就不需要设置优先级。
Harzard模块中,有哪几类冲突需要插入气泡(NOP指令),分别使流水线停顿几个周期。(提示:有三类冲突)
答:
Harzard模块中,采用静态分支预测器,即默认不跳转,遇到branch指令时,如何控制flush和stall信号?
答:
Branch指令在EX段判断。如果发生分支,则需要Flush IF/ID和ID/EX段寄存器来保证数据不被后方指令错误使用。否则不需要flush或stall。
在实验中,我学习了RISC-V指令集架构的基本概念和特点,了解了RISC-V Core的基本组成和工作原理,学习了每条指令的数据流和控制信号。
总的来说,通过本实验的学习,我对RISC-V指令集架构有了更深入的理解,了解了每条指令的数据流和控制信号,同时也对RISC-V Core的组成和工作原理有了更清晰的认识。在实验中,我还了解了一些常用的数字电路和计算机体系结构的概念,例如时钟信号、寄存器、ALU等等。
计算时,操作数还未读出来。在EX段Stall,使流水线停顿1个周期