RISC-V流水线CPU模拟器(c语言实现)
作者:互联网
2020 年秋季学期计算机体系结构
Project 04——RISC-V流水线处理器
2020年11月27日
一、时序模拟和功能模拟分离
该RISC-V流水线处理器分为两部分:功能模拟部分,时序模拟部分。
功能时序分离的优势有两点:
- 不同功能模块化,减小耦合性,可以增强可扩展性。
- 有效降低流水线实现的复杂度和工作量。
具体实现上,功能模拟部分大体沿用之前编写的单/多周期CPU,在其基础上改进,加上了与时序模拟部分相互通信的接口,将进行时序模拟所需要的信息输出到buffer文件中;而时序部分读取buffer文件,通过功能模拟部分所提供的信息,计算流水线的时序信息,并统计输出。
接下来是时序模拟的设计框架
二、各级流水线执行顺序
虽然实际各级流水线是同时执行,但由于C语言的限制,所以需要选择一个顺序。
IF->WB
若直接执行会违反时序,如果需要实现则要将每个阶段分成两部分:取数据和执行,两部分分阶段执行。
流水线寄存器后的段先取指令,全部取到指令后再顺次执行。
不足是会造成各阶段的割裂,带来一些不必要的麻烦。
WB->IF
符合时序要求,从后往前,在一个阶段内连续完成从上一个流水线寄存器取值、执行、写入下一个流水线寄存器。处理顺序也是按照指令流入的先后顺序进行,是相对理想的实现方案。
各级流水线大体执行框架如下:
void control()
{
if(hazard_type == xxx){//发现冲突,且需要暂停处理
WB();
MEM();
EX();
ID();
IF();
}
else{//无需暂停处理(无冲突或可数据定向)
WB();
MEM();
EX();
ID();
IF();
}
return ;
}
三、各段完成的操作
取指IF
PC的更新
NPC的选择:NPC来源有ID段的跳转地址、PC+4、PC+2
获取指令:根据此时的PC,从指令存储器中读入指令,统一读入32位,冗余字段在后续过程中不会被使用,不会造成影响
指令长度确认:确定指令是否为压缩指令,如果是则会将PC+4传入多路选择器,反之将PC+2传入
框架如下:(仅表示IF的逻辑,实际实现中不会出现)
void IF()
{
LL insn = read_mem_longlong(PC);
if(JPC != -1){//JPC有效
NPC = JPC;
}
else if(insn&3 == 3){//32位指令
NPC = PC + 4;
}
else{//压缩指令
NPC = PC + 2;
}
//写入流水线寄存器
IFIDReg.insn = insn;
IFIDReg.C = (insn&3!=3);
return ;
}
译码ID
从指令中提取相应信息:提取出各个字段,生成出相应的立即数。采取的是固定字段译码,在实际电路中各数据通路是并行的,固定字段可以降低整体复杂度。
生成控制信号:根据指令类型生成相应的控制信号(如果需要的话),并写入流水线寄存器
判断分支转移:测试分支条件寄存器,以尽快完成分支转移是否成功的检测
计算分支目标:为避免结构相关,使用一个新的加法器部件(而非ALU)进行分支目标地址的计算
在ID段处理分支指令可以减少流水线的暂停带来的效率损失
框架如下:(仅表示ID的逻辑,实际实现中不会出现)
void ID()
{
//取值
ULL insn = IFIDReg.insn;
bool C = IFIDReg.C;
//译码并写入流水寄存器
insn = (int)insn;
IDEXReg.OPCODE = insn & 0x7f;
IDEXReg.RD = (insn >> 7) & 0x1f;
IDEXReg.RS1 = (insn >> 15) & 0x1f;
IDEXReg.RS2 = (insn >> 20) & 0x1f;
IDEXReg.IMM_I = ((int)insn) >> 20;
IDEXReg.UIMM_I = (unsigned int)(insn) >> 20;
IDEXReg.FUNC3 = (insn >> 12) & 0x7;
IDEXReg.FUNC7 = (insn >> 25) & 0x7f;
IDEXReg.SHAMT = (insn >> 20) & 0x3f;
IDEXReg.IMM_J = ( (insn >> 11) & 0xfff00000 ) | ( insn & 0xff000 ) | ( (insn >> 9) & 0x800 ) | ( ((insn >> 21) & 0x3ff) << 1 );
IDEXReg.UIMM_J = ( (insn >> 11) & 0x100000 ) | ( insn & 0xff000 ) | ( (insn >> 9) & 0x800 ) | ( ((insn >> 21) & 0x3ff) << 1 );
IDEXReg.IMM_U = (insn & 0xfffff000) >> 12;
IDEXReg.IMM_B = ( ((insn >> 8) & 0xf) << 1 ) | ( ((insn >> 25) & 0x3f) << 5 ) | ( ((insn >> 7) & 0x1 ) << 11) | ((insn >> 20) & 0xfffff000);
IDEXReg.UIMM_B = ( ((insn >> 8) & 0xf) << 1 ) | ( ((insn >> 25) & 0x3f) << 5 ) | ( ((insn >> 7) & 0x1 ) << 11) | ((insn >> 20) & 0x1000);
IDEXReg.IMM_S = ((insn >> 7) & 0x1f) | ((insn >> 25) << 5);
IDEXReg.UIMM_S = (((insn >> 7) & 0x1f) | ((insn >> 25) << 5))&0x00000fff;
IDEXReg.IMM12 = (insn >> 20) & 0xfff;
IDEXReg.IMM5L = (insn >> 7) & 0x1f;
IDEXReg.IMM7 = (insn >> 25) & 0x7f;
IDEXReg.IMM5H = (insn >> 27) & 0x1f;
IDEXReg.FMT = (insn >> 25) & 0x3;
IDEXReg.RM = (insn >> 12) & 0x7;
IDEXReg.RS3 = IMM5H;
IDEXReg.WIDTH = RM;
IDEXReg.OP_16 = insn & 0x3;
IDEXReg.RS2_16 = (insn >> 2) & 0x1f;
IDEXReg.RS1_16 = (insn >> 7) & 0x1f;
IDEXReg.RD_16 = RS1_16;
IDEXReg.FUNC6_16 = (insn >> 10) & 0x3f;
IDEXReg.FUNC4_16 = (insn >> 12) & 0xf;
IDEXReg.FUNC3_16 = (insn >> 13) & 0x7;
IDEXReg.FUNC2_16 = (insn >> 5) & 0x3;
IDEXReg.OFFSETL_16 = (insn >> 2) & 0x1f;
IDEXReg.OFFSETH_16 = (insn >> 10) & 0x7;
IDEXReg.IMML_CI = RS2_16;
IDEXReg.IMMH_CI = (insn >> 12) & 0x1;
IDEXReg.IMM_CSS = (insn >> 7) & 0x3f;
IDEXReg.IMM_CIW = (insn >> 5) & 0xff;
IDEXReg.IMML_CLS = FUNC2_16;
IDEXReg.IMMH_CLS = OFFSETH_16;
IDEXReg.RDLA_16 = (insn >> 2) & 0x7;
IDEXReg.RS2A_16 = RDLA_16;
IDEXReg.RDHA_16 = (insn >> 7) & 0x7;
IDEXReg.RS1A_16 = RDHA_16;
IDEXReg.TARGET_16 = (insn >> 2) & 0x7ff;
IDEXReg.CSR = (((insn>>20)<<20)>>20);
IDEXReg.ZIMM = ((insn>>15)&0x1f) & 0xffffffff;
//提前处理分支指令
LL RS1 = get_longlong(IDEXReg.RS1);
LL RS2 = get_longlong(IDEXReg.RS2);
ULL uRS1 = get_ulonglong(IDEXReg.RS1);
ULL uRS2 = get_ulonglong(IDEXReg.RS2);
JPC = -1;
if(C){
//略
}
else{
switch (IDEXReg.OPCODE)
{
case 111://jal
JPC = PC + IDEXReg.IMM_J;
break;
case 103://jalr
JPC = (RS1 + IDEXReg.IMM_I) & (~1);
break;
case 99://branch
switch (IDEXReg.FUNC3)
{
case 0://beq
if(RS1 == RS2) JPC = PC + IDEXReg.IMM_B;
break;
case 1://bne
if(RS1 != RS2) JPC = PC + IDEXReg.IMM_B;
break;
case 4://blt
if(RS1 < RS2) JPC = PC + IDEXReg.IMM_B;
break;
case 5://bge
if(RS1 >= RS2) JPC = PC + IDEXReg.IMM_B;
break;
case 6://bltu
if(uRS1 < uRS2) JPC = PC + IDEXReg.IMM_B;
break;
case 7://bgeu
if(uRS1 >= uRS2) JPC = PC + IDEXReg.IMM_B;
break;
default:
printf("ERROR: No such instruction!\n");
break;
}
break;
default:
printf("ERROR: No such instruction!\n");
break;
}
}
//判断访存行为类型,略——这里直接赋值true
IDEXReg.W_R = true;
return ;
}
执行EX
ALU单元:从流水线寄存器读取控制信号,并根据控制信号选择相应的操作数、立即数并进行处理,得到结果ALUoutput
传递控制信号:为访存段确定访存行为(读取 or 写入),将控制信号写入流水线寄存器
大体框架如下:(仅表示ID的逻辑,实际实现中不会出现)
void EX()
{
//读取流水线寄存器IDEXReg,与ID段的写入类似,重复且过于冗长,此处省略
ULL insn = IDEXReg.insn;
bool C = IDEXReg.C;
bool W_R = IDEXReg.W_R;
LL aluoutput;
if(c){
//.......
}
else{
switch(OPCODE){
case R_type: break;
case B_type: break;
//......
default:
printf("ERROR: No such instruction!\n");
break;
}
}
//写入流水线寄存器EXMEMReg
}
访存MEM
确定访存地址:由流水线寄存器读取
确定访存行为:根据流水线寄存器中读取的控制信号来确定
执行访存动作:根据地址和行为,具体执行相应操作。如果是读取内存的行为,结果放入ReadData,并向后传递。
逻辑框架较简单,不在此赘述。
写回WB
选择数据和寄存器:根据控制信号选择正确的数据,确定目的寄存器
写回寄存器:将所选数据写回相应寄存器
逻辑框架较简单,不在此赘述。
四、流水线暂停
检测机制
冲突控制单元:将各种冒险的检测集中在冲突控制单元处理。冲突控制单元是一个处理和传递全局控制信息的部件,从各流水线寄存器中读取数据,进行分析,若发现存在冒险,则生成全局控制信号,控制各部件进行相应操作,以解决冒险。
控制信号:控制信号的更新不按照固定时钟周期更新,而是依据各级流水线运行情况动态执行。需要在各级流水线均完成自己的处理任务,并将数据写入下一级流水线寄存器后,才能开始新一次的处理,处理过程包括从各级流水线寄存器读取最新数据,然后更新控制信号,并即时传递给各部件,然后等待流水线下一次的流动。
暂停
暂停:部分冲突可以通过数据重定向来解决,但有些冲突则必须进行暂停处理。暂停的控制是由冲突控制单元通过传递控制信号来完成,当某一级流水线接收到暂停的信号后,就不从上一个流水线寄存器取值。当然,当某一级流水线被暂停时,它前面的各级流水线也会被暂停,这一点依然是由冲突控制单元来保证。
插入气泡:被暂停的连续几级流水线的最后一级,它需要向下一个流水线寄存器写入NOP指令(亦可采用其他执行空操作的信号机制),否则下一级流水线将重复操作,在执行某些指令的情况下会造成错误。这个过程就是插入气泡。
实现
冲突检测的实现分为两部分,分别处理数据冲突和控制冲突。
数据冲突:因为一条指令在ID段可得到所有译码信息,包括源寄存器。而数据冲突的产生,正是后面指令是源寄存器与前面指令的目的寄存器相同,造成的若不处理会带来错误的相关。所以所有数据冲突最早可以在ID段确定下来,故数据冲突的检测,是在ID段出现新指令后,将其信息与前面几条指令相匹配,检测是否存在冲突,且标记冲突类型。冲突类型标记用于后面的冲突处理。在出现多个冲突同时出现时,将判断冲突是否可合并,可合并是指ID段源寄存器和EX,MEM,WB段中的多个同时出现了冲突,且冲突的寄存器相同,这样实际只需要将最靠近ID段的作为冲突即可,因为ID段需要的是最新的信息。而不可合并的冲突,都会计入统计中。
控制冲突:该方案尚未采用分支预测机制,在每一个分支跳转处都暂停一个周期,作为延迟槽。具体实现是在单/多周期流水线中,在向buffer中输出信息时,若当前指令是分支跳转指令,则在后面再输出一个特定的nop指令,这样在TimingSimulator读取buffer时,相当于已经对控制冲突进行了处理,只需正常处理nop指令即可。
五、计时和计数
时间驱动
优点:可以确定每一个时钟周期整个CPU的状态信息。
缺点:可能过于陷入细节,各级流水线执行速度的差异将增加整体的复杂性。
事件驱动
优点:可以将每一级流水线内部的操作视为原子操作,降低复杂度,隐藏很多不必要的细节。
缺点:难以任意地跟踪查看周期级CPU的状态信息。
此处选择事件驱动的方式。
实现:维护一个全局的事件队列,即此时流水线中正在运行的指令,每一条指令的执行视为一个事件。在队列中的每个事件有一些属性:该指令的名称、源寄存器、目的寄存器、指令执行开始时间、指令执行结束时间,在每一级流水线处理所需要的时间。其中,指令在各阶段结束时间的更新,需要由队列根据所有事件来统一确定,否则会因为各指令的执行周期数差异造成混乱。
事件队列结构大致如下:
struct event{
int name;
int rs1;
int rs2;
int rd;
int csrd;
int time_start;
int time_cost[5];
int time_end;
};
typedef struct event event;
event queue[5];
还有一些配套的函数方法(具体见代码)。
计数
计数分为两部分。一是在事件队列每次正常出队列时查看被弹出事件的信息,并进行持续地统计。统计各类型指令数量、执行周期数等。二是与冲突控制单元数据交互,每次出现数据冒险和控制冒险就进行记录。还有一些功能模拟时的必要信息,通过buffer进行传递。
统计信息
统计信息暂时分为4部分:
- 所执行的所有指令时序信息,每一条包括:指令名称,开始时间,结束时间,总耗时。
- 各种指令的执行数量,并从高到低进行了排序,便于查看哪些指令出现频率高。
- 冲突计数,统计了各类型数据冲突的数量,以及控制冲突的数量。
- 计算了CPI。
注意,这里的数据冲突是流水线事件队列每一次更新后进行检测,且均以ID段为中心,有些需要暂停的冲突,在暂停之后,会变成仅需要数据定向的冲突,此时会统计为两次冲突。如果需要更换统计模式,仅记为一次冲突的话,对统计结果进行简单的减法即可(因为这样的多次统计必然是一一对应的)。
六、时序模拟部分总框架
逐级向下进行部分关键函数的展示(为简略,省去了细节,具体实现请阅读代码)
int main()
{
init();
TimingSimulator();
print();
return 0;
}
void init(){
memset(count, 0, sizeof(count));
memset(hazard_cnt, 0, sizeof(hazard_cnt));
for(int i=0; i<5; i++){
queue[i].name = nop;
queue[i].rs1 = -1;
queue[i].rs2 = -1;
queue[i].rd = -1;
queue[i].csrd = -1;
queue[i].time_start = queue[i].time_end = 0;
for(int j=0; j<5; j++)
queue[i].time_cost[j] = 0;
}
}
void TimingSimulator()
{
freopen("buffer.txt", "r", stdin);
freopen("Timing.txt", "w", stdout);
while(1){
hazard();
out = control();
input();
output();
}
cycles = out.time_end;
}
具体代码会在该课程结束后上传......
标签:IDEXReg,RISC,insn,指令,冲突,寄存器,流水线,CPU,模拟器 来源: https://www.cnblogs.com/gaozhenyu/p/14166473.html