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

数字IC实践项目(1)——简化的RISC_CPU设计

  • 写在前面的话
  • 项目简介和学习目的
  • CPU简介
  • RISC_CPU内部结构和Verilog实现
    • 时钟发生器
    • 指令寄存器
    • 累加器
    • 算术运算器
    • 数据控制器
    • 地址多路器
    • 程序计数器
    • 状态控制器
    • 主状态机
    • 外围模块
      • 地址译码器
      • RAM
      • ROM
    • 顶层模块
    • Testbench
      • Test1程序
      • Test2程序
      • Test3程序
      • 完整的testbench
  • Modelsim前仿
  • Quartus综合结果
  • 总结

写在前面的话

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

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

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

夏宇闻老师的经典教材——《Verilog 数字系统设计教程》:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第1张图片

项目简介和学习目的

这个项目主要针对教学,是作为学习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中的电路图如下:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第2张图片

时钟发生器

模块图:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第3张图片

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

仿真波形:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第4张图片
可以看到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

指令寄存器

模块图:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第5张图片
端口描述:
寄存器是将数据总线送来的指令存入高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  

累加器

模块图:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第6张图片
端口描述:
累加器用于存放当前结果,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 

算术运算器

模块图:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第7张图片
端口描述:
算术逻辑运算单元可以根据输入的操作码分别实现相应的加、与、异或、跳转等基本操作运算。
本单元支持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 

数据控制器

模块图:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第8张图片
端口描述:
数据控制器的作用是控制累加器的数据输出,数据总线是分时复用的,会根据当前状态传输指令或者数据。
数据只在往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 

地址多路器

模块图:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第9张图片
端口描述:

用于选择输出地址是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 

程序计数器

模块图:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第10张图片
端口描述:
程序计数器用来提供指令地址,指令按照地址顺序存放在存储器中。包含两种生成途径:
(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 

状态控制器

模块图:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第11张图片

端口描述:
状态控制器接收复位信号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 

主状态机

模块图:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第12张图片
端口描述:
主状态机是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 

顶层模块

模块图:
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第13张图片

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前仿

对所有代码进行编译仿真
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第14张图片

test1程序仿真结果
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第15张图片
test2程序仿真结果
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第16张图片
test3程序仿真结果
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第17张图片

Quartus综合结果

使用Quartus (Quartus Prime 17.1) Standard Edition对RTL进行综合,对综合后的资源占用和电路图进行检查。
RTL图
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第18张图片
FSM图
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第19张图片
chip plan图
蓝色为占用部分
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第20张图片

资源占用
数字IC实践项目(1)——简化的RISC_CPU设计(经典教材中的开山鼻祖)_第21张图片

总结

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

你可能感兴趣的:(数字IC经典电路设计和实践项目,fpga开发)