RISC-V架构单周期CPU设计
本文章旨在设计一个32位的单周期CPU,使用RISC-V架构,并且通过编写verilog语言,最终在FPGA上面实现CPU的完整功能。
前言
本文章旨在设计一个32位的单周期CPU,使用RISC-V架构,并且通过编写verilog语言,最终在FPGA上面实现CPU的完整功能。
一、指令集系统的设计
不同的CPU所能运行的指令集是不同的,所以每一款CPU都要为其设计指令集。这个CPU计划设计十五条指令,来满足基本的运算需要。下图为本CPU可以运行的指令。
二、数据通路的设计
本设计采用《计算机组成与设计硬件软件接口risc-v版》中的架构,如下图所示,这是典型的哈佛结构,即指令和数据分开存储,分别放在instr_mem和data_mem中。并且通过取指、译码、执行、存储、写回五步来完成一整个周期的运行。
- 取指(IF):根据PC读出指令地址,根据地址从指令存储器中取出指令,随后PC+4准备下一次取指。
- 译码(ID):从指令中解析出相关控制信号,并读取通用寄存器堆
- 执行(EX):运算器对通用寄存器堆读出的操作数进行计算
- 访存(MEM):将执行结果读取或者写入到存储器。在CISC中,CPU可以对内存直接进行读取和写入操作;但在RISC中,CPU需要通过寄存器对内存操作,而寄存器与内存间的通信则需要LSU来完成。load store unit为加载存储单元,管理所有load, store操作。
- 写回(WB):将结果写回到通用寄存器堆
下面我们将会按照设计的数据通路来进行各个模块的编写,并且适当的添加一些需要的模块,最后通过顶层模块作为数据通路,连接我们写的各项模块,从而完成CPU的编写。
三、具体模块的设计
1.寄存器堆的设计
1.1、功能说明
寄存器堆在中央处理单元(CPU)中是非常关键的一个组件,它主要执行以下功能:
-
快速存取数据:寄存器堆提供了一组可以快速访问的小容量存储空间,用于暂存和传递在运算中频繁使用的数据。因为寄存器的访问速度远快于主存(即RAM),所以使用寄存器可以显著提高计算效率。
-
执行指令的中间存储:在执行程序中的各种算术和逻辑运算时,寄存器堆存储运算的输入值和输出结果。这些运算通常涉及从寄存器读取值,将它们送入算术逻辑单元(ALU),然后将结果写回寄存器。
-
程序状态记录:某些特殊的寄存器(如程序计数器PC、状态寄存器等)用于控制和监视程序的执行状态。程序计数器存储下一条将要执行的指令的地址,而状态寄存器则记录了如上一次运算结果的零标志、进位标志、溢出标志等重要信息,这些信息对于程序的控制流(如分支和循环)至关重要。
-
支持指令的快速解码和执行:寄存器的编号通常被编码在CPU的指令中,这使得指令的解码过程可以迅速确定指令操作的数据。此外,由于寄存器的数量有限,这也简化了指令集的设计,使得指令更加紧凑。
1.2、接口列表
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
clk | input | 1 | 输入的时钟信号 |
rst_n | input | 1 | 输入的复位信号 |
regwrite | input | 1 | 寄存器的写使能信号 |
write_register | input | 5 | 要更改的寄存器序号,就是目的寄存器的序号 |
write_data | input | 32 | 要更改寄存器的值 |
read_register1 | input | 5 | 输入的寄存器rs1的序号 |
read_data1 | output | 32 | 从rs1中读出来的值 |
read_register2 | input | 5 | 输入的寄存器rs2的序号 |
read_data2 | output | 32 | 从rs2中读出来的值 |
1.3、整体代码
module register(
input clk,
input rst_n,
input regwrite,
input[4:0] write_register,
input[31:0] write_data,
input [4:0] read_register1,
output [31:0] read_data1,
input[4:0] read_register2,
output [31:0] read_data2
);
reg[31:0] register[0:31];
/**********************************************鏁版嵁鍐欏叆瀵勫瓨鍣?*************************************************************************/
always @ (negedge clk or negedge rst_n) begin
if(!rst_n) begin:reset_all_registers
integer i;
for(i=0;i<32;i=i+1)
register[i] <= 32'd00000001;
end
else begin
if((regwrite == 1'b1) && (write_register != 5'h0)) begin
register[write_register] <= write_data;
end
end
end
/**********************************************************鏁版嵁璇诲彇1******************************************************************************/
assign read_data1 = (read_register1 == 5'd0) ? 32'd0 : register[read_register1];
/******************************************************鏁版嵁璇诲彇2**********************************************************************************/
assign read_data2 = (read_register2 == 5'd0) ? 32'd0 : register[read_register2];
endmodule
2.指令存储器的设计
2.1、功能说明
在CPU的设计中,指令存储器(Instruction Memory)是一个关键组件,其主要功能包括:
-
存储机器指令:指令存储器用于存储执行程序所需的所有机器指令。这些指令是预先编译好的,直接反映了要执行的操作,如数据移动、算术运算、逻辑运算、控制流操作等。
-
提供指令获取:CPU在程序执行过程中,按照程序计数器(PC)的指示,从指令存储器中顺序地或者根据跳转和分支逻辑非顺序地获取指令。这一过程是自动进行的,确保CPU能够持续地执行指令。
-
保证执行速度:为了优化性能,指令存储器通常设计得尽可能快速,以匹配CPU的执行速度。在某些设计中,指令存储器可能会利用缓存(如指令缓存)来进一步加速指令的获取过程。
-
隔离程序代码和数据:在许多体系结构中,指令存储器和数据存储器是分开的,这种设计被称为哈佛架构。这样做的优势是可以同时独立地访问指令和数据,提高总体访问效率和处理速度。相对的,冯·诺依曼架构中指令和数据使用同一物理存储,可以简化CPU和内存之间的交互。
2.2、接口列表
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
ReadAddress | input | 10 | 指令存储的地址 |
Instruction | output | 32 | 读取的指令 |
2.3、整体代码
module Instruction_Memory(ReadAddress,Instruction);
input[9:0] ReadAddress;
output[31:0] Instruction;
reg[31:0] Instruction;
wire[7:0] InstMem[127:0];
//small store
assign InstMem[0] = 8'bz, //0000000_00000_00000_000_00000_0110011 //ADD $0 $0 $0
InstMem[1] = 8'bz,
InstMem[2] = 8'bz,
InstMem[3] = 8'bz;
assign InstMem[4] = 8'b1_0110011, //0000000_00010_00001_000_00011_0110011 //ADD $3 $1 $2
InstMem[5] = 8'b1_000_0001,
InstMem[6] = 8'b0010_0000,
InstMem[7] = 8'b0000000_0;
assign InstMem[8] = 8'b0_0110011, //0100000_00101_00100_000_00110_0110011 //SUB $6 $4 $5
InstMem[9] = 8'b0_000_0011,
InstMem[10] = 8'b0101_0010,
InstMem[11] = 8'b0100000_0;
assign InstMem[12] = 8'b1_0110011, //0000000_01000_00111_001_01001_0110011 //SLL $9 $7 $8
InstMem[13] = 8'b1_001_0100,
InstMem[14] = 8'b1000_0011,
InstMem[15] = 8'b0000000_0;
assign InstMem[16] = 8'b0_0110011, //0000000_01011_01010_011_01100_0110011 //SLTU $12 $10 $11
InstMem[17] = 8'b0_011_0110,
InstMem[18] = 8'b1011_0101,
InstMem[19] = 8'b0000000_0;
assign InstMem[20] = 8'b1_0110011, //0000000_01110_01101_101_01111_0110011 //SRL $15 $13 $14
InstMem[21] = 8'b1_101_0111,
InstMem[22] = 8'b1110_0110,
InstMem[23] = 8'b0000000_0;
assign InstMem[24] = 8'b0_0110011, //0000000_10001_10000_111_10010_0110011 //AND $18 $16 $17
InstMem[25] = 8'b0_111_1001,
InstMem[26] = 8'b0001_1000,
InstMem[27] = 8'b0000000_1;
assign InstMem[28] = 8'b1_0110011 , //0000000_10100_10011_110_10101_0110011 //OR $21 $19 $20
InstMem[29] = 8'b1_110_1010,
InstMem[30] = 8'b0100_1001,
InstMem[31] = 8'b0000000_1;
assign InstMem[32] = 8'b1_0000011, //000000000001_10110_010_10111_0000011 //LW $23 1($22)
InstMem[33] = 8'b0_010_1011,
InstMem[34] = 8'b0001_1011,
InstMem[35] = 8'b00000000;
assign InstMem[36] = 8'b1_0010011, //000000000001_11000_000_11001_0010011 //ADDI $25 $24 32'h00000001
InstMem[37] = 8'b0_000_1100,
InstMem[38] = 8'b0001_1100,
InstMem[39] = 8'b0000000_0;
assign InstMem[40] = 8'b1_0010011, //000000000011_11010_110_11011_0010011 //ORI $27 $26 32'h00000003
InstMem[41] = 8'b0_110_1101,
InstMem[42] = 8'b0011_1101,
InstMem[43] = 8'b00000000;
assign InstMem[44] = 8'b1_0010011, //000000000010_11100_001_11101_0010011 //SLLI $29 $28 32'h00000002
InstMem[45] = 8'b0_001_1110,
InstMem[46] = 8'b0010_1110,
InstMem[47] = 8'b00000000;
assign InstMem[48] = 8'b1_0010011, //000000000010_11110_101_11111_0010011 //SRLI $31 $30 32'h00000002
InstMem[49] = 8'b0_101_1111,
InstMem[50] = 8'b010_1111,
InstMem[51] = 8'b00000000;
assign InstMem[52] = 8'b0_0100011, //0000000_00010_00001_010_01000_0100011 //SW MEM($1+32'h00000008) $2
InstMem[53] = 8'b1_010_0100,
InstMem[54] = 8'b0010_0000,
InstMem[55] = 8'b0000000_0;
assign InstMem[56] = 8'b0_1100111, //0000000_00100_00011_000_00010_1100111 //BEQ $4 $3 32'00000001
InstMem[57] = 8'b1_000_0001,
InstMem[58] = 8'b0100_0001,
InstMem[59] = 8'b0000000_0;
assign InstMem[60] = 8'b1_0110111, //00000000000000000110_01101_0110111 //LUI $5 32'h00000000000000000110_000000000000
InstMem[61] = 8'b0110_0110,
InstMem[62] = 8'b00000000,
InstMem[63] = 8'b00000000;
assign InstMem[64] = 8'b0_1101111, //00000000010000000000,00110,1101111//JAL $6,32'h00000008
InstMem[65] = 8'b0000_0011,
InstMem[66] = 8'b1000_0000,
InstMem[67] = 8'b00000000;
//assign InstMem[68] = 8'b0_1100011, //0000000_11111_00011_000_10010_1100011 //IF $3==$31 then JUMP to PC+32'd36 (BEQ)
// InstMem[69] = 8'b1_000_1001,
// InstMem[70] = 8'b1111_0001,
// InstMem[71] = 8'b0000000_1;
//assign InstMem[104] = 8'b0_1101111, //00000000011000000000_11110_1101111 //JUMP to PC+32'h12, SAVE PC+4 to $30
// InstMem[105] = 8'b0000_1111,
// InstMem[106] = 8'b01100000,
// InstMem[107] = 8'b00000000;
always@(ReadAddress)
Instruction = {InstMem[ReadAddress+32'd3],InstMem[ReadAddress+32'd2],
InstMem[ReadAddress+32'd1],InstMem[ReadAddress]};
endmodule
3.PC取指模块设计
3.1、功能说明
PC寄存器,又名程序计数器(PC,Program counter),用于存放指令的地址,为了保证程序(在操作系统中理解为进程)能够连续地执行下去,CPU必须具有某些手段来确定下一条指令的地址。当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称,为“取指令”。与此同时,PC中的地址或自动加4或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。
3.2、接口列表
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
clk | input | 1 | 输入的时钟信号 |
rst_n | input | 1 | 复位信号 |
PCout | output | 10 | 该取到的指令地址 |
imm | input | 32 | beq与jal等跳转信号输入的立即数 |
branch | input | 1 | 控制单元输出的跳转信号 |
ALU_zero | input | 1 | ALU运算的判断信号 |
3.3、整体代码
module PC_reg(
clk,
rst_n,
PCout,
imm,
branch,
ALU_zero
);
input clk ;
input rst_n ;
input [31:0]imm;
input branch;
input ALU_zero;
output reg [9:0]PCout ;
reg [9:0]PC;
always@(posedge clk or negedge rst_n)
if(!rst_n)
PC <= 10'd0;
else if(branch & ALU_zero)
PC <= PC + imm;
else
PC <= PC + 10'd4;
always@(*)
PCout <= PC;
endmodule
4.数据存储器设计
4.1、功能说明
在CPU中,数据寄存器扮演着至关重要的角色,主要包括以下几个功能:
-
临时存储数据:数据寄存器主要用于暂存从内存或其他寄存器中读取的数据。这些数据通常是即将被CPU中的算术逻辑单元(ALU)使用的操作数或者是ALU操作的结果。这种快速的存储访问大大提高了处理速度,因为寄存器的访问时间远远短于内存的访问时间。
-
支持算术和逻辑运算:在执行算术和逻辑运算时,操作数通常从数据寄存器中获得。例如,进行加法运算时,两个操作数可能分别存储在两个不同的数据寄存器中,运算后的结果也可以存回一个数据寄存器。
-
指令执行的中介:数据寄存器作为执行指令过程中的中介,存储需要重复使用的值,减少对慢速内存的多次访问。例如,循环计数器的值通常存储在数据寄存器中,以便快速访问和更新。
-
实现快速数据交换:数据寄存器可以用来实现CPU内部的快速数据交换,例如,在处理多个数据流或执行多任务操作时,寄存器可以用来临时保存和交换信息。
4.2、接口列表
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
CLK | input | 1 | 输入的时钟信号 |
addr | input | 5 | 读取或者该写的寄存器地址 |
DIN | input | 32 | 输入的改写值 |
DOUT | output | 32 | 输出的读取值 |
WRn | input | 1 | 寄存器写使能信号 |
RDn | input | 1 | 寄存器读使能信号 |
4.3、整体代码
module memory(
CLK ,
addr,
DIN ,
DOUT ,
WRn ,
RDn
);
input CLK;
input WRn;
input RDn;
input [4:0] addr;
input [31:0] DIN ;
output reg [31:0] DOUT ;
reg [31:0]memory_unit[0:31];
/***************************************鍐欐暟鎹�*********************************************************/
always @( posedge CLK)
begin
if (WRn) memory_unit[addr]<= DIN;
end
/***************************************璇绘暟鎹�*********************************************************/
always @( * )
begin
if (RDn) DOUT<= memory_unit[addr];
end
endmodule
5.立即数拓展单元的设计
5.1、功能说明
在CPU中,立即数扩展单元(Immediate Extension Unit 或 Sign Extension Unit)是处理器的一个关键组件,它主要负责对指令中的立即数进行扩展,以便这些数值能够与处理器的其他数据路径兼容。下面是立即数扩展单元的主要功能说明:
-
数据格式的兼容性:在不同的指令中,立即数的位宽通常较小,如8位或16位。处理器的数据路径(如ALU或寄存器)可能是32位或64位宽。立即数扩展单元的任务是将这些较短的立即数扩展到数据路径的全宽,例如从16位扩展到32位。
-
符号扩展:当立即数为有符号整数时,扩展时需保留数值的符号(正负)。这通常通过符号扩展完成,即将立即数最高位的符号位复制到新位中。例如,如果一个16位的立即数的最高位是1(表示负数),在扩展到32位时,新的高16位将全部设为1。
-
零扩展:对于无符号数值,扩展操作涉及在数值的高位填充0。这种扩展不改变数值的实际大小,只是格式上的调整以适应更宽的数据路径。
-
处理器指令的支持:立即数扩展单元支持各种指令,如数据传送、算术运算和逻辑运算等,这些指令需要将较短的立即数值用于计算或数据操作。
5.2、接口列表
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
instruct | input | 32 | 读取的指令 |
immediate | output | 32 | 拓展完的立即数 |
5.3、整体代码
module immgen(
instruct,
immediate
);
input [31:0] instruct ;
output reg [31:0] immediate;
always@(*)
if(instruct[6:0] == 7'b0010011) //addi,ori,slli,srli
immediate = {20'b0,instruct[31:20]};
else if( (instruct[6:0] == 7'b0000011) && ( (instruct[14:12] == 3'b010) || (instruct[14:12] == 3'b000) ) ) //lw,lb
immediate = {20'b0,instruct[31:20]};
else if( (instruct[6:0] == 7'b0100011) && ( (instruct[14:12] == 3'b010) || (instruct[14:12] == 3'b000) ) ) //sw,sb
immediate = {20'b0,instruct[31:25],instruct[11:7]};
else if(instruct[6:0] == 7'b1100011)begin //B-type
if(instruct[31] == 1'b1)
immediate = {instruct[31],19'b1,instruct[7],instruct[30:25],instruct[11:8],1'b0};
else
immediate = {instruct[31],19'b0,instruct[7],instruct[30:25],instruct[11:8],1'b0};
end
else if(instruct[6:0] == 7'b1101111)begin //J-type
if(instruct[31] == 1'b1)
immediate = {instruct[31],11'b1,instruct[19:12],instruct[20],instruct[30:21],1'b0};
else
immediate = {instruct[31],11'b0,instruct[19:12],instruct[20],instruct[30:21],1'b0};
end
else
immediate = 32'b0;
endmodule
6. ALU设计
6.1、功能说明
在CPU中,算术逻辑单元(Arithmetic Logic Unit,简称ALU)是执行各种算术和逻辑操作的核心组件。它负责处理所有数学和逻辑相关的指令,是计算机运算的基础。下面是ALU的主要功能说明:
-
算术运算:ALU执行所有基本的算术运算,包括加法、减法、乘法和除法。这些运算支持整数和浮点数(取决于ALU设计的复杂性和指定的功能)。
-
逻辑运算:除了算术运算外,ALU还能执行逻辑运算,如AND、OR、NOT、XOR(异或)、NAND、NOR等。这些逻辑运算在处理条件语句和控制流、数据比较以及其他逻辑决策中非常重要。
-
位运算:ALU还负责位级运算,例如位移(左移和右移)、位旋转和位反转等。这些操作对于数据处理、加密、压缩和其他需要直接操作二进制数据的任务至关重要。
-
比较运算:ALU可以比较两个数值的大小,支持等于、不等于、大于、小于等比较操作。比较结果通常用于程序流程控制,如循环和条件跳转。
-
数据传输和处理:在一些设计中,ALU也参与数据的传输操作,如数据之间的复制和修改。虽然这些功能可能更多地由其他CPU组件(如寄存器和总线)处理,但ALU在这方面仍然发挥作用。
-
状态标志更新:在执行操作时,ALU通常会更新处理器的状态标志,如零标志(ZF)、进位标志(CF)、溢出标志(OF)和符号标志(SF)。这些标志帮助处理器确定后续操作,如决定是否跳转或继续执行。
6.2接口列表
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
control_signal | input | 4 | ALU的控制信号 |
read_data1 | input | 32 | 输入ALU的一号数据 |
read_data2 | input | 32 | 输入ALU的二号数据 |
immediate | input | 32 | 输入ALU的立即数 |
ALU_src | input | 1 | 控制第二个运算值是来自立即数还是寄存器 |
ALU_result | output | 32 | 运算结果 |
ALU_zero | output | 1 | 判断是否为零的信号 |
6.3整体代码
module ALU(
control_signal,
read_data1 ,
read_data2 ,
immediate ,
ALU_src ,
ALU_result ,
ALU_zero
);
input ALU_src;
input [3:0 ] control_signal;
input [31:0] read_data1;
input [31:0] read_data2;
input [31:0] immediate;
output reg [31:0] ALU_result;
output reg ALU_zero ;
reg [31:0] ALU_data2;
always@(*)
if(ALU_src)
ALU_data2 = immediate;
else
ALU_data2 = read_data2;
always@(read_data1 or ALU_data2 or control_signal)
case(control_signal)
4'b0000: ALU_result <= read_data1+ALU_data2; //add ,addi,lw,lb,sw,sb
4'b0001: ALU_result <= read_data1+(~ALU_data2[30:0])+32'd1; //sub
4'b0010: ALU_result <= read_data1&ALU_data2; //and
4'b0011: ALU_result <= read_data1|ALU_data2; //ori or
4'b0100: ALU_result <= read_data1<<ALU_data2; //sll,slli
4'b0101: ALU_result <= read_data1>>ALU_data2; //srl,srli
4'b0110: ALU_result <= (read_data1<ALU_data2) ? 32'd1 :32'd0; //sltu
default: ALU_result = 32'dz;
endcase
always@(*)
case(control_signal)
4'b1000:ALU_zero = (read_data1 == ALU_data2) ? 1'b1:1'b0; //beq
4'b1001:ALU_zero = 1'b1; //jal
default:ALU_zero = 1'b0;
endcase
endmodule
7.主控单元的设计
7.1、功能说明
在CPU中,主控单元(Control Unit,简称CU)是负责指挥和协调计算机的其他部件工作的关键组件。它不参与数据的实际处理,但控制和管理整个计算机系统的操作流程。主控单元的主要功能包括:
-
取指令(Instruction Fetch):主控单元负责从内存中获取指令,并将其传送到CPU内部进行解码和执行。这是程序执行的第一步,确保了程序指令能够被连续处理。
-
指令解码(Instruction Decode):在取得指令后,主控单元对指令进行解码,确定指令的类型和所需的操作。解码过程涉及分析指令的操作码(opcode)和操作数,以确定如何执行指令。
-
执行控制(Execution Control):根据解码的结果,主控单元发出具体的控制信号,指挥ALU和其他部件执行指令所需的算术逻辑运算、数据传输等操作。
-
协调时序(Timing Control):主控单元负责生成和管理时钟信号,以协调各个部件的操作时序。这确保了数据和控制信号在适当的时间内正确地传递到相应的部件。
-
指令流控制(Instruction Flow Control):主控单元控制指令的执行顺序,包括顺序执行、跳转、循环等。这涉及到处理分支和循环等复杂的程序逻辑。
-
寄存器管理:主控单元管理和控制寄存器的使用,包括指令寄存器(IR)、程序计数器(PC)、状态寄存器等,以及如何在这些寄存器之间传输数据。
7.2、接口列表
端口名称 | 信号类型 | 位宽 | 说明 |
---|---|---|---|
instruction | input | 7 | 进行译码操作时输入的指令七位的OPcode |
branch | output | 1 | 跳转使能信号 |
memread | output | 1 | 读取数据存储器使能信号 |
memtoreg | output | 2 | 向寄存器写入时数选器选择信号 |
ALUop | output | 2 | 输入ALU_control的控制信号 |
memwrite | output | 1 | 存储器写使能信号 |
ALUsrc | output | 1 | 输入到ALU的数选器选择信号 |
regwrite | output | 1 | 寄存器写使能信号 |
jal | output | 1 | 跳转使能信号 |
7.3、整体代码
module control(
input [6:0]instruction,
output reg branch,
output reg memread,
output reg [1:0]memtoreg,
output reg [1:0]ALUop,
output reg memwrite,
output reg ALUsrc,
output reg regwrite,
output reg jal
);
always@(*)
begin
case(instruction[6:0])
7'b0110011:begin branch<=1'bz;memread<=1'bz;memtoreg<=2'b00;ALUop<=2'b11;memwrite<=1'bz;ALUsrc<=1'b0;regwrite<=1'b1;jal<=1'bz;end
7'b0010011:begin branch<=1'bz;memread<=1'bz;memtoreg<=2'b00;ALUop<=2'b10;memwrite<=1'bz;ALUsrc<=1'b1;regwrite<=1'b1;jal<=1'bz;end
7'b0000011:begin branch<=1'bz;memread<=1'b1;memtoreg<=2'b01;ALUop<=2'b10;memwrite<=1'bz;ALUsrc<=1'b1;regwrite<=1'b1;jal<=1'bz;end
7'b0100011:begin branch<=1'bz;memread<=1'bz;memtoreg<=2'bzz;ALUop<=2'b10;memwrite<=1'b1;ALUsrc<=1'b1;regwrite<=1'bz;jal<=1'bz;end
7'b1100111:begin branch<=1'b1;memread<=1'bz;memtoreg<=2'bzz;ALUop<=2'b01;memwrite<=1'bz;ALUsrc<=1'bz;regwrite<=1'bz;jal<=1'bz;end
7'b0110111:begin branch<=1'bz;memread<=1'bz;memtoreg<=2'b11;ALUop<=2'bzz;memwrite<=1'bz;ALUsrc<=1'bz;regwrite<=1'b1;jal<=1'bz;end
7'b1101111:begin branch<=1'b1;memread<=1'bz;memtoreg<=2'b10;ALUop<=2'b00;memwrite<=1'bz;ALUsrc<=1'bz;regwrite<=1'b1;jal<=1'b1;end
default: begin branch<=1'bz;memread<=1'bz;memtoreg<=2'bzz;ALUop<=2'bzz;memwrite<=1'bz;ALUsrc<=1'bz;regwrite<=1'bz;jal<=1'bz;end
endcase
end
endmodule
8.ALU控制单元的设计
8.1功能说明
在CPU中,算术逻辑单元(Arithmetic Logic Unit, ALU)控制单元是负责管理和控制ALU的部分,它使ALU能够执行各种算术和逻辑运算。ALU控制单元的主要功能可以概括为以下几点:
-
解析操作码(Opcode Interpretation):ALU控制单元首先需要解释主控单元送来的指令中的操作码部分,以确定要执行的具体算术或逻辑操作。这些操作可能包括加法、减法、乘法、除法、位操作(如与、或、非、异或)和比较操作等。
-
生成控制信号(Control Signal Generation):根据解析出的操作码,ALU控制单元生成相应的控制信号,这些信号控制ALU的内部逻辑电路,指示它进行特定的操作。
-
控制操作的执行(Execution Control):ALU控制单元负责监控和指导ALU执行操作的整个过程,确保操作正确无误地进行。这包括管理数据输入、操作执行和结果输出的过程。
-
设置标志状态(Flag Setting):在操作完成后,ALU控制单元根据结果设置状态标志。这些标志位通常包括进位标志(Carry Flag)、溢出标志(Overflow Flag)、零标志(Zero Flag)和负标志(Negative Flag),它们被存储在状态寄存器中,用于指示上一次操作的特定情况,比如是否产生了溢出、结果是否为零等。
8.2接口列表
端口名称 | 信号类型 | 位宽 | 说明 |
---|---|---|---|
instruction | input | 32 | 读取的指令 |
ALUop | input | 2 | 输入的控制信号 |
control_signal | output | 4 | 输出到ALU的控制信号 |
8.3整体代码
module ALUcontrol(ALUop,control_signal,instruction);
input [31:0]instruction;
input [1:0] ALUop;
output [3:0] control_signal;
reg [3:0] control_signal;
always@(ALUop or instruction)
case(ALUop)
2'b00: control_signal=4'b1001;//jal
2'b01: control_signal=4'b1000;//beq
2'b10:case(instruction[14:12])//funct3
3'b010:control_signal=4'b0000;//lw,sw
3'b000:control_signal=4'b0000;//addi,lb,sb
3'b110:control_signal=4'b0011;//ori
3'b001:control_signal=4'b0100;//slli
3'b101:control_signal=4'b0101;//srli
default:control_signal=4'bxxxx;
endcase
2'b11:case(instruction[14:12])//funct3
3'b000:if(instruction[30]==0) control_signal=4'b0000;//add
else control_signal=4'b0001; //sub
3'b111:control_signal=4'b0010;//and
3'b110:control_signal=4'b0011;//or
3'b001:control_signal=4'b0100; //sll
3'b101:control_signal=4'b0101;//srl
3'b011:control_signal=4'b0110;//sltu
default:control_signal=4'bxxxx;
endcase
endcase
endmodule
9.数选器的设计
9.1功能说明
在CPU中,数选器(通常称为多路选择器或MUX)是一个非常重要的数字逻辑组件,用于在多个输入信号中选择一个输出。数选器在CPU的设计中起着关键的作用,特别是在数据路径管理和控制信号的路由上。以下是数选器在CPU中的一些主要功能:
-
输入选择:数选器可以从多个输入信号中根据控制信号的值选择一个并输出。在CPU中,这允许基于当前执行的指令或状态选择不同的数据源或指令流。
-
指令分支与条件执行:在执行条件分支指令(如if-else结构)时,数选器可以根据条件标志(如比较指令后的标志寄存器结果)选择下一个执行的指令地址,这对于实现程序的逻辑控制非常关键。
-
寄存器选择:在执行指令过程中,数选器用于从多个寄存器中选择数据。例如,算术逻辑单元(ALU)可能需要从多个寄存器中获取操作数,数选器就可以根据指令的需求选择正确的寄存器数据输入到ALU。
-
数据路径管理:CPU内部的数据通常需要在多个单元间传输,如寄存器、ALU、缓存等。数选器负责在这些单元之间正确路由数据,确保数据在正确的时间送达正确的目的地。
9.2接口列表
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
memtoreg | input | 2 | 写回阶段的数据选择信号 |
jal | input | 1 | 跳转指令信号 |
instruct | input | 32 | 输入的指令 |
out1 | input | 32 | 存储器读取的值 |
out2 | input | 32 | ALU运算的结果 |
out3 | input | 10 | 准备跳转的地址 |
writedata | output | 32 | 输出的选择后的信号 |
9.3整体代码
module mux2(
memtoreg,
jal ,
instruct,
out1 , //memory output data
out2 , //ALUresult
out3 , //PC+4
writedata
);
input [1:0]memtoreg;
input jal ;
input [31:0]instruct;
input [31:0]out1;
input [31:0]out2;
input [9:0 ]out3;
output reg [31:0]writedata;
always@(*)
if(jal == 1'b1)
writedata = {22'b0,out3};
else if(memtoreg == 2'b01)
writedata = out1;
else if(memtoreg == 2'b11)
writedata = {instruct[31:12],12'b0};
else
writedata = out2;
endmodule
10.完整数据通路的设计
10.1功能说明
数据通路是设计的顶层文件,相当于C语言中的main函数。数据通路需要按照第二部分所画的数据通路图来编写。将前面编写的九个模块进行实例化,并且将各个模块的输出与需要其作为输入的模块端口进行连接,从而完成整个CPU的编写。
10.2接口列表
端口名称 | 类型 | 位宽 | 说明 |
---|---|---|---|
clk | input | 1 | 由外部输入的时钟信号 |
rst_n | input | 1 | 由外部输入的复位信号 |
instruct | output | 32 | 从IM中读取的指令 |
address | output | 10 | 所读取的指令的地址 |
ALU_result | output | 32 | ALU运算的结果 |
rd_address | output | 5 | 目的寄存器的地址 |
其实整个top文件只需要clk与rst_n这两个端口就够了,其他的各种信号都可以在仿真中直接调用出来查看其波形。但是为了查看方便一点,我就把一些想看的信号写到输出端口了。
10.3整体代码
module top(
clk,
rst_n,
instruct,
address,
ALU_result,
rd_address
);
input rst_n;
input clk ;
output [31:0]instruct;
output [9:0] address ;
output [31:0] ALU_result ;
output [4:0] rd_address;
wire branch ;
wire memread ;
wire [1:0]memtoreg;
wire memwrite;
wire ALUsrc ;
wire regwrite;
//wire [9:0]address;
//wire [31:0]instruct;
wire jal ;
/****************************************************************/
wire [1:0 ] ALUop ;
wire [31:0] write_data ;
wire [31:0] read_data1 ;
wire [31:0] read_data2 ;
wire [31:0] immediate ;
wire [3:0 ] control_signal ;
wire [31:0] ALU_result ;
wire ALU_zero ;
wire [31:0] q1 ;
wire [4:0] rd ;
/****************************************************************/
/*********************************************************************/
/*********************************************************************/
PC_reg u_PC_reg
(
.clk (clk ),
.rst_n (rst_n ),
.PCout (address ),
.imm (immediate),
.branch (branch ),
.ALU_zero(ALU_zero )
);
Instruction_Memory u_instruction
(
.ReadAddress (address ),
.Instruction (instruct )
);
register u_register
(
.clk (clk ),
.rst_n (rst_n ),
.read_register1(instruct[19:15]),
.read_register2(instruct[24:20]),
.write_register(instruct[11:7] ),
.write_data (ALU_result ),
.read_data1 (read_data1 ),
.read_data2 (read_data2 ),
.regwrite (regwrite )
);
ALU u_ALU
(
.control_signal(control_signal),
.read_data1 (read_data1 ),
.read_data2 (read_data2 ),
.immediate (immediate ),
.ALU_src (ALUsrc ),
.ALU_result (ALU_result ),
.ALU_zero (ALU_zero )
);
control u_control
(
.instruction (instruct[6:0] ),
.branch (branch ),
.memread (memread ),
.memtoreg(memtoreg ),
.ALUop (ALUop ),
.memwrite(memwrite ),
.ALUsrc (ALUsrc ),
.regwrite(regwrite ),
.jal (jal )
);
ALUcontrol u_ALUcontrol
(
.ALUop (ALUop ),
.instruction (instruct ),
.control_signal(control_signal)
);
immgen u_immgen
(
.instruct (instruct ),
.immediate(immediate)
);
memory u_memory(
.CLK (clk ),
.addr (ALU_result ),
.DIN (read_data2 ),
.DOUT (q1 ),
.WRn (memwrite ),
.RDn (memread )
);
mux2 u_mux2(
.memtoreg (memtoreg ),
.jal (jal ),
.out1 (q1 ), //memory output data
.out2 (ALU_result),
.out3 (address ),
.instruct (instruct ),
.writedata(write_data)
);
assign rd_address=instruct[11:7];
/*********************************************************************/
/*********************************************************************/
endmodule
四、整体功能的验证
本小节进行刚才编写完成的CPU功能验证。仿真的波形图如下:
由图可知,CPU的功能正常,各个指令运行的结果正确,并且可以根据前半个周期读后半个周期写的原则,正确的修改目的寄存器的值。下面附上测试文件供大家验证。
`timescale 1ns / 1ps
module test_top;
// Inputs
reg clk;
reg rst_n;
// Outputs
wire [31:0] instruct;
wire [9:0] address;
wire [31:0] ALU_result ;
wire [4:0] rd_address;
// Instantiate the Unit Under Test (UUT)
top u_top (
.clk(clk),
.rst_n(rst_n),
.instruct(instruct),
.address(address),
.ALU_result(ALU_result),
.rd_address(rd_address)
);
always #50 clk = ~clk;
initial begin
// Initialize Inputs
clk = 0;
rst_n = 0;
// Wait 100 ns for global reset to finish
#20 rst_n = 1;
// Add stimulus here
end
endmodule
本文结束谢谢大家,如有不足之处请大家指正。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)