3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Risc-VをverilogHDLで書いていく話(load命令)

Posted at

はじめに

新社会人として、某企業戦士となり少し落ち着いてきたので趣味の活動を進めていこうと思う。
今回はRisc-Vをverilog-HDLを使って書いていこうと思う。
大まかなフローとして

  • load 命令実装
  • store 命令実装
  • immdiate 命令実装
  • register 命令実装
  • branch 命令実装

こんな感じで回路図とコードを少しずつバージョンアップさせながら開発していこうと思う。

Risc-Vとは

まず Risc-VとはオープンISA(命令セット)である。あくまでISAなのでアーキテクチャ設計などは人によってまちまちである。
最近ではApple がRisc-Vの開発者を募集したりと近年ホットな話題である。

ここでは、自分なりの処理機構を作っていくので今後高速化や命令の最適化の楽しみを含みつつ実装していく。

アーキテクチャの仕様

今回設計するアーキテクチャを以下に示す

  • RV32Iに対応した命令処理系
  • 内部レジスタは8bitデータを32個(rstでレジスタ初期化は行わない)
  • データバスは32bit
  • リトルエンディアンを採用
  • 命令メモリとデータメモリは分割して実装(構造ハザード回避のため)
  • 5段パイプライン制御 (IF,ID,EX,MEM,WB)

load命令

まずload命令を実装しようと思う。理由としては内部レジスタに値を書き込まないと他の処理ができないからである。
load命令には以下のように6つに命令が分類される。

Screen Shot 2021-10-16 at 13.33.10.png

簡単に説明すると、

  • lb : データの下位8bitを符号拡張し、レジスタに書き込む
  • lh : データの下位16bitを符号拡張し、レジスタに書き込む
  • lw : データをそのまま(32bit)レジスタに書き込む
  • lbu : データの下位8bitを符号拡張せずにレジスタに書き込む
  • lhu : データの下位16bitを符号拡張せずにレジスタに書き込む

load命令の処理フローを以下に示す。

1.命令メモリから命令を取得

pc(プログラムカウンタ)で示すアドレスを命令メモリに与え、命令メモリから返ってきた値をFF(フリップフロップ)に保存。

2.命令をデコード

上記に示した命令ごとにレジスタアドレス、オペコード、即値に分解する。この時即値は符号拡張を行い32bitにする。
また、内部レジスタからrs1のアドレスに格納されているデータを出力させる。

3.レジスタの値と即値を用いてデータメモリのアドレスを指定

alu(内部計算機)を用いて、内部レジスタから出力された値と即値を加算し、外部メモリのアドレスを確定する。

4.指定先から得たデータを命令にそって分解

aluの結果を用いて外部メモリにアクセスし、メモリのデータを受け取る。
また、受信したデータを命令に沿って分解する。

5.指定のレジスタにデータを書き込む

命令のrdが示すレジスタアドレスに加工したデータを格納する。

回路図

次に上記で示した処理フローを簡単な回路図で示す。
load.png

コード

rv32iのインターフェースを以下に示す。基本的にはメモリとのやりとりを行うだけである。
また、parameter を設定している。

rv32i

