数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)

写在前面的话

这个实践项目来源于夏宇闻老师的经典教材——《Verilog 数字系统设计教程》,也是我本科期间的专业教材之一,每次看到这个蓝色的封面都感到很亲切。而对于书中提及到的简化CPU,也是从大学开始就非常感兴趣的一个章节,虽然本科老师只是简单的带过,但是一直对书里提到的CPU结构以及最后使用CPU完成斐波那契数列计算的整个流程充满了兴趣。

这里也是怀揣着敬佩之心,对这个简化的RISC_CPU完成复刻,虽然整个项目是偏向教学目的,结构和功能也是非常简单,甚至在今天这种就业环境下没法写入到项目经历中去,但是在整个项目过程中能锻炼自己的Coding Style和设计技能。

项目难度:⭐⭐
项目推荐度:⭐⭐⭐
项目推荐天数:0.5~1天

夏宇闻老师的经典教材——《Verilog 数字系统设计教程》:
在这里插入图片描述

项目简介和学习目的

这个项目主要针对教学,是作为学习Verilog语法后的一个练手项目,更多的是了解CPU内部构成,练习Verilog语法和Coding Style,熟悉设计工具以及锻炼Debug能力。

项目实践环境:
前仿: Modelsim SE-64 2019.2
综合: Quartus (Quartus Prime 17.1) Standard Edition

项目学习目的:
(1)学习实践项目工程管理;
(2)熟悉Verilog HDL仿真和FPGA综合工具;
(3)学习RISC_CPU基本结构和基础原理;
(4)练习Verilog语法和验证方法;
(5)熟练掌握Modelsim。

CPU简介

CPU(Central Processing Unit),中文全称中央处理器,作为四大U之首(CPU/GPU/TPU/NPU),是计算机系统的运算和控制核心,也是当今数字系统中不可或缺的组成部分。CPU自诞生到如今发展超过50年,借助冯诺依曼体系,CPU掀起一股又一股的科技浪潮。RISC作为精简了指令集的CPU,除了指令更加简洁,还拥有简单合理的内部结构,从而提高了运算速度。

CPU工作的5个阶段:
(1)取指(IF,Instruction Fetch),将指令从存储器取出到指令寄存器。每取一条指令,程序计数器自加一。
(2)译指(ID,Instruction Decode),对取出的指令按照规定格式进行拆分和译码。
(3)执行(EX,Execute),执行具体指令操作。
(4)访问存储(MEM,Memory),根据指令访问存储、完成存储和读取。
(5)写回(WB,Write Back),将计算结果写回到存储器。

CPU内部关键结构:
(1)算术逻辑运算器(ALU);
(2)累加器;
(3)程序计数器;
(4)指令寄存器和译码器;
(5)时序和控制部件。

RISC_CPU内部结构和Verilog实现

本项目中的RISC_CPU一共有9个模块组成,具体如下:
(1)时钟发生器;
(2)指令寄存器;
(3)累加器;
(4)算术逻辑运算单元;
(5)数据控制器;
(6)状态控制器;
(7)主状态机;
(8)程序计数器;
(9)地址多路器。
在Modelsim中的电路图如下:
在这里插入图片描述

时钟发生器

模块图:
在这里插入图片描述

端口描述:
reset是高电平复位信号;
clk是外部时钟信号;
fetch是控制信号,是clk的八分频信号;fetch为高电平时,触发执行指令以及地址多路器输出指令地址和数据地址。
alu_ena是算术逻辑运算单元的使能信号。

仿真波形:
在这里插入图片描述
可以看到alu_ena提前fetch高电平一个clk周期,fetch是clk的8分频信号。

Verilog代码:
这里按照原文思路来复现。

// Description: RISC——CPU 时钟发生器
// -----------------------------------------------------------------------------
module clk_gen (
	input 			clk 		,    // Clock
	input 			reset 		, 	 // High level reset
	output	 reg 	fetch		,	 // 8 frequency division
	output 	 reg 	alu_ena 		 // Arithmetic enable
);

		reg [7:0] state;
		
		//One-piece state machine
		parameter S1 = 8'b0000_0001,
				  S2 = 8'b0000_0010,
				  S3 = 8'b0000_0100,
				  S4 = 8'b0000_1000,
				  S5 = 8'b0001_0000,
				  S6 = 8'b0010_0000,
				  S7 = 8'b0100_0000,
				  S8 = 8'b1000_0000,
				  idle = 8'b0000_0000;
		
		always@(posedge clk)begin
			if(reset)begin
					fetch <= 0;
					alu_ena <= 0;
					state <= idle;
			end
			else begin
					case(state)
						S1:
							begin 
								alu_ena <= 1;
								state <= S2;
							end
						S2:
							begin
								alu_ena <= 0;
								state <= S3;
							end
						S3:
							begin
								fetch <= 1;
								state <=S4;
							end
						S4:
							begin
								state <= S5;
							end
						S5:
							state <= S6;
						S6:
							state <= S7;
						S7:
							begin
								fetch <= 0;
								state <= S8;
							end
						S8:
							begin
								state <= S1;
							end
						idle: state <= S1;
						default: state <=idle;
					endcase
			end
		end
