2017/06/08: 当时单周期cpu写的比较仓促,没有深入的进行调试,我准备在放假的时候重构一下代码, 然后把博文改进一下,现在实在没有时间,很抱歉~ 不过多周期我有调试过的,所以有需要的可以移步到我的多周期cpu设计
(1) 掌握单周期CPU数据通路图的构成、原理及其设计方法; (2) 掌握单周期CPU的实现方法,代码实现方法; (3) 认识和掌握指令与CPU的关系; (4) 掌握测试单周期CPU的方法。
设计一个单周期CPU,该CPU至少能实现以下指令功能操作。需设计的指令与格式如下: 特别说明: immediate是从PC+4地址开始和转移到的指令之间指令条数。immediate符号扩展之后左移2位再相加。为什么要左移2位?由于跳转到的指令地址肯定是4的倍数(每条指令占4个字节),最低两位是“00”,因此将immediate放进指令码中的时候,是右移了2位的,也就是以上说的“指令之间指令条数”。
补充:
1、PC、寄存器组和存储器写状态使用时钟触发。 2、指令存储器和数据存储器存储单元宽度一律使用8位,即一个字节的存储单位。不能使用32位作为存储器存储单元宽度。 3、控制器部分要学会用控制信号真值表方法分析问题并写出逻辑表达式;或者用case语句方法逐个产生各指令控制信号。 4、必须写一段测试用的汇编程序,而且必须包含所要求的所有指令,beq指令必须检查两种情况:“等”和“不等”。
单周期CPU指的是一条指令的执行在一个时钟周期内完成,然后开始下一条指令的执行,即一条指令用一个时钟周期完成。电平从低到高变化的瞬间称为时钟上升沿,两个相邻时钟上升沿之间的时间间隔称为一个时钟周期。时钟周期一般也称振荡周期(如果晶振的输出没有经过分频就直接作为CPU的工作时钟,则时钟周期就等于振荡周期。若振荡周期经二分频后形成时钟脉冲信号作为CPU的工作时钟,这样,时钟周期就是振荡周期的两倍。)
CPU在处理指令时,一般需要经过以下几个步骤: (1) 取指令(IF):根据程序计数器PC中的指令地址,从存储器中取出一条指令,同时,PC根据指令字长度自动递增产生下一条指令所需要的指令地址,但遇到“地址转移”指令时,则控制器把“转移地址”送入PC,当然得到的“地址”需要做些变换才送入PC。 (2) 指令译码(ID):对取指令操作中得到的指令进行分析并译码,确定这条指令需要完成的操作,从而产生相应的操作控制信号,用于驱动执行状态中的各种操作。 (3) 指令执行(EXE):根据指令译码得到的操作控制信号,具体地执行指令动作,然后转移到结果写回状态。 (4) 存储器访问(MEM):所有需要访问存储器的操作都将在这个步骤中执行,该步骤给出存储器的数据地址,把数据写入到存储器中数据地址所指定的存储单元或者从存储器中得到数据地址单元中的数据。 (5) 结果写回(WB):指令执行的结果或者访问存储器中得到的数据写回相应的目的寄存器中。 单周期CPU,是在一个时钟周期内完成这五个阶段的处理。
图2是一个简单的基本上能够在单周期CPU上完成所要求设计的指令功能的数据通路和必要的控制线路图。其中指令和数据各存储在不同存储器中,即有指令存储器和数据存储器。访问存储器时,先给出地址,然后由读或写信号控制操作。对于寄存器组,读操作时,先给出地址,输出端就直接输出相应数据;而在写操作时,在WE使能信号为1时,在时钟边沿触发写入。图中控制信号作用如表1所示,表2是ALU运算功能表。
相关部件及引脚说明: Instruction Memory:指令存储器, Iaddr,指令存储器地址输入端口 IDataIn,指令存储器数据输入端口(指令代码输入端口) IDataOut,指令存储器数据输出端口(指令代码输出端口) RW,指令存储器读写控制信号,为1写,为0读 Data Memory:数据存储器, Daddr,数据存储器地址输入端口 DataIn,数据存储器数据输入端口 DataOut,数据存储器数据输出端口 RD,数据存储器读控制信号,为1读 WR,数据存储器写控制信号,为1写 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输出1,否则输出0
需要说明的是以上数据通路图是根据要实现的指令功能的要求画出来的,同时,还必须确定ALU的运算功能(当然,以上指令没有完全用到提供的ALU所有功能,但至少必须能实现以上指令功能操作)。从数据通路图上可以看出控制单元部分需要产生各种控制信号,当然,也有些信号必须要传送给控制单元。从指令功能要求和数据通路图的关系得出以上表1,这样,从表1可以看出各控制信号与相应指令之间的相互关系,根据这种关系就可以得出控制信号与指令之间的关系表(留给学生完成),再根据关系表可以写出各控制信号的逻辑表达式,这样控制单元部分就可实现了。
指令执行的结果总是在时钟下降沿开始保存到寄存器和存储器中,PC的改变是在时钟上升沿进行的,这样稳定性较好。另外,值得注意的问题,设计时,用模块化的思想方法设计,关于ALU设计、存储器设计、寄存器组设计等等,也是必须认真考虑的问题。
PC机一台,BASYS 3 板一块,Xilinx Vivado 开发软件一套。
下面是我设计单周期CPU的详细过程: 1.设计Control Unit 由数据通路图可知, **输入信号为:**opCode、zero 输出信号为: PCWre, ALUSrcA, ALUSrcB,DBDataSrc,RegWre,InsMemRW, RD,WR, ExtSel,RegDst,PCSrc,ALUOp 设计control unit,必须列出控制信号与指令的关系表 然后根据该表assign对应的值。
`timescale 1ns / 1ps // // Company: // Engineer: // // Create Date: 2017/04/23 20:43:40 // Design Name: // Module Name: controlUnit // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // // module controlUnit( // 根据数据通路图定义输入和输出 input [5:0] opCode, input zero, output PCWre, output ALUSrcA, output ALUSrcB, output DBDataSrc, output RegWre, output InsMemRW, output RD, output WR, output ExtSel, output RegDst, output PCSrc, output [2:0] ALUOp ); // 根据opcode定义控制信号为1或0 assign PCWre = (opCode == 6'b111111) ? 0 : 1; assign ALUSrcA = (opCode == 6'b011000) ? 1 : 0; assign ALUSrcB = (opCode == 6'b000001 || opCode == 6'b010000 || opCode == 6'b100110 || opCode == 6'b100111) ? 1 : 0; assign DBDataSrc = (opCode == 6'b100111) ? 1 : 0; assign RegWre = (opCode == 6'b100110 || opCode == 6'b110000 || opCode == 6'b111111) ? 0 : 1; assign InsMemRW = 0; assign RD = (opCode == 6'b100111) ? 0 : 1; assign WR = (opCode == 6'b100110) ? 0 : 1; assign ExtSel = (opCode == 6'b010000) ? 0 : 1; assign RegDst = (opCode == 6'b000001 || opCode == 6'b010000 || opCode == 6'b100111) ? 0 : 1; assign PCSrc = (opCode == 6'b110000 && zero == 1) ? 1 : 0; assign ALUOp[2] = (opCode == 6'b010000 || opCode == 6'b010001 || opCode == 6'b010000) ? 1 : 0; assign ALUOp[1] = 0; assign ALUOp[0] = (opCode == 6'b000010 || opCode == 6'b010001 || opCode == 6'b110000) ? 1 : 0; endmodule2.设计ALU 由数据通路图可知, 输入信号为: ReadData1, ReadData2,inExt,insa,ALUSrcA,ALUSrcB,ALUOp 输出信号为: zero, result 然后根据表2 ALU运算功能表对result与zero赋值
`timescale 1ns / 1ps // // Company: // Engineer: // // Create Date: 2017/04/23 21:32:08 // Design Name: // Module Name: ALU // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // // module ALU( // 根据数据通路图定义下列的输入输出 input [31:0] ReadData1, input [31:0] ReadData2, input [31:0] inExt, input [5:0] insa, input ALUSrcA, ALUSrcB, input [2:0] ALUOp, output reg zero, output reg [31:0] result ); // 定义两个输入端口 wire [31:0] A; wire [31:0] B; // ALU输入端口的数据选择器 assign A = ALUSrcA ? insa :ReadData1; assign B = ALUSrcB? inExt : ReadData2; // 只要输入的值发生变化,就执行begin与end之间的内容 always @(ReadData1 or ReadData2 or inExt or ALUSrcA or ALUSrcB or ALUOp or A or B) begin case(ALUOp) // 根据ALUOp相应的实现运算功能 3'b000: begin result = A + B; zero = (result == 0)? 1 : 0; end 3'b001: begin result = A - B; zero = (result == 0)? 1 : 0; end 3'b010: begin result = (A < B) ? 1 : 0; zero = (result == 0)? 1 : 0; end 3'b100: begin result = A | B; zero = (result == 0)? 1 : 0; end 3'b101: begin result = A & B; zero = (result == 0)? 1 : 0; end 3'b011: begin result = B << A; zero = (result == 0)? 1 : 0; end 3'b110: begin result = A ^ B; zero = (result == 0)? 1 : 0; end 3'b111: begin result = A ^~ B; zero = (result == 0)? 1 : 0; end endcase end endmodule3.设计PC 由数据通路图可知, 输入信号为: clk, Reset, PCWre, PCSrc, immediate, 输出信号为: Address 判断是否有Reset信号,如果有,将PC置为0; 判断是否有PCSrc信号,如果有,将immediate作为偏移值加上PC中原有值存在pc中; 否则pc自增。
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: // // Create Date: 2017/04/23 21:47:33 // Design Name: // Module Name: PC // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module PC(clk, Reset, PCWre, PCSrc, immediate, Address); // 根据数据通路图定义输入输出 input clk, Reset, PCWre, PCSrc; input [15:0] immediate; // 从指令中取出进行符号扩展后得来的 output [31:0] Address; reg [31:0] Address; // clock上升沿到来或Reset下降沿到来时,执行下列函数 always @(posedge clk or negedge Reset) begin if (Reset == 0) begin Address = 0; end else if (PCWre) begin // PCWre为1时PC更改,PCWre为0时PC不更改 if (PCSrc) Address = Address + 4 + immediate*4; // 跳转指令 else Address = Address + 4; // 跳转到下一指令 end end endmodule4.设计signZeroExtend 由数据通路图可知, 输入信号为: immediate, ExtSel 输出信号为: out 符号扩展很简单,根据立即数的最高位进行补位: 如果立即数最高位为1,则前面全补1; 如果立即数最高位为0,则前面全补0.
`timescale 1ns / 1ps // // Company: // Engineer: // // Create Date: 2017/04/23 21:52:27 // Design Name: // Module Name: signZeroExtend // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // // module signZeroExtend( // 根据数据通路图定义输入和输出 input [15:0] immediate, input ExtSel, output [31:0] out ); assign out[15:0] = immediate; // 后15位存储立即数 assign out[31:16] = ExtSel? (immediate[15]? 16'hffff : 16'h0000) : 16'h0000; // 前16位根据立即数符号进行补1或0的操作 endmodule5.设计DataMemory 由数据通路图可知, 输入信号为: Daddr, DataIn,RD,WR 输出信号为: DataOut 根据WR,RD判断数据的读写,然后执行相应的读写操作。
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: // // Create Date: 2017/04/23 22:06:47 // Design Name: // Module Name: dataMemory // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module dataMemory( // 根据数据通路图定义输入和输出 input [31:0] DAddr, input [31:0] DataIn, input RD, input WR, output reg [31:0] DataOut ); // 实验要求:指令存储器和数据存储器存储单元宽度一律使用8位 // 所以将一个32位的数据拆成4个8位的存储器单元存储 // 将4个8位存储器恢复成32位存储器 reg[7:0] memory[0:127]; reg[31:0] address; // read data always @(RD) begin if (RD == 0) begin // 因为一条指令由4个存储单元存储,所以要乘以4 address = (DAddr << 2); // DataOut是32位的,将4个八位的内存单元合并生成32位 // 左移24位用于设置前八位,以此类推 DataOut = (memory[address]<<24)+(memory[address+1]<<16)+(memory[address+2]<<8)+memory[address+3]; end end // write data integer i; initial begin for (i = 0; i < 128; i = i+1) memory[i] <= 0; end always @(WR or DAddr or DataIn) begin if (WR == 0) begin address = DAddr << 2; memory[address] = DataIn[31:24]; memory[address+1]= DataIn[23:16]; memory[address+2]=DataIn[15:8]; memory[address+3]=DataIn[7:0]; end end endmodule6.设计instructionMemory 由数据通路图可知, instructionMemory 输入信号为: pc 输出信号为: op, rs, rt, rd, immediate,sa
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: // // Create Date: 2017/04/23 23:00:48 // Design Name: // Module Name: instructionMemory // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module instructionMemory( // 根据数据通路图定义输入和输出 input [31:0] pc, output [5:0] op, output [4:0] rs, rt, rd, output [15:0] immediate, output [5:0] sa); // 实验要求:指令存储器和数据存储器存储单元宽度一律使用8位 // 所以将一个32位的指令拆成4个8位的存储器单元存储 // 从文件取出后将他们合并为32位的指令 reg [7:0] mem[0:127]; reg [31:0] address; reg [31:0] instruction; initial begin $readmemb("D:/Xilinx/VivadoProject/SingleCPU/instructions.txt", mem); // 从文件中读取指令二进制代码赋值给mem instruction = 0; // 指令初始化 end always @(pc) begin // pc中一个单元是1byte,即8位,那么32位地址需要4个单元 // pc++ <=> pc += 4(100),即pc的最后两位都为0 // 从第三位开始取,即是代表指令的个数 address = pc[5:2] << 2; // 因为4个内存单元存储一个指令,所以除以4得到第一个内存单元的下标 // 将4个8位的内存单元合并为32位的指令 instruction = (mem[address]<<24) + (mem[address+1]<<16) + (mem[address+2]<<8) + mem[address+3]; end // output assign op = instruction[31:26]; assign rs = instruction[25:21]; assign rt = instruction[20:16]; assign rd = instruction[15:11]; assign immediate = instruction[15:0]; assign sa = instruction[10:6]; endmoduleInstructions文件的部分截图: 每4行构成一个32位的指令,对照指令表便可写出来。
7.设计Regfile 由数据通路图可知, 输入信号为: clk, RegWre, RegOut, opCode, rs, rt, rd, im, ALUM2Reg, dataFromALU, dataFromRW 输出信号为: Data1,Data2 下面列出我测试的指令列表:
`timescale 1ns / 1ps // // Company: // Engineer: // // Create Date: 2017/04/23 22:18:52 // Design Name: // Module Name: Regfile // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // // module Regfile(clk, RegWre, RegDst, opCode, rs, rt, rd, im, DBDataSrc, dataFromALU, dataFromRW, Data1, Data2); // 根据数据通路图定义输入和输出 input clk, RegDst, RegWre, DBDataSrc; input [5:0] opCode; input [4:0] rs, rt, rd; input [10:0] im; input [31:0] dataFromALU, dataFromRW; output [31:0] Data1, Data2; wire [4:0] writeReg; // 要写的寄存器端口 wire [31:0] writeData; // 要写的数据 // RegDst为真时,处理R型指令,rd为目标操作数寄存器,为假时处理I型指令,详见控制信号作用表 assign writeReg = RegDst? rd : rt; // ALUM2Reg为0时,使用来自ALU的输出,为1时,使用来自数据存储器(DM)的输出,详见控制信号作用表 assign writeData = DBDataSrc? dataFromRW : dataFromALU; // 实现数据选择器 // 初始化寄存器 reg [31:0] register[0:31]; integer i; initial begin for (i = 0; i < 32; i = i+1) register[i] <= 0; end // output:随register变化而变化 // Data1 为ALU运算时的A,当指令为sll时,A的值从立即数的16位中获得 // Data2 位ALU运算中的B,其值始终是为rt assign Data1 = (opCode == 6'b011000) ? im[10:6] : register[rs]; assign Data2 = register[rt]; always @(RegDst or RegWre or DBDataSrc or writeReg or writeData) begin if (RegWre && writeReg) register[writeReg] = writeData; // 防止数据写入0号寄存器(writeReg=0) end endmodule8.编写顶层模块 定义各个模块的input与output,然后调用各个模块。
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: // // Create Date: 2017/04/24 00:56:01 // Design Name: // Module Name: SingleCycleCPU // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module SingleCycleCPU( input clk, Reset, CLR, output wire[5:0] opCode, output wire[31:0] Out1, Out2, curPC, Result ); wire[2:0] ALUOp; wire[31:0] Extout, DMOut; wire[15:0] immediate; wire[4:0] rs, rt, rd; wire[5:0] sa; wire zero, PCWre, ALUSrcA, ALUSrcB, DBDataSrc, RegWre,InsMemRW, RD, WR, ExtSel, RegDst, PCSrc; ALU alu(Out1, Out2, Extout, sa, ALUSrcA, ALUSrcB, ALUOp, zero, Result); PC pc(clk, Reset, PCWre, PCSrc, immediate, curPC); controlUnit CU(opCode, zero, PCWre, ALUSrcA, ALUSrcB, DBDataSrc, RegWre, InsMemRW, RD, WR, ExtSel, RegDst, PCSrc, ALUOp); dataMemory dm(Result, Out2, RD, WR, DMOut); instructionMemory im(curPC, opCode, rs, rt, rd, immediate, sa); Regfile registerfile(clk, RegWre, RegDst, opCode, rs, rt, rd, immediate, DBDataSrc, Result, DMOut, Out1, Out2); signZeroExtend sze(immediate, ExtSel, Extout); endmodule9.编写测试代码 由于输入只有clk与reset信号,所以测试文件中只需要输入这两个值即可。为了显示更多结果,我在测试文件中也添加了相应代码以方便模拟。
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: // // Create Date: 2017/04/24 01:21:55 // Design Name: // Module Name: cpu_sim // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// /* input clk, Reset, output wire[5:0] opCode, output wire[31:0] Out1, Out2, curPC, Result*/ module SingleCircleCPUTest; // Inputs reg CLK; reg Reset; // Outputs wire [31:0] Out1; wire [31:0] Out2; wire [31:0] curPC; wire [31:0] Result; wire [5:0] opCode; // Instantiate the Unit Under Test (UUT) SingleCycleCPU uut ( .clk(CLK), .Reset(Reset), .opCode(opCode), .Out1(Out1), .Out2(Out2), .curPC(curPC), .Result(Result) ); initial begin // Initialize Inputs CLK = 0; Reset = 0; #50; // 刚开始设置pc为0 CLK = 1; #50; Reset = 1; forever #50 begin // 产生时钟信号 CLK = !CLK; end end endmodule实验截图:
addi操作:
ori操作:
add操作:
sub操作: and操作:or操作:beq操作:sll操作sw操作lw操作beq操作:halt操作:
以上内容皆为本人观点,欢迎大家提出批评和指导,我们一起探讨!
