(Verilog)多周期CPU设计

来源:互联网 发布:手柄助手映射软件 编辑:程序博客网 时间:2024/05/14 02:14

(Verilog)多周期CPU设计

写在前面:在参考别人的博客自己做了一遍单周期cpu后,觉得不是很难,于是自己尝试了做一下多周期cpu,然后被各种bug糊脸。。。果然,自己尝试和有大佬指路还是有很大区别。。。


先把代码链接发上:多周期CPU代码


依旧还是基础资料:

一.实验内容

设计一个多周期CPU,该CPU至少能实现以下指令功能操作。需设计的指令与格式如下:(说明:操作码按照以下规定使用,都给每类指令预留扩展空间,后续实验相同。)

==>算术运算指令

(1)add rd, rs, rt

000000 rs(5位) rt(5位) rd(5位) reserved

功能:rd<-rs + rt
(2)sub rd, rs, rt

000001 rs(5位) rt(5位) rd(5位) reserved

完成功能:rd<-rs - rt
(3)addi rt, rs, immediate
000010 rs(5位) rt(5位) immediate(16位)
功能:rt<-rs + (sign-extend)immediate

==>逻辑运算指令

(4)or rd, rs, rt

010000 rs(5位) rt(5位) rd(5位) reserved

功能:rd<-rs | rt
(5)and rd, rs, rt

010001 rs(5位) rt(5位) rd(5位) reserved

功能:rd<-rs & rt
(6)ori rt, rs, immediate

010010 rs(5位) rt(5位) immediate

功能:rt<-rs | (zero-extend)immediate

==>移位指令

(7)sll rd, rs,sa

011000 rs(5位) 未用 rd(5位) sa reserved

功能:rd<-rs<<(zero-extend)sa,左移sa位 ,(zero-extend)sa

==>传送指令

(8)move rd, rs

100000 rs(5位) 00000 rd(5位) reserved

功能:rd<-rs + $0

==>比较指令

(9) slt rd, rs, rt

100111 rs(5位) rt(5位) rd(5位) reserved

功能:如果(rs < rt),则rd=1; 否则 rd=0

==>存储器读写指令

(10)sw rt, immediate(rs)

110000 rs(5位) rt(5位) immediate(16位)

功能:memory[rs+ (sign-extend)immediate]<-rt
(11)lw rt, immediate(rs)

110001 rs(5位) rt(5位) immediate(16位)

功能:rt <- memory[rs + (sign-extend)immediate]

==>分支指令

(12)beq rs,rt, immediate (说明:immediate是从pc+4开始和转移到的指令之间间隔条数)

110100 rs(5位) rt(5位) immediate(16位)

功能:if(rs=rt) pc <-pc + 4 + (sign-extend)immediate <<2

==>跳转指令

(13)j addr

111000 addr[27..2]

功能:pc <{pc[31..28],addr[27..2],0,0},转移
(14)jr rs

111001 rs(5位) 未用 未用 reserved

功能:pc <- rs,转移

==>调用子程序指令

(15)jal addr

111010 addr[27..2]

功能:调用子程序,pc <- {pc[31..28],addr[27..2],0,0};$31<-pc+4,返回地址设置;子程序返回,需用指令 jr $31。

==>停机指令

(16)halt (停机指令)

111111 00000000000000000000000000(26位)

不改变pc的值,pc保持不变。

二.实验原理

多周期CPU指的是将整个CPU的执行过程分成几个阶段,每个阶段用一个时钟去完成,然后开始下一条指令的执行,而每种指令执行时所用的时钟数不尽相同,这就是所谓的多周期CPU。CPU在处理指令时,一般需要经过以下几个阶段:
(1) 取指令(IF):根据程序计数器pc中的指令地址,从存储器中取出一条指令,同时,pc根据指令字长度自动递增产生下一条指令所需要的指令地址,但遇到“地址转移”指令时,则控制器把“转移地址”送入pc,当然得到的“地址”需要做些变换才送入pc。
(2) 指令译码(ID):对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,从而产生相应的操作控制信号,用于驱动执行状态中的各种操作。
(3) 指令执行(EXE):根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。
(4) 存储器访问(MEM):所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得到数据地址单元中的数据。
(5) 结果写回(WB):指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。
实验中就按照这五个阶段进行设计,这样一条指令的执行最长需要五个(小)时钟周期才能完成,但具体情况怎样?要根据该条指令的情况而定,有些指令不需要五个时钟周期的,这就是多周期的CPU。

这里写图片描述
图1 多周期CPU指令处理过程
MIPS32的指令的三种格式:
R类型:

31-26 25-21 20-16 15-11 10-6 5-0 op rs rt rd sa func 6位 5位 5位 5位 5位 6位

I类型:

31-26 25-21 20-16 15-0 op rs rt immediate 6位 5位 5位 16位

J类型:

31-26 25-0 op address 6位 26位

其中,
op:为操作码;
rs:为第1个源操作数寄存器,寄存器地址(编号)是00000~11111,00~1F;
rt:为第2个源操作数寄存器,或目的操作数寄存器,寄存器地址(同上);
rd:为目的操作数寄存器,寄存器地址(同上);
sa:为位移量(shift amt),移位指令用于指定移多少位;
func:为功能码,在寄存器类型指令中(R类型)用来指定指令的功能;
immediate:为16位立即数,用作无符号的逻辑操作数、有符号的算术操作数、数据加载(Laod)/数据保存(Store)指令的数据地址字节偏移量和分支指令中相对程序计数器(PC)的有符号偏移量;
address:为地址。

