はじめに
新社会人として、某企業戦士となり少し落ち着いてきたので趣味の活動を進めていこうと思う。
今回は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つに命令が分類される。
簡単に説明すると、
- lb : データの下位8bitを符号拡張し、レジスタに書き込む
- lh : データの下位16bitを符号拡張し、レジスタに書き込む
- lw : データをそのまま(32bit)レジスタに書き込む
- lbu : データの下位8bitを符号拡張せずにレジスタに書き込む
- lhu : データの下位16bitを符号拡張せずにレジスタに書き込む
load命令の処理フローを以下に示す。
1.命令メモリから命令を取得
pc(プログラムカウンタ)で示すアドレスを命令メモリに与え、命令メモリから返ってきた値をFF(フリップフロップ)に保存。
2.命令をデコード
上記に示した命令ごとにレジスタアドレス、オペコード、即値に分解する。この時即値は符号拡張を行い32bitにする。
また、内部レジスタからrs1のアドレスに格納されているデータを出力させる。
3.レジスタの値と即値を用いてデータメモリのアドレスを指定
alu(内部計算機)を用いて、内部レジスタから出力された値と即値を加算し、外部メモリのアドレスを確定する。
4.指定先から得たデータを命令にそって分解
aluの結果を用いて外部メモリにアクセスし、メモリのデータを受け取る。
また、受信したデータを命令に沿って分解する。
5.指定のレジスタにデータを書き込む
命令のrdが示すレジスタアドレスに加工したデータを格納する。
回路図
コード
rv32iのインターフェースを以下に示す。基本的にはメモリとのやりとりを行うだけである。
また、parameter を設定している。
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)信号を観測し、入力データを内部レジスタに書き込む。
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(プログラムカウンタ)に対するアドレスに保存されている命令を取得する。
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によって書き込む値を変更している。
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行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
.
.
.
実際にx1の信号線(下から3つめ)に0x00000000 -> 0x0000100 -> 0x03020100 が観測できている。
ちなみにx1の信号線は内部レジスタのx1を観測している信号である。
終わりに
今回から少しずつコードと回路図を更新していこうと思う。
あまりプロセッサなどの低レイヤーに興味がある人は少ないと思うが、やってみたら知識も深まるのでおもしろいとおもう。
もし間違ってるとこなどあれば指摘していただきたい。
まとまったコードは一通り実装した後にgithubで公開しようとおもう。