三步教你用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板级测试有关的,这一步暂且没用到。
指令集
指令为三地址格式,操作码长度5bit,根据操作数的不同可以把指令分为三种类型,即寄存器类型R type、立即数类型I type、混合类型RI type,不过后面在代码编写的时候,为了方便,会依据其它标准进行划分。
规范一下表示方式,r1或者gr[r1]表示访问寄存器r1,m[r2+val3]表示访问r2+val3这个地址,{val2,val3}表示立即数访问,val2为MSB,val3为LSB。
本文设计一共实现了28条机器指令,剩下未用的4个操作码(10100,10101,10110,10111)可自行补充为其它操作,比如自增INC、自减DEC。这里指令的编码是比较随意的,而且由于代码实现中使用了宏定义,因此可以任意更改指令的编码,不过如果想做进一步的优化,就要仔细考虑编码方式了。以下是指令集的具体格式与操作,设计CPU的时候有两张图是必须时刻看着的,我都把它们打印出来,这是其中一张。
五级流水线
除了指令集之外,设计CPU最重要的就是下面这张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状态。
2、IF
IF阶段的任务就是要根据PC的值从指令内存中读取一条指令,并且设置下一周期PC的值(指令可以顺序执行,也可以跳转到某个特定的地址)。因为读取内存是内存模块实现的功能,因此这里CPU只需要给出指令地址i_addr就能得到对应的指令i_datain。
3、ID
ID阶段要根据指令的功能(即操作码)从指令中提取对应的操作数,操作数可能来自通用寄存器r0-r7,也可能是立即数。另外如果指令是STORE指令,也要准备好要存储到内存中的数据。
4、EX
EX阶段执行的是ALU运算和标志寄存器设置,另外如果是STORE指令也要给出内存写的使能信号dw以及将要写到内存中的数据smdr1。
5、MEM
MEM阶段要根据指令功能和上一阶段的运算结果(内存操作的时候作为内存地址)决定是否要访问内存以及如何访问,只对需要内存操作的指令有效。
6、WB
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