这里写图片描述
图2 多周期CPU状态转移图
状态的转移有的是无条件的,例如从IF状态转移到ID 和 EXE状态就是无条件的;有些是有条件的,例如ID 或 EXE状态之后不止一个状态,到底转向哪个状态由该指令功能,即指令操作码决定。每个状态代表一个时钟周期。

这里写图片描述
图3 多周期CPU控制部件的原理结构图
图3是多周期CPU控制部件的电路结构,三个D触发器用于保存当前状态,是时序逻辑电路,RST用于初始化状态“000“,另外两个部分都是组合逻辑电路,一个用于产生下一个阶段的状态,另一个用于产生每个阶段的控制信号。从图上可看出,下个状态取决于指令操作码和当前状态;而每个阶段的控制信号取决于指令操作码、当前状态和反映运算结果的状态zero标志等。

这里写图片描述
图4 多周期CPU数据通路和控制线路图

图4是一个简单的基本上能够在单周期上完成所要求设计的指令功能的数据通路和必要的控制线路图。其中指令和数据各存储在不同存储器中,即有指令存储器和数据存储器。访问存储器时,先给出地址,然后由读/写信号控制(1-写,0-读。当然,也可以由时钟信号控制,但必须在图上画出来)。对于寄存器组,读操作时,给出寄存器地址(编号),输出端就直接输出相应数据;而在写操作时,在 WE使能信号为1时,在时钟边沿触发写入。图中控制信号功能如表1所示,表2是ALU运算功能表。

特别提示,图上增加IR指令寄存器,目的是使指令代码保持稳定,还有pc增加写使能控制信号pcWre,也是确保pc适时修改,原因都是和多周期工作的CPU有关。ADR、BDR、ALUout、ALUM2DR四个寄存器不需要写使能信号,其作用是切分数据通路,将大组合逻辑切分为若干个小组合逻辑,大延时变为多个分段小延时。

表1 控制信号作用

控制信号名 状态“0” 状态“1” PCWre PC不更改,相关指令:halt PC更改,相关指令:除指令halt外 ALUSrcB 来自寄存器堆data2输出,相关指令:add、sub、addi、or、and、ori、move、beq、slt 来自sign或zero扩展的立即数,相关指令:addi、ori、lw、sw、sll ALUM2Reg 来自ALU运算结果的输出,相关指令:add、sub、addi、or、and、ori、slt、sll、move 来自数据存储器(Data MEM)的输出,相关指令:lw RegWre 无写寄存器组寄存器,相关指令:beq、j、sw、jr、halt 寄存器组寄存器写使能,相关指令:add、sub、addi、or、and、ori、move、slt、sll、lw、jal WrRegData 写入寄存器组寄存器的数据来自pc+4(pc4),相关指令:jal,写$31 写入寄存器组寄存器的数据来自存储器、寄存器组寄存器和ALU运算结果,相关指令:add、addi、sub、or、and、ori、slt、sll、move、lw InsMemRW 读指令存储器(Ins. Data),初始化为0 写指令存储器 DataMemRW 读数据存储器(Data MEM),相关指令:lw 写数据存储器,相关指令:sw IRWre IR(指令寄存器)不更改 IR寄存器写使能。向指令存储器发出读指令代码后,这个信号也接着发出,在时钟上升沿,IR接收从指令存储器送来的指令代码。与每条指令都相关。

特别信号:
这里写图片描述

相关部件及引脚说明:

  • Instruction Memory:指令存储器,
    • Iaddr,指令地址输入端口
    • DataIn,存储器数据输入端口
    • DataOut,存储器数据输出端口
    • RW,指令存储器读写控制信号,为1写,为0读
  • Data Memory:数据存储器,
    • Daddr,数据地址输入端口
    • DataIn,存储器数据输入端口
    • DataOut,存储器数据输出端口
    • RW,数据存储器读写控制信号,为1写,为0读
  • Register File:(寄存器组)
    • Read Reg1,rs寄存器地址输入端口
    • Read Reg2,rt寄存器地址输入端口
    • Write Reg,将数据写入的寄存器,其地址输入端口(rt、rd)
    • Write Data,写入寄存器的数据输入端口
    • Read Data1,rs寄存器数据输出端口
    • Read Data2,rt寄存器数据输出端口
    • WE,写使能信号,为1时,在时钟上升沿写入
  • IR: 指令寄存器,用于存放正在执行的指令代码
  • ALU:
    • result,ALU运算结果
    • zero,运算结果标志,结果为0输出1,否则输出0

表2 ALU运算功能表
PS:功能和单周期并不一样

ALUOp[2..0] 功能 描述 000 Y = A + B 加 001 Y = A – B 减 010 if (A < B)Y = 1; else Y = 0; 比较A与B 011 Y = A >> B A右移B位 100 Y = A << B A左移B位 101 Y = A ∨ B 或 110 Y = A ∧ B 与 111 Y = A ⊕ B 异或

值得注意的问题,设计时,用模块化的思想方法设计,关于Control Unit 设计、ALU设计、存储器设计、寄存器组设计等等,是必须认真考虑的问题。