endmodule

指令寄存器

模块图:
在这里插入图片描述
端口描述:
寄存器是将数据总线送来的指令存入高8位或低8位寄存器中。
ena信号用来控制是否寄存。
每条指令为两个字节,16位,高3位是操作码,低13位是地址(CPU地址总线为13位,寻址空间为8K字节)。
本设计的数据总线为8位,每条指令需要取两次,先取高8位,再取低8位。

Verilog代码:

// Description: RISC—CPU 指令寄存器 
// -----------------------------------------------------------------------------
module register (
	input 	[7:0]		data 		,
	input 				clk 		,
	input 				rst 		,
	input 				ena 		,
	output reg [15:0]	opc_iraddr
	
);

	reg state 	;
			//
		 always@( posedge clk ) begin
			if( rst ) begin
					opc_iraddr <= 16'b 0000_0000_0000_0000;
					state <= 1'b 0;
			end // if rst
    
    // If load_ir from machine actived, load instruction data from rom in 2 clock periods.
    // Load high 8 bits first, and then low 8 bits.
			else if( ena ) begin
					case( state )
					1'b0    : begin opc_iraddr [ 15 : 8 ] <= data;                    
									state <= 1; 
							  end
					1'b1    : begin opc_iraddr [  7 : 0 ] <= data;                    
									state <= 0; 
							  end
					default : begin opc_iraddr [ 15 : 0 ] <= 16'bxxxx_xxxx_xxxx_xxxx; 
									state <= 1'bx; 
							  end
					endcase // state
			end // else if ena
      
			else state <= 1'b0;
		end 

endmodule  

累加器

模块图:
在这里插入图片描述
端口描述:
累加器用于存放当前结果,ena信号有效时,在clk上升沿输出数据总线的数据。

Verilog代码:

// Description: RISC-CPU  累加器模块
// -----------------------------------------------------------------------------
module accum (
	input 				clk 	,   // Clock
	input 				ena 	, 	// Enable
	input  				rst 	,   // Asynchronous reset active high
	input [7:0]			data 	,	// Data bus
	output reg [7:0] 	accum 
	
);

		
		always@(posedge clk)begin
			if(rst)
					accum <= 8'b0000_0000;//Reset
			else if(ena)
					accum <= data;
			end

endmodule 

算术运算器

模块图:
在这里插入图片描述
端口描述:
算术逻辑运算单元可以根据输入的操作码分别实现相应的加、与、异或、跳转等基本操作运算。
本单元支持8种操作运算。
opcode用来选择计算模式
data是数据输入
accum是累加器输出
alu_ena是模块使能信号
clk是系统时钟

⭐这里在做前仿真时遇见一个错误,将代码改动了一下⭐
Verilog代码:

// Description: RISC-CPU 算术运算器
// -----------------------------------------------------------------------------
module alu (
	input 				clk 		,    	// Clock
	input 				alu_ena		, 		// Enable
	input 	[2:0] 		opcode 		,  		// High three bits are used as opcodes
	input 	[7:0]		data		,		// data
	input 	[7:0]		accum 		,		// accum out
	output reg [7:0]	alu_out 	,
	output 				zero	 
);

parameter 
			HLT 	=	3'b000	,
			SKZ 	=	3'b001 	,
			ADD 	=	3'b010	,
			ANDD 	=	3'b011	,
			XORR 	=	3'b100 	,
			LDA 	=	3'b101	,
			STO 	=	3'b110	,
			JMP 	=	3'b111	;
                                  
    always @(posedge alu_ena) begin 

    		casex(opcode)
    			HLT: 	alu_out		<=	accum 			;
    			SKZ: 	alu_out 	<=	accum			;
    			ADD: 	alu_out 	<=  data + accum 	;
    			ANDD:	alu_out		<= 	data & accum 	;
    			XORR: 	alu_out 	<=	data ^ accum 	;
    			LDA : 	alu_out 	<= 	data 			;
    			STO : 	alu_out 	<=	accum 			;
    			JMP : 	alu_out		<=	accum			;
    			default: alu_out 	<=	8'bxxxx_xxxx	;
    		endcase

    end

    assign zero = !accum;

endmodule 

数据控制器

模块图:
在这里插入图片描述
端口描述:
数据控制器的作用是控制累加器的数据输出,数据总线是分时复用的,会根据当前状态传输指令或者数据。
数据只在往RAM区或者端口写时才允许输出,否则呈现高阻态。
in是8bit数据输入
data_ena是使能信号
data是8bit数据输出

Verilog代码:

// Description: RISC-CPU 数据控制器
// -----------------------------------------------------------------------------
module datactl (
	input 	[7:0]		in 			,    	// Data input
	input 				data_ena 	, 		// Data Enable
	output 	wire [7:0]	data  				// Data output
	
);

assign  data = (data_ena )? in: 8'bzzzz_zzzz 	;

endmodule 

地址多路器

模块图:
在这里插入图片描述
端口描述:

