基于五阶段流水线的RISC-V CPU模拟器实现 (4)

指令的执行过程参见Simulator::execute()函数,这个函数简单粗暴地根据指令类型直接执行相应的行为。在结尾,会根据当前指令和解码阶段的情况,检测数据冒险、控制冒险和内存访问冒险,并作出相应的操作。在这个阶段,跳转指令会得到是否跳转的结果,并在预测错误的情况下在流水线寄存器中插入对应的Bubble。

指令的访存过程参见Simulator::memoryAccess()函数,这个函数首先执行内存读写操作,并且检测数据冒险和转发数据。在检测数据冒险时,既需要考虑到一般的数据冒险,也必须考虑到上个周期因为内存访问冒险而流水线Stall的情况,此外,也必须考虑数据转发的优先级,memoryAccess()作为后面的指令,数据转发的优先级是低于execute()的,否则可能会出现较老的数据被转发并覆盖新数据的情况。

指令的写回过程参见Simulator::writeBack()函数,这个函数将执行结果写回寄存器,并且类似之前的情况处理相关的数据冒险。

流水线寄存器的控制信号设置如下,注意其中fReg表示的是下一个周期开始时,从取值阶段传输到解码阶段的数据,以此类推。

出现的情况 fReg dReg eReg mReg
分支预测错误   Bubble   Bubble   Normal   Normal  
内存访问冒险   Stall   Stall   Bubble   Normal  
预测跳转   Bubble   Normal   Normal   Normal  

有一种情况需要特别说明,就是分支预测器的情况。在当前的模拟器设计中,由于到了解码阶段结束才得知跳转指令的存在,因此如果预测跳转的话必须向流水线中插入一个Bubble,才能确保取指阶段取出的是跳转后的指令。这不会增加分支预测错误的开销,但是会使得预测正确的开销多了一个周期。如果要改进这个设计的话,必须将分支预测模块转移到取指阶段实现。

3.4 系统调用和库函数接口的处理

本模拟器使用自定义的系统调用接口。系统调用的ecall指令会使用a0和a7寄存器,其中a7寄存器保存的是系统调用号,a0寄存器保存的是系统调用参数,返回值会保存在a0寄存器中。为了能让系统调用指令能被集成进当前的流水线,ecall指令只支持一个返回值和一个参数。所有系统调用的语义见下表。

系统调用名称 系统调用号 参数 返回值
输出字符串   0   字符串起始地址    
输出字符   1   字符的值    
输出数字   2   数字的值    
退出程序   3      
读入字符   4     读入的字符  
读入数字   5     读入的数字  

对应的系统调用接口如下

void print_d(int num); void print_s(const char *str); void print_c(char ch); void exit_proc(); char read_char(); long long read_num();

具体的实现需要使用内联汇编,请参考test/lib.c。

3.5 性能计数相关模块

在当前模拟器架构下,对于模拟器进行性能统计只需在代码里适当的地方加入统计代码即可。数据统计模块的定义如下

struct History { uint32_t instCount; uint32_t cycleCount; uint32_t predictedBranch; // Number of branch that is predicted successfully uint32_t unpredictedBranch; // Number of branch that is not predicted successfully uint32_t dataHazardCount; uint32_t controlHazardCount; uint32_t memoryHazardCount; std::vector<std::string> instRecord; std::vector<std::string> regRecord; std::string memoryDump; } history;

其中,最后三个数据项用于记载CPU的执行历史,便于在调试的时候使用。为了防止模拟器占用过多内存,instRecord和regRecord当内容多于100000条时会被清空,memoryDump只会在要求生成内存快照时被使用。

3.6 调试接口

由于对CPU模拟器的调试相对比较困难,CPU模拟器的调试接口和错误执行接口必须被非常小心地设计,以便于尽可能早地发现程序中的Bug。在当前模拟器的代码中,存在大量对模拟器状态和输入值合法性的检查,以便尽可能早地发现错误。Simulator类中存在专门的错误处理函数panic()。

void Simulator::panic(const char *format, ...) { char buf[BUFSIZ]; va_list args; va_start(args, format); vsprintf(buf, format, args); fprintf(stderr, "%s", buf); va_end(args); this->dumpHistory(); fprintf(stderr, "Execution history and memory dump in dump.txt\n"); exit(-1); }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyfxjz.html