科普部分:

首先,进行操作之前,需要先了解什么是多周期cpu:

多周期不是流水线!
多周期不是流水线!
多周期不是流水线!
重要的事情先说三次。

Q:什么是多周期?和单周期区别在哪?
A:单周期是一个大时钟周期(不妨叫指令周期),完成IF,ID,EXE,MEM,WB五个模块。多周期是把这个大的时钟周期,分成五个小的时钟周期,每个时钟周期只执行IF,或ID…..等其中一个小功能。
说这么多,不如来张图吧:
这里写图片描述
可以看出,其实多周期和单周期执行时间并没有什么区别,只是把一个大时钟周期拆开成了五个时钟周期而已。

所以问题又来了:
Q:单周期cpu改成多周期cpu会有什么提升呢?
A:单周期所有的指令都要按顺序执行IF->ID->EXE->MEM->WB(无论有没有用上)。但多周期可以不用,比如beq指令就只需要IF->ID->EXE,jal指令只需要IF->ID,就可以省很多不必要的时间。
举个例子,让单周期和多周期同时执行j和beq两条指令,结果如下:
这里写图片描述
时间短了一倍,很明显看得出多周期的优点了。

Q:怎么把一个指令周期变成多个小时钟周期?
A:首先我们得先明白一点:我们仿真模拟设置的时钟周期时间实际上比执行时间大太多,导致其实在时钟上升沿一刹那后,所有结果就计算出来了。所以,要把单周期分成多周期,我们就要强行延长周期,让IF结果在第一个时钟周期完成,让ID结果在第二个时钟周期完成,以此类推。关键的来了,用什么去实现呢?用上升沿触发的寄存器。

Q:寄存器怎么实现延迟时钟周期的效果?
A:读万卷书,不如行万里路。来实验下就知道了:
我先用一个小程序Test来模拟这个效果:(Test代码点我下载)

主模块(把四个上升沿触发的寄存器串联):

module Test(    input CLK,     input value    );    wire value1;    wire value2;    wire value3;    wire value4;    wire value5;    WireToReg r1(CLK, 1, value, value1);    WireToReg r2(CLK, 1, value1, value2);    WireToReg r3(CLK, 1, value2, value3);    WireToReg r4(CLK, 1, value3, value4);    WireToReg r5(CLK, 1, value4, value5);endmodule

WireToReg(上升沿触发的寄存器):

module WireToReg(     input CLK,     input Enable,     input [31:0] in,     output reg[31:0] out    );    initial     begin        out = 0;     end    always@(posedge CLK)     begin        out = Enable ? in : out;     endendmodule

测试文件(简单的时钟周期变化):

    initial begin        // Initialize Inputs        CLK = 0;        value = 0;        // Wait 100 ns for global reset to finish        #100;        value = 1;  // 将初始值变成1        // Add stimulus here        // 时钟周期变化        forever #10         begin            CLK = !CLK;         end    end

简单的测试一下,得到结果:
这里写图片描述
很明显看出来,本来应该同时变成1的五个value成功的被延时,顺序的在上升沿变成了1,这5个值就代表着多周期的五个时钟周期。

原因:由于所有寄存器是在上升沿同时触发,并同时根据输入改变输出,由于有微小的时间差,所以其实得到的输入是上一个周期的输入。
画个图来表示:
这里写图片描述
由于同时触发,所以value1和value2的值在t1时刻判断输入,此时输入value为1,value1为0,所以value1改变,value2不改变,value1在t2时刻改成1,但是此时已经过了时钟上升沿,所以value2不会改变,因此value2被延迟了一个周期。

结论:上升沿触发的寄存器,输出改变时间会比输入改变时间多一个周期。

多周期的原理和实现方法已经科普完了,下面就是实现部分了。


正文部分:

数据通路图:
这里写图片描述
经过科普,可以知道那几个新的寄存器的作用了,就是为了延长时钟周期。
(PS:数据通路图我改了一下右下角,只有这样才能使算术逻辑指令只经过3个寄存器,只执行4个周期。)

实现思路:

每一个组件都能写成一个下层模块,实现相应的功能。
对比单周期cpu,新加入一个WireToReg和PCJUMP模块(左下角那个),用于分割时钟周期和补全J型指令的address。
顶层模块调用各个下层模块,根据数据通路图将模块之间连线,保证有限状态机状态改变后,控制模块改变控制信号,然后其他所有的模块都根据控制信号的改变,发生对应的变化。
测试模块控制CLK和Reset信号,使有限状态机中的状态发生改变。


具体代码:

每个组件都能写成下层模块,下面把每个组件都写成模块:

新加入模块:

我新加入了几个辅助模块,分别是四路选择器,线转寄存器模块和新的指令JUMP模块。
PS:由于RW模块所需要的寄存器IR太多,因此合在了一起。

1.多路选择器(32线和5线,4路输入和2路输入):

