这个实践项目来源于夏宇闻老师的经典教材——《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(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一共有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用于存放临时数据,可读可写。
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
模块说明:
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包含三个测试程序,这个部分不能综合。
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
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程序是一个计算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
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
test1程序仿真结果
test2程序仿真结果
test3程序仿真结果
使用Quartus (Quartus Prime 17.1) Standard Edition对RTL进行综合,对综合后的资源占用和电路图进行检查。
RTL图
FSM图
chip plan图
蓝色为占用部分
至此,整个练手项目完成,从完成度和难度来讲,这个小项目更加偏向于教学练习,CPU也是数字IC的重要研究方向,对此感兴趣的同学可以找点论文和开源资料进行学习。之所以把这个项目放到第一来讲,是因为不要小瞧这个项目,虽然看上去简单,但是对工程文件的管理以及项目实践的习惯非常重要,希望大家都能培养一个良好的工程习惯,书本上的代码也有一点问题,这里贴上的并不是最优解,只是带着大家走了一个简单的流程,最后综合的工具也是FPGA相关的,并没有使用DC等数字IC专业的EDA软件,后续有时间会把这个地方进行补齐。