【IC/CPU设计】极简的RISC_CPU设计
我的学记|刘航宇的博客

【IC/CPU设计】极简的RISC_CPU设计

刘航宇
2年前发布 /正在检测是否收录...
温馨提示:
本文最后更新于2023年04月08日,已超过717天没有更新,若内容或图片失效,请留言反馈。

CPU为SOC系统的核心环节,该项目来自于夏宇闻老师的经典教材——《Verilog 数字系统设计教程》,通过此练习方便数字ICer更好的入门
本次项目实践环境:
前仿: Modelsim
综合: Design Compile

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)地址多路器。

整体结构

图片[1] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客

时钟发生器

模块图:
image.png
端口描述:
reset是高电平复位信号;
clk是外部时钟信号;
fetch是控制信号,是clk的八分频信号;fetch为高电平时,触发执行指令以及地址多路器输出指令地址和数据地址。
alu_ena是算术逻辑运算单元的使能信号。
图片[2] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
可以看到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

指令寄存器

模块图:
图片[3] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
端口描述:
寄存器是将数据总线送来的指令存入高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  

累加器

模块图:
图片[4] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
端口描述:
累加器用于存放当前结果,ena信号有效时,在clk上升沿输出数据总线的数据。

// 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 

算术运算器

模块图:
image.png
端口描述:
算术逻辑运算单元可以根据输入的操作码分别实现相应的加、与、异或、跳转等基本操作运算。
本单元支持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 clk) begin 
            if(alu_ena) begin
            casex(opcode)//操作码来自指令寄存器的输出 opc_iaddr(15..0)的第三位
                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
    end

    assign zero = !accum;

endmodule 

数据控制器

模块图:
image.png
端口描述:
数据控制器的作用是控制累加器的数据输出,数据总线是分时复用的,会根据当前状态传输指令或者数据。
数据只在往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 

地址多路器

模块图:
image.png
端口描述:

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

程序计数器

模块图:
image.png
端口描述:
程序计数器用来提供指令地址,指令按照地址顺序存放在存储器中。包含两种生成途径:
(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 

状态控制器&主状态机

模块图:
image.png
(图左边)状态机端口描述:
状态控制器接收复位信号rst,rst有效,控制输出ena为0,fetch有效控制ena为1。

// 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无变化。

// 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(范围):1_1xxx_xxxx_xxxx
17FFH —— 0000H ROM(范围):0_xxxx_xxxx_xxxx+1_0xxx_xxxx_xxxx

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 

顶层模块

模块图:
图片[5] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
图片[6] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
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汇编程序:(.pro文件/存放于ROM)

//机器码-地址-汇编助记符-注释
@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数据文件:(.dat/存放于RAM)

/-----------------------------------
@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_tb;
  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----------------------
  RISC_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_decode 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 
$ readmemb ( "test1. pro" ,t_ rom. . memory );
$ readmemb ( "testl. dat",t_ ram_ . ram);
即可把编译好的汇编机器码装人虚拟ROM,把需要参加运算的数据装人虚拟RAM就可以开始仿真。上面语句中的第一项为打开的文件名,后一项为系统层次管理下的ROM模块和RAM模块中的存储器memory和ram。

源代码&脚本

前仿真结果

test1
图片[7] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
test2
图片[8] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
test3
图片[9] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
图片[10] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客

DC后仿真

采用SMIC180工艺在典型环境下进行测试
时序报告:
图片[11] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
面积报告:
图片[12] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
功耗报告:
图片[13] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
综合电路图:
图片[14] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
图片[15] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客
图片[16] - 【IC/CPU设计】极简的RISC_CPU设计 - 我的学记|刘航宇的博客

总结

该项目更加偏向于教学练习,CPU也是数字IC的重要研究方向,对此感兴趣的同学可以找点论文和开源资料进行学习。可以进一步优化如流水线、运算单元,扩展成SOC系统等。

© 版权声明
THE END
喜欢就支持一下吧
点赞 5 分享 赞赏
评论 共8条
取消
  1. 头像
    suppt
     · 
    回复

    学习的好资料!

  2. 头像
    qinh
     · 
    回复

    学习的好资料!

  3. 头像
    ddd
     · 
    回复

    牛啊

  4. 头像
    sss
     · 
    回复

    回复

  5. 头像
    SAS
     · 
    回复

    不逆 不耻 不憾

  6. 头像
    QUI
     · 
    回复

    为你,千千万万遍。

  7. 头像
    无殇
     · 
    回复

    强?

  8. 头像
    小孩子
     · 
    回复

    相信十年后的八月,我们还会相遇。