用于选择输出地址是PC(程序计数)地址还是数据/端口地址。每个指令周期的前4个时钟周期用于从ROM种读取指令,输出的是PC地址;后四个时钟周期用于对RAM或端口读写。
地址多路器和数据控制器实现的功能十分相似。
fetch信号用来控制地址输出,高电平输出pc_addr ,低电平输出ir_addr ;
pc_addr 指令地址;
ir_addr ram或端口地址。

Verilog代码:

// Description: RISC-CPU 地址多路器
// -----------------------------------------------------------------------------
module adr (
	input 				fetch 		,   // enable
	input [12:0]		ir_addr 	, 	// 	
	input [12:0] 		pc_addr 	,  	// 	
	output wire [12:0]	addr 	
);


assign 	addr = fetch? pc_addr :ir_addr	;

endmodule 

程序计数器

模块图:
在这里插入图片描述
端口描述:
程序计数器用来提供指令地址,指令按照地址顺序存放在存储器中。包含两种生成途径:
(1)顺序执行的情况
(2)需要改变顺序,例如JMP指令
rst复位信号,高电平时地址清零;
clock 时钟信号,系统时钟;
ir_addr目标地址,当加载信号有效时输出此地址;
pc_addr程序计数器地址
load地址装载信号

Verilog代码:

// Description: RISC-CPU 程序计数器
// -----------------------------------------------------------------------------
module counter (
	input [12:0]		ir_addr  	,    	// program address
	input 				load  		, 		// Load up signal
	input 				clock		,		// CLock
	input 				rst 		,		// Reset
	output	reg [12:0]	pc_addr				// insert program address
);

		always@(posedge clock or posedge rst) begin
				if(rst)
					pc_addr <= 13'b0_0000_0000_0000;
				else if(load)
						pc_addr <= ir_addr;
				else
						pc_addr <= pc_addr + 1;
		end


endmodule 

状态控制器

模块图:
在这里插入图片描述

端口描述:
状态控制器接收复位信号rst,rst有效,控制输出ena为0,fetch有效控制ena为1。

Verilog代码:

// Description: RISC-CPU 状态控制器
// -----------------------------------------------------------------------------
module machinectl (
	input clk 			,    	// Clock
	input rst 			, 		// Asynchronous reset
	input fetch 		,  		// Asynchronous reset active low
	output reg ena 				// Enable 
	
);

		always@(posedge clk)begin
				if(rst)
						ena <= 0;
				else if(fetch)
						ena <=1;
		end

endmodule 

主状态机

模块图:
在这里插入图片描述
端口描述:
主状态机是CPU的控制核心,用于产生一系列控制信号。
指令周期由8个时钟周期组成,每个时钟周期都要完成固定的操作。
(1)第0个时钟,CPU状态控制器的输出rd和load_ir 为高电平,其余为低电平。指令寄存器寄存由ROM送来的高8位指令代码。
(2)第1个时钟,与上一个时钟相比只是inc_pc从0变为1,故PC增1,ROM送来低8位指令代码,指令寄存器寄存该8位指令代码。
(3)第2个时钟,空操作。
(4)第3个时钟,PC增1,指向下一条指令。
操作符为HLT,输出信号HLT为高。
操作符不为HLT,除PC增1外,其余控制线输出为0.
(5)第4个时钟,操作。
操作符为AND,ADD,XOR或LDA,读取相应地址的数据;
操作符为JMP,将目的地址送给程序计数器;
操作符为STO,输出累加器数据。
(6)第5个时钟,若操作符为ANDD,ADD或者XORR,算术运算器完成相应的计算;
操作符为LDA,就把数据通过算术运算器送给累加器;
操作符为SKZ,先判断累加器的值是否为0,若为0,PC加1,否则保持原值;
操作符为JMP,锁存目的地址;
操作符为STO,将数据写入地址处。
(7)第6个时钟,空操作。
(8)第7个时钟,若操作符为SKZ且累加器为0,则PC值再加1,跳过一条指令,否则PC无变化。

Verilog代码:

// Description: RISC-CPU 主状态机
// -----------------------------------------------------------------------------
module machine (
	input 		clk 			,    	// Clock
	input  		ena				, 		// Clock Enable
	input 		zero			,  		// Asynchronous reset active low
	input [2:0]	opcode 			,		// OP code
	output 	reg inc_pc 			,		//
	output  reg load_acc		, 		//	
	output	reg	load_pc 		, 		//
	output 	reg rd 				,		//
	output 	reg wr 				, 		//
	output 	reg load_ir 		, 		//
	output 	reg datactl_ena 	, 		//
	output  reg halt 			
);

	reg  [2:0] state  ;
