一 顶层模块的实现
顶层模块用于对之前文章里介绍的五级流水线的各个模块进行例化,也就是连线,那么顶层模块的输入输出接口如何呢?
首先输入要有时钟复位信号,还要有一个来接收指令存储器里的数据记为rom_data_i
输出因为要去读取指令存储器中的数据,所以要输出读地址以及一个使能信号。
具体实现就参照我们上一节所做好的数据通路进行连线,连接和数据通路图如下:CPU设计实战-第一条指令ori的实现即最简单的五级流水线的实现
module my_mips(
input clk,
input rst,
input [31:0] rom_data_i,
output [4:0] rom_addr_o,
output rom_ce_o
);
wire [31:0] pc;
//if/id输出与id模块输入模块的连接
wire [31:0] id_pc_i;
wire [31:0] id_inst_i;
//ID模块的输出与ID/EX输入的连接
wire [7:0] id_aluop_o ;
wire [2:0] id_alusel_o;
wire [31:0] id_reg1_o ;
wire [31:0] id_reg2_o ;
wire [4:0] id_wd_o ;
wire id_wreg_o ;
//ID/EX模块的输出与EX输入的连接
wire [7:0] ex_aluop_i ;
wire [2:0] ex_alusel_i;
wire [31:0] ex_reg1_i ;
wire [31:0] ex_reg2_i ;
wire [4:0] ex_wd_i ;
wire ex_wreg_i ;
//EX模块的输出与EX/MEM输入的连接
wire [31:0] ex_wdata_o ;
wire [4:0] ex_wd_o ;
wire ex_wreg_o ;
//EX/MEM模块的输出与MEM输入的连接
wire [31:0] mem_wdata_i;
wire [4:0] mem_wd_i ;
wire mem_wreg_i ;
//MEM模块的输出与MEM/WB输入的连接
wire [31:0] mem_wdata_o;
wire [4:0] mem_wd_o ;
wire mem_wreg_o ;
//MEM/WB模块输出与regfile寄存器堆的连接
wire [31:0] wb_wdata_i;
wire [4:0] wb_wd_i ;
wire wb_reg_i ;
//ID译码模块与寄存器堆模块的连接
wire [4:0] reg1_addr ;
wire [4:0] reg2_addr ;
wire reg1_read ;
wire reg2_read ;
wire [31:0] reg1_data ;
wire [31:0] reg2_data ;
pc_reg u_pc_reg (
.clk ( clk ),
.rst ( rst ),
.pc ( pc ),
.ce ( rom_ce_o )
);
//输出到指令存储器的地址就是pc地址
assign rom_addr_o = pc;
if_id u_if_id (
.clk ( clk ),
.rst ( rst ),
.if_pc ( pc),
.if_inst ( rom_data_i ),
.id_pc ( id_pc_i ),
.id_inst ( id_inst_i)
);
id u_id (
.clk ( clk ),
.rst ( rst ),
.pc_i ( id_pc_i ),
.inst_i ( id_inst_i),
.reg1_data_i ( reg1_data ),
.reg2_data_i ( reg2_data ),
.reg1_addr_o ( reg1_addr ),
.reg2_addr_o ( reg2_addr ),
.reg1_read_o ( reg1_read ),
.reg2_read_o ( reg2_read ),
.aluop_o ( id_aluop_o ),
.alusel_o ( id_alusel_o ),
.reg1_o ( id_reg1_o ),
.reg2_o ( id_reg2_o ),
.wd_o ( id_wd_o ),
.wreg_o ( id_wreg_o )
);
id_ex u_id_ex (
.clk ( clk ),
.rst ( rst ),
.id_aluop ( id_aluop_o ),
.id_alusel ( id_alusel_o ),
.id_reg1 ( id_reg1_o ),
.id_reg2 ( id_reg2_o ),
.id_wd ( id_wd_o ),
.id_wreg ( id_wreg_o ),
.ex_aluop ( ex_aluop_i ),
.ex_alusel ( ex_alusel_i ),
.ex_reg1 ( ex_reg1_i ),
.ex_reg2 ( ex_reg2_i ),
.ex_wd ( ex_wd_i ),
.ex_wreg ( ex_wreg_i )
);
ex u_ex (
.rst ( rst ),
.aluop_i ( ex_aluop_i ),
.alusel_i ( ex_alusel_i ),
.reg1_i ( ex_reg1_i ),
.reg2_i ( ex_reg2_i ),
.wd_i ( ex_wd_i ),
.wreg_i ( ex_wreg_i ),
.wdata_o ( ex_wdata_o ),
.wd_o ( ex_wd_o ),
.wreg_o ( ex_wreg_o )
);
ex_mem u_ex_mem (
.clk ( clk ),
.rst ( rst ),
.ex_wdata ( ex_wdata_o ),
.ex_wd ( ex_wd_o ),
.ex_wreg ( ex_wreg_o ),
.mem_wdata ( mem_wdata_i ),
.mem_wd ( mem_wd_i ),
.mem_wreg ( mem_wreg_i )
);
mem u_mem (
.rst ( rst ),
.wdata_i ( mem_wdata_i ),
.wd_i ( mem_wd_i ),
.wreg_i ( mem_wreg_i ),
.wdata_o ( mem_wdata_o ),
.wd_o ( mem_wd_o ),
.wreg_o ( mem_wreg_o )
);
mem_wb u_mem_wb (
.clk ( clk ),
.rst ( rst ),
.mem_wdata ( mem_wdata_o ),
.mem_wd ( mem_wd_o ),
.mem_wreg ( mem_wreg_o ),
.wb_wdata ( wb_wdata_i ),
.wb_wd ( wb_wd_i ),
.wb_reg ( wb_reg_i )
);
regfile u_regfile (
.clk ( clk ),
.rst ( rst ),
.raddr1 ( reg1_addr ),
.re1 ( reg1_read ),
.raddr2 ( reg2_addr ),
.re2 ( reg2_read ),
.waddr ( wb_wd_i ),
.wdata ( wb_wdata_i ),
.we ( wb_reg_i ),
.rdata1 ( reg1_data ),
.rdata2 ( reg2_data )
);
endmodule
二 指令存储器ROM的实现
之前说过我们的my_mips模块的输入输出都需要从指令存储器进行关联。
指令存储器中存储的指令码,需要进行的操作就是对其按照指令地址进行读取指令码
故输入指令地址addr,输出指令码inst,另外需要一个使能信号,如下图:
具体实现过程注意以下几点:
1.首先要定义这个rom的大小,方法和实现寄存器堆一样,本质上是一个数组寄存器堆实现
2.然后要把指令写入这个rom,这里采用系统函数¥readmemh,它可以将指定的文件中的数据写入rom中
3.最后进行读操作时需要注意,指令存储器的寻址方式是字节型,而我们pc端口模块给出的地址是32位的,所以需要除以4再写入地址,具体的操作用左移两位来实现的。那么既然要移位地址肯定要知道地址的宽度,需要知道地址宽度如何计算。
2^地址宽度=元素大小,根据define文件发现定义的元素大小为131071即128k,那么地址宽度为17
`include "defines.v"
module inst_rom(
input ce,
input [4:0] addr,
output reg [31:0] inst
);
reg[31:0] inst_mem[0:`InstMemNum-1];
initial $readmemh ("inst_rom.data", inst_mem);
always @(*) begin
if (!ce) begin
inst = 0;
end
else begin
inst = inst_mem[addr[`InstMemNumLog2+1:2]];//左移两位实现除以4
end
end
endmodule
三 data文件分析(由于编译环境还未搭建,本质上是一个手动编译指令的操作)
inst_rom.data
34011100
34210020
3421ff00
342100ff
分析一下data文件中指令数据,其是按照十六进制存储的,我们展开一个第一个来看看
001101 | 00000 | 00001 | 0001 0001 0000 0000 |
ORI指令的op | rs | rt | imm立即数 |
回顾ORI指令的功能
指令用法为: ori rs, rt, immediate,作用是将指令中的16位立即数immediate进行无符号
扩展至32位,然后与索引为rs的通用寄存器的值进行逻辑“或”运算,运算结果保存到索引
为rt的通用寄存器中。
可以明白第一行指令的操作是将0号寄存器中的值和立即数进行逻辑或运算,由于0号寄存器中的值恒为0,故结果还是原来的imm立即数,并将这个结果保存到1号寄存器中。剩余指令以此类推
四 最小SOPC的实现
有了cpu和指令存储器,我们大概可以搭建一个最简单的SOPC,将两个模块连接如下:
最小SOPC对应的模块为mymips_min_sopc,其输入只有时钟和复位信号,然后按照上图进行例化即可。
module mymips_min_sopc(
input clk,
input rst
);
wire rom_ce;
wire [31:0] inst;
wire [4:0] inst_addr;
my_mips u_my_mips (
.clk ( clk ),
.rst ( rst ),
.rom_data_i ( inst ),
.rom_addr_o ( inst_addr ),
.rom_ce_o ( rom_ce )
);
inst_rom u_inst_rom (
.ce ( rom_ce ),
.addr ( inst_addr ),
.inst ( inst)
);
endmodule
五 编写测试程序
读者可能还是会疑惑data文件里的数据是怎么得来的,其实是基于我们编写的测试文件inst_rom.S
此文件为汇编文件,用于测试ori指令
ori $1,$0,0x1100
ori $2,$0,0x0020
ori $3,$0,0xff00
ori $4,$0,0xffff
可以知道此代码的功能是将立即数与0号寄存器(恒为0)进行或运算故结果还是原来的立即数
按照正常顺序是通过编译器编译此文件得出指令寄存器中的二进制data文件,由于配置编译环境花费额外篇幅讲解,此处采用手动编译的方法。
例如第一行指令对应的二进制如下图:
转化为16进制为data文件中第一行的数据,其他直径以此类推,这就解释了data文件中的数据是怎么来的。
接下来就可以写testbech来测试我们的程序了,仿真文件很好写,对于sopc模块只需要给与时钟信号和复位信号即可,通过观察仿真文件里的寄存器的值即可知道指令运行是否正常。