module MUX4L_5(     input [1:0] control,    input [4:0] in00,    input [4:0] in01,     input [4:0] in10,     input [4:0] in11,    output [4:0] out    );    // 5线4路选择器    assign out = control[0] ? (control[1] ? in11 : in01) : (control[1] ? in10 : in00);endmodule
module MUX4L_32(     input [1:0]control,    input [31:0] in00,    input [31:0] in01,     input [31:0] in10,     input [31:0] in11,    output [31:0] out    );    // 32线4路选择器    assign out = control[0] ? (control[1] ? in11 : in01) : (control[1] ? in10 : in00);endmodule
module MUX2L_32(     input control,    input [31:0] in0,    input [31:0] in1,    output [31:0] out    );    // 32线2路选择器    assign out = control ? in1 : in0;endmodule

输入:四/两个输入
控制信号:control
输出:out
解释:很简单,就不解释了。

2.WireToReg:线转寄存器,用来分割时钟周期

module WireToReg(     input CLK,     input Enable,     input [31:0] in,     output reg[31:0] out    );    initial     begin        out = 0;     end    always@(posedge CLK)     begin        out = Enable ? in : out;     endendmodule

输入:in
控制信号:CLK,Enable
输出:out
解释:CLK上升沿修改,Enable为使能端,也不多解释了。

3.PCJUMP:address补全成指令地址