//parameter 
		parameter 
					HLT 	= 3'b000	,
					SKZ 	= 3'b001	,
					ADD 	= 3'b010	,
					ANDD 	= 3'b011	,
					XORR 	= 3'b100	,
					LDA 	= 3'b101	,
					STO 	= 3'b110	,
					JMP 	= 3'b111	;
		always@(negedge clk) begin
				if(!ena)  //收到复位信号rst,进行复位操作
					begin
						state <= 3'b000;
						{inc_pc,load_acc,load_pc,rd} <= 4'b0000;
						{wr,load_ir,datactl_ena,halt} <= 4'b0000;
					end
				else
					ctl_cycle;
		end

			//------- task ctl_cycle -------
			
			task ctl_cycle;
				begin
					casex(state)
						3'b000:   //load high 8bits in struction
								begin	
									{inc_pc,load_acc,load_pc,rd} <= 4'b0001;
									{wr,load_ir,datactl_ena,halt} <= 4'b0100;
									state <= 3'b001;
								end
						3'b001://pc increased by one then load low 8bits instruction
								begin
									{inc_pc,load_acc,load_pc,rd} <= 4'b1001;
									{wr,load_ir,datactl_ena,halt} <= 4'b0100;
									state <= 3'b010;
								end
						3'b010: //idle
								begin
									{inc_pc,load_acc,load_pc,rd} <= 4'b0000;
									{wr,load_ir,datactl_ena,halt} <= 4'b0000;
									state <= 3'b011;
								end
						3'b011:  //next instruction address setup 分析指令开始点
								begin
									if(opcode == HLT)//指令为暂停HLT
										begin
											{inc_pc,load_acc,load_pc,rd} <= 4'b1000;
											{wr,load_ir,datactl_ena,halt} <= 4'b0001;
										end
									else
										begin
											{inc_pc,load_acc,load_pc,rd} <= 4'b1000;
											{wr,load_ir,datactl_ena,halt} <= 4'b0000;
										end
								state <= 3'b100;
								end
						3'b100: //fetch oprand
								begin
									if(opcode == JMP)
										begin
											{inc_pc,load_acc,load_pc,rd} <= 4'b0010;
											{wr,load_ir,datactl_ena,halt} <= 4'b0000;
										end
									else if(opcode == ADD || opcode == ANDD || opcode == XORR || opcode == LDA)
											begin
												{inc_pc,load_acc,load_pc,rd} <= 4'b0001;
												{wr,load_ir,datactl_ena,halt} <= 4'b0000;
											
											end
									else if(opcode == STO)
											begin
												{inc_pc,load_acc,load_pc,rd} <= 4'b0000;
												{wr,load_ir,datactl_ena,halt} <= 4'b0010;	
											end
									else	
										begin
												{inc_pc,load_acc,load_pc,rd} <= 4'b0000;
												{wr,load_ir,datactl_ena,halt} <= 4'b0000;	
										end
									state <= 3'b101;								
								end
						3'b101://operation
								begin
									if(opcode == ADD || opcode == ANDD ||opcode ==XORR ||opcode == LDA)//过一个时钟后与累加器的内存进行运算
										begin
												{inc_pc,load_acc,load_pc,rd} <= 4'b0101;
												{wr,load_ir,datactl_ena,halt} <= 4'b0000;	
										end
									else if(opcode == SKZ && zero == 1)// & and &&
											begin
												{inc_pc,load_acc,load_pc,rd} <= 4'b1000;
												{wr,load_ir,datactl_ena,halt} <= 4'b0000;	
											end
										else if(opcode == JMP)
												begin
													{inc_pc,load_acc,load_pc,rd} <= 4'b1010;
													{wr,load_ir,datactl_ena,halt} <= 4'b0000;	
												end
										else if(opcode == STO)
													begin//过一个时钟后吧wr变为1,写到RAM中
														{inc_pc,load_acc,load_pc,rd} <= 4'b0000;
														{wr,load_ir,datactl_ena,halt} <= 4'b1010;	
													end
										else	
												begin
														{inc_pc,load_acc,load_pc,rd} <= 4'b0000;
														{wr,load_ir,datactl_ena,halt} <= 4'b0000;	
												end
										state <= 3'b110;
								end
						3'b110:
									begin
										if(opcode == STO)
											begin
												{inc_pc,load_acc,load_pc,rd} <= 4'b0000;
												{wr,load_ir,datactl_ena,halt} <= 4'b0010;	
											end
										else if(opcode == ADD || opcode == ANDD || opcode == XORR || opcode == LDA)
												begin
													{inc_pc,load_acc,load_pc,rd} <= 4'b0001;
													{wr,load_ir,datactl_ena,halt} <= 4'b0000;	
												end
										else
												begin
													{inc_pc,load_acc,load_pc,rd} <= 4'b0000;
													{wr,load_ir,datactl_ena,halt} <= 4'b0000;	
												end
									state <= 3'b111;
									end
						3'b111:
								begin
									if(opcode == SKZ && zero == 1)
										begin
											{inc_pc,load_acc,load_pc,rd} <= 4'b1000;
											{wr,load_ir,datactl_ena,halt} <= 4'b0000;	
										end
									else
										begin
											{inc_pc,load_acc,load_pc,rd} <= 4'b0000;
											{wr,load_ir,datactl_ena,halt} <= 4'b0000;	
										end
								state <= 3'b000;
								end
						default:
									begin
										{inc_pc,load_acc,load_pc,rd} <= 4'b0000;
										{wr,load_ir,datactl_ena,halt} <= 4'b0000;	
										state <= 3'b000;
									end
					endcase
				end
			endtask