module rv32i #(
    //  --------------------------------------------------------------------
    //  parameter declare
    //  --------------------------------------------------------------------
    parameter   MEMORY_S    =   2**8,
    parameter   OPCODE_W    =   7,
    parameter   SHAMT_W     =   5,
    parameter   OP          =   3,
    parameter   PC_W        =   8,
    parameter   REG_W       =   5,
    parameter   DATA_W      =   32,
    parameter   REG_S       =   32,
    parameter   FUNCT3      =   3,
    parameter   FUNCT7      =   7,
    parameter   IMM         =   32,
    parameter   BYTE        =   8,
    parameter   HALF        =   2*BYTE,
    parameter   WORD        =   4*BYTE,
    parameter   STORE_M     =   2
)(
    // input wire
    input wire                  clk,
    input wire                  n_rst,

    // input from instruction mem
    input wire  [DATA_W-1:0]    instruction,
    // output to instruction mem
    output wire [PC_W-1:0]      pc,

    // input from data mem
    input wire  [DATA_W-1:0]    d_in,

    // output to data mem
    output wire                 wr_en,
    output wire [STORE_M-1:0]   mode,
    output wire [PC_W-1:0]      wr_addr,
    output wire [PC_W-1:0]      rd_addr,
    output wire [DATA_W-1:0]    d_out
);

    reg [PC_W - 1:0]        pc_reg;
    reg                     pc_en_reg;
    reg [DATA_W - 1:0]      inst;
    wire                    r_we;

  
    assign pc    = pc_reg;

    //  --------------------------------------------------------------------
    //  Fetch STAGE
    //  --------------------------------------------------------------------


    always @(posedge clk or negedge n_rst) begin
        if (!n_rst) begin
            pc_reg <= 0;
        end else begin
            pc_reg <= pc_reg + 8'd4;
        end
    end

    always @(posedge clk or negedge n_rst) begin
        if (!n_rst) begin
            inst <= 0;
        end else begin
            inst <= instruction;
        end
    end


    //  --------------------------------------------------------------------
    //  Decode STAGE
    //  --------------------------------------------------------------------

    wire    [REG_W-1:0]     rs1,rs2,rd;
    wire    [DATA_W-1:0]    rdata1,rdata2;
    wire                    aluop;
    wire    [OPCODE_W-1:0]  opcode;
    wire    [FUNCT3-1:0]    funct3;
    wire    [IMM-1:0]       imm;
    wire    [19:0   ]       sext;

    reg     [REG_W-1:0]     rd_E;
    reg     [DATA_W-1:0]    rdata_E1,rdata_E2;
    reg                     aluop_E;
    reg     [OP-1:0]        funct3_E;
    reg     [IMM-1:0]       imm_E;
    reg     [OPCODE_W-1:0]  opcode_E;

    assign funct7   = inst[31:25];
    assign rs2      = inst[24:20];
    assign rs1      = inst[19:15];
    assign funct3   = inst[14:12];
    assign rd       = inst[11:7];
    assign opcode   = inst[6:0];
    assign aluop    = inst[30];
    assign sext     = {20{inst[31]}};
    assign imm      = {sext, funct7, rs2};
       
    rfile #(
        .REG_W(REG_W),
        .DATA_W(DATA_W),
        .REG_S(REG_S)
    )rfile(
        .clk(clk),
        .a1(rs1),       // read address 1
        .a2(rs2),       // read address 2
        .a3(rd_W),      // write address
        .rd1(rdata1),   // read data 1
        .rd2(rdata2),   // read data 2
        .wd(wd),        // write data
        .we(r_we)       // write enable
    );

    always @(posedge clk) begin
        rdata_E1    <= rdata1;
        rdata_E2    <= rdata2;
        rd_E        <= rd;
        funct3_E    <= funct3;
        aluop_E     <= aluop;
        opcode_E    <= opcode;
        imm_E       <= imm;
    end

    //  --------------------------------------------------------------------
    //  Execute STAGE
    //  --------------------------------------------------------------------

    reg     [DATA_W-1:0]    alu_res_M;
    reg     [REG_W-1:0]     rd_M;
    reg     [FUNCT3-1:0]    funct3_M;
    reg     [DATA_W-1:0]    rdata_M1,rdata_M2;
    reg     [OPCODE_W-1:0]  opcode_M;
    wire    [DATA_W-1:0]    alu_res;
    wire    [DATA_W-1:0]    in_a, in_b;
    wire    [FUNCT3-1:0]    s;
   

    assign beq      = (funct3_E == `OP_BEQ);
    assign bne      = (funct3_E == `OP_BNE);
    assign blt      = (funct3_E == `OP_BLT);
    assign bge      = (funct3_E == `OP_BGE);
    assign bltu     = (funct3_E == `OP_BLTU);
    assign bgeu     = (funct3_E == `OP_BGEU);
    assign s        = (opcode_E[4]) ? funct3_E : 0;
    assign in_a     = rdata_E1;
    assign in_b     = imm_E;

    alu #(
        .DATA_W(DATA_W),
        .SHAMT_W(SHAMT_W),
        .OP(OP)
    )alu(
        .a(in_a),
        .b(in_b),
        .s(s), // need 0
        .ext(aluop_E),
        .y(alu_res)
    );

    always @(posedge clk)begin
        alu_res_M   <= alu_res;
        rd_M        <= rd_E;
        funct3_M    <= funct3_E;
        opcode_M    <= opcode_E;
        rdata_M1    <= rdata_E1;
        rdata_M2    <= rdata_E2;
    end

    //  --------------------------------------------------------------------
    //  Memory STAGE
    //  --------------------------------------------------------------------

    wire    [DATA_W-1:0]    rd_data;

    reg     [OPCODE_W-1:0]  opcode_W;
    reg     [DATA_W-1:0]    rd_data_W;
    reg     [DATA_W-1:0]    alu_res_W;
    reg     [REG_W-1:0]     rd_W;

    assign rd_addr  = alu_res_M;
    assign rd_data  = rd_data_sel(funct3_M,d_in);

    function [DATA_W-1:0] rd_data_sel(
        input [FUNCT3-1:0] funct,
        input [DATA_W-1:0] data
    );
        case(funct)
            3'b000 : rd_data_sel = (data[7]) ? {24'hFFFFFF,data[7:0]}:{24'h0,data[7:0]};
            3'b001 : rd_data_sel = (data[15]) ? {16'hFFFF,data[15:0]}:{16'h0,data[15:0]};
            3'b010 : rd_data_sel = data;
            3'b100 : rd_data_sel = {24'h0,data[7:0]};
            3'b101 : rd_data_sel = {16'h0,data[15:0]};
            default: rd_data_sel = 32'h0;
        endcase
    endfunction


    always @(posedge clk) begin
        opcode_W    <= opcode_M;
        rd_data_W   <= rd_data;
        rd_W        <= rd_M;
        alu_res_W   <= alu_res_M;
    end

    //  --------------------------------------------------------------------
    //  Write Back STAGE
    //  --------------------------------------------------------------------

    wire [DATA_W-1:0]   wd;

    assign wd       = rd_data_W;
    assign r_we     = (opcode_W == `OP_LOAD);
endmodule

### 内部レジスタ
入力であるa1,a2,a3はレジスタのアドレスを指す。
また、risc-vでは内部レジスタの0番地をzero_registerとする仕様があるため、a1,a2で示すアドレスが0の場合出力として0を出力する。
書き込みに関しては、we(write enable)信号を観測し、入力データを内部レジスタに書き込む。

reg.v
module rfile #(
    parameter REG_W = -1,
    parameter REG_S = -1,
    parameter DATA_W = -1
)(
    input wire                  clk,
    input wire  [REG_W-1:0]     a1,a2,a3,
    output wire  [DATA_W-1:0]   rd1,rd2,
    input wire  [DATA_W-1:0]    wd,
    input wire                  we
);
    reg [DATA_W-1:0] rf [0:REG_S-1];

    //debug ---------------------
    wire [DATA_W-1:0] x1;
    assign x1 = rf[1];
    // -------------------------

    assign rd1 = |a1 == 0 ? 0 : rf[a1];
    assign rd2 = |a2 == 0 ? 0 : rf[a2];

    always @(posedge clk) begin
        if (we) begin
            rf[a3] <= wd;
        end
    end
endmodule

命令メモリ

i_mem.vは入力されたpc(プログラムカウンタ)に対するアドレスに保存されている命令を取得する。

i_mem.v
module i_mem #(
    parameter   M_WIDTH     =   -1,
    parameter   M_STACK     =   -1,
    parameter   DATA_W      =   -1,
    parameter   PC_WIDTH    =   -1,
    parameter   ADDR_WIDTH  =   -1
)(
    input wire                  clk,
    input wire                  n_rst,
    input wire  [PC_WIDTH-1:0]  rd_addr,
    output wire [DATA_W-1:0]    d_out
);

    reg     [ADDR_WIDTH-1:0] ram [0:M_STACK-1];

    initial $readmemb("mem.bin",ram);

    assign d_out    = {ram[rd_addr],ram[rd_addr+1],ram[rd_addr+2],ram[rd_addr+3]};

endmodule

データメモリ

d_mem.vに関しては今後Store命令の実装を考えてあらかじめStore命令用の仕様を採用しておく。
あらかじめ、外部からデータメモリの初期値を取り込み、あとは内部で変更していく仕組みである。
また、Store命令にも8bit,16bit,32bitアクセスが存在するため、modeによって書き込む値を変更している。

d_mem.v
module d_mem #(
    parameter   M_WIDTH     =   -1,
    parameter   M_STACK     =   -1,
    parameter   DATA_W      =   -1,
    parameter   PC_WIDTH    =   -1,
    parameter   STORE_M     =   -1,
    parameter   ADDR_WIDTH  =   -1
)(
    input wire                  clk,
    input wire                  n_rst,
    input wire                  wr_en,
    input wire  [PC_WIDTH-1:0]  rd_addr,
    input wire  [PC_WIDTH-1:0]  wr_addr,
    input wire  [STORE_M-1:0]   mode,
    input wire  [DATA_W-1:0]    d_in,
    output wire [DATA_W-1:0]    d_out
);
    localparam ST_B = 2'b00;
    localparam ST_H = 2'b01;
    localparam ST_W = 2'b10;

    reg     [ADDR_WIDTH-1:0] ram [0:M_STACK-1];


    initial $readmemb("data_mem.dat",ram);

    assign d_out    = {ram[rd_addr],ram[rd_addr+1],ram[rd_addr+2],ram[rd_addr+3]};

    always @(posedge clk) begin
        if (wr_en) begin
            if (mode == ST_B) begin
                ram[wr_addr] <= d_in[ADDR_WIDTH-1:0];
            end else if (mode == ST_H) begin
                {ram[wr_addr+1],ram[wr_addr]} <= {d_in[(ADDR_WIDTH*2)-1:ADDR_WIDTH],d_in[ADDR_WIDTH-1:0]};
            end else if (mode == ST_W) begin
                {ram[wr_addr+3],ram[wr_addr+2],ram[wr_addr+1],ram[wr_addr]} <= d_in;
            end else begin
                {ram[wr_addr],ram[wr_addr+1],ram[wr_addr+2],ram[wr_addr+3]} <= 32'hzzzzzzzz;
            end
        end
    end

endmodule

テストベンチ

テストベンチを行うには、命令メモリにバイナリコードを書き込む必要がある。(mem.binに書き込む)
命令をバイナリにする流れを示す。

  1. 擬似命令の記述
  2. バイナリに変換
  3. 1行8bitに変換する

こんな感じでバイナリファイルを作成する。
バイナリの変換などはpythonなどを用いて行ったら簡単かもしれない。
擬似コードを簡単に示す

** load **
lb  rd, rs1, offset
lh  rd, rs1, offset
lw  rd, rs1, offset
lbu rd, rs1, offset
lhu rd, rs1, offset

reg[rd] <= mem[offset+rs1]

** store **
sb rs2, rs1, offset
sh rs2, rs1, offset
sw rs2, rs1, offset

mem[offset+rs1] <= reg[rs2]

** cal **
add  rd, rs1, rs2
sub  rd, rs1, rs2
sll  rd, rs1, rs2
slt  rd, rs1, rs2
sltu rd, rs1, rs2
xor  rd, rs1, rs2
srl  rd, rs1, rs2
sra  rd, rs1, rs2
or   rd, rs1, rs2
and  rd, rs1, rs2

x[rd] <= x[rs1] + x[rs2]

** immediate **
addi  rd, rs1, imm
slti  rd, rs1, imm
sltiu rd, rs1, imm
xori  rd, rs1, imm
ori   rd, rs1, imm
andi  rd, rs1, imm

x[rd] <= x[rs1] + imm

** branch **
beq  rs1, rs2, offset
bne  rs1, rs2, offset
blt  rs1, rs2, offset
bge  rs1, rs2, offset
bltu rs1, rs2, offset
bgeu rs1, rs2, offset

そして、上記のload命令に対応したコードをシミュレーションする。
まず実行する命令を示す

lb x1, x0, 0
lh x1, x0, 0
lw x1, x0, 0

これは、データメモリの0番地にあるデータを8bit,16bit,32bitで順々に内部レジスタのx1が示すアドレスに保存していくと言う命令である。
次にデータメモリの初期値を示す

00000011  // 3
00000010  // 2
00000001  // 1
00000000  // 0
00000111
00000110
10000101
10000100
00001000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
   . 
   .
   .

実際に波形としてダンプしたものを示す。
Screen Shot 2021-10-17 at 13.40.31.png

実際にx1の信号線(下から3つめ)に0x00000000 -> 0x0000100 -> 0x03020100 が観測できている。
ちなみにx1の信号線は内部レジスタのx1を観測している信号である。

終わりに

今回から少しずつコードと回路図を更新していこうと思う。
あまりプロセッサなどの低レイヤーに興味がある人は少ないと思うが、やってみたら知識も深まるのでおもしろいとおもう。

もし間違ってるとこなどあれば指摘していただきたい。
まとまったコードは一通り実装した後にgithubで公開しようとおもう。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?