从零开始搭建一个智能处理器(一)

从零开始搭建一个智能处理器(一)

1.背景介绍

在这里插入图片描述
我们都知道,随着人工智能的告诉发展,人工智能的核心——神经网络对计算性能的需求也越来越高,而乘加运算则是神经网络运算中的最主要的部分,所以对MAC运算的加速成为了人工智能处理器的关键部分。

从今天开始我打算开一个制作智能处理器系列的专栏,从零开始用verilog写出一个智能处理器,最终该智能处理器能够对神经网络中的MAC运算进行加速。

2.自定义一份极简指令集架构

要想做出一个处理器,那指令集架构必不可少,在这里可以仿照RISC-V指令集定义一份专门用于人工智能领域的极简的16位指令集,该指令集非常简单,只有4条指令,分别是LoadStoreMACMOV,其中指令的格式如下表所示:
在这里插入图片描述
我们大致可以把上述四条指令分为上表中的三种格式,其中需要注意几点:

  • 我们一共有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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值