endmodule 

外围模块

为了对RISC-CPU进行测试,需要对ROM、RAM和地址译码器进行设计。

地址译码器

模块说明:
地址译码器用于产生选通信号,选通ROM或者RAM
1FFFH —— 1800H RAM
17FFH —— 0000H ROM

Verilog代码:

// Description: RISC-CPU 地址译码器
// -----------------------------------------------------------------------------
module addr_decode (
	input [12:0]	addr 	,   // Address
	output reg 		ram_sel ,	// Ram sel
	output reg 		rom_sel 	// Rom sel
);


		always@(addr)begin
			casex(addr)
					13'b1_1xxx_xxxx_xxxx:{rom_sel,ram_sel} <= 2'b01;
					13'b0_xxxx_xxxx_xxxx:{rom_sel,ram_sel} <= 2'b10;
					13'b1_0xxx_xxxx_xxxx:{rom_sel,ram_sel} <= 2'b10;
					default: {rom_sel,ram_sel} <= 2'b00;
			endcase
					
		end

endmodule 

RAM

模块说明:
RAM用于存放临时数据,可读可写。

Verilog代码:

// Description: RISC-CPU RAM模块
// -----------------------------------------------------------------------------
module ram (
	input 				ena  		,    		// Enable
	input 				read		, 			// read Enable
	input 				write		,  			// write Enable
	inout wire [7:0]	data 		,			// data
	input [9:0]			addr 					// address
);

	reg [7:0]	ram [10'h3ff:0]	;
	
		assign data = (read && ena )? ram[addr]:8'h zz;
		
		always@(posedge write) begin
				ram[addr] <= data;
		end
endmodule 

ROM

模块说明:
RAM用于存放只读数据。

Verilog代码:

// Description: RISC-CPU ROM模块
// -----------------------------------------------------------------------------
module rom (
	input 	[12:0]		addr 	,
	input 				read 	,
	input 				ena 	,
	output wire [7:0]	data 
);
		reg [7:0] memory [13'h1ff:0];
		assign data = (read && ena)? memory[addr]:8'b zzzz_zzzz;

endmodule 

顶层模块

模块图:
在这里插入图片描述

Verilog代码:

// Description: RISC-CPU 顶层模块
// -----------------------------------------------------------------------------
//`include "clk_gen.v"
//`include "accum.v"
//`include "adr.v"
//`include "alu.v"
//`include "machine.v"
//`include "counter.v"
//`include "machinectl.v"
//`iclude "machine.v"
//`include "register.v"
//`include "datactl.v"
module RISC_CPU (
		input 				clk 	,
		input 				reset 	,
		output 	wire 		rd 		,
		output 	wire 		wr 		,
		output 	wire 		halt 	,
		output 	wire 	 	fetch 	,
		//addr
		output 	wire [12:0]	addr 	,
		output 	wire [12:0]	ir_addr ,
		output 	wire [12:0]	pc_addr ,
		inout 	wire [7:0]	data 	,
		//op
		output 	wire [2:0]	opcode 	
);

		wire [7:0] alu_out 	; 
		wire [7:0] accum	;

		wire 	 	zero 		;
		wire 		inc_pc		;
		wire		load_acc	;
		wire 		load_pc		;
		wire		load_ir		;
		wire		data_ena	;
		wire 		contr_ena	;
		wire 		alu_ena		;

//inst

		clk_gen mclk_gen(
			.clk 		(clk 		),
			.reset 		(reset 		),
			.fetch 		(fetch		),
			.alu_ena 	(alu_ena	)
			);
			
		register m_register(
				.data 		(data 				),
				.ena 		(load_ir 			),
				.rst 		(reset 				),
				.clk 		(clk 				),
				.opc_iraddr ({opcode,ir_addr}	)
				);

		accum m_accum(
				.data  	(alu_out		),
				.ena 	(load_acc 		),
				.clk 	(clk 			),
				.rst 	(reset   		),
				.accum 	(accum 			)
				);

		alu m_alu(
			.data 		(data 		),
			.accum 		(accum 		),
			.clk 		(clk 		),
			.alu_ena 	(alu_ena 	),
			.opcode 	(opcode 	),
			.alu_out 	(alu_out 	),
			.zero 		(zero 		)
			);

		machinectl m_machinectl(
			.clk 		(clk 		),
			.rst 		(reset 		),
			.fetch 		(fetch 		),
			.ena 		(contr_ena 	)
			);

		machine m_machine(
			.inc_pc 	(inc_pc 		),
			.load_acc 	(load_acc 		),
			.load_pc 	(load_pc 		),
			.rd 		(rd 			),
			.wr 		(wr 			),
			.load_ir 	(load_ir 		),
			.clk 		(clk 			),
			.datactl_ena(data_ena 		),
			.halt 		(halt 			),
			.zero 		(zero 			),
			.ena 		(contr_ena 		),
			.opcode	 	(opcode 		)
			);

		datactl m_datactl(
			.in 		(alu_out 		),
			.data_ena 	(data_ena 		),
			.data 		(data 			)
			);

		adr m_adr(
			.fetch  	(fetch 		),
			.ir_addr 	(ir_addr 	),
			.pc_addr 	(pc_addr 	),
			.addr 		(addr 		)
			);

		counter m_counter(
			.clock 		(inc_pc 	),
			.rst 		(reset 		),
			.ir_addr 	(ir_addr 	),
			.load 		(load_pc 	),
			.pc_addr 	(pc_addr 	)
			);

endmodule 

Testbench

Testbench包含三个测试程序,这个部分不能综合。

Test1程序

TEST1程序用于验证RISC-CPU的逻辑功能,根据汇编语言由人工编译的。
若各条指令正确,应该在地址2E(hex)处,在执行HLT时刻停止。若程序在任何其他位置停止,则必有一条指令运行错误,可以按照注释找到错误的指令。

test1汇编程序:

//机器码
@00
//address statement
111_0000     //00 BEGIN: JMP TST_JMP
0011_1100
000_0000	//02 HLT //JMP did not work
0000_0000
000_00000	//04	HLT //JMP did not load PC skiped
0000_0000	
101_1100   //06 JMP_OK: LDA DATA
0000_0000
001_00000  //08 SKZ
0000_0000
000_0000	//0a HLT
0000_0000
101_11000	//0C LDA DATA_2
0000_0001
001_00000	//0E SKZ
0000_0000
111_0000	//10	JMP SKZ_OK
001_0100
000_0000	//12	HLT
0000_0000
110_11000	//14	SKZ_OK: STO TEMP
0000_0010
101_11000	//16	LDA DATA_1
0000_0000
110_11000	//18	STO TEMP
0000_0010
101_11000	//1A LDA TEMP
0000_0010
001_00000	//1C SKZ
0000_0000
000_00000	//1E HLT
0000_0000
100_11000	//20 XOR DATA_2
0000_0001
001_00000 	//22	SKZ
0000_0000
111_00000	//24 	JMP XOR_OK
0010_1000
000_00000	//26 HLT
0000_0000
100_11000	//28	XOR_OK XOR DATA_2
0000_0001
001_00000	//2A	SKZ
0000_0000
000_00000	//2C HLT
0000_0000
000_0000	//2E END
0000_0000
111_00000	//30	JMP BEGIN
0000_0000

@3c
111_00000 //3c TST_JMP IMR OK
0000_0110
000_00000	//3E HLT


test1数据文件:

/-----------------------------------
@00		///address statement at RAM
00000000	//1800  DATA_1
11111111	//1801 DATA_2
10101010	//1082	TEMP

Test2程序

TEST1程序用于验证RISC-CPU的逻辑功能,根据汇编语言由人工编译的。
这个程序是用来测试RISC-CPU的高级指令集,若执行正确,应在地址20(hex)处在执行HLT时停止。

test2汇编程序:

@00
101_11000	//00	BEGIN
0000_0001
011_11000	//02	AND DATA_3
0000_0010
100_11000	//04	XOR DATA_2
0000_0001	
001_00000	//06	SKZ
0000_0000
000_00000	//08 HLT
0000_0000
010_11000	//0A	ADD DATA_1
0000_0000
001_00000	//0C	SKZ
0000_0000
111_00000	//0E	JMP	ADD_OK
0001_0010
111_00000	//10	HLT
0000_0000
100_11000	//12	ADD_OK XOR DATA_3
0000_0010
010_11000	//14	ADD DATA_1
0000_0000
110_11000	//16	STO TEMP
0000_0011	
101_11000	//18	LDA DATA_1
0000_0000
010_11000	//1A	ADD TEMP
0000_0001
001_00000	//1C	SKZ
0000_0000
000_00000	//1E	HLT
0000_0000
000_00000	//END	HLT
0000_0000
111_00000	//JMP BEGIN
0000_0000

test2数据文件:

@00
00000001	//1800	DATA_1
10101010	//1801	DATA_2
11111111	//1802	DATA_3
00000000	//1803	TEMP

Test3程序

TEST3程序是一个计算0~144的斐波那契数列的程序,用来验证CPU整体功能。

test3汇编程序:

@00
101_11000	//00	LOOP:LDA FN2
0000_0001
110_11000	//02	STO TEMP
0000_0010
010_11000	//04	ADD	FN1
0000_0000
110_11000	//06	STO FN2
0000_0001
101_11000	//08	VLDA TEMP
0000_0010
110_11000	//0A	STO	FN1
0000_0000
100_11000	//0C	XOR	LIMIT
0000_0011
001_00000	//0E	SKZ
0000_0000
111_00000	//10	JMP	LOOP
0000_0000
000_00000	//12	DONE HLT
0000_0000

test3数据文件:

@00
00000001		//1800	FN1
00000000		//1801	FN2
00000000		//1802	TEMP
10010000		//1803	LIMIT

完整的testbench

Verilog代码:

// Description: RISC-CPU 测试程序
// -----------------------------------------------------------------------------
`include "RISC_CPU.v"
`include "ram.v"
`include "rom.v"
`include "addr_decode.v"

`timescale 1ns/1ns

`define PERIOD 100 // matches clk_gen.v

module cputop;
  reg [( 3 * 8 ): 0 ] mnemonic; // array that holds 3 8 bits ASCII characters
  reg  [ 12 : 0 ] PC_addr, IR_addr;
  reg  reset_req, clock;
  wire [ 12 : 0 ] ir_addr, pc_addr; // for post simulation.
  wire [ 12 : 0 ] addr;
  wire [  7 : 0 ] data;
  wire [  2 : 0 ] opcode;           // for post simulation.
  wire fetch;                       // for post simulation.
  wire rd, wr, halt, ram_sel, rom_sel;
  integer test;
  
  //-----------------DIGITAL LOGIC----------------------
  cpu t_cpu (.clk( clock ),.reset( reset_req ),.halt( halt ),.rd( rd ),.wr( wr ),.addr( addr ),.data( data ),.opcode( opcode ),.fetch( fetch ),.ir_addr( ir_addr ),.pc_addr( pc_addr ));
  ram t_ram (.addr ( addr [ 9 : 0 ]),.read ( rd ),.write ( wr ),.ena ( ram_sel ),.data ( data ));
  rom t_rom (.addr ( addr          ),.read ( rd ),              .ena ( rom_sel ),.data ( data ));
  addr_decoder t_addr_decoder (.addr( addr ),.ram_sel( ram_sel ),.rom_sel( rom_sel ));
  
  //-------------------SIMULATION-------------------------
  initial begin
    clock = 0;
    // display time in nanoseconds
    $timeformat ( -9, 1, "ns", 12 );
    display_debug_message;
    sys_reset;
    test1; $stop;
    test2; $stop;
    test3;
    $finish; // simulation is finished here.
  end // initial
  
  task display_debug_message;
    begin
      $display ("\n************************************************"  );
      $display (  "* THE FOLLOWING DEBUG TASK ARE AVAILABLE:      *"  );
      $display (  "* \"test1;\" to load the 1st diagnostic program. *");
      $display (  "* \"test2;\" to load the 2nd diagnostic program. *");
      $display (  "* \"test3;\" to load the     Fibonacci  program. *");
      $display (  "************************************************\n");
    end
  endtask // display_debug_message
  
  task test1;
    begin
      test = 0;
      disable MONITOR;
      $readmemb ("test1.pro", t_rom.memory );
      $display ("rom loaded successfully!");
      $readmemb ("test1.dat", t_ram.ram );
      $display ("ram loaded successfully!");
      #1 test = 1;
      #14800;
      sys_reset;
    end
  endtask // test1
  
  task test2;
    begin
      test = 0;
      disable MONITOR;
      $readmemb ("test2.pro", t_rom.memory );
      $display ("rom loaded successfully!");
      $readmemb ("test2.dat", t_ram.ram );
      $display ("ram loaded successfully!");
      #1 test = 2;
      #11600;
      sys_reset;
    end
  endtask // test2
  
  task test3;
    begin
      test = 0;
      disable MONITOR;
      $readmemb ("test3.pro", t_rom.memory );
      $display ("rom loaded successfully!");
      $readmemb ("test3.dat", t_ram.ram );
      $display ("ram loaded successfully!");
      #1 test = 3;
      #94000;
      sys_reset;
    end
  endtask // test1
  
  task sys_reset;
    begin
      reset_req = 0;
      #( `PERIOD * 0.7 ) reset_req = 1;
      #( 1.5 * `PERIOD ) reset_req = 0;
    end
  endtask // sys_reset
  
  //--------------------------MONITOR--------------------------------
  always@( test ) begin: MONITOR
    case( test )
      1: begin // display results when running test 1
        $display("\n*** RUNNING CPU test 1 - The Basic CPU Diagnostic Program ***");
        $display("\n        TIME      PC      INSTR      ADDR      DATA          ");
        $display("         ------    ----    -------    ------    ------         ");
        while( test == 1 )@( t_cpu.pc_addr ) begin // fixed
          if(( t_cpu.pc_addr % 2 == 1 )&&( t_cpu.fetch == 1 )) begin // fixed
            #60  PC_addr <= t_cpu.pc_addr - 1;
                 IR_addr <= t_cpu.ir_addr;
            #340 $strobe("%t %h %s %h %h", $time, PC_addr, mnemonic, IR_addr, data ); // Here data has been changed t_cpu.m_register.data
          end // if t_cpu.pc_addr % 2 == 1 && t_cpu.fetch == 1
        end // while test == 1 @ t_cpu.pc_addr
      end
        
      2: begin // display results when running test 2
        $display("\n*** RUNNING CPU test 2 - The Basic CPU Diagnostic Program ***");
        $display("\n        TIME      PC      INSTR      ADDR      DATA          ");
        $display("         ------    ----    -------    ------    ------         ");
        while( test == 2 )@( t_cpu.pc_addr ) begin // fixed
          if(( t_cpu.pc_addr % 2 == 1 )&&( t_cpu.fetch == 1 )) begin // fixed
            #60  PC_addr <= t_cpu.pc_addr - 1;
                 IR_addr <= t_cpu.ir_addr;
            #340 $strobe("%t %h %s %h %h", $time, PC_addr, mnemonic, IR_addr, data ); // Here data has been changed t_cpu.m_register.data
          end // if t_cpu.pc_addr % 2 == 1 && t_cpu.fetch == 1
        end // while test == 2 @ t_cpu.pc_addr
      end
        
      3: begin // display results when running test 3
        $display("\n*** RUNNING CPU test 3 - An Executable Program **************");
        $display("***** This program should calculate the fibonacci *************");
        $display("\n        TIME      FIBONACCI NUMBER          ");
        $display("         ------    -----------------_         ");
        while( test == 3 ) begin
          wait( t_cpu.opcode == 3'h 1 ) // display Fib. No. at end of program loop
          $strobe("%t     %d", $time, t_ram.ram [ 10'h 2 ]);
          wait( t_cpu.opcode != 3'h 1 );
        end // while test == 3
      end
    endcase // test
  end // MONITOR: always@ test
  
  //-------------------------HALT-------------------------------
  always@( posedge halt ) begin // STOP when HALT intruction decoded
    #500 $display("\n******************************************");
         $display(  "** A HALT INSTRUCTION WAS PROCESSED !!! **");
         $display(  "******************************************");
  end // always@ posedge halt
  
  //-----------------------CLOCK & MNEMONIC-------------------------
  always#(`PERIOD / 2 ) clock = ~ clock;
  
  always@( t_cpu.opcode ) begin // get an ASCII mnemonic for each opcode
    case( t_cpu.opcode )
      3'b 000 : mnemonic = "HLT";
      3'b 001 : mnemonic = "SKZ";
      3'b 010 : mnemonic = "ADD";
      3'b 011 : mnemonic = "AND";
      3'b 100 : mnemonic = "XOR";
      3'b 101 : mnemonic = "LDA";
      3'b 110 : mnemonic = "STO";
      3'b 111 : mnemonic = "JMP";
      default : mnemonic = "???";
    endcase 
  end 
endmodule 

Modelsim前仿

对所有代码进行编译仿真
在这里插入图片描述

test1程序仿真结果
在这里插入图片描述
test2程序仿真结果
在这里插入图片描述
test3程序仿真结果
在这里插入图片描述

Quartus综合结果

使用Quartus (Quartus Prime 17.1) Standard Edition对RTL进行综合,对综合后的资源占用和电路图进行检查。
RTL图
在这里插入图片描述
FSM图
在这里插入图片描述
chip plan图
蓝色为占用部分
在这里插入图片描述

资源占用
在这里插入图片描述

总结

至此,整个练手项目完成,从完成度和难度来讲,这个小项目更加偏向于教学练习,CPU也是数字IC的重要研究方向,对此感兴趣的同学可以找点论文和开源资料进行学习。之所以把这个项目放到第一来讲,是因为不要小瞧这个项目,虽然看上去简单,但是对工程文件的管理以及项目实践的习惯非常重要,希望大家都能培养一个良好的工程习惯,书本上的代码也有一点问题,这里贴上的并不是最优解,只是带着大家走了一个简单的流程,最后综合的工具也是FPGA相关的,并没有使用DC等数字IC专业的EDA软件,后续有时间会把这个地方进行补齐。

  • 40
    点赞
  • 200
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
夏宇是一种用于数字电路设计的硬件描述语言,常用于VLSI芯片设计FPGA开发。 Verilog(又称为Verilog HDL)是硬件描述语言(HDL)的一种,用于描述数字电路和系统级硬件。它是一种结构化的语言,可以用于模拟、验证和综合电路设计。 夏宇闻Verilog可能指的是夏宇对Verilog语言的学习和了解。了解Verilog语言可以让夏宇更深入地理解数字电路设计的原理和方法,并能够将其应用于实际的工程项目。 学习Verilog语言需要理解其语法规则和基本概念,如模块、端口、信号、赋值语句等。夏宇可以通过阅读Verilog语言的教材、参加培训课程或是进行在线学习来学习Verilog语言。 掌握Verilog语言后,夏宇可以使用Verilog编写数字电路的描述,包括逻辑门、时序电路和处理器等。夏宇可以通过Verilog语言模拟和验证电路设计的功能和性能,在设计阶段发现和解决问题,提高设计的正确性和可靠性。 夏宇还可以使用Verilog语言将电路设计综合到特定的目标芯片或FPGA平台上。通过综合,夏宇可以将Verilog描述的电路转化为底层硬件的配置和布局,从而实现电路的物理实现。 总之,夏宇闻Verilog是指夏宇正在学习和了解Verilog语言,并且希望能够利用Verilog语言进行数字电路设计、模拟验证和综合实现。掌握Verilog语言可以提升夏宇在数字电路设计领域的专业能力和实践经验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值