首先,我们为我们的CPU编写通用寄存器部分。
本CPU采用的指令集为典型的RISC指令集,最大允许同时对三个寄存器进行操作,两个寄存器进行输出,一个寄存器进行写入。因此,我们的寄存器堆需要两个输出端口,一个输入端口。
代码如下
`include "cpu.h"
`include "isa.h"
`include "stddef.h"
`include "nettype.h"
`include "global_config.h"
module gpr(
input wire clk, //时钟信号
input wire reset, //复位信号
input wire [`RegAddrBus] rd_addr_0, //读取端口0,输入
output wire [`WordDataBus] rd_data_0, //读取端口0,输出
input wire [`RegAddrBus] rd_addr_1, //读取端口1,输入
output wire [`WordDataBus] rd_data_1, //读取端口1,输出
input wire we_, //写入端口
input wire [`RegAddrBus] wr_addr, //写入端口,地址线
input wire [`w=WordDataBus] wr_data //写入端口,数据
);
reg [`RegAddrBus] gpr[`REG_NUM-1:0]; //内部信号,寄存器序列
integer i;
//读取端口0
assign rd_data_0 = ((we_ == `ENABLE_) && (wr_addr == rd_addr_0)) ? wr_data : gpr[rd_addr_0];
//输出数据,当该端口选中,并读取到地址,则输出选中的信号,否则输出写入端口的数据
//读取端口1
assign rd_data_1 = ((we_ == `ENABLE_) && (wr_addr == rd_addr_1)) ? wr_data : gpr[rd_addr_1];
//输出数据,当该端口选中,并读取到地址,则输出选中的信号,否则输出写入端口的数据
/* 写入访问 */
always @ (posedge clk or `RESET_EDGE reset) begin
if(reset == `RESET_ENABLE) begin
//异步复位
for(i =0; i < `REG_NUM; i = i + 1) begin
gpr[i] <= #1 `WORD_DATA_W'h0;
end
end
else begin
//写入
if (we_ == `ENABLE_) begin
gpr[wr_addr] <= #1 wr_data;
end
end
end
endmodule
在本部分中,我们的通用寄存器主要有三个功能
读取访问
我们通过两行assign对数据进行读取,当we_信号有效,且没有进行对当前寄存器进行写入操作时,会输出当前寄存器中的数据,否则,会将当前寄存器写入的数据直接输出
异步复位
该寄存器堆使用异步复位信号,当复位信号低电平有效时,对寄存器进行复位清零
写入控制
当we_信号有效时,允许向指定的寄存器内写入数据
SPM是指CPU可以不经过总线即可直接访问的专用内存。
在网上并没有找到太多的关于SPM的资料,书中解释,SPM用于每个周期线流水线提供指令或数据。它和缓存类似。是一种高速的存储器。
使用它可以使得流水更加高效,同时,因为不经过总线,速度更快,也可以避免因总线被占用而使得流水阻塞。
SPM会和CPU流水中的IF和MEM阶段相连,则两个阶段都需要访问外部的存储器,故可以使用SPM加快访问速度。IF阶段主要用于将PC中的值送入内存,读取指令到IR;MEM阶段用于对内存的访问。
SPM使用一个名为spm的模块构成,储存器使用FPGA的Dual Port RAM实现。
在本程序中,我们大量采用宏定义,本模块的宏如下:
`ifndef __SPM_HEADER__
`define __SPM_HEADER__
`define SPM_SIZE 16384 //SPM的容量
`define SPM_DEPTH 4096 //SPM的深度
`define SPM_ADDR_W 12 //地址宽度
`define SpmAddrBus 11:0 //地址总线
`define SpmAddrLoc 11:0 //地址的位置
`endif
本模块的具体代码如下:
`include "spm.h"
`include "cpu.h"
`include "isa.h"
`include "stddef.h"
`include "nettype.h"
`include "global_config.h"
module spm(
input wire clk, //时钟信号
//if阶段,A端口信息
input wire [`SpmAddrBus] if_spm_addr, //地址线
input wire if_spm_as_, //地址选通
input wire if_spm_rw, //读写信号,低电平写
input wire [`WordDataBus] if_spm_wr_data, //输入数据
output wire [`WordDataBus] if_spm_rd_data, //输出数据
//mem阶段,B端口信息
input wire [`SpmAddrBus] mem_spm_addr, //地址线
input wire mem_spm_as_, //地址选通
input wire mem_spm_rw, //读写信号
input wire [`WordDataBus] mem_spm_wr_data, //输入数据
output wire [`WordDataBus] mem_spm_rd_data //输出数据
)
//内部信号,写入有效
reg wea;
reg web;
/* 写入有效信号的生成 */
always @(*) begin
//如果A端口被选中并且读写信号为写,写入有效,否则无效
if ((if_spm_as_ == `ENABLE_) && (if_spm_rw == `ENABLE_)) begin
wea = `MEM_ENABLE;
end
else begin
wea = `MEM_DISABLE;
end
//如果B端口被选中且读写信号为写,写入有效,否则无效
if ((mem_spm_as_ == `ENABLE_) && (mem_spm_rw == `ENABLE_)) begin
web = `MEM_ENABLE;
end
else begin
web = `MEM_DISABLE;
end
end
/* 实例化FPGA中的RAM模块 */
x_s3e_dpram x_s3e_dpram(
//A端口,用于IF阶段
.clka (clk), //时钟信号
.addra (if_spm_addr), //地址
.dina (if_spm_wr_data), //写入的数据
.wea (wea), //写入有效
.douta (if_spm_rd_data), //读取的数据
//B端口,用于MEM阶段
.clkb (clk), //时钟信号
.addrb (mem_spm_addr), //地址
.dinb (mem_spm_wr_data), //写入的数据
.web (web), //写入有效
.doutb (mem_spm_rd_data) //读取的数据
);
endmodule
在IF和MEM阶段我们需要对内存进行访问,当涉及到访问内存时,我们就必须使用总线。而总线接口就是用来对总线的访问进行控制的。
在上述两个阶段,我们引入了SPM的概念,使用SPM可以使得CPU不经过使用总线访问内存即可直接读取SPM中的数据进行操作,但由于SPM过小,必然会存在如Cache一样的问题,即命中问题。如果CPU需要的数据没有保存在在SPM中,CPU必然需要通过总线对内存进行访问。
这样,我们就总结出了通过总线访问内存的条件:当前总线空闲,CPU未执行流水的刷新操作,地址选通有效,对1号之外的总线从属进行访问(设置为1号总线从属为SPM)。
在本模块中,总线接口一共由两部分构成:一部分是用于总线控制内存访问的组合电路;另一部分是用于控制总线接口状态的时序电路。
该模块的完整代码如下:
`include "cpu.h"
`include "isa.h"
`include "bus.h"
`include "stddef.h"
`include "nettype.h"
`include "global_config.h"
module bus_if (
input wire clk, //时钟信号
input wire reset, //复位信号
input wire stall, //延迟信号
input wire flush, //刷新信号
input wire busy, //总线忙信号
/* CPU 接口 */
input wire [`WordAddrBus] addr, //地址
input wire as_, //有效信号
input wire rw, //读写信号
input wire [`WordDataBus] wr_data, //写入数据
output reg [`WordDataBus] rd_data, //读取数据
/* SPM 接口 */
input wire [`WordDataBus] spm_rd_data, //读取数据
output wire [WordAddrBus] spm_addr, //地址
output reg spm_as_, //有效信号
output wire spm_rw, //读写信号
output wire [`WordDataBus] spm_wr_data, //写入数据
/* 总线 接口 */
input wire [`WordDataBus] bus_rd_data, //读取数据
input wire bus_rdy_, //就绪信号
input wire bus_grnt_, //许可信号
output reg bus_req_, //请求信号
output wire [`WordAddrBus] bus_addr, //地址
output reg bus_as_, //地址选通信号
output wire bus_rw, //读写信号
output wire [`WordDataBus] bus_wr_data //写入数据
);
reg [`BusIfStateBus] state; //总线状态
reg [`WordDataBus] rd_buf; //数据缓冲
wire [`BusSlaveIndexBus] s_index; //总线从属索引
assign s_index = addr[`BusSlaveIndexLoc]; //生成总线索引
/* 输出赋值 */
assign spm_addr = addr;
assign spm_rw = rw;
assign spm_wr_data = wr_data;
/* 内存访问控制 */
always @(*) begin
//设置默认值
rd_data = `Word_Data_w'h0;
spm_as_ = `DISABLE;
busy = `DISABLE;
//总线接口的状态
case(state) begin
//空闲
`BUS_IF_STATE_IDLE : begin
//当流水线未刷新,且选通信号有效时
if ((flush == `DISABLE) && (as_ == `ENABLE_)) begin
//选择是访问SPM还是总线
if (s_index == `BUS_SLAVE_1) begin
//如果选择SPM,需要确定当前流水线是否存在延迟,SPM只能在没有延迟的时候才能访问
if (stall == `DISABLE) begin
spm_as_ == `ENABLE_; //SPM选通
if (rw == `READ) begin //读取控制
rd_data = spm_rd_data;
end
end
end
//如果访问的不是SPm,则去访问总线,由于当前总线空闲,则可以直接使用总线,并将总线置忙
else begin
busy = `ENABLE;
end
end
end
//总线请求
`BUS_IF_STATE_REQ : begin
busy = `ENABLE;
end
//获得总线使用权,访问总线
`BUS_IF_STATE_ACCESS : begin
//等待总线就绪信号,如果总线准备就绪,则进行使用
if(bus_rdy_ == `ENABLE_) begin
//读取控制
if (rw == `READ) begin
rd_data = bus_rd_data;
end
end
//如果总线没有准备就绪,则进行原地等待,继续占用总线
else begin
busy = `ENABLE;
end
end
//流水线存在延迟
`BUS_IF_STATE_STALL : begin
//如果流水线存在延迟,但当前状态为读取状态
//由于读取已经完成,数据保存在buffer中
//可以直接将buffer中的数据输出,将总线置为不忙
if (rw == `READ) begin
rd_data = rd_buf;
end
end
endcase
end
/* 总线接口状态控制 */
always @ (posedge clk or `RESET_EDGE reset) begin
//异步复位
if (reset == `RESET_ENABLE) begin
state <= #1 `BUS_IF_STATE_IDLE;
bus_req_ <= #1 `DISABLE_;
bus_addr <= #1 `WORD_ADDR_W'h0;
bus_as_ <= #1 `DISABLE_;
bus_rw <= #1 `READ;
bus_wr_data <= `WORD_DATA_W'h0;
rd_buf <= #1 `WORD_DATA_W'h0;
end
else begin
// 总线状态控制
case (state) begin
//空闲
`BUS_IF_STATE_IDLE : begin
//如果流水线未进行刷新,且选通信号有效
if((flush == `DISABLE) && (as_ == `ENABLE_)) begin
//如果选择的总线从属不是一号,即SPM,则转移到下一个状态,即请求总线状态
if (s_index != `BUS_SLAVE_1) begin
state <= #1 `BUS_IF_STATE_REQ;
bus_req_ <= #1 `ENABLE_;
bus_addr <= #1 addr;
bus_rw <= #1 rw;
bus_wr_data <= #1 wr_data;
end
end
end
//总线请求状态
`BUS_IF_STATE_REQ : begin
//如果总线许可信号有效,则转移到总线访问状态,且将总线地址状态置为有效
if (bus_grnt_ == `ENABLE_) begin
state <= #1 `BUS_IF_STATE_ACCESS;
bus_as_ <= #1 `ENABLE_;
end
end
//总线访问状态
`BUS_IF_STATE_ACCESS : begin
//将总线地址选通置为无效
bus_as_ <= #1 `DISABLE_;
//如果总线已经准备就绪,则将信息传输出去
if (bus_rdy_ == `ENABLE_) begin
bus_req_ <= #1 `DISABLE_;
bus_addr <= #1 `WORD_ADDR_W'h0;
bus_rw <= #1 `READ;
bus_wr_data <= #1 `WORD_DATA_W'h0;
//保存读取到的数据
if (bus_rw == `READ) begin
rd_buf <= #1 bus_rd_data;
end
//检测是否存在延迟,如果存在延迟则进入延迟状态
if (stall == `ENABLE) begin
state <= #1 `BUS_IF_STATE_STALL;
end
//没有延迟则总线进入空闲
else begin
state <= #1 `BUS_IF_STATE_IDLE;
end
end
end
//延迟状态
`BUS_IF_STATE_STALL : begin
//如果延迟状态解除,则回到空闲状态
if (stall == `DISABLE) begin
state <= #1 `BUS_IF_STATE_IDLE;
end
end
endcase
end
endmodule
在本模块中,我们先根据输入的信息生成总线从属索引。然后默认将访问SPM,将数据信息送入SPM端口。
在内存访问部分,我们先将各个信号置为默认值,然后对总线状态进行检测。
如果总线状态为空闲,则查看流水线是否在刷新以及地址选通信号是否有效,如果流水线没有刷新且地址选通有效,则开始判断选择的总线从属是哪一个。如果选择的总线从属为1号,即SPM,则直接访问,然后检测流水线是否存在延迟,如果没有则将SPM选通信号置为有效,然后如果是读取操作则将读取到的信息送入CPU中;如果访问的是其他从属,由于总线当前为空闲状态,则直接将总线置为忙。
如果总线接口的状态为总线请求状态,则将总线置为忙。
如果总线接口的状态为总线使用状态,则对总线就绪信号进行检测,如果总线就绪且为读取状态,则将读取到的信息送入CPU中。如果总线没有就绪,则将总线继续置为忙,继续占用总线。
如果总线状态为延迟,则证明流水线存在延迟,由于当前为读取,总线的访问已经结束,则可以直接将读取的数据送入CPU端口,并将总线忙置为无效。
在总线控制部分,如果检测到为复位信号,则进行异步复位。否则进行总线接口状态检测。
如果当前总线接口为空闲状态,当流水线未处在刷新状态,CPU地址选通,且从属为非1号从属,则将所有信息输送到总线接口上,并将总线接口状态置为请求状态。
在总线请求状态中,如果总线返回许可,则将总线状态置为访问状态,并将总线地址选通置为有效。
在进入总线访问状态后,首先将总线地址选通置为无效,然后等待就绪信号。如果返回了就绪信号,则将所有数据传输到总线接口的缓冲区,并将总线接口复位。最后检查流水线是否存在延迟,如果没有,则返回空闲状态,否则进入延迟状态。
在延迟状态中,监测延迟状态,一旦取消延迟,则即刻返回空闲状态。
在本模块中,内存访问控制和总线接口控制为同时进行的。其中内存访问控制只要状态发生了转移即刻产生变化,而总线接口控制则需要等待时钟信号的控制。
意即,每一次状态的改变,都会引发两个部分的代码的执行。