三步教你用Verilog写一个CPU
第一步:小试牛刀
也许在不少人眼里,这个世界有两座难以企及的大山,一座是操作系统,还有一座就是CPU。无可否认,无论开发一个操作系统还是做一个CPU都是一件极其浩大的工程,需要一个优秀的团队前赴后继的努力。我相信有不少想涉足这两方面的人开始的时候都会有种无从下手的感觉,但是,经过我这一系列CPU的教程之后,我有十足把握,你肯定会对做CPU有根本性的改观,忽略太多工业上的要求以及硬件的具体实现,最后你会发现设计一个勉强能用的CPU其实不过是四五百行代码而已。
杂谈
眼下,主流的操作系统就只有Windows、Linux、Mac OS,形成三足鼎立的局面,开发操作系统是极其困难的事情,起码我们国家就没有真正意义上自主的国产操作系统,而要形成一个新生操作系统的生态环境更是难上加难。的确,即使是要写一个精简版的mini操作系统,想要看到一点点成果都不是简单的事情。
而另一方面,虽然说做CPU的厂商有不少,但真正要有核心竞争力也实属不易。国产的有“龙芯”,但毕竟不是用在日常使用的电脑、手机上面的,近段时间华为的海思处理器大有动作,但真正能取得什么样的成绩也还难判定。
有时候我们真的应该好好想想,我们是不是应该对祖国有所贡献?比如参与开发国产操作系统,开发国产CPU这些令人骄傲的事情。可能我们不知不觉都习惯于“过日子”了,不是说没有了理想,每个学IT的人毕业了总会打拼一番,比如在微软、在谷歌,步步高升当上了产品经理甚至更高的职位,但除此之外,你有没有一些更加疯狂的想法?你有没有一些更加伟大的人生目标?你有没有热血沸腾的时候想为祖国争光?从小时候的“为中华之崛起而读书”,到现在习以为常地认为毕业了就应该找个大公司发挥自己的才能,干一番大事业,我感觉这里头味道已经变了,迷失了....
上面只是我的一些个人感慨,衷心希望以后在IT行业能看到更多中国品牌的东西以及在核心领域占据领先优势的局面。
废话少说,我必须跟读者申明的一点是,写操作系统的话你很难看得到成果,但是CPU不一样,因为我们并不是CPU生产厂商,所以不需要你去组合硬件,你只需要设计出CPU的电路结构,到时候依据电路图自然就可以组合成实实在在的CPU,而且通过一些仿真软件直接就可以在电脑上面看到你自己设计的电路的运行情况,又或者可以把你设计的电路的程序下载到FPGA开发板,它自动就帮你在硬件上构造了一个具有CPU功能的电路结构。之后你会发现,实现CPU的基本功能并不复杂,所以说,CPU的入门是相对比较简单的,而且不必太费劲就能看到成果,只不过要做得好就不是一天两天、一年两年的事情了。
基础
课程要求:数字电路、计算机组成原理、程序设计
编程语言:Verilog
开发平台:xilinx ISE
FPGA开发板:Nexys3
教学大纲
第一步
指令集设计与五级流水线的实现
第二步
内存设计与CPU测试
第三步
指令冲突避免
实现目标
本文实现的CPU是一个五级流水线的精简版CPU(也叫PCPU,即pipeline),包括IF(取指令)、ID(解码)、EX(执行)、MEM(内存操作)、WB(回写)。
指令集:RISC
指令集大小:2^5
数据宽度:16bit
数据内存:2^8×16bit
指令内存:2^8×16bit
通用寄存器:8×16bit
标志寄存器:NF(negative flag)、ZF(zero flag)、CF(carry flag)
控制信号:clock、reset、enable、start
介绍与设计
CPU顶层视图
第一步只要求实现简单的五级流水线,不要求实现指令内存、数据内存模块,因此CPU内部与内存有关的信号都简化为输入输出信号了,CPU的顶层视图看起来应该如下图,其中select_y、y信号是跟CPU板级测试有关的,这一步暂且没用到。
图1 CPU top view
指令集
指令为三地址格式,操作码长度5bit,根据操作数的不同可以把指令分为三种类型,即寄存器类型R type、立即数类型I type、混合类型RI type,不过后面在代码编写的时候,为了方便,会依据其它标准进行划分。
图2 指令格式
规范一下表示方式,r1或者gr[r1]表示访问寄存器r1,m[r2+val3]表示访问r2+val3这个地址,{val2,val3}表示立即数访问,val2为MSB,val3为LSB。
图3 机器代码示例
本文设计一共实现了28条机器指令,剩下未用的4个操作码(10100,10101,10110,10111)可自行补充为其它操作,比如自增INC、自减DEC。这里指令的编码是比较随意的,而且由于代码实现中使用了宏定义,因此可以任意更改指令的编码,不过如果想做进一步的优化,就要仔细考虑编码方式了。以下是指令集的具体格式与操作,设计CPU的时候有两张图是必须时刻看着的,我都把它们打印出来,这是其中一张。
图4 指令集
五级流水线
除了指令集之外,设计CPU最重要的就是下面这张CPU块级电路图,五级流水线的代码实现都必须依赖于这张图,因此必须理解图中每一步的作用。
图5 CPU块级电路图
其实这个图也并不复杂,CPU无非就是组合逻辑电路和时序电路的结合,而图中所有矩形框标出来的都是CPU内部的寄存器,整个电路图展示了CPU内部指令以及数据的流动方向。每到时钟上升沿,上一级流水线的寄存器的数据就会经过中间的组合逻辑电路流动到下一级流水线的寄存器,因此,5个时钟周期之后一条机器指令便执行完毕了。简单描述一条指令的执行过程就是,首先根据PC的值到内存中取一条指令,解码指令提取两个操作数进行运算,根据指令功能以及运算结果决定是否访问数据内存以及如何访问,最后同样根据指令功能决定是否要进行回写操作,即修改寄存器的值。
下面将分别讲解CPU控制以及五级流水线每一级的行为,为了简单起见,这里仅考虑NOP、HALT、LOAD、STORE、ADD、CMP、BZ、BN这几条指令,明白了流水线的行为之后再加上其它的指令也是一样的道理。
1、CPU控制
CPU控制自然是基于状态机,只有两个状态idle和exec,CPU在idle状态下只有enable、start同时使能才会进入exec状态。
图6 CPU control
2、IF
IF阶段的任务就是要根据PC的值从指令内存中读取一条指令,并且设置下一周期PC的值(指令可以顺序执行,也可以跳转到某个特定的地址)。因为读取内存是内存模块实现的功能,因此这里CPU只需要给出指令地址i_addr就能得到对应的指令i_datain。
图7 IF阶段
3、ID
ID阶段要根据指令的功能(即操作码)从指令中提取对应的操作数,操作数可能来自通用寄存器r0-r7,也可能是立即数。另外如果指令是STORE指令,也要准备好要存储到内存中的数据。
图8 ID阶段
4、EX
EX阶段执行的是ALU运算和标志寄存器设置,另外如果是STORE指令也要给出内存写的使能信号dw以及将要写到内存中的数据smdr1。
图9 EX阶段
5、MEM
MEM阶段要根据指令功能和上一阶段的运算结果(内存操作的时候作为内存地址)决定是否要访问内存以及如何访问,只对需要内存操作的指令有效。
图10 MEM阶段
6、WB
WB阶段同样根据指令的功能以及上一阶段的结果决定是否要修改寄存器的值以及如何修改,只对需要修改寄存器值的指令有效。
图11 WB阶段
代码实现
上面的设计中只给出了时序电路部分的代码,而且只针对个别指令,另外还应该有一部分组合逻辑电路的代码则是处理上一级流水线寄存器的数据如何流动到下一级寄存器的。下面是完整的代码实现。
// def.v `define idle 1'b0 `define exec 1'b1 `define NOP 5'b0_0000 `define HALT 5'b0_0001 `define LOAD 5'b0_0010 `define STORE 5'b0_0011 `define SLL 5'b0_0100 `define SLA 5'b0_0101 `define SRL 5'b0_0110 `define SRA 5'b0_0111 `define ADD 5'b0_1000 `define ADDI 5'b0_1001 `define SUB 5'b0_1010 `define SUBI 5'b0_1011 `define CMP 5'b0_1100 `define AND 5'b0_1101 `define OR 5'b0_1110 `define XOR 5'b0_1111 `define LDIH 5'b1_0000 `define ADDC 5'b1_0001 `define SUBC 5'b1_0010 `define SUIH 5'b1_0011 `define JUMP 5'b1_1000 `define JMPR 5'b1_1001 `define BZ 5'b1_1010 `define BNZ 5'b1_1011 `define BN 5'b1_1100 `define BNN 5'b1_1101 `define BC 5'b1_1110 `define BNC 5'b1_1111 `define gr0 3'b000 `define gr1 3'b001 `define gr2 3'b010 `define gr3 3'b011 `define gr4 3'b100 `define gr5 3'b101 `define gr6 3'b110 `define gr7 3'b111
// pcpu.v `timescale 1ns / 1ps `include "def.v" module pcpu( input clock, input enable, input reset, input start, input [15:0] i_datain, input [15:0] d_datain, output [7:0] i_addr, output [7:0] d_addr, output d_we, output [15:0] d_dataout ); reg cf_buf; reg [15:0] ALUo; reg state, next_state; reg zf, nf, cf, dw; reg [7:0] pc; reg [15:0] id_ir, ex_ir, mem_ir, wb_ir; reg [15:0] reg_A, reg_B, reg_C, reg_C1, smdr, smdr1; reg [15:0] gr[7:0]; wire branch_flag; //************* CPU Control *************// always @(posedge clock) begin if (!reset) state <= `idle; else state <= next_state; end //************* CPU Control *************// always @(*) begin case (state) `idle : if ((enable == 1'b1) && (start == 1'b1)) next_state <= `exec; else next_state <= `idle; `exec : if ((enable == 1'b0) || (wb_ir[15:11] == `HALT)) next_state <= `idle; else next_state <= `exec; endcase end //************* IF *************// assign i_addr = pc; always @(posedge clock or negedge reset) begin if (!reset) begin id_ir <= {`NOP, 11'b000_0000_0000}; pc <= 8'b0000_0000; end else if (state ==`exec) begin id_ir <= i_datain; if(branch_flag) pc <= reg_C[7:0]; else pc <= pc + 1; end end //************* ID *************// always @(posedge clock or negedge reset) begin if (!reset) begin ex_ir <= {`NOP, 11'b000_0000_0000}; reg_A <= 16'b0000_0000_0000_0000; reg_B <= 16'b0000_0000_0000_0000; smdr <= 16'b0000_0000_0000_0000; end else if (state == `exec) begin ex_ir <= id_ir; if (id_ir[15:11] == `STORE) smdr <= gr[id_ir[10:8]]; else smdr <= smdr; if (id_ir[15:11] == `JUMP) reg_A <= 16'b0000_0000_0000_0000; else if (I_R1_TYPE(id_ir[15:11])) reg_A <= gr[id_ir[10:8]]; else if (I_R2_TYPE(id_ir[15:11])) reg_A <= gr[id_ir[6:4]]; else reg_A <= reg_A; if (I_V3_TYPE(id_ir[15:11])) reg_B <= {12'b0000_0000_0000, id_ir[3:0]}; else if (I_ZEROV2V3_TYPE(id_ir[15:11])) reg_B <= {8'b0000_0000, id_ir[7:0]}; else if (I_V2V3ZERO_TYPE(id_ir[15:11])) reg_B <= {id_ir[7:0], 8'b0000_0000}; else if (I_R3_TYPE(id_ir[15:11])) reg_B <= gr[id_ir[2:0]]; else reg_B <= reg_B; end end //************* EX *************// always @(posedge clock or negedge reset) begin if (!reset) begin mem_ir <= {`NOP, 11'b000_0000_0000}; reg_C <= 16'b0000_0000_0000_0000; smdr1 <= 16'b0000_0000_0000_0000; dw <= 1'b0; zf <= 1'b0; nf <= 1'b0; cf <= 1'b0; end else if (state == `exec) begin reg_C <= ALUo; mem_ir <= ex_ir; if ((ex_ir[15:11] == `LDIH) || (ex_ir[15:11] == `SUIH) || (ex_ir[15:11] == `ADD) || (ex_ir[15:11] == `ADDI) || (ex_ir[15:11] == `ADDC) || (ex_ir[15:11] == `SUB) || (ex_ir[15:11] == `SUBI) || (ex_ir[15:11] == `SUBC) || (ex_ir[15:11] == `CMP) || (ex_ir[15:11] == `AND) || (ex_ir[15:11] == `OR) || (ex_ir[15:11] == `XOR) || (ex_ir[15:11] == `SLL) || (ex_ir[15:11] == `SRL) || (ex_ir[15:11] == `SLA) || (ex_ir[15:11] == `SRA)) begin cf <= cf_buf; if (ALUo == 16'b0000_0000_0000_0000) zf <= 1'b1; else zf <= 1'b0; if (ALUo[15] == 1'b1) nf <= 1'b1; else nf <= 1'b0; end else begin zf <= zf; nf <= nf; cf <= cf; end if (ex_ir[15:11] == `STORE) begin dw <= 1'b1; smdr1 <= smdr; end else begin dw <= 1'b0; smdr1 <= smdr1; end end end always @(*) begin if (ex_ir[15:11] == `AND) begin cf_buf <= 1'b0; ALUo <= reg_A & reg_B; end else if (ex_ir[15:11] == `OR) begin cf_buf <= 1'b0; ALUo <= reg_A | reg_B; end else if (ex_ir[15:11] == `XOR) begin cf_buf <= 1'b0; ALUo <= reg_A ^ reg_B; end else if (ex_ir[15:11] == `SLL) {cf_buf, ALUo[15:0]} <= {cf, reg_A[15:0]} << reg_B[3:0]; else if (ex_ir[15:11] == `SRL) {ALUo[15:0], cf_buf} <= {reg_A[15:0], cf} >> reg_B[3:0]; else if (ex_ir[15:11] == `SLA) {cf_buf, ALUo[15:0]} <= {cf, reg_A[15:0]} <<< reg_B[3:0]; else if (ex_ir[15:11] == `SRA) {ALUo[15:0], cf_buf} <= {reg_A[15:0], cf} >>> reg_B[3:0]; else if ((ex_ir[15:11] == `SUB) || (ex_ir[15:11] == `SUBI) || (ex_ir[15:11] == `CMP) || (ex_ir[15:11] == `SUIH)) {cf_buf, ALUo} <= reg_A - reg_B; else if (ex_ir[15:11] == `SUBC) {cf_buf, ALUo} <= reg_A - reg_B - cf; else if (ex_ir[15:11] == `ADDC) {cf_buf, ALUo} <= reg_A + reg_B + cf; else {cf_buf, ALUo} <= reg_A + reg_B; end //************* MEM *************// assign d_addr = reg_C[7:0]; assign d_we = dw; assign d_dataout = smdr1; assign branch_flag = ((mem_ir[15:11] == `JUMP) || (mem_ir[15:11] == `JMPR) || ((mem_ir[15:11] == `BZ) && (zf == 1'b1)) || ((mem_ir[15:11] == `BNZ) && (zf == 1'b0)) || ((mem_ir[15:11] == `BN) && (nf == 1'b1)) || ((mem_ir[15:11] == `BNN) && (nf == 1'b0)) || ((mem_ir[15:11] == `BC) && (cf == 1'b1)) || ((mem_ir[15:11] == `BNC) && (cf == 1'b0))); always @(posedge clock or negedge reset) begin if (!reset) begin wb_ir <= {`NOP, 11'b000_0000_0000}; reg_C1 <= 16'b0000_0000_0000_0000; end else if (state == `exec) begin wb_ir <= mem_ir; if (mem_ir[15:11] == `LOAD) reg_C1 <= d_datain; else reg_C1 <= reg_C; end end //************* WB *************// always @(posedge clock or negedge reset) begin if (!reset) begin gr[0] <= 16'b0000_0000_0000_0000; gr[1] <= 16'b0000_0000_0000_0000; gr[2] <= 16'b0000_0000_0000_0000; gr[3] <= 16'b0000_0000_0000_0000; gr[4] <= 16'b0000_0000_0000_0000; gr[5] <= 16'b0000_0000_0000_0000; gr[6] <= 16'b0000_0000_0000_0000; gr[7] <= 16'b0000_0000_0000_0000; end else if (state == `exec) begin if (I_REG_TYPE(wb_ir[15:11])) gr[wb_ir[10:8]] <= reg_C1; end end //***** Judge an instruction whether alter the value of a register *****// function I_REG_TYPE; input [4:0] op; begin I_REG_TYPE = ((op == `LOAD) || (op == `LDIH) || (op == `ADD) || (op == `ADDI) || (op == `ADDC) || (op == `SUIH) || (op == `SUB) || (op == `SUBI) || (op == `SUBC) || (op == `AND) || (op == `OR) || (op == `XOR) || (op == `SLL) || (op == `SRL) || (op == `SLA) || (op == `SRA)); end endfunction //************* R1 as reg_A *************// function I_R1_TYPE; input [4:0] op; begin I_R1_TYPE = ((op == `LDIH) || (op == `SUIH) || (op == `ADDI) || (op == `SUBI) || (op == `JMPR) || (op == `BZ) || (op == `BNZ) || (op == `BN) || (op == `BNN) || (op == `BC) || (op == `BNC)); end endfunction //************* R2 as reg_A *************// function I_R2_TYPE; input [4:0] op; begin I_R2_TYPE = ((op == `LOAD) || (op == `STORE) || (op == `ADD) || (op == `ADDC) || (op == `SUB) || (op == `SUBC) || (op == `CMP) || (op == `AND) || (op == `OR) || (op == `XOR) || (op == `SLL) || (op == `SRL) || (op == `SLA) || (op == `SRA)); end endfunction //************* R3 as reg_B *************// function I_R3_TYPE; input [4:0] op; begin I_R3_TYPE = ((op == `ADD) || (op == `ADDC) || (op == `SUB) || (op == `SUBC) || (op == `CMP) || (op == `AND) || (op == `OR) || (op == `XOR)); end endfunction //************* val3 as reg_B *************// function I_V3_TYPE; input [4:0] op; begin I_V3_TYPE = ((op == `LOAD) || (op == `STORE) || (op == `SLL) || (op == `SRL) || (op == `SLA) || (op == `SRA)); end endfunction //************* {0000_0000,val2,val3} as reg_B *************// function I_ZEROV2V3_TYPE; input [4:0] op; begin I_ZEROV2V3_TYPE = ((op == `ADDI) || (op == `SUBI) || (op == `JUMP) || (op == `JMPR) || (op == `BZ) || (op == `BNZ) || (op == `BN) || (op == `BNN) || (op == `BC) || (op == `BNC)); end endfunction //************* {val2,val3,0000_0000} as reg_B *************// function I_V2V3ZERO_TYPE; input [4:0] op; begin I_V2V3ZERO_TYPE = ((op == `LDIH) || (op == `SUIH)); end endfunction endmodule
相关推荐
在“三步教你用Verilog写一个CPU”的系列中,我们已经进入了第三步,这通常涉及到CPU核心的实现。在这里,我们将会讨论如何将之前的概念转化为实际的代码。 ### 第一步:理解CPU的基本结构 CPU是计算机的中央处理...
在本文中,我们将探讨如何使用Verilog语言设计一个简化的RISC(精简指令集计算)CPU。RISC CPU以其高效的指令集和简洁的硬件结构著称,旨在提高计算机的运算速度。通过这个设计过程,我们可以深入理解CPU的工作原理...
1. **指令取指(Fetch)**:这是CPU周期的第一步,程序计数器(PC)生成下一个要执行指令的地址,然后从内存中读取该指令。在Verilog中,我们可以定义一个`memory_interface`模块来处理与外部存储器的交互。 2. **...
通过对本教学模型的学习,我们可以了解到如何使用Verilog HDL来设计一个简化的RISC CPU。虽然该设计可能不是最优化的方案,但它为学习者提供了一个深入了解CPU内部工作原理的机会。此外,这也展示了Verilog HDL作为...
在设计CPU时,第一步通常需要使用硬件描述语言(HDL),比如Verilog HDL。Verilog HDL是一种用于电子系统设计和硬件描述的语言,广泛应用于数字电路设计。使用Verilog HDL,我们可以创建模拟电路和处理器的模型,并...
取指是CPU执行指令的第一步。首先,地址从地址寄存器AR加载到存储器,然后读取数据到数据寄存器DR。在这个过程中,PC加一,表示下一条指令的地址。取指过程分为三个阶段: - Fetch1:AR - Fetch2:DR ,PC - ...
2. **指令集架构(ISA)**:设计CPU的第一步是定义指令集,即CPU理解和执行的二进制命令。16位CPU可能会包含加法、减法、逻辑运算、分支和加载/存储指令等基本操作。 3. **数据通路和控制单元**:数据通路负责执行...
在Verilog中创建CPU,每个部分都有一个测试平台。 用仅使用74xx兼容原语(例如 )但仍通过测试平台的Verilog替换原始Verilog。 将74xx-Verilog转换为KiCad原理图。 建立CPU 当我弄清楚CPU实际如何工作时,步骤1...
1. **概念设计**:这是CPU设计的第一步,主要确定CPU的架构,包括指令集体系结构(ISA)、总线结构、寄存器配置等。例如,选择RISC(精简指令集计算)还是CISC(复杂指令集计算)架构,会影响到后续的逻辑设计。 2....
1. **取指阶段**:从内存中读取指令到指令寄存器(IR),这是流水线的第一步。通常包括从程序计数器(PC)中获取下一条指令的地址,然后从内存读取指令。 2. **译码阶段**:将指令从机器码形式解码为控制信号,确定...
在第4步中,编译Func的过程涉及到MIPS-GCC交叉编译工具,这是因为实验通常使用MIPS架构的CPU,需要在非MIPS环境下编译MIPS指令集的程序。编译过程包括清理旧文件(make reset)和构建新文件(make)。 第5步的仿真...
**安装ModelSim**是使用它的第一步。通常,安装过程包括下载软件安装包,然后按照向导的指示进行。确保选择与你的操作系统相匹配的版本(例如,Windows、Linux或MacOS)。在安装过程中,注意勾选必要的组件,如...
实现八位加法器的第一步是设计逻辑电路。这通常涉及到使用基本逻辑门,如与门(AND)、或门(OR)和非门(NOT),以及半加器(Half Adder)和全加器(Full Adder)。半加器负责计算两个单比特数的和及进位,而全加器...
第一步,掌握硬件描述语言(HDL)。学习FPGA的基础是理解并熟练运用硬件描述语言,如Verilog或VHDL。这些语言允许开发者用代码描述数字电路的行为和结构。建议使用相关的教程,如夏宇闻的书籍,进行系统学习。C语言...
这一过程是执行指令周期的第一步,也是计算机执行任何操作的前提。取指令部件通常包括地址产生单元、读控制逻辑以及必要的数据传输路径。地址产生单元决定下一条要执行的指令在存储器中的位置,而读控制逻辑则协调...
设计CPU的第一步是定义其体系结构。这包括ALU(算术逻辑单元)用于执行算术和逻辑操作,PC(程序计数器)用于存储下一条要执行的指令地址,以及控制单元来协调整个系统的操作。每个组件都需要根据MIPS4指令集来设计...
当逻辑设计完成之后,接下来的仿真分为两步:第一步是编写逻辑时使用Quartus软件自带的仿真工具进行测试,第二步是在逻辑编写完成后使用Modelsim等第三方仿真软件进行更加深入的验证。Modelsim虽然功能强大,但对...
一、性格与职业选择:成为FPGA工程师的第一步是自我认知。夏宇闻老师提出,工程师需要特别安静的性格,并能够耐得住寂寞,因为这项工作需要不断学习和研究以提升设计能力,同时也要适应经常性的熬夜和加班。这暗示了...
- **定义与重要性**:明确产品的需求是整个设计流程的第一步。 - **具体内容**:用户需求收集、市场趋势分析、竞品比较等。 **2. 开发可行性分析** - **定义与重要性**:评估项目的可行性和风险,为后续设计提供...