module PCJUMP(    input [31:0] PC0,          // 指令    input [25:0] inAddress,    // 输入地址    output [31:0] outAddress   // 输出地址(指令)    );    // outAddress = PC + inAddress + 00    assign outAddress[31:28] = PC0[31:28];    assign outAddress[27:2] = inAddress;    assign outAddress[1:0] = 2'b00;endmodule

输入:PC0,inAddress
控制信号:无
输出:outAddress
解释:outAddress = PC0[31:28] + inAddress << 2 + 00(主要为了实现j型指令)

单周期cpu原有模块修改:

根据通路图,我修改了不少模块,对每个原有模块都进行了改进,下面我会对每个模块进行说明。

4.PC:CLK上升沿触发,更改指令地址

module PC(    input CLK,                         // 时钟    input Reset,                       // 重置信号    input PCWre,                       // PC是否更改,如果为0,PC不更改    input [31:0] newAddress,           // 新指令    output reg[31:0] currentAddress    // 当前指令    );    initial begin        currentAddress <= 0;  // 非阻塞赋值    end    always@(posedge CLK or posedge Reset)     begin        if (Reset == 1)  currentAddress <= 0;  // 如果重置,赋值为0        else          begin            if (PCWre)  currentAddress <= newAddress;            else  currentAddress <= currentAddress;         end     end endmodule

修改:毫无修改,因此直接拿的上一个实验,区别是PCWre终于有实际的作用了(不再一直为1)。
输入:newAddress
控制信号:CLK,Reset,PCWre
输出:currentAddress
解释:由于指令地址存储在寄存器里,一开始需要赋currentAddress为0。Reset是重置信号,当为1时,指令寄存器地址重置。PCWre的作用为保留现场,如果PCWre为0,指令地址不变。

5.InstructionMemory:储存指令,分割指令(包含寄存器)

module InstructionMemory(     input InsMemRW,            // 读写控制信号,1为写,0位读    input [31:0] IAddr,        // 指令地址输入入口     //input IDataIn,           // 没用到      input CLK,                  // 时钟信号     input IRWre,                // 输出寄存器写使能    output reg[5:0] op,    output reg[4:0] rs,    output reg[4:0] rt,    output reg[4:0] rd,    output reg[15:0] immediate, // 指令代码分时段输出     output reg[25:0] address    );    reg[7:0] mem[0:63];  // 新建一个32位的数组用于储存指令    initial      begin        // 初始化        op <= 0;        rs <= 0;        rt <= 0;        rd <= 0;        immediate <= 0;        address <= 0;        $readmemb("test/test.txt", mem);  //读取测试文档中的指令     end    // 从地址取值,然后输出    always@(posedge CLK or posedge IRWre)     begin        if (IRWre == 1)         begin            op = mem[IAddr][7:2];            rs[4:3] = mem[IAddr][1:0];            rs[2:0] = mem[IAddr + 1][7:5];            rt = mem[IAddr + 1][4:0];            rd = mem[IAddr + 2][7:3];            immediate[15:8] = mem[IAddr + 2];            immediate[7:0] = mem[IAddr + 3];            // 地址赋值            address[25:21] = rs;            address[20:16] = rt;            address[15:0] = immediate;         end     endendmodule

修改:将输出的指令变成了寄存器,并且新加了address输出,同时新加入CLK和IRWre控制信号
输入:IAddr
控制信号:InsMenRW,CLK,IRWre
输出:op,rs,rt,rd,immediate,address
解释:该部分为指令寄存器,通过一个64大小的8位寄存器数组来保存从文件输入的全部指令。然后通过输入的地址,找到相应的指令,并分割成op,rs,rt,rd,immediate,address输出。其中InsMenRW并没有什么卵用,IRWre为寄存器使能端。(由于寄存器地址+4,所以不用右移变换成真正的地址)

6.RegisterFile:储存寄存器组,并根据地址对寄存器组进行读写

module RegisterFile(     input CLK,                       // 时钟     input RegWre,                    // 写使能信号,为1时,在时钟上升沿写入    input [4:0] rs,                  // rs寄存器地址输入端口    input [4:0] rt,                  // rt寄存器地址输入端口    input [4:0] WriteReg,            // 将数据写入的寄存器端口,其地址来源rt或rd字段    input [31:0] WriteData,          // 写入寄存器的数据输入端口     output [31:0] ReadData1,         // rs数据输出端口    output [31:0] ReadData2          // rt数据输出端口    );    reg [31:0] register[0:31];  // 新建32个寄存器,用于操作    // 初始时,将32个寄存器和ReadData全部赋值为0    integer i;    initial      begin        for(i = 0; i < 32; i = i + 1)  register[i] <= 0;     end    // 直接读寄存器    assign ReadData1 = register[rs];    assign ReadData2 = register[rt];    // 接受信号并读寄存器    always@(posedge RegWre)     begin        // 如果寄存器不为0,并且RegWre为真,写入数据        if (RegWre && WriteReg != 0)  register[WriteReg] = WriteData;     end endmodule

修改:毫无修改,仅仅把寄存器数量从16个扩充成32个(为了实现jal指令)
输入:rs,rt,WriteReg,WriteData
控制信号:CLK,RegWre
输出:ReadData1,ReadData2
解释:该部分为寄存器读写单元,RegWre的作用是控制寄存器是否写入。同上,通过一个32大小的32位寄存器数组来模拟寄存器,开始时全部置0。通过访问寄存器的地址,来获取寄存器里面的值,并进行操作。在jal指令执行时,返回的指令会保存31PS0恒为0,所以写入寄存器的地址不能为0)

7.ALU(算术逻辑单元):用于逻辑指令计算和跳转指令比较

module ALU(     input [2:0] ALUOp,           // ALU操作控制    input [31:0] A,              // 输入1    input [31:0] B,              // 输入2    output reg zero,             // 运算结果result的标志,result为0输出1,否则输出0     output reg[31:0] result      // ALU运算结果    );    initial      begin        zero <= 0;     end    // 进行ALU计算    always@(*)     begin        // 进行ALU直接运算        case (ALUOp)            3'b000 :  result <= A + B;             // 加法            3'b001 :  result <= A - B;             // 减法            3'b010 :  result <= (A < B) ? 1 : 0;   // 判断A是否<B,z只能这么赋值            3'b011 :  result <= A >> B;            // A右移B位            3'b100 :  result <= A << B;            // A左移B位            3'b101 :  result <= A | B;             // A或B            3'b110 :  result <= A & B;             // A与B            3'b111 :  result <= A ^ B;             // 异或        endcase        // 设置zero        if (result)  zero = 0;        else  zero = 1;     endendmodule

修改:用于ALU表变了,所以指令代表的操作也发生了变化,因此进行了操作的修改
输入:A,B
控制信号:ALUOp
输出:zero,result
解释:ALUOp用于控制算数的类型,AB为输入数,result为运算结果,zero主要用于beq和bne指令的判断。

8.SignZeroExtend:用于immediate和sa的扩展

module SignZeroExtend(    input [1:0]ExtSel,              // 控制补位,如果为1X,进行符号扩展                                     // 如果为01,immediate全补0                                     // 如果为00,sa全补0    input [15:0] immediate,         // 16位立即数    output [31:0] extendImmediate   // 输出的32位立即数    );    // 进行扩展    assign extendImmediate[4:0] = (ExtSel == 2'b00) ? immediate[10:6] : immediate[4:0];    assign extendImmediate[15:5] = (ExtSel == 2'b00) ? 3'b00000000000 : immediate[15:5];    // 前半段填充    assign extendImmediate[31:16] = (ExtSel == 2'b10) ? (immediate[15] ? 16'hffff : 16'h0000) : 16'h0000;endmodule

修改:由于控制信号由一位变成了两位(新加入了sa的扩展),因此赋值也进行了修改
输入:immediate
控制信号:ExtSel
输出:extendImmediate
解释:比较简单的一个模块。ExtSel为控制补位信号。判断0位确定扩充immediate还是sa,判断1位决定是否进行符号扩展。

9.DataMemory:用于内存存储,内存读写

module DataMemory(     input DataMemRW,            // 数据存储器读写控制信号,为1写,为0读    input [31:0] DAddr,         // 数据存储器地址输入端口    input [31:0] DataIn,        // 数据存储器数据输入端口    output reg [31:0] DataOut   // 数据存储器数据输出端口    );    // 模拟内存,以8位为一字节存储,共64字节    reg [7:0] memory[0:63];    // 初始赋值    integer i;    initial     begin        for (i = 0; i < 64; i = i + 1)  memory[i] <= 0;     end    // 读写内存    always@(DAddr or DataMemRW)     begin        // 写内存        if (DataMemRW)         begin           memory[DAddr] <= DataIn[31:24];            memory[DAddr + 1] <= DataIn[23:16];            memory[DAddr + 2] <= DataIn[15:8];            memory[DAddr + 3] <= DataIn[7:0];         end        // 读内存        else         begin           DataOut[31:24] <= memory[DAddr];            DataOut[23:16] <= memory[DAddr + 1];            DataOut[15:8] <= memory[DAddr + 2];            DataOut[7:0] <= memory[DAddr + 3];         end     endendmodule

修改:无
输入:DAddr,DataIn
控制信号:DataMenRW
输出:DataOut
解释:该部分控制内存存储。同上,用64大小的8位寄存器数组模拟内存(内存小主要是因为编译快),内存部分采用小端模式。DataMenRW控制内存读写。由于指令为真实地址,所以不需要*4。


最重要的ControlUnit模块:

可能做完单周期cpu,会觉得这个模块也不过如此,但是在多周期cpu中,这个模块可是重中之重,因此千万不能小看!(我bug全出在这里了)
下面结合图解进行说明:
这里写图片描述
首先是控制信号表,乍一看可能和单周期一样,但是却略有不同。在表中,控制信号分为三种颜色:黑色,红色,蓝色,下面我会一一分析颜色代表的意思。

黑色:黑色意味着这种状态可以在一个指令的全部时钟周期一直保持(即可以保持二-五个时钟周期)。因为:
1.黑色控制信号状态不变并不会导致寄存器一直写入,而影响寄存器的值。
2.就算输入信号改变,但是输出信号要么不变,要么不会影响本次操作的中间值或结果。
红色:红色意味着这个状态没有什么卵用,完全可以去掉:因为:
1.红色控制信号在多周期CPU中没起到控制作用,拿来摆设。(InsMemRW)
2.红色控制信号保持一个值,在所有指令中都没有改变过。(IRWre)
蓝色:蓝色意味着这个状态只能持续一个时钟周期(即一个状态机),过了这个时钟周期后必须改变。因为:
1.蓝色控制信号在一个指令中只能执行其中一个时钟周期。比如PC赋值,只能在一个指令中赋值一次。
2.蓝色控制信号和寄存器或内存写入有关,执行多次会发生奇怪bug(心痛= =)。
因此,控制信号写法就很明了了。红色信号一开始赋值即可,之后不变,黑色根据指令不同赋值不同,在每个IF状态赋值即可,蓝色信号随着状态机变化赋值。

下表是蓝色状态随着状态机赋值的标准:
这里写图片描述

得到这些信息,就可以写这个模块了。

输入输出部分:

module ControlUnit(     input CLK,              // 时钟     input reset,            // 重置信号    input [5:0] op,         // op操作符    input zero,             // ALU的zero输出     // 一堆控制信号    output reg PCWre,           // (PC)PC是否更改,如果为0,PC不更改,                                          // 另外,除D_Tri == 000状态之外,其余状态也不能改变PC的值。    output reg ALUSrcB,         // 多路选择器    output reg ALUM2Reg,        // 多路选择器    output reg RegWre,          // (RF)写使能信号,为1时,在时钟上升沿写入     output reg WrRegData,       // 2路选择器,判断数据写入是否为PC指令,如果为1,则不是,jar用到    output reg InsMemRW,        // (IM)读写控制信号,1为写,0位读,固定为0    output reg DataMemRW,       // (DM)数据存储器读写控制信号,为1写,为0读     output reg IRWre,           // 寄存器写使能,暂时没什么用,固定为1    output reg[1:0] ExtSel,     // (EXT)控制补位,如果为1,进行符号扩展,如果为0,全补0     output reg[1:0] PCSrc,      // 4路选择器,选择PC指令来源    output reg[1:0] RegOut,     // 4路选择器,判断写寄存器地址的来源    output reg[2:0] ALUOp       // (ALU)ALU操作控制     );

输入:无
控制信号:CLK,reset,op,zero
输出:各类控制信号
解释:和单周期CPU差别不大,主要在于新加入了新的控制信号和状态机。

指令定义:类似于常量赋值

    // 有限状态机宏定义    parameter [2:0]         IF = 3'b000,        ID = 3'b001,        EXELS = 3'b010,        MEM = 3'b011,        WBL = 3'b100,        EXEBR = 3'b101,        EXEAL = 3'b110,        WBAL = 3'b111;    // 指令宏定义,由于有些指令为关键字,因此全部首字符大写    parameter [5:0]        Add = 6'b000000,        Addi = 6'b000010,         Sub = 6'b000001,       Ori = 6'b010010,          And = 6'b010001,        Or = 6'b010000,      Sll = 6'b011000,        Move = 6'b100000,        Slt = 6'b100111,        Sw = 6'b110000,        Lw = 6'b110001,        Beq = 6'b110100,        J = 6'b111000,        Jr = 6'b111001,         Jal = 6'b111010,        Halt = 6'b111111; 

状态机变化:(为了避免竞争冒险,我将D触发器改成了下降沿触发)

D触发器用3线寄存器模拟。

    // 3位D触发器,代表8个状态    /* 000 -> IF     * 001 -> ID     * 010 -> EXELS     * 011 -> MEM     * 100 -> WBL     * 101 -> EXEBR     * 110 -> EXEAL     * 111 -> WBAL     */    reg [2:0] D_Tri;    // D触发器变化,PS:为了避免竞争冒险,所有值变化改为下降沿触发    // PCWre,RegWre和DataMemRW的变化影响很大,要在这里写    always@(negedge CLK or posedge reset)     begin        // 重置属性        if (reset)           begin            D_Tri = IF;            PCWre = 0;            RegWre = 0;         end        else         begin            case (D_Tri)                // IF -> ID                IF:                 begin                    D_Tri <= ID;                    // 禁止写指令,寄存器,和内存                    PCWre = 0;                    RegWre = 0;                    DataMemRW = 0;                 end                // ID -> EXE                ID:                 begin                    case (op)                        // 如果是beq指令,跳到EXEBR                        Beq:  D_Tri <= EXEBR;                        // 如果是sw,lw指令,跳到EXELS                        Sw, Lw:  D_Tri <= EXELS;                        // 如果是j,jal,jr,halt,跳到IF                        J, Jal, Jr, Halt:                         begin                           D_Tri = IF;                            // 如果指令是halt,禁止写指令                            if (op == Halt)  PCWre = 0;                              else  PCWre = 1;                            // 如果指令是jal,允许写寄存器                            if (op == Jal)  RegWre = 1;                            else  RegWre = 0;                         end                        // 其他,跳到EXEAL                        default:  D_Tri = EXEAL;                    endcase                 end                // EXEAL -> WBAL                EXEAL:                 begin                    D_Tri = WBAL;                    // 允许写寄存器                    RegWre = 1;                  end                 // EXELS -> MEM                EXELS:                   begin                    D_Tri = MEM;                    // 如果指令为sw,允许写内存                    if (op == Sw)  DataMemRW = 1;                 end                // MEM -> WBL                MEM:                 begin                    // 如果指令为sw,MEM -> IF                    if (op == Sw)                     begin                        D_Tri = IF;                        // 允许写指令                        PCWre = 1;                     end                    // 如果指令为lw,MEM -> WBL                    else                     begin                        D_Tri = WBL;                        // 允许写寄存器                        RegWre = 1;                     end                 end                 // 其他 -> IF                default:                 begin                    D_Tri = IF;                    // 允许写指令                    PCWre = 1;                    // 禁止写寄存器                    RegWre = 0;                 end            endcase         end     end

(其他部分同单周期CPU,略)


编写主模块和测试单元:

最水的模块,还是那句话,只要底层ok,图ok,这个模块就不会有问题。

主模块:(没什么好说的,就是把线连到一起)

module SingleCPU(    input CLK,    input Reset,    output [5:0] op,     output [4:0] rs,     output [4:0] rt,     output [4:0] rd,     output [15:0] immediate,    output [31:0] ReadData1,    output [31:0] ReadData2,     output [31:0] WriteData,     output [31:0] DataOut,    output [31:0] currentAddress,    output [31:0] result,     output PCWre    );    // 各种临时变量   wire [31:0] B, newAddress;   wire [31:0] currentAddress_4, extendImmediate, currentAddress_immediate, outAddress, ALUM2DR;         wire [4:0] WriteReg;      wire [25:0] address;   wire zero, ALUSrcB, ALUM2Reg, RegWre, WrRegData, InsMemRW, DataMemRW, IRWre;    wire [1:0] ExtSel, PCSrc, RegOut;    wire [2:0] ALUOp;    // 寄存器输出值    wire [31:0] RegReadData1, RegReadData2, RegResult, RegDataOut;    /*module ControlUnit(     input CLK,              // 时钟     input reset,            // 重置信号    input [5:0] op,         // op操作符    input zero,             // ALU的zero输出     // 一堆控制信号    output reg PCWre,           // (PC)PC是否更改,如果为0,PC不更改,                                          // 另外,除D_Tri == 000状态之外,其余状态也不能改变PC的值。    output reg ALUSrcB,         // 多路选择器    output reg ALUM2Reg,        // 多路选择器    output reg RegWre,          // (RF)写使能信号,为1时,在时钟上升沿写入     output reg WrRegData,       // 2路选择器,判断数据写入是否为PC指令,如果为1,则不是,jar用到    output reg InsMemRW,        // (IM)读写控制信号,1为写,0位读,固定为0    output reg DataMemRW,       // (DM)数据存储器读写控制信号,为1写,为0读     output reg IRWre,           // 寄存器写使能,暂时没什么用,固定为1    output reg[1:0] ExtSel,     // (EXT)控制补位,如果为1,进行符号扩展,如果为0,全补0     output reg[1:0] PCSrc,      // 4路选择器,选择PC指令来源    output reg[1:0] RegOut,     // 4路选择器,判断写寄存器地址的来源    output reg[2:0] ALUOp       // (ALU)ALU操作控制     );*/    ControlUnit cu(CLK, Reset, op, zero, PCWre, ALUSrcB, ALUM2Reg,        RegWre, WrRegData, InsMemRW, DataMemRW, IRWre, ExtSel, PCSrc, RegOut, ALUOp);    /*module PC(    input CLK,                         // 时钟    input Reset,                       // 重置信号    input PCWre,                       // PC是否更改,如果为0,PC不更改    input [31:0] newAddress,           // 新指令    output reg[31:0] currentAddress    // 当前指令    );*/    PC pc(CLK, Reset, PCWre, newAddress, currentAddress);    /*module InstructionMemory(     input InsMemRW,            // 读写控制信号,1为写,0位读    input [31:0] IAddr,        // 指令地址输入入口     //input IDataIn,           // 没用到      input CLK,                  // 时钟信号     input IRWre,                // 输出寄存器写使能    output reg[5:0] op,    output reg[4:0] rs,    output reg[4:0] rt,    output reg[4:0] rd,    output reg[15:0] immediate, // 指令代码分时段输出     output reg[25:0] address    );*/    InstructionMemory im(InsMemRW, currentAddress, CLK, IRWre, op, rs, rt, rd, immediate, address);    /*module RegisterFile(     input CLK,                       // 时钟     input RegWre,                    // 写使能信号,为1时,在时钟上升沿写入    input [4:0] rs,                  // rs寄存器地址输入端口    input [4:0] rt,                  // rt寄存器地址输入端口    input [4:0] WriteReg,            // 将数据写入的寄存器端口,其地址来源rt或rd字段    input [31:0] WriteData,          // 写入寄存器的数据输入端口     output [31:0] ReadData1,         // rs数据输出端口    output [31:0] ReadData2          // rt数据输出端口    );*/    RegisterFile rf(CLK, RegWre, rs, rt, WriteReg, WriteData, ReadData1, ReadData2);    /*module ALU(     input [2:0] ALUOp,           // ALU操作控制    input [31:0] A,              // 输入1    input [31:0] B,              // 输入2    output reg zero,             // 运算结果result的标志,result为0输出1,否则输出0     output reg[31:0] result      // ALU运算结果    );*/    ALU alu(ALUOp, ReadData1, B, zero, result);    /*module SignZeroExtend(    input [1:0]ExtSel,              // 控制补位,如果为1X,进行符号扩展                                     // 如果为01,immediate全补0                                     // 如果为00,sa全补0    input [15:0] immediate,         // 16位立即数    output [31:0] extendImmediate   // 输出的32位立即数    );*/    SignZeroExtend sze(ExtSel, immediate, extendImmediate);    /*module DataMemory(     input DataMemRW,            // 数据存储器读写控制信号,为1写,为0读    input [31:0] DAddr,         // 数据存储器地址输入端口    input [31:0] DataIn,        // 数据存储器数据输入端口    output reg [31:0] DataOut   // 数据存储器数据输出端口    );*/    DataMemory dm(DataMemRW, RegResult, RegReadData2, DataOut);    /*module PCJUMP(    input [31:0] PC0,          // 指令    input [25:0] inAddress,    // 输入地址    output [31:0] outAddress   // 输出地址(指令)    );*/    PCJUMP pcj(currentAddress, address, outAddress);    assign currentAddress_4 = currentAddress + 4;    assign currentAddress_immediate = currentAddress_4 + (extendImmediate << 2);    // 线转寄存器    WireToReg wtrA(CLK, 1, ReadData1, RegReadData1);    WireToReg wtrB(CLK, 1, ReadData2, RegReadData2);    WireToReg wtrALU(CLK, 1, result, RegResult);    WireToReg wtrMEM(CLK, 1, DataOut, RegDataOut);    // 2路选择器    MUX2L_32 mux2_1(WrRegData, currentAddress_4, ALUM2DR, WriteData);    MUX2L_32 mux2_2(ALUSrcB, RegReadData2, extendImmediate, B);    MUX2L_32 mux2_3(ALUM2Reg, result, RegDataOut, ALUM2DR);    // 4路选择器    MUX4L_5 mux4_1(RegOut, 5'b11111, rt, rd, 5'b00000, WriteReg);    MUX4L_32 mux4_2(PCSrc, currentAddress_4, currentAddress_immediate,        ReadData1, outAddress, newAddress);endmodule

测试文件:(将单周期CPU的指令进行少许修改,其实并没什么变化)

    initial begin        // Initialize Inputs        CLK = 0;        Reset = 1;        // Wait 50 ns for global reset to finish        //#10; // 刚开始设置pc为0      //   CLK = !CLK;  // 下降沿,使PC先清零      #10;         Reset = 0;  // 清除保持信号      forever #10         begin // 产生时钟信号,周期为10s         CLK = !CLK;       end    end

测试结果:

在单周期指令的基础上,新加入了几个测试指令:

//addi $1, $0, 4000010 00000 00001 0000000000000100//addi $2, $0, 8000010 00000 00010 0000000000001000//sw $2, 0($2)110000 00010 00010 0000000000000000//add $3, $2, $1000000 00010 00001 00011 00000000000//sub $3, $3, $1000001 00011 00001 00011 00000000000//beq $2, $3, -2110100 00010 00011 1111111111111110//jal 0x00000008 (ori指令)111010 00000000000000000000001000//halt 111111 00000000000000000000000000//ori $1, $1, 1010010 00001 00001 0000000000000001//or $3, $2, $1010000 00010 00001 00011 00000000000//move $3, $2100000 00010 00000 00011 00000000000//and $1, $3, $2010001 00011 00010 00001 00000000000//lw $4, 0($2)110001 00010 00100 0000000000000000//sll $4, $4, 2011000 00100 00000 00100 00010 000000//slt $2, $4, $5100111 00010 00100 00101 00000000000//jr $31111001 11111 000000000000000000000
00001000000000010000000000000100000010000000001000000000000010001100000001000010000000000000000000000000010000010001100000000000000001000110000100011000000000001101000001000011111111111111111011101000000000000000000000001000111111000000000000000000000000000100100000100001000000000000000101000000010000010001100000000000100000000100000000011000000000000100010001100010000010000000000011000100010001000000000000000000011000001000000000100000100000001001110001000100001010000000000011100111111000000000000000000000

测试结果如下:
这里写图片描述
结果并没有什么问题。


总结:

写多周期cpu,让我知道了什么才是竞争冒险,怎么解决竞争冒险。还有想吐槽的一点,这软件也太难debug了吧,完全靠脑测啊= =