一、实验目的、任务与环境
实验目的:设计并实现流水线CPU,使用verilog编写代码及仿真。
实验环境:Quartus2 9.1
二、核心设计思想
完成5段 流水线CPU的设计,
数据通路设计
控制信号设计
引入段寄存器,传送数据,控制信号
执行指令
考虑冒险情况,解决冒险情况
仿真结果分析
三、设计实验
五段流水线设计图及说明
lw与subu发生load-use冒险,插入一个nop,并转发MEM/WR寄存器中的值到ALU入口
Beq发生控制冒险,在ID段判断beq并将转移后的地址直接送到IF中改变当前已经取出的错误指令
Ori与add发生数据冒险,将EXE/MEM寄存器中的值转发到ALU入口
Jump发生控制冒险,在ID段判断jump并将跳转后的地址直接送到IF中改变当前已经取出的错误指令
完整代码(已放到github中)
顶层文件PipeLineCPU
取指令部件Instruction
IF/ID段寄存器RegIF
译码取数Controller
二选一确定目的寄存器mux2to1_regdst
二选一mux2to1
检测load-use冒险LoadUse
ID/EXE段寄存器RegID
立即数扩展signZeroExtend
检测数据冒险DataHazard
四选一mux4to1
ALU
EXE/MEM段寄存器RegEXE
存储部件Mem
MEM/WR段寄存器RegMEM
四、实验过程
指令设计
IF段
依次按顺序取出每一条指令,前四个周期分别为:
//add $3,$1,$2 3号寄存器值为 3
//sub $4,$1,$2 4号寄存器值为 -1
//lw $5,$1,7
//subu $6,$5,$1 6号寄存器值为 8-1=7
此时在第四个周期时取出subu指令,第五个周期ID时发生loaduse冒险,因此保持pc不+1,ID/EXE寄存器清零,IF/ID指令不传递,相当于加了一个nop。所以从下面的图中可以看到pc保持不变了一个周期
第5-8个周期分别为:
//nop
//beq $1,$1,2 PC+1+2=4+1+2=7, 下一条是PC=7 4
//sltu $5,$1,$2 无符合小于置1 5号寄存器值为 1
//sw $3,$1,5 将3号寄存器的值存到 存储器 6中
但是由于在第六个周期是beq跳转指令,因此取出的指令变成
//nop
//beq $1,$1,2 PC+1+2=4+1+2=7, 下一条是PC=7 4
//slt $7,$4,$1 有符合小于置1 7号寄存器值为 1
//sltu $8,$4,$1 无符合小于置1 8号寄存器值为 0
第9-12个周期取出的指令为
//addiu $9, $1,立即数
//sw $10,$1,15 将10号寄存器的值存到 存储器 16中
//ori $11,$7,2 7号寄存器值为 1 与2 相或,结果为3 送11号寄存器
//add $5,$1,$11
第13-14个周期取出的指令
//jump 跳转指令, 跳转到2
由于jump跳转到2,所以后面取出的指令又变成第二条指令
//lw $5,$1,7
最终指令的顺序为:
0 //add $3,$1,$2 3号寄存器值为 3
1 //sub $4,$1,$2 4号寄存器值为 -1
2 //lw $5,$1,7
3 //subu $6,$5,$1 6号寄存器值为 8-1=7
//nop
4 //beq $1,$1,2 PC+1+2=4+1+2=7, 下一条是PC=7 4
7 //slt $7,$4,$1 有符合小于置1 7号寄存器值为 1
8 //sltu $8,$4,$1 无符合小于置1 8号寄存器值为 0
9 //addiu $9, $1,立即数
10 //sw $10,$1,15 将10号寄存器的值存到 存储器 16中
11 //ori $11,$7,2 7号寄存器值为 1 与2 相或,结果为3 送11号寄存器
12 //add $5,$1,$11
13 //jump 跳转指令, 跳转到2
2 //lw $5,$1,7
ID段
第1-4个周期
第1个周期为空
第2个周期Rs,Rt,Rw分别为1,2,3,busA,busB分别为1,2,AluCtr为1即add
第3个周期Rs,Rt,Rw分别为1,2,4,busA,busB分别为1,2,AluCtr为5即sub
第4个周期Rs,Rt,Rw分别为1,5,5,busA,imm16分别为1,7,AluCtr为0,op为10011即lw
第5-8个周期
第5个周期Rs,Rt,Rw分别为5,1,6,busA,busB分别为5,1,AluCtr为4即subu,需要注意这里发生了load-use数据冒险,因为用到的数据是5号寄存器,但是此时的EXE段中5号寄存器是目的寄存器。
因此在第6个周期保持指令不变,相当于插入了一个nop。
第7个周期为beq指令,跳转到pc=1+4+2=7的位置处
第8个周期指令为pc=7的指令,AluCtr=7即slt,Rs,Rt,Rw分别为4,1,7,busA,busB分别为-1,1
第9-12个周期
第9个周期Rs,Rt,Rw分别为4,1,8,busA,busB分别为-1,1,AluCtr为6即sltu
第10个周期Rs,Rw分别为1,9,busA,imm16分别为1,-8,op为001001即addiu
第11个周期Rs,Rw分别为1,10,busA,imm16分别为1,15,op为101011即sw
第12个周期Rs,Rw分别为7,11,busA,imm16分别为1,2,op为001101即ori
第13-15个周期
第13个周期Rs,Rt,Rw分别为1,11,5,busA,busB分别为1,11,AluCtr为1即add
第14周期,op为000010即jump,imm16为2,跳转到第二条指令
第15周期Rs,Rw分别为1,5,busA,imm16分别为1,7,op为100011,即lw
EXE段
第1-4个周期
前两个周期为空
第3个周期EXE_AluCtr=1,即add,Rs,Rt,Rw分别为1,2,3,EXE_busOutA=1,EXE_busOutB=2,EXE_Result=3
第4个周期EXE_AluCtr=5,即sub,Rs,Rt,Rw分别为1,2,4,EXE_busOutA=1,EXE_busOutB=2,EXE_Result=-1
第5-8个周期
第5个周期发现C=1,即检测到load-use冒险,当前要写入的寄存器为5,但是后一条指令目前处于译码阶段,已经发现后一条指令的源寄存器为5,此时在后面插入一个nop。并且本条指令EXE_busOutA=1,EXE_busOutB=7,执行lw操作。
第6个周期为之前插入的nop,控制信号全部清零
第7个周期EXE_AluCtr=4,即subu,EXE_busOutA=8,EXE_busOutB=1,注意这里AluSrcA=1意味着发生数据冒险,这里A口的数据为MEM/WR寄存器中已经得出但还没写入的5号寄存器的值,实现了转发,得出正确的值为8,EXE_Result=7。
第8个周期其实是beq操作,我将beq的地址计算提前到ID阶段执行,如果检测到ID阶段的指令为beq或jump则直接得出转移后的地址并送入IF改变当前取出的指令,避免对错误取出的指令进行操作,并且使ID阶段的控制信号清零,相当于插入一个nop。因此这是EXE中控制信号都为0。
第9-12个周期
第9个周期为EXE_AluCtr=7,即slt,EXE_busOutA=-1,EXE_busOutB=1,结果为EXE_Result=1
第10个周期为EXE_AluCtr=6,即sltu,EXE_busOutA=-1,EXE_busOutB=1,结果为EXE_Result=0
第11个周期为addiu,EXE_busOutA=1,EXE_busOutB=-8,结果为EXE_Result=-7
第12个周期为sw,EXE_busOutA=1,EXE_busOutB=15,结果为EXE_Result=16
第13-16个周期
第13个周期EXE_AluCtr=2,即ori,EXE_busOutA=1,EXE_busOutB=2,EXE_Result=3
第14个周期EXE_AluCtr=1,即add,EXE_busOutA=1,EXE_busOutB=3,EXE_Result=4,注意这里发生了数据冒险,这里的B口数据为11号寄存器的数据,但是上一条指令将要改变11号寄存器的值,因此直接从EXE/MEM寄存器中取出11号寄存器的值转发到B口,得出正确的值3,解决了数据冒险。
第15个周期是jump,在ID阶段就判断出需要jump直接改变IF阶段的指令,ID信号清零,因此EXE阶段控制信号全为0
第16个周期又重新执行pc=2的指令即lw
MEM段
第1-4个周期
前三个周期为空
第4个周期MEM_busW=3,MEM_Rw=3
第5-8个周期
第5个周期MEM_busW=-1,MEM_Rw=4
第6个周期MEM_busW=8,MEM_Rw=5
第7个周期为nop,全为0
第8个周期MEM_busW=7,MEM_Rw=6
第9-12个周期
第9个周期为beq,因此全为0
第10个周期MEM_busW=1,MEM_Rw=7
第11个周期MEM_busW=0,MEM_Rw=8
第12个周期MEM_busW=-7,MEM_Rw=9
第13-15个周期
第13个周期MEM_busW=16,MEM_Rw=10
第14个周期MEM_busW=3,MEM_Rw=11
第15个周期MEM_busW=4,MEM_Rw=5
Wr段
第1-4个周期
前四个为空
第5-8个周期
第5个周期Wr_busW=3,Wr_Rw=3
第6个周期Wr_busW=-1,Wr_Rw=4
第7个周期Wr_busW=8,Wr_Rw=5
第8个周期为nop
第9-12个周期
第9个周期Wr_busW=7,Wr_Rw=6
第10个周期为beq,全为0
第11个周期Wr_busW=1,Wr_Rw=7
第12个周期Wr_busW=0,Wr_Rw=8
第13-16个周期
第13个周期Wr_busW=-7,Wr_Rw=9
第14个周期Wr_busW=16,Wr_Rw=10
第15个周期Wr_busW=3,Wr_Rw=11
第16个周期Wr_busW=4,Wr_Rw=5
难点及解决方案
问题1:各个流水段之间数据读取和更新周期不匹配
解决方案:统一在上半个周期进行数据的计算和更新,在下半个周期进行数据的读取。即在每个时钟下降沿时计算与更新,上升沿读取。
问题2:寄存器分为读写两个口,但是无法将二维数组作为参数传递进模块
解决方案:在顶层模块中定义寄存器组,两个always,一个在上升沿读寄存器,下降沿写寄存器,就实现了读写分离。
问题3:数据冒险与load-use判断条件有冲突
解决方案:在load-use发生时,普通数据冒险的条件也成立,这时会把AluSrcA改变,但其实本来应该插入一条nop,AluSrcA不应该立即改变,所以加一个判断条件需要C同时为0的时候。
五、实验结果
IF段结果
ID段结果
EXE段
MEM段
Wr段
六、实验总结
从单周期CPU到流水线CPU,是设计思想上的变化,增添了各个流水段的寄存器,和关于冒险的解决。一开始编写代码时并没有先想好整体的架构,因此每次遇到问题都填填补补,进度缓慢,错误百出。后来重新构思了整体架构,确定了数据读取和更新的时机后遇到的问题少了许多。在编写代码前充分的思考很有必要,绝对不是浪费时间,思考的越充分,改bug的时间越少,整体的代码也更清晰易懂。