文章目录
从零开始搭建一个智能处理器(一)
1.背景介绍
我们都知道,随着人工智能的告诉发展,人工智能的核心——神经网络对计算性能的需求也越来越高,而乘加运算则是神经网络运算中的最主要的部分,所以对MAC运算的加速成为了人工智能处理器的关键部分。
从今天开始我打算开一个制作智能处理器系列的专栏,从零开始用verilog写出一个智能处理器,最终该智能处理器能够对神经网络中的MAC运算进行加速。
2.自定义一份极简指令集架构
要想做出一个处理器,那指令集架构必不可少,在这里可以仿照RISC-V指令集定义一份专门用于人工智能领域的极简的16位指令集,该指令集非常简单,只有4条指令,分别是Load、Store、MAC、MOV,其中指令的格式如下表所示:
我们大致可以把上述四条指令分为上表中的三种格式,其中需要注意几点:
- 我们一共有4条指令,所以opcode字段需要三位,一共可以支持8条指令,但是我们只需用到4条指令。
- 由于我们是一个16位的架构,所以寄存器的数目有16个,所以源寄存器和目的寄存器的指令字段都是4位,一共支持2^4(也就是16)个寄存器
- 剩下的指令字段我们可以用来构建立即数字段。另外一个比较特殊的是funct字段,这个会在后面提到。
上表是对上述提出的4条指令的功能的一个具体描述:
- Load指令: Load指令属于上面提到的三种指令格式的第3种,其作用就是从我们的内存中取出一个数据放到我们的寄存器中,其中目的寄存器是我们的rd寄存器,源寄存器rs存的是基地址,5b的立即数存的是我们的相对于基地址的偏移量。
- Store指令: Store指令属于第一种指令格式,其作用是将寄存器里面的数据存放到内存中去。所以Store指令的rd指令字段是无效的,默认全为0。Store指令需要读取两个源寄存器rs1和rs2,其中rs1存的是放入内存中的地址,rs2存放的是放入内存中的数据,此外funct字段对于Store指令来说也是无效的。
- MOV指令: MOV指令属于第二种指令格式,最为简单。其作用就是将一个9b的立即数经过0拓展后存入目的寄存器rd中。
- MAC指令: MAC指令最为复杂,需要funct字段来配合。MAC指令的作用就是乘累加,其最基本的作用就是实现a*b + c。当funct=1时,就把c的值存在MAC_ALU的内部寄存器当中,当funct=0时,实现a*b,并且把其内部寄存器的值(也就是我们上一步存入的c的值)加到a*b中去,实现一个乘加的功能。
3.四条指令的硬件电路实现
在用硬件实现这四条指令之前,我们先来回顾一下一条指令执行的基本顺序。第一步就是取指,首先就是把指令从我们的指令存储器里面取出来。第二步就是译码,所谓译码就是分析这条指令是干嘛的,并且从寄存器堆里面取出我们需要的操作数。第三步就是执行,也就是将我们的操作数送入ALU进行运算,并且吐出运算结果。第四部分就是访存,Load和Store指令都需要在这一步访问我们的内存。最后一步就是写回寄存器堆,无论是运算的结果还是访存的结果,最后都需要把它存起来,这一步就叫写回。总结一下指令执行的基本顺序:
取指------>译码------>执行------>访存------>写回
但是,要想设计出这四条指令的硬件电路,我们首先得画出这四条指令的数据通路。
3.1 MOV指令数据通路
MOV指令的数据通路非常简单,decoder模块产生两个信号,9b的立即数和要写入的目的寄存器rd的地址,9b的立即数经过imm Gen模块以后(也就是经过拓展后)直接作为写入目的寄存器的数据。
3.2 Load指令数据通路
Load指令的数据通路稍微复杂一点,decoder产生三个信号,分别是5b的立即数,源寄存器rs1的地址以及目的寄存器rd的地址,其中,rs1的数据和5b的立即数相加后作为从内存load数据的地址,然后取出的数据作为写入rd的数据。
3.3 Store指令数据通路
Store指令的数据通路也比较简单,decoder模块产生两个信号,分别是rs1和rs2的地址,从中读取的数据一个作为存入内存的地址,另一个作为存入内存的数据。
3.4 MAC指令数据通路
MAC的数据通路也比较复杂,decoder模块输出四个信号,分别是rs1、rs2以及rd的地址,以及funct字段。funct字段决定MAC是进行初始赋值清除还是乘加。MAC单元运算的结果则作为写入目的寄存器的数据。
3.5 所有指令整体数据通路
上面这张图展示的是我们所有指令的一个数据通路,也称为处理器的微架构,我们的verilog代码其实就是根据我们的微架构写出来的。其中有两个MUX比较关键。上面一个MUX对输入DCM的地址进行选择:
- 对于Load指令,其地址的产生由5b的立即数和rs1里面的数据相加而来的。
- 对于Store指令,其地址的产生则是直接由rs1里面的数据得来。
下面一个MUX则是对写入目的寄存器的数据进行选择:
- 对于MOV指令,写入的数据直接由9b的立即数产生。
- 对于Load指令,其由内存里面读出的数据决定。
- 对于MAC指令,其由MAC单元的运算结果产生。
4.智能处理器的verilog实现
有了上面的关键的数据通路以后,我们就可以用verilog设计出其中的关键模块了。
`define LOAD 3'b001
`define STORE 3'b010
`define MOV 3'b011
`define MAC 3'b100
`define Opcode_Width 3
`define ISA_WIDTH 16
`define Reg_Addr_Width 4
`define Reg_Data_Width 16
`define MEM_ADDR_WIDTH 5
首先列出verilog中包含的头文件,避免下列代码产生混淆。
4.1 DCM模块
module dcmem #(
parameter MEM_ADDR_WIDTH = 5,
parameter MEM_DATA_WIDTH = 16,
parameter MEM_NUM = 32
) (
input clk,
input MemWEn,
input [MEM_ADDR_WIDTH-1:0] addr,
input [MEM_DATA_WIDTH-1:0] dataw,
input [MEM_DATA_WIDTH-1:0] datar
);
reg [MEM_DATA_WIDTH-1:0] RAM[MEM_NUM-1:0];
//----write logic-----
always @(posedge clk) begin
if(MemWEn)
RAM[addr] <= dataw;
end
//----read_logic------
assign datar = RAM[addr];
endmodule
其实就是分成两块逻辑,写memory是时序逻辑,读memory是组合逻辑。
4.2 ICM模块
module icmem #(
parameter PC_WIDTH = 16,
parameter ISA_WIDTH = 16
) (
input clk,
input rst_n,
input inst_wen,
input [ISA_WIDTH-1:0] input_inst,
output [ISA_WIDTH-1:0] current_inst
);
reg[PC_WIDTH-1:0] pc;
always @(posedge clk or negedge rst_n)
if(!rst_n)
pc <= 0;
else
pc <= pc + 1;
dcmem u_dcmem(
.clk(clk),
.MemWEn(inst_wen),
.addr(pc),
.dataw(input_inst),
.datar(current_inst)
);
endmodule
ICM模块的处理也非常简单,其实就是例化一块DCM,然后PC指针随着时钟周期每次都+1。
4.3 Decoder模块
`include "includeme.h"
module decoder #(
parameter Inst_Width = 16,
parameter Reg_Addr_Width = 4,
parameter imm5_Width = 5,
parameter imm9_Width = 9
) (
input [Inst_Width-1:0] inst,
output reg [Reg_Addr_Width-1:0] rs1,
output reg [Reg_Addr_Width-1:0] rs2,
output reg [Reg_Addr_Width-1:0] rd,
output reg [imm5_Width-1:0] imm5,
output reg [imm9_Width-1:0] imm9,
output reg funct,
output reg RegWEn,
output reg MemWEn,
output [`Opcode_Width-1:0] opcode
);
assign opcode = inst[15:13];
always @(*)
begin
case(opcode)
`LOAD:
begin
rs1 = inst[8:5];
rs2 = 0;
rd = inst[12:9];
imm5 = inst[4:0];
imm9 = 0;
RegWEn = 1'b1;
MemWEn = 1'b0;
end
`STORE:
begin
rs1 = inst[8:5];
rs2 = inst[4:1];
rd = 0;
imm5 = 0;
imm9 = 0;
RegWEn = 1'b0;
MemWEn = 1'b1;
end
`MOV:
begin
rs1 = 0;
rs2 = 0;
rd = inst[12:9];
imm5 = 0;
imm9 = inst[8:0];
RegWEn = 1'b1;
MemWEn = 1'b0;
end
`MAC:
begin
rs1 = inst[8:5];
rs2 = inst[4:1];
rd = inst[12:9];
imm5 = 0;
imm9 = 0;
funct = inst[0];
RegWEn = 1'b1;
MemWEn = 1'b0;
end
default:
begin
rs1 = 4'b0;
rs2 = 4'b0;
rd = 4'b0;
imm5 = 0;
imm9 = 0;
RegWEn = 1'b0;
MemWEn = 1'b0;
end
endcase
end
endmodule
Decoder模块要做的主要的事情就是确定是否需要读取源寄存器、是否需要写入目的寄存器、是否需要读取内存以及输出立即数字段。其中输出opcode字段是为了方便后续做MUX选择。
4.4 RegFile模块
module regfile #(
parameter REG_ADDR_WIDTH = 4,
parameter REG_DATA_WIDTH = 16,
parameter REG_NUM = 16
) (
input clk,
input rst_n,
input [REG_ADDR_WIDTH-1:0] rs1_addr,
input [REG_ADDR_WIDTH-1:0] rs2_addr,
input [REG_ADDR_WIDTH-1:0] rd_addr,
input [REG_DATA_WIDTH-1:0] rd_data,
input RegWEn,
output [REG_DATA_WIDTH-1:0] rs1_data,
output [REG_DATA_WIDTH-1:0] rs2_data
);
reg[REG_DATA_WIDTH-1:0] regfile[REG_NUM-1:0];
//------write operation----------//
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin:init
integer i;
for(i=0; i<REG_NUM; i=i+1) begin
regfile[i] <= 0;
end
end:init
else if(RegWEn && rd_addr != 0)
regfile[rd_addr] <= rd_data;
end
//-----read operation---------//
assign rs1_data = (rs1_addr == 0) ? 0 : regfile[rs1_addr];
assign rs2_data = (rs2_addr == 0) ? 0 : regfile[rs2_addr];
endmodule
4.5 MAC模块
module MAC_ALU #(
parameter REG_DATA_WIDTH = 16
) (
input clk,
input funct,
input [REG_DATA_WIDTH-1:0] rs1,
input [REG_DATA_WIDTH-1:0] rs2,
output [REG_DATA_WIDTH-1:0] rd
);
wire [REG_DATA_WIDTH-1:0] product;
wire [REG_DATA_WIDTH-1:0] addend1;
wire [REG_DATA_WIDTH-1:0] addend2;
reg [REG_DATA_WIDTH-1:0] psum;
always @(posedge clk) begin
if(funct)
psum <= rd;
else
psum <= 0;
end
assign product = $signed(rs1) * $signed(rs2);
assign addend1 = funct ? 0 : product;
assign addend2 = funct ? rs1 : psum;
assign rd = addend1 + addend2;
endmodule
- 当funct为1时,其将乘累加的加数从rs1取出并且存入psum中。
- 在下一个时钟周期时,funct为0,其将乘累加的两个乘数从rs1和rs2取出做乘法并把结果存入product里面,然后把product和psum相加,实现乘累加的功能。
PS:实现一次MAC运算需要两条MAC指令配合,第一条指令的funct字段为1,用于取加数,第二条指令的funct字段为0,用于乘累加。并且这两条MAC指令要紧挨在一起。
4.6 顶层模块top
`include "includeme.h"
module top(
input clk,
input rst_n,
input inst_wen,
input [`ISA_WIDTH-1:0] input_inst
);
wire [`ISA_WIDTH-1:0] current_inst;
wire [`Reg_Addr_Width-1:0] rs1_addr;
wire [`Reg_Addr_Width-1:0] rs2_addr;
wire [`Reg_Addr_Width-1:0] rd_addr;
wire funct;
wire RegWEn;
wire MemWEn;
wire [`Reg_Data_Width-1:0] rs1_data;
wire [`Reg_Data_Width-1:0] rs2_data;
reg [`Reg_Data_Width-1:0] rd_data;
wire [4:0] imm5;
wire [8:0] imm9;
reg [`MEM_ADDR_WIDTH-1:0] addr;
wire [`Opcode_Width-1:0] opcode;
wire [`Reg_Data_Width-1:0] dcmem_rd_data;
wire [`Reg_Data_Width-1:0] MAC_rd_data;
wire [15:0] imm5_extend;
wire [15:0] imm9_extend;
regfile u_regfile(
.clk(clk),
.rst_n(rst_n),
.rs1_addr(rs1_addr),
.rs2_addr(rs2_addr),
.rd_addr(rd_addr),
.rd_data(rd_data),
.RegWEn(RegWEn),
.rs1_data(rs1_data),
.rs2_data(rs2_data)
);
dcmem u_dcmem(
.clk(clk),
.MemWEn(MemWEn),
.addr(addr),
.dataw(rs2_data),
.datar(dcmem_rd_data)
);
icmem u_icmem(
.clk(clk),
.rst_n(rst_n),
.inst_wen(inst_wen),
.input_inst(input_inst),
.current_inst(current_inst)
);
MAC_ALU u_MAC_ALU(
.clk(clk),
.funct(funct),
.rs1(rs1_data),
.rs2(rs2_data),
.rd(MAC_rd_data)
);
decoder u_decoder(
.inst(current_inst),
.rs1(rs1_addr),
.rs2(rs2_addr),
.rd(rd_addr),
.imm5(imm5),
.imm9(imm9),
.funct(funct),
.RegWEn(RegWEn),
.MemWEn(MemWEn),
.opcode(opcode)
);
imm_gen u_imm_gen(
.imm5(imm5),
.imm9(imm9),
.imm5_extend(imm5_extend),
.imm9_extend(imm9_extend)
);
always @(*) begin
case(opcode)
`LOAD:begin
addr = rs1_data + imm5_extend;
end
`STORE:begin
addr = rs1_data;
end
default:begin
addr = 0;
end
endcase
end
always @(*) begin
case(opcode)
`LOAD:begin
rd_data = dcmem_rd_data;
end
`MAC:begin
rd_data = MAC_rd_data;
end
`MOV:begin
rd_data = imm9_extend;
end
default:begin
rd_data = 0;
end
endcase
end
endmodule
top模块中的两个always模块实际上就是在做数据通路图中的两个MUX。
5.仿真验证
首先需要搭建testbench平台,其中最重要的两部分就是初始化指令内存和数据内存。
`timescale 1ns/1ps
module top_tb();
reg clk;
reg rst_n;
initial begin
clk = 1'b0;
forever #10 clk = ~clk;
end
initial begin
rst_n = 1'b0;
#700
rst_n = 1'b1;
#1000
$finish;
end
// initial inst_mem
initial begin
$readmemh("./asm/machine.txt",u_top.u_icmem.u_dcmem.RAM);
end
// initial data_mem
initial begin
$readmemh("./asm/data.txt",u_top.u_dcmem.RAM);
end
top u_top(
.clk(clk),
.rst_n(rst_n),
.inst_wen(),
.input_inst()
);
initial begin
$fsdbDumpfile("top.fsdb");
$fsdbDumpvars;
$fsdbDumpMDA();
end
endmodule
同时,为了验证我们所实现的智能处理器的功能是否正确,我们需要写一段汇编程序去验证。汇编程序如下:
MOV R1, $0 // DCM address 0 ------> b
Load R5, R1, $0 // b -------> R2
MOV R1, $1 // DCM address 1 -------> x1
MOV R2, $3 // DCM address 3 ------->w1
Load R3, R1, $0 // x1 --------> R3
Load R4, R2, $0 // w1 --------> R4
MAC R5, R5, /, $1
MAC R5, R3, R4, $0
Load R3, R1, $1 // DCM address 2 -------> x2
Load R4, R2, $1 // DCM address 4 -------> w2
MAC R5, R5, /, $1
MAC R5, R3, R4, $0
MOV R1, $5 // DCM address 5 -------> y
Store R1, R5
这段汇编程序实现的功能就是实现y=b + x1*w1 + x2*w2,并且把计算的结果y最后存入data memory的地址5。
其中,data memory里面一共初始化了5个数,从地址0到地址4,依次存的是我们的b x1 x2 w1 w2,以下是data.txt里面的内容:
0001
0002
0003
0004
0005
所以,我们最终需要验证data memory里面的地址5是不是存的是最后的结果y = b + x1*w1 + x2*w2 = 1 + 2*4 + 3*5,也就是24。
打开vcs的memory查看窗口,可以看到:
最终地址5里面存的数据就是我们所期盼的24,所以这可以验证我们所设计的智能处理器在功能上是正确的。
6.附录
如果需要源代码的话可以通过以下链接自取:
链接: https://pan.baidu.com/s/1ddN3iDBkwFy3Ya-eNjHJ7A
提取码:bpp8