CC BY 4.0 (除特别声明或转载文章外)
如果这篇博客帮助到你,可以请我喝一杯咖啡~
(更新于 2022)给 YatCPU 打个广告。
实验目的
- 掌握单周期 CPU 数据通路图的构成、原理及其设计方法;
- 掌握单周期 CPU 的实现方法,代码实现方法;
- 认识和掌握指令与 CPU 的关系;
- 掌握测试单周期 CPU 的方法。
实验内容
设计一个单周期 CPU,该 CPU 至少能实现以下指令功能操作,指令与格式如下。
算术运算指令
add rd rs rt
000000 | rs(5 位) | rt(5 位) | rd(5 位) | reserved |
---|---|---|---|---|
功能:rd←rs + rt;reserved 为预留部分,即未用,一般填「0」。
sub rd rs rt
000001 | rs(5 位) | rt(5 位) | rd(5 位) | reserved |
---|---|---|---|---|
功能:rd←rs - rt。
addiu rt rs immediate
000010 | rs(5 位) | rt(5 位) | immediate(16 位) |
---|---|---|---|
功能:rt←rs + (sign-extend)immediate;immediate 符号扩展再参加「加」运算。
逻辑运算指令
andi rt rs immediate
010000 | rs(5 位) | rt(5 位) | immediate(16 位) |
---|---|---|---|
功能:rt←rs & (zero-extend)immediate;immediate 做「0」扩展再参加「与」运算。
and rd rs rt
010001 | rs(5 位) | rt(5 位) | rd(5 位) | reserved |
---|---|---|---|---|
功能:rd←rs & rt;逻辑与运算。
ori rt rs immediate
010010 | rs(5 位) | rt(5 位) | immediate(16 位) |
---|---|---|---|
功能:rt←rs | (zero-extend)immediate;immediate 做「0」扩展再参加「或」运算。
or rd rs rt
010011 | rs(5 位) | rt(5 位) | rd(5 位) | reserved |
---|---|---|---|---|
功能:rd←rs | rt;逻辑或运算。
移位指令
sll rd rt sa
011000 | 未用 | rt(5 位) | rd(5 位) | sa(5 位) | reserved |
---|---|---|---|---|---|
功能:rd<-rt«(zero-extend)sa,左移 sa 位,(zero-extend)sa。
比较指令
slti rt rs immediate
011100 | rs(5 位) | rt(5 位) | immediate(16 位) |
---|---|---|---|
功能:if (rs< (sign-extend)immediate) rt =1 else rt=0, 带符号比较,详见 ALU 运算功能表。
存储器读/写指令
sw rt immediate(rs)
100110 | rs(5 位) | rt(5 位) | immediate(16 位) |
---|---|---|---|
功能:memory[rs+ (sign-extend)immediate]←rt;immediate 符号扩展再相加。即将 rt 寄存器的内容保存到 rs 寄存器内容和立即数符号扩展后的数相加作为地址的内存单元中。
lw rt immediate(rs)
100111 | rs(5 位) | rt(5 位) | immediate(16 位) |
---|---|---|---|
功能:rt ← memory[rs + (sign-extend)immediate];immediate 符号扩展再相加。即读取 rs 寄存器内容和立即数符号扩展后的数相加作为地址的内存单元中的数,然后保存到 rt 寄存器中。
分支指令
beq rs rt immediate
110000 | rs(5 位) | rt(5 位) | immediate(16 位) |
---|---|---|---|
功能:if(rs=rt) pc←pc + 4 + (sign-extend)immediate «2 else pc ←pc + 4
特别说明:immediate 是从 PC+4 地址开始和转移到的指令之间指令条数。immediate 符号扩展之后左移 2 位再相加。为什么要左移 2 位?由于跳转到的指令地址肯定是 4 的倍数(每条指令占 4 个字节),最低两位是「00」,因此将 immediate 放进指令码中的时候,是右移了 2 位的,也就是以上说的「指令之间指令条数」。
bne rs rt immediate
110001 | rs(5 位) | rt(5 位) | immediate(16 位) |
---|---|---|---|
功能:if(rs!=rt) pc ← pc + 4 + (sign-extend)immediate «2 else pc ← pc + 4 特别说明:与 beq 不同点是,不等时转移,相等时顺序执行。
bltz rs immediate
110010 | rs(5 位) | 00000 | immediate(16 位) |
---|---|---|---|
功能:if(rs<$zero) pc ← pc + 4 + (sign-extend)immediate «2 else pc ← pc + 4。
跳转指令
j addr
111000 | addr[27:2] |
---|---|
功能:pc <-{(pc+4)[31:28],addr[27:2],2'b00}
,无条件跳转。
说明:由于 MIPS32 的指令代码长度占 4 个字节,所以指令地址二进制数最低 2 位均为 0,将指令地址放进指令代码中时,可省掉!这样,除了最高 6 位操作码外,还有 26 位可用于存放地址,事实上,可存放 28 位地址,剩下最高 4 位由 pc+4 最高 4 位拼接上。
停机指令
halt
111111 | 00000000000000000000000000(26 位) |
---|---|
功能:停机;不改变 PC 的值,PC 保持不变。
实验原理
单周期 CPU 指的是一条指令的执行在一个时钟周期内完成,然后开始下一条指令的执行,即一条指令用一个时钟周期完成。电平从低到高变化的瞬间称为时钟上升沿,两个相邻时钟上升沿之间的时间间隔称为一个时钟周期。时钟周期一般也称振荡周期(如果晶振的输出没有经过分频就直接作为 CPU 的工作时钟,则时钟周期就等于振荡周期。若振荡周期经二分频后形成时钟脉冲信号作为 CPU 的工作时钟,这样,时钟周期就是振荡周期的两倍)。
CPU 在处理指令的几个步骤
flowchart LR
取指令IF-->指令译码ID
指令译码ID-->指令执行EXE
指令执行EXE-->存储器访问MEM
存储器访问MEM-->结果写回WB
结果写回WB-->取指令IF
图 1 CPU 指令处理过程
取指令(IF)
根据程序计数器 PC 中的指令地址,从存储器中取出一条指令,同时,PC 根据指令字长度自动递增产生下一条指令所需要的指令地址,但遇到「地址转移」指令时,则控制器把「转移地址」送入 PC,当然得到的「地址」需要做些变换才送入 PC。
指令译码(ID)
对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,从而产生相应的操作控制信号,用于驱动执行状态中的各种操作。
指令执行(EXE)
根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。
存储器访问(MEM)
所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得到数据地址单元中的数据。
结果写回(WB)
指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。
单周期 CPU,是在一个时钟周期内完成这五个阶段的处理。
MIPS 指令的三种格式
缩写 | 说明 |
---|---|
op | 操作码 |
rs | 只读,为第 1 个源操作数寄存器,寄存器地址(编号)是00000~11111,00~1F |
rt | 可读可写,为第 2 个源操作数寄存器,或目的操作数寄存器,寄存器地址(同上) |
rd | 只写,为目的操作数寄存器,寄存器地址(同上) |
sa | 位移量(shift amt),移位指令用于指定移多少位 |
funct | 功能码,在寄存器类型指令中(R 类型)用来指定指令的功能与操作码配合使用 |
immediate | 16 位立即数,用作无符号的逻辑操作数、有符号的算术操作数、数据加载(Laod)/数据保存(Store)指令的数据地址字节偏移量和分支指令中相对程序计数器(PC)的有符号偏移量; |
address | 地址 |
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 位 |
单周期 CPU 数据通路和控制线路图
上图是一个简单的基本上能够在单周期 CPU 上完成所要求设计的指令功能的数据通路和必要的控制线路图。
指令执行的结果总是在时钟下降沿保存到寄存器和存储器中,PC 的改变是在时钟上升沿进行的,这样稳定性较好。另外,值得注意的问题,设计时,用模块化的思想方法设计,关于 ALU 设计、存储器设计、寄存器组设计等等,也是必须认真考虑的问题。
其中指令和数据各存储在不同存储器中,即有指令存储器和数据存储器。访问存储器时,先给出内存地址,然后由读或写信号控制操作。对于寄存器组,先给出寄存器地址,读操作时不需要时钟信号,输出端就直接输出相应数据;而在写操作时,在 WE 使能信号为 1 时,在时钟边沿触发将数据写入寄存器。
控制信号的作用表
以上数据通路图是根据要实现的指令功能的要求画出来的,同时,还必须确定 ALU 的运算功能(当然,以上指令没有完全用到提供的 ALU 所有功能,但至少必须能实现以上指令功能操作)。从数据通路图上可以看出控制单元部分需要产生各种控制信号,当然,也有些信号必须要传送给控制单元。从指令功能要求和数据通路图的关系得出以上表 1,这样,从表 1 可以看出各控制信号与相应指令之间的相互关系,根据这种关系就可以得出控制信号与指令之间的关系(见下面表中的「相关指令」),从而写出各控制信号的逻辑表达式,这样控制单元部分就可实现了。
控制信号名 | 状态「0」 | 状态「1」 |
---|---|---|
Reset | 初始化 PC 为 0 | PC 接收新地址 |
PCWre | PC 不更改,相关指令:halt | PC 更改,相关指令:除指令 halt 外 |
ALUSrcA | 来自寄存器堆 data1 输出,相关指令:add、sub、addiu、or、and、andi、ori、slti、beq、bne、bltz、sw、lw | 来自移位数 sa,同时,进行(zero-extend)sa,即,相关指令:sll |
ALUSrcB | 来自寄存器堆 data2 输出,相关指令:add、sub、or、and、beq、bne、bltz | 来自 sign 或 zero 扩展的立即数,相关指令:addiu、andi、ori、slti、sw、lw |
DBDataSrc | 来自 ALU 运算结果的输出,相关指令:add、addiu、sub、ori、or、and、andi、slti、sll | 来自数据存储器(Data MEM)的输出,相关指令:lw |
RegWre | 无写寄存器组寄存器,相关指令:beq、bne、bltz、sw、halt | 寄存器组写使能,相关指令:add、addiu、sub、ori、or、and、andi、slti、sll、lw |
InsMemRW | 写指令存储器 | 读指令存储器(Ins. Data) |
mRD | 输出高阻态 | 读数据存储器,相关指令:lw |
mWR | 无操作 | 写数据存储器,相关指令:sw |
RegDst | 写寄存器组寄存器的地址,来自 rt 字段,相关指令:addiu、andi、ori、slti、lw | 写寄存器组寄存器的地址,来自 rd 字段,相关指令:add、sub、and、or、sll |
ExtSel | (zero-extend)immediate(0 扩展),相关指令:andi、ori | (sign-extend)immediate(符号扩展),相关指令:addiu、slti、sw、lw、beq、bne、bltz |
ALUOp 的功能表
ALUOp[2..0] |
功能 | 描述 | 相关指令 |
---|---|---|---|
000 | Y=A+B | 加 | add、addiu、sw、lw、j、halt |
001 | Y=A–B | 减 | sub、beq、bne、bltz |
010 | Y=B«A | B 左移 A 位 | sll |
011 | Y=A∨B | 或 | ori、or |
100 | Y=A∧B | 与 | andi、and |
101 | Y=A<B | 不带符号比较 A<B | |
110 | Y=A[31]!=B[31]?A[31]>B[31]:A<B | 带符号比较 A<B | slti |
111 | Y=A^B | 异或 |
PCSrc 的功能表
PCSrc[1..0] | 功能 | 相关指令 |
---|---|---|
00 | pc<-pc+4 | add、addiu、sub、or、ori、and、andi、slti、sll、sw、lw、beq(zero=0)、bne(zero=1)、bltz(sign=0) |
01 | pc<-pc+4+(sign-extend)immediate«2 | beq(zero=1)、bne(zero=0)、bltz(sign=1) |
10 | pc<-{(pc+4)[31:28],addr[27:2],2'b00} |
j |
11 | 未用 |
相关部件及引脚说明
Instruction Memory
指令存储器。
Iaddr | 指令存储器地址输入端口 |
---|---|
IDataIn | 指令存储器数据输入端口(指令代码输入端口) |
IDataOut | 指令存储器数据输出端口(指令代码输出端口) |
RW | 指令存储器读写控制信号,为 0 写,为 1 读 |
Data Memory
数据存储器。
Daddr | 数据存储器地址输入端口 |
---|---|
DataIn | 数据存储器数据输入端口 |
DataOut | 数据存储器数据输出端口 |
RD | 数据存储器读控制信号,为 0 读 |
WR | 数据存储器写控制信号,为 0 写 |
Register File
寄存器组。
Read Reg1 | rs 寄存器地址输入端口 |
---|---|
Read Reg2 | rt 寄存器地址输入端口 |
Write Reg | 将数据写入的寄存器端口,其地址来源 rt 或 rd 字段 |
Write Data | 写入寄存器的数据输入端口 |
Read Data1 | rs 寄存器数据输出端口 |
Read Data2 | rt 寄存器数据输出端口 |
WE | 写使能信号,为 1 时,在时钟边沿触发写入 |
ALU
算术逻辑单元
result | ALU 运算结果 |
---|---|
zero | 运算结果标志,结果为 0,则 zero=1;否则 zero=0 |
sign | 运算结果标志,结果最高位为 0,则 sign=0,正数;否则,sign=1,负数 |
实验器材
电脑一台,Xilinx Vivado 2017.4 软件一套,Basys3 实验板一块。
实验过程与结果
代码实现
SingleCPU.v
单周期 CPU 的顶层连接文件,主要是调用下层模块并将它们输入输出连在一起,并计算下一个指令的地址(正常+4 或跳转)。
`timescale 1ns / 1ps
module SingleCPU(
input CLK, //时钟信号
input Reset, //置零信号
output[31:0] CurPC, //当前指令地址
output[31:0] newaddress, //下一个指令地址
output[31:0] instcode, //rs,rt寄存器所在指令
output[31:0] Reg1Out, //寄存器组rs寄存器的值
output[31:0] Reg2Out, //寄存器组rt寄存器的值
output[31:0] ALU_Out, //ALU的result输出值
output[31:0] WriteData //DB总线值
);
wire ExtSel; //位扩展信号,1为符号扩展,0为0扩展
wire PCWre; //PC工作信号,0不更改,1更改
wire InsMemRW; //指令寄存器信号,0为写,1为读
wire RegDst; //指令读取时判断是rt还是rd进入寄存器组的写数据端,0为rt,1为rd
wire RegWre; //寄存器组是否需要写功能,0为无写功能,1为些功能
wire[2:0] ALUOp; //ALU8种运算功能选择
wire[1:0] PCSrc; //PC正常+4还是要跳转,0为正常+4,1为跳转
wire ALUSrcA; //寄存器组Data1的输出,0为寄存器本身输出,1为指令码的最后16位立即数
wire ALUSrcB; //寄存器组Data2的输出,0位本身的输出,1为扩展后的立即数
wire RD; //读数据存储器功能,0时读取
wire WR; //写数据存储器功能,1时写
wire DBDataSrc; //决定将什么数据传入寄存器组Write Data端,0为ALU结果,1为存储器
wire[4:0] WriteRegAddr; //寄存器组Write Reg输入端
wire[31:0] ALU_Input_A; //ALU的A输入端
wire[31:0] ALU_Input_B; //ALU的B输入端
wire zero; //ALU的zero输出
wire sign; //ALU的sign输出
wire[31:0] MemOut; //存储器的输出
wire[31:0] Ext_Imm; //位扩展后的立即数
wire[31:0] CurPC4=CurPC+4;
assign newaddress= (PCSrc==2'b01)?{CurPC4[31:28],instcode[25:0],2'b00}:
(PCSrc==2'b10)?CurPC4+(Ext_Imm<<2):CurPC4;
PC pc(CLK,Reset,PCWre,newaddress,CurPC);
ALU alu(ALU_Input_A,ALU_Input_B,ALUOp,ALU_Out,zero,sign);
DataMemory dm(ALU_Out,CLK,RD,WR,Reg2Out,MemOut);
SignZeroExtend sze(instcode[15:0],ExtSel,Ext_Imm);
Multiplexer5 mux21R(RegDst,instcode[20:16],instcode[15:11],WriteRegAddr);
Multiplexer32 mux21A(ALUSrcA,Reg1Out,{27'b000000000000000000000000000,instcode[10:6]},ALU_Input_A);
Multiplexer32 mux21B(ALUSrcB,Reg2Out,Ext_Imm,ALU_Input_B);
Multiplexer32 mux21RW(DBDataSrc,ALU_Out,MemOut,WriteData);
RegisterFile rf(RegWre,CLK,instcode[25:21],instcode[20:16],WriteRegAddr,WriteData,Reg1Out,Reg2Out);
ControlUnit cu(ExtSel,PCWre,InsMemRW,RegDst,RegWre,ALUOp,PCSrc,ALUSrcA,ALUSrcB,RD,WR,DBDataSrc,instcode[31:26],zero,sign);
InstructionMemory im(CurPC,InsMemRW,instcode);
endmodule
ControlUnit.v
控制信号模块,通过解析 op 得到该指令的各种控制信号。定义了很多用到的常量,可读性还是比较高的。
控制单元通过输入的 zero 零标志位与当前指令中对应的指令部分来确定当前整个 CPU 程序中各模块的工作和协作情况,根据 CPU 运行逻辑,事先对整个 CPU 中控制信号的控制,以此来达到指挥各个模块协同工作的目的。
`timescale 1ns / 1ps
module ControlUnit(
output ExtSel,
output PCWre,
output InsMemRW,
output RegDst,
output RegWre,
output[2:0] ALUOp,
output[1:0] PCSrc,
output ALUSrcA,
output ALUSrcB,
output mRD,
output mWR,
output DBDataSrc,
input[5:0] op,
input zero,
input sign
);
parameter ADD= 6'b000000;
parameter SUB= 6'b000001;
parameter ADDIU= 6'b000010;
parameter ANDI= 6'b010000;
parameter AND= 6'b010001;
parameter ORI= 6'b010010;
parameter OR= 6'b010011;
parameter SLL= 6'b011000;
parameter SLTI= 6'b011100;
parameter SW= 6'b100110;
parameter LW= 6'b100111;
parameter BEQ= 6'b110000;
parameter BNE= 6'b110001;
parameter BLTZ= 6'b110010;
parameter J= 6'b111000;
parameter HALT= 6'b111111;
parameter _ADD= 3'b000;
parameter _SUB= 3'b001;
parameter _SLL= 3'b010;
parameter _OR= 3'b011;
parameter _AND= 3'b100;
parameter _SLTU= 3'b101;
parameter _SLT= 3'b110;
parameter _XOR= 3'b111;
assign PCWre= op!=HALT;
assign ALUSrcA= op==SLL;
assign ALUSrcB= op==ADDIU||op==ANDI||op==ORI||op==SLTI||op==SW||op==LW;
assign DBDataSrc= op==LW;
assign RegWre= op!=BEQ&&op!=BNE&&op!=BLTZ&&op!=SW&&op!=HALT;
assign InsMemRW= 0;
assign mRD= op==LW;
assign mWR= op==SW;
assign RegDst= op!=ADDIU&&op!=ANDI&&op!=ORI&&op!=SLTI&&op!=LW;
assign ExtSel= op!=ANDI&&op!=ORI;
assign PCSrc[0]= op==J;
assign PCSrc[1]= op==BEQ&&zero==1||op==BNE&&zero==0||op==BLTZ&&sign==1;
assign ALUOp= op==SUB||op==BNE||op==BEQ||op==BLTZ?_SUB:
op==SLL?_SLL:
op==ORI||op==OR?_OR:
op==ANDI||op==AND?_AND:
op==SLTI?_SLT:_ADD;
endmodule
ALU.v
该部分为算术逻辑单元,用于逻辑指令计算和跳转指令比较。ALUOp 用于控制算数的类型,A、B 为输入数,result 为运算结果,zero、sign 主要用于 beq、bne、bltz 等指令的判断。
ALU 算术逻辑单元的功能是根据控制信号从输入的数据中选取对应的操作数,根据操作码进行运算并输出结果与零标志位。
`timescale 1ns / 1ps
module ALU(
input[31:0] A, //输入A
input[31:0] B, //输入B
input[2:0] ALUOp, //ALU操作控制
output reg[31:0] result, //ALU运算结果
output zero, //运算结果result的标志,result为0输出1,否则输出0
output sign //运算结果result的正负性(有符号数的情况),result为负数输出1,否则输出0
);
parameter _ADD= 3'b000;
parameter _SUB= 3'b001;
parameter _SLL= 3'b010;
parameter _OR= 3'b011;
parameter _AND= 3'b100;
parameter _SLTU= 3'b101;
parameter _SLT= 3'b110;
parameter _XOR= 3'b111;
assign zero= result==0;
assign sign= result[31];
always@(*)begin //进行ALU计算
case(ALUOp) //进行运算
_ADD: result= A+B; //加法
_SUB: result= A-B; //减法
_SLL: result= B<<A; //B左移A位
_OR: result= A|B; //或
_AND: result= A&B; //与
_SLTU: result= A<B; //比较A<B不带符号
_SLT: result= A[31]!=B[31]?A[31]>B[31]:A<B; //比较A<B带符号
_XOR: result= A^B; //异或
default: result= 0;
endcase
end
endmodule
DataMemory.v
该部分控制内存存储,用于内存存储、读写。用 255 大小的 8 位寄存器数组模拟内存,采用小端模式。DataMenRW 控制内存读写。由于指令为真实地址,所以不需要<<2
。
`timescale 1ns / 1ps
module DataMemory(
input[31:0] DAddr,
input CLK,
input mRD,
input mWR,
input[31:0] DataIn,
output reg[31:0] DataOut
);
reg[7:0] dataMemory [255:0];
always@(mRD or DAddr)begin
if(mRD)begin
DataOut[7:0]= dataMemory[DAddr+3];
DataOut[15:8]= dataMemory[DAddr+2];
DataOut[23:16]= dataMemory[DAddr+1];
DataOut[31:24]= dataMemory[DAddr];
end
end
always@(negedge CLK)begin //总是在时钟下降沿到来时触发
if(mWR)begin
dataMemory[DAddr+3]<= DataIn[7:0];
dataMemory[DAddr+2]<= DataIn[15:8];
dataMemory[DAddr+1]<= DataIn[23:16];
dataMemory[DAddr]<= DataIn[31:24];
end
end
endmodule
InstructionMemory.v
该部分为指令寄存器,通过一个 256 大小的 8 位寄存器数组来保存从文件输入的全部指令。然后通过输入的地址,找到相应的指令,输出到 IDataOut。
指令存储器的功能是存储读入的所有 32-bit 位宽的指令,根据程序计数器 PC 中的指令地址进行取指令操作并对指令类型进行分析,通过指令类型对所取指令的各字段进行区分识别,最后将对应部分传递给其他模块进行后续处理。
指令存储器中每个单元的位宽为 8-bit,也就是存储每条 32-bit 位宽的指令都需要占据 4 个单元,所以第 n(n 大于或等于 0)条指令所对应的起始地址为 4n,且占据第 4n,4n+1,4n+2,4n+3 这四个单元。取出指令就是将这四个单元分别取出,因为指令的存储服从高位指令存储在低位地址的规则,所以 4n 单元中的字段是该条指令的最高 8 位,后面以此类推,并通过左移操作将指令的四个单元部分移动到相对应的位置,以此来得到所存指令。
`timescale 1ns / 1ps
module InstructionMemory(
input[31:0] IAddr,
input RW,
output reg[31:0] IDataOut
);
reg[7:0] InstMemory[255:0];
initial begin //此处为绝对地址,注意斜杠方向
$readmemb("C:/Users/wukan/Documents/VIVADO/SingleCPU/input.txt",InstMemory);
end
always@(IAddr or RW)begin
if(RW==0)begin
IDataOut= {InstMemory[IAddr],InstMemory[IAddr+1],InstMemory[IAddr+2],InstMemory[IAddr+3]};
end
end
endmodule
Multiplexer32.v
三十二线双路选择器。
`timescale 1ns / 1ps
module Multiplexer32(
input Select,
input[31:0] DataIn1,
input[31:0] DataIn2,
output[31:0] DataOut
);
assign DataOut= Select?DataIn2:DataIn1;
endmodule
Multiplexer5.v
五线双路选择器。
`timescale 1ns / 1ps
module Multiplexer5(
input Select,
input[4:0] DataIn1,
input[4:0] DataIn2,
output[4:0] DataOut
);
assign DataOut= Select?DataIn2:DataIn1;
endmodule
PC.v
CLK 上升沿触发,更改指令地址。由于指令地址存储在寄存器里,一开始需要赋 currentAddress 为 0。Reset 是重置信号,当为 1 时,指令寄存器地址重置。PCWre 的作用为保留现场,如果 PCWre 为 0,指令地址不变。
PC 程序计数器用于存放当前指令的地址,当 PC 的值发生改变的时候,CPU 会根据程序计数器 PC 中新得到的指令地址,从指令存储器中取出对应地址的指令。在单周期 CPU 的运行周期中,PC 值的变化是最先的,而且是根据 PCSrc 控制信号的值选择指令地址是要进行 PC+4 或者跳转等操作。若 PC 程序计数器检测到 Reset 输入信号为 0 时,则对程序计数器存储的当前指令地址进行清零处理。
`timescale 1ns / 1ps
module PC(
input CLK,
input Reset,
input PCWre,
input[31:0] newAddress,
output reg[31:0] PCAddr
);
initial begin
PCAddr= 0;
end
always@(posedge CLK or negedge Reset)begin
if(Reset==0)begin
PCAddr= 0;
end
else if(PCWre)begin
PCAddr= newAddress;
end
end
endmodule
RegisterFile.v
该部分为寄存器读写单元,储存寄存器组,并根据地址对寄存器组进行读写。WE 的作用是控制寄存器是否写入。同上,通过一个 32 大小的 32 位寄存器数组来模拟寄存器,开始时全部置 0。通过访问寄存器的地址,来获取寄存器里面的值,并进行操作。(由于$0 恒为 0,所以写入寄存器的地址不能为 0)
寄存器组中的每个寄存器位宽 32-bit,是存放 ALU 计算所需要的临时数据的,与数据存储器不同,可能会在程序执行的过程中被多次覆盖,而数据存储器内的数据一般只有 sw 指令才能进行修改覆盖。寄存器组会根据操作码 opCode 与 rs,rt 字段相应的地址读取数据,同时将 rs,rt 寄存器的地址和其中的数据输出,在 CLK 的下降沿到来时将数据存放到 rd 或者 rt 字段的相应地址的寄存器内。
`timescale 1ns / 1ps
module RegisterFile(
input WE,
input CLK,
input[4:0] ReadReg1,
input[4:0] ReadReg2,
input[4:0] WriteReg,
input[31:0] WriteData,
output[31:0] ReadData1,
output[31:0] ReadData2
);
reg[31:0] registers[0:31];
integer i;
initial begin //初始时,将32个寄存器全部赋值为0
for(i=0; i<32; i=i+1)registers[i]<= 0;
end
assign ReadData1= ReadReg1?registers[ReadReg1]:0;
assign ReadData2= ReadReg2?registers[ReadReg2]:0;
always@(negedge CLK)begin
if(WriteReg&&WE)begin
registers[WriteReg]= WriteData;
end
end
endmodule
SignZeroExtend.v
比较简单的一个模块,用于立即数的扩展。ExtSel 为控制补位信号。判断后,将 extendImmediate 的前 16 位全补 1 或 0 即可。
`timescale 1ns / 1ps
module SignZeroExtend(
input[15:0] immediate,
input ExtSel,
output[31:0] extendImmediate
);
assign extendImmediate= {ExtSel&&immediate[15]?16'hffff:16'h0000,immediate};
endmodule
仿真检验
将指令转换成二进制代码,如下表:
地址 | 汇编程序 | op(6) | rs(5) | rt(5) | rd(5)/immediate(16) | 16 进制数代码 |
---|---|---|---|---|---|---|
0x00000000 | addiu $1,$0,8 |
000010 | 00000 | 00001 | 00000000 00001000 | 08010008 |
0x00000004 | ori $2,$0,2 |
010010 | 00000 | 00010 | 00000000 00000010 | 48020002 |
0x00000008 | add $3,$2,$1 |
000000 | 00010 | 00001 | 00011000 00000000 | 00411800 |
0x0000000C | sub $5,$3,$2 |
000001 | 00011 | 00010 | 00101000 00000000 | 04622800 |
0x00000010 | and $4,$5,$2 |
010001 | 00101 | 00010 | 00100000 00000000 | 44a22000 |
0x00000014 | or $8,$4,$2 |
010011 | 00100 | 00010 | 01000000 00000000 | 4c824000 |
0x00000018 | sll $8,$8,1 |
011000 | 00000 | 01000 | 01000000 01000000 | 60084040 |
0x0000001C | bne $8,$1,-2 |
110001 | 01000 | 00001 | 11111111 11111110 | c501fffe |
0x00000020 | slti $6,$2,4 |
011100 | 00010 | 00110 | 00000000 00000100 | 70460004 |
0x00000024 | slti $7,$6,0 |
011100 | 00110 | 00111 | 00000000 00000000 | 70c70000 |
0x00000028 | addiu $7,$7,8 |
000010 | 00111 | 00111 | 00000000 00001000 | 08e70008 |
0x0000002C | beq $7,$1,-2 |
110000 | 00111 | 00001 | 11111111 11111110 | c0e1fffe |
0x00000030 | sw $2,4($1) |
100110 | 00001 | 00010 | 00000000 00000100 | 98220004 |
0x00000034 | lw $9,4($1) |
100111 | 00001 | 01001 | 00000000 00000100 | 9c290004 |
0x00000038 | addiu $10,$0,-2 |
000010 | 00000 | 01010 | 11111111 11111110 | 080afffe |
0x0000003C | addiu $10,$10,1 |
000010 | 01010 | 01010 | 00000000 00000001 | 094a0001 |
0x00000040 | bltz $10,-2 |
110010 | 01010 | 00000 | 11111111 11111110 | c940fffe |
0x00000044 | andi $11,$2,2 |
010000 | 00010 | 01011 | 00000000 00000010 | 404b0002 |
0x00000048 | j 0x00000050 |
111000 | 00000 | 00000 | 00000000 00010100 | e0000014 |
0x0000004C | or $8,$4,$2 |
010011 | 00100 | 00010 | 01000000 00000000 | 4c824000 |
0x00000050 | halt |
111111 | 00000 | 00000 | 00000000 00000000 | fc000000 |
input.txt
00001000 00000001 00000000 00001000
01001000 00000010 00000000 00000010
00000000 01000001 00011000 00000000
00000100 01100010 00101000 00000000
01000100 10100010 00100000 00000000
01001100 10000010 01000000 00000000
01100000 00001000 01000000 01000000
11000101 00000001 11111111 11111110
01110000 01000110 00000000 00000100
01110000 11000111 00000000 00000000
00001000 11100111 00000000 00001000
11000000 11100001 11111111 11111110
10011000 00100010 00000000 00000100
10011100 00101001 00000000 00000100
00001000 00001010 11111111 11111110
00001001 01001010 00000000 00000001
11001001 01000000 11111111 11111110
01000000 01001011 00000000 00000010
11100000 00000000 00000000 00010100
01001100 10000010 01000000 00000000
11111100 00000000 00000000 00000000
Sim.v
仿真模块。
`timescale 1ns / 1ps
module Sim;
reg CLK; //时钟信号
reg Reset; //置零信号
SingleCPU scpu(CLK,Reset);
initial begin
CLK= 0;
Reset= 0; //刚开始设置pc为0
#50; //等待Reset完成
CLK= !CLK; //下降沿,使PC先清零
#50;
Reset= 1; //清除保持信号
forever #50 begin //产生时钟信号,周期为50s
CLK= !CLK;
end
end
endmodule
仿真波形
波形比较长,分成三部分逐一分析。
前 100ps 为初始化,各寄存器的值被初始化为 0。
第 800ps 时执行了bne $8,$1,-2
,此时寄存器$8
的值是 4,寄存器$1
的值是 8,两者不等,发生了一步跳转,于是第 900ps 的地址跳转到00000018
。
在第 900ps 前寄存器 1~11 的值依次为(16 进制)8,2,a,0,8,0,0,4,0,0,0
。
第 1000ps 时再次执行了bne $8,$1,-2
,此时寄存器$8
的值是8
,寄存器$1
的值是8
,两者
相等,pc+4 执行下一条指令。
第 1400ps 时执行了beq $7,$1,-2
,此时寄存器$7
的值是8
,寄存器$1
的值是8
,两者相等,发生了一步跳转,于是第 1500ps 的地址跳转到00000028
。
第 1600ps 时再次执行了beq $7,$1,-2
,此时寄存器$7
的值是10
,寄存器$1
的值是 8,两者不等,pc+4 执行下一条指令。
在第 1800ps 前寄存器 1~11 的值依次为(16 进制)8,2,a,0,8,1,10,8,0,0,0
。
第 2100ps 时执行了bltz $10,-2
,此时寄存器$10
的值是ffffffff
,小于0
,发生了一步跳转,于是第 2200ps 的地址跳转到0000003c
。
第 2300ps 时再次执行了bltz $10,-2
,此时寄存器$10
的值是0
,不小于0
,pc+4 执行下一条指令。
第 2500ps 时执行了j 0x00000050
,于是第 2600ps 的地址跳转到00000050
。
第 2600ps 时执行了halt
,程序终止,pc 不再跳转。
在第 2700ps 前寄存器 1~11 的值依次为(16 进制)8,2,a,0,8,1,10,8,2,0,2
。
烧写到 Basys3 实验板
Basys3.v
顶层模块。
`timescale 1ns / 1ps
module Basys3(
input CLK,
input[1:0] SW, //选择输出信号
input Reset, //重置按钮
input Button, //单脉冲
output reg[3:0] AN, //数码管位选择信号
output[7:0] Out //数码管输入信号
);
parameter T1MS= 100000;
reg[16:0] showCounter;
wire[31:0] ALU_Out; //ALU的result输出值
wire[31:0] CurPC;
wire[31:0] WriteData; //DB总线值
wire[31:0] Reg1Out; //寄存器组rs寄存器的值
wire[31:0] Reg2Out; //寄存器组rt寄存器的值
wire[31:0] instcode;
wire myCLK;
reg[3:0] store; //记录当前要显示位的值
wire[31:0] newAddress;
SingleCPU scpu(myCLK,Reset,CurPC,newAddress,instcode,Reg1Out,Reg2Out,ALU_Out,WriteData);
Debounce debounce(CLK,Button,myCLK);
initial begin
showCounter<= 0;
AN<= 4'b0111;
end
always@(posedge CLK)
begin
if(Reset==0)begin
showCounter<= 0;
AN<= 4'b0000;
end else begin
showCounter<= showCounter+1;
if(showCounter==T1MS)
begin
showCounter<= 0;
case(AN)
4'b1110: begin
AN<= 4'b1101;
end
4'b1101: begin
AN<= 4'b1011;
end
4'b1011: begin
AN<= 4'b0111;
end
4'b0111: begin
AN<= 4'b1110;
end
4'b0000: begin
AN<= 4'b0111;
end
endcase
end
end
end
SegLED led(store,Reset,Out);
always@(myCLK)begin
case(AN)
4'b1110: begin
case(SW)
2'b00: store<= newAddress[3:0];
2'b01: store<= Reg1Out[3:0];
2'b10: store<= Reg2Out[3:0];
2'b11: store<= WriteData[3:0];
endcase
end
4'b1101: begin
case(SW)
2'b00: store<= newAddress[7:4];
2'b01: store<= Reg1Out[7:4];
2'b10: store<= Reg2Out[7:4];
2'b11: store<= WriteData[7:4];
endcase
end
4'b1011: begin
case(SW)
2'b00: store<= CurPC[3:0];
2'b01: store<= instcode[24:21];
2'b10: store<= instcode[19:16];
2'b11: store<= ALU_Out[3:0];
endcase
end
4'b0111 : begin
case(SW)
2'b00: store<= CurPC[7:4];
2'b01: store<= {3'b000,instcode[25]};
2'b10: store<= {3'b000,instcode[20]};
2'b11: store<= ALU_Out[7:4];
endcase
end
endcase
end
endmodule
Debounce.v
按键消抖模块。Basys3 板采用的是机械按键,在按下按键时按键会出现人眼无法观测但是系统会检测到的抖动变化,这可能会使短时间内电平频繁变化,导致程序接收到许多错误的触发信号而出现许多不可知的错误。消抖操作是每当检测到 CLK 上升沿到来时检测一次当前电平信号并记录,同计数器开始计数,若在计数器达到 5000 之前电平发生变化,则将计数器清零,若达到 5000,则将该记录电平取反输出。
因为程序开始时已经运行第一条指令,为避免跳过第一条指令计算值的写入,我们的输入需要从下降沿开始,因此我们给按键信号取反后再输入。
`timescale 1ns / 1ps
module Debounce(clk,key_in,key_out);
parameter SAMPLE_TIME= 5000;
input clk;
input key_in;
output key_out;
reg[21:0] count_low;
reg[21:0] count_high;
reg key_out_reg;
always@(posedge clk)begin
count_low<= key_in?0:count_low+1;
count_high<= key_in?0:count_high+1;
if(count_high == SAMPLE_TIME)
key_out_reg<= 1;
else if(count_low == SAMPLE_TIME)
key_out_reg<= 0;
end
assign key_out=!key_out_reg;
endmodule
SegLED.v
数码管译码模块。译码模块将 CPU 运算的结果转换成 7 段数码管中各个数码管显示所需的高低电平信号,该单元的输入为 4-bit 位宽的二进制数。其中,七段数码管的八个电平控制输出中最低位是小数点的显示信号,但小数点在 CPU 运行时没有用到,恰好用于标记 Reset 状态。
`timescale 1ns / 1ps
module SegLED(
input[3:0] Store,
input Reset,
output reg[7:0] Out
);
always@(Store or Reset)begin
if(Reset==0)begin
Out= 8'b11111110;
end
else begin
case(Store)
4'b0000: Out= 8'b00000011; //0
4'b0001: Out= 8'b10011111; //1
4'b0010: Out= 8'b00100101; //2
4'b0011: Out= 8'b00001101; //3
4'b0100: Out= 8'b10011001; //4
4'b0101: Out= 8'b01001001; //5
4'b0110: Out= 8'b01000001; //6
4'b0111: Out= 8'b00011111; //7
4'b1000: Out= 8'b00000001; //8
4'b1001: Out= 8'b00001001; //9
4'b1010: Out= 8'b00010001; //A
4'b1011: Out= 8'b11000001; //b
4'b1100: Out= 8'b01100011; //C
4'b1101: Out= 8'b10000101; //d
4'b1110: Out= 8'b01100001; //E
4'b1111: Out= 8'b01110001; //F
default: Out= 8'b00000000; //all light
endcase
end
end
endmodule
运行结果
端口映射
初始化
所有寄存器被初始化为 0。
第 1 条指令addiu $1,$0,8
当前地址 00,下一地址 04。
0 号寄存器,值为 0。
1 号寄存器,值为 0。
ALU 结果为 8。
第 2 条指令ori $2,$0,2
当前地址 04,下一地址 08。
0 号寄存器,值为 0。
2 号寄存器,值为 0。
ALU 结果为 2。
第 3 条指令add $3,$2,$1
当前地址 08,下一地址 0c。
2 号寄存器,值为 2。
1 号寄存器,3 号寄存器。
输出结果为 0a。
第 4 条指令sub $5,$3,$2
当前地址 0c,下一地址 10。
3 号寄存器,值为 0a。
2 号寄存器,值为 2。
ALU 结果为 8。
第 5 条指令and $4,$5,$2
当前地址 10,下一地址 14。
5 号寄存器,值为 3。
2 号寄存器,值为 2。
ALU 结果为 0。
实验心得
对于第一次使用 verilog 语言的我来说,设计单周期 CPU 是一个不小的挑战。总的来说,从开始构思到真正写板完成大约用了三整天的时间,期间遇到了很多有困难的地方,也翻了很多网上的博客。不得不说,网上能找到的很多博客给出的代码都是有些问题的,确实在一定程度上误导了自己。但是这样一个比较复杂的系统又确实很难什么都不去参考而直接写出来。
因此,还是希望老师能够多做一些有关 verilog 语言的讲学吧,遇到的很多问题其实都是关于语法方面的,因为 vivado 并不会对语法错误进行提示(我用的 2017.4 版会在可能错误的地方画一道波浪线但是并不会提示错在哪里),而我遇到的问题很多都是语法方面的(例如,在 readmemb 的时候地址的斜杠和操作系统的是反的,这花费了我很多时间检查才发现)。