《CPU自制入门》笔记——通用寄存器、SPM、总线接口

1. 通用寄存器

首先,我们为我们的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

在本部分中,我们的通用寄存器主要有三个功能

  1. 读取访问
    我们通过两行assign对数据进行读取,当we_信号有效,且没有进行对当前寄存器进行写入操作时,会输出当前寄存器中的数据,否则,会将当前寄存器写入的数据直接输出

  2. 异步复位
    该寄存器堆使用异步复位信号,当复位信号低电平有效时,对寄存器进行复位清零

  3. 写入控制
    当we_信号有效时,允许向指定的寄存器内写入数据

2. SPM

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

3. 总线接口

在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号从属,则将所有信息输送到总线接口上,并将总线接口状态置为请求状态。
在总线请求状态中,如果总线返回许可,则将总线状态置为访问状态,并将总线地址选通置为有效。
在进入总线访问状态后,首先将总线地址选通置为无效,然后等待就绪信号。如果返回了就绪信号,则将所有数据传输到总线接口的缓冲区,并将总线接口复位。最后检查流水线是否存在延迟,如果没有,则返回空闲状态,否则进入延迟状态。
在延迟状态中,监测延迟状态,一旦取消延迟,则即刻返回空闲状态。
在本模块中,内存访问控制和总线接口控制为同时进行的。其中内存访问控制只要状态发生了转移即刻产生变化,而总线接口控制则需要等待时钟信号的控制。
意即,每一次状态的改变,都会引发两个部分的代码的执行。

你可能感兴趣的:(CPU自制入门)