C拡張の実装
概要
これまでに実装した命令はすべて32ビット幅のものでした。 RISC-Vには32ビット幅以外の命令が定義されており、 命令の下位ビットで何ビット幅の命令か判断できます(表1)。
表13.1: RISC-Vの命令長とエンコーディング
| 命令幅 | 命令の下位5ビット |
|---|---|
| 16-bit (aa≠11) | xxxaa |
| 32-bit (bbb≠111) | bbb11 |
全てのRVC命令には同じ操作をする32ビット幅の命令が存在します[1]。
RVC命令は図1の9つのフォーマットが定義されています。

rs1'、rs2'、rd'は3ビットのフィールドで、 よく使われる8番(x8)から15番(x15)のレジスタを指定します。 即値の並び方やそれぞれの命令の具体的なフォーマットについては、 仕様書か「13.6.2 32ビット幅の命令に変換する」のコードを参照してください。
RV64IのCPUに実装されるC拡張には表2のRVC命令が定義されています。
表13.2: C拡張の命令
| 命令 | 同じ意味の32ビット幅の命令 | 形式 |
|---|---|---|
| C.LWSP | lw rd, offset(x2) | CI |
| C.LDSP | ld rd, offset(x2) | CI |
| C.SWSP | sw rs2, offset(x2) | CSS |
| C.SDSP | sd rs2, offset(x2) | CSS |
| C.LW | lw rd, offset(rs) | CL |
| C.LD | ld rd, offset(rs) | CL |
| C.SW | sw rs2, offset(rs1) | CS |
| C.SD | sd rs2, offset(rs1) | CS |
| C.J | jal x0, offset | CJ |
| C.JR | jalr x0, 0(rs1) | CR |
| C.JALR | jalr x1, 0(rs1) | CR |
| C.BEQZ | beq rs1, x0, offset | CB |
| C.BNEZ | bne rs1, x0, offset | CB |
| C.LI | addi rd, x0, imm | CI |
| C.LUI | lui rd, imm | CI |
| C.ADDI | addi rd, rd, imm | CI |
| C.ADDIW | addiw rd, rd, imm | CI |
| C.ADDI16SP | addi x2, x2, imm | CI |
| C.ADDI4SPN | addi rd, x2, imm | CIW |
| C.SLLI | slli rd, rd, shamt | CI |
| C.SRLI | srli rd, rd, shamt | CB |
| C.SRAI | srai rd, rd, shamt | CB |
| C.ANDI | andi rd, rd, imm | CB |
| C.MV | add rd, x0, rs2 | CR |
| C.ADD | add rd, rd, rs2 | CR |
| C.AND | and rd, rd, rs2 | CA |
| C.OR | or rd, rd, rs2 | CA |
| C.XOR | xor rd, rd, rs2 | CA |
| C.SUB | sub rd, rd, rs2 | CA |
| C.EBREAK | ebreak | CR |
C拡張は浮動小数点命令をサポートするF、D拡張が実装されている場合に他の命令を定義しますが、 基本編ではF、D拡張を実装しないため実装、解説しません。
IALIGNの変更
「10.5 命令アドレスのミスアライン例外」で解説したように、 命令はIALIGNビットに整列したアドレスに配置されます。 C拡張はIALIGNによる制限を16ビットに緩め、全ての命令が16ビットに整列されたアドレスに配置されるように変更します。 これにより、RVC命令と32ビット幅の命令の組み合わせがあったとしても効果的にコードサイズを削減できます。
eeiパッケージに定数IALIGNを定義します (リスト1)。
▼リスト13.1: IALIGNの定義 (eei.veryl) 差分をみる
const IALIGN: u32 = 16;
mepcレジスタの書き込みマスクを変更して、 トラップ時のジャンプ先アドレスに16ビットに整列されたアドレスを指定できるようにします (リスト2)。
▼リスト13.2: MEPCの書き込みマスクを変更する (eei.veryl) 差分をみる
const MEPC_WMASK : UIntX = 'hffff_ffff_ffff_fffe;
命令アドレスのミスアライン例外の判定を変更します。 IALIGNが16の場合は例外が発生しないようにします (リスト3)。 ジャンプ、分岐命令は2バイト単位のアドレスしか指定できないため、 C拡張が実装されている場合には例外が発生しません。
▼リスト13.3: IALIGNが16のときに例外が発生しないようにする (core.veryl) 差分をみる
let instruction_address_misaligned: logic = IALIGN == 32 && memq_wdata.br_taken && memq_wdata.jump_addr[1:0] != 2'b00;
実装方針
本章では次の順序でC拡張を実装します。
- 命令フェッチ処理(IFステージ)をcoreモジュールから分離する
- 16ビットに整列されたアドレスに配置された32ビット幅の命令を処理できるようにする
- RVC命令を32ビット幅の命令に変換するモジュールを作成する
- RVC命令を32ビット幅の命令に変換してcoreモジュールに供給する
最終的な命令フェッチ処理の構成は図図2のようになります。

命令フェッチモジュールの実装
インターフェースを作成する
まず、命令フェッチを行うモジュールとcoreモジュールのインターフェースを定義します。
src/core_inst_if.verylを作成し、次のように記述します (リスト4)。
▼リスト13.4: core_inst_if.veryl 差分をみる
import eei::*;
interface core_inst_if {
var rvalid : logic;
var rready : logic;
var raddr : Addr ;
var rdata : Inst ;
var is_hazard: logic;
var next_pc : Addr ;
modport master {
rvalid : input ,
rready : output,
raddr : input ,
rdata : input ,
is_hazard: output, // control hazard
next_pc : output, // actual next pc
}
modport slave {
..converse(master)
}
}
rvalid、rready、raddr、rdataは、 coreモジュールのFIFO(if_fifo)のwvalid、wready、wdata.addr、wdata.bitsと同じ役割を果たします。 is_hazard、next_pcは制御ハザードの情報を伝えるための変数です。
coreモジュールのIFステージを削除する
coreモジュールのIFステージを削除し、 core_inst_ifインターフェースで代替します[2]。
coreモジュールのi_membusの型をcore_inst_ifに変更します (リスト5)。
▼リスト13.5: i_membusの型を変更する (core.veryl) 差分をみる
i_membus: modport core_inst_if::master,
IFステージ部分のコードを次のように変更します (リスト6)。
▼リスト13.6: IFステージの変更 (core.veryl) 差分をみる
///////////////////////////////// IF Stage /////////////////////////////////
var control_hazard : logic;
var control_hazard_pc_next: Addr ;
always_comb {
i_membus.is_hazard = control_hazard;
i_membus.next_pc = control_hazard_pc_next;
}
coreモジュールの新しいIFステージ部分は、制御ハザードの情報をインターフェースに割り当てるだけの簡単なものになっています。 if_fifo_type型、if_fifo_から始まる変数は使わなくなったので削除してください。
IDステージとcore_inst_ifインターフェースを接続します (リスト7、 リスト8)。 もともとif_fifoのrvalid、rready、rdataだった部分をi_membusに変更しています。
▼リスト13.7: IDステージとi_membusを接続する (core.veryl) 差分をみる
let ids_valid : logic = i_membus.rvalid;
let ids_pc : Addr = i_membus.raddr;
let ids_inst_bits : Inst = i_membus.rdata;
▼リスト13.8: EXステージに進められるときにrreadyを1にする (core.veryl) 差分をみる
always_comb {
// ID -> EX
i_membus.rready = exq_wready;
exq_wvalid = i_membus.rvalid;
exq_wdata.addr = i_membus.raddr;
exq_wdata.bits = i_membus.rdata;
exq_wdata.ctrl = ids_ctrl;
exq_wdata.imm = ids_imm;
inst_fetcherモジュールを作成する
IFステージの代わりに命令フェッチをするinst_fetcherモジュールを作成します。 inst_fetcherモジュールでは命令フェッチ処理をfetch、issueの2段階で行います。
- fetch
- メモリから64ビットの値を読み込み、issueとの間のFIFOに格納する。 アドレスを`8`進めて、次の64ビットを読み込む。
- issue
- fetchとの間のFIFOから64ビットを読み込み、 32ビットずつcoreモジュールとの間のFIFOに格納する。
fetchとissueは並列に独立して動かします。
inst_fetcherモジュールのポートを定義します。 src/inst_fetcher.verylを作成し、次のように記述します (リスト9)。
▼リスト13.9: ポートの定義 (inst_fetcher.veryl) 差分をみる
module inst_fetcher (
clk : input clock ,
rst : input reset ,
core_if: modport core_inst_if::slave,
mem_if : modport Membus::master ,
) {
core_ifはcoreモジュールとのインターフェース、 mem_ifはメモリとのインターフェースです。
fetchとissue、issueとcore_ifの間のFIFOを作成します (リスト10、 リスト11)。
▼リスト13.10: fetchとissueを繋ぐFIFOの作成 (inst_fetcher.veryl) 差分をみる
struct fetch_fifo_type {
addr: Addr ,
bits: logic<MEMBUS_DATA_WIDTH>,
}
var fetch_fifo_flush : logic ;
var fetch_fifo_wvalid: logic ;
var fetch_fifo_wready: logic ;
var fetch_fifo_wdata : fetch_fifo_type;
var fetch_fifo_rdata : fetch_fifo_type;
var fetch_fifo_rready: logic ;
var fetch_fifo_rvalid: logic ;
inst fetch_fifo: fifo #(
DATA_TYPE: fetch_fifo_type,
WIDTH : 3 ,
) (
clk ,
rst ,
flush : fetch_fifo_flush ,
wready : _ ,
wready_two: fetch_fifo_wready,
wvalid : fetch_fifo_wvalid,
wdata : fetch_fifo_wdata ,
rready : fetch_fifo_rready,
rvalid : fetch_fifo_rvalid,
rdata : fetch_fifo_rdata ,
);
▼リスト13.11: issueとcoreモジュールを繋ぐFIFOの作成 (inst_fetcher.veryl) 差分をみる
struct issue_fifo_type {
addr: Addr,
bits: Inst,
}
var issue_fifo_flush : logic ;
var issue_fifo_wvalid: logic ;
var issue_fifo_wready: logic ;
var issue_fifo_wdata : issue_fifo_type;
var issue_fifo_rdata : issue_fifo_type;
var issue_fifo_rready: logic ;
var issue_fifo_rvalid: logic ;
inst issue_fifo: fifo #(
DATA_TYPE: issue_fifo_type,
WIDTH : 3 ,
) (
clk ,
rst ,
flush : issue_fifo_flush ,
wready: issue_fifo_wready,
wvalid: issue_fifo_wvalid,
wdata : issue_fifo_wdata ,
rready: issue_fifo_rready,
rvalid: issue_fifo_rvalid,
rdata : issue_fifo_rdata ,
);
メモリへのアクセス処理(fetch)を実装します。 FIFOに空きがあるとき、64ビットの値を読み込んでPCを8進めます (リスト12、 リスト13、 リスト14)。 この処理はcoreモジュールの元のIFステージとほとんど同じです。
▼リスト13.12: PCと状態管理用の変数の定義 (inst_fetcher.veryl) 差分をみる
var fetch_pc : Addr ;
var fetch_requested : logic;
var fetch_pc_requested: Addr ;
▼リスト13.13: メモリへの要求の割り当て (inst_fetcher.veryl) 差分をみる
always_comb {
mem_if.valid = 0;
mem_if.addr = 0;
mem_if.wen = 0;
mem_if.wdata = 0;
mem_if.wmask = 0;
if !core_if.is_hazard {
mem_if.valid = fetch_fifo_wready;
if fetch_requested {
mem_if.valid = mem_if.valid && mem_if.rvalid;
}
mem_if.addr = fetch_pc;
}
}
▼リスト13.14: PC、状態の更新 (inst_fetcher.veryl) 差分をみる
always_ff {
if_reset {
fetch_pc = INITIAL_PC;
fetch_requested = 0;
fetch_pc_requested = 0;
} else {
if core_if.is_hazard {
fetch_pc = {core_if.next_pc[XLEN - 1:3], 3'b0};
fetch_requested = 0;
fetch_pc_requested = 0;
} else {
if fetch_requested {
if mem_if.rvalid {
fetch_requested = mem_if.ready && mem_if.valid;
if mem_if.ready && mem_if.valid {
fetch_pc_requested = fetch_pc;
fetch_pc += 8;
}
}
} else {
if mem_if.ready && mem_if.valid {
fetch_requested = 1;
fetch_pc_requested = fetch_pc;
fetch_pc += 8;
}
}
}
}
}
メモリから読み込んだ値をissueとの間のFIFOに格納します (リスト15)。
▼リスト13.15: ロードした64ビットの値をFIFOに格納する (inst_fetcher.veryl) 差分をみる
// memory -> fetch_fifo
always_comb {
fetch_fifo_flush = core_if.is_hazard;
fetch_fifo_wvalid = fetch_requested && mem_if.rvalid;
fetch_fifo_wdata.addr = fetch_pc_requested;
fetch_fifo_wdata.bits = mem_if.rdata;
}
coreモジュールに命令を供給する処理(issue)を実装します。 FIFOにデータが入っているとき、32ビットずつcoreモジュールとの間のFIFOに格納します。 2つの32ビットの命令をFIFOに格納出来たら、fetchとの間のFIFOを読み進めます (リスト16、 リスト17)。
▼リスト13.16: オフセットの更新 (inst_fetcher.veryl) 差分をみる
var issue_pc_offset: logic<3>;
always_ff {
if_reset {
issue_pc_offset = 0;
} else {
if core_if.is_hazard {
issue_pc_offset = core_if.next_pc[2:0];
} else {
if issue_fifo_wready && issue_fifo_wvalid {
issue_pc_offset += 4;
}
}
}
}
▼リスト13.17: issue_fifoに32ビットずつ命令を格納する (inst_fetcher.veryl) 差分をみる
// fetch_fifo <-> issue_fifo
always_comb {
let raddr : Addr = fetch_fifo_rdata.addr;
let rdata : logic<MEMBUS_DATA_WIDTH> = fetch_fifo_rdata.bits;
let offset: logic<3> = issue_pc_offset;
fetch_fifo_rready = 0;
issue_fifo_wvalid = 0;
issue_fifo_wdata = 0;
if !core_if.is_hazard && fetch_fifo_rvalid {
if issue_fifo_wready {
fetch_fifo_rready = offset == 4;
issue_fifo_wvalid = 1;
issue_fifo_wdata.addr = {raddr[msb:3], offset};
issue_fifo_wdata.bits = case offset {
0 : rdata[31:0],
4 : rdata[63:32],
default: 0,
};
}
}
}
core_ifとFIFOを接続します (リスト18)。
▼リスト13.18: issue_fifoとインターフェースを接続する (inst_fetcher.veryl) 差分をみる
// issue_fifo <-> core
always_comb {
issue_fifo_flush = core_if.is_hazard;
issue_fifo_rready = core_if.rready;
core_if.rvalid = issue_fifo_rvalid;
core_if.raddr = issue_fifo_rdata.addr;
core_if.rdata = issue_fifo_rdata.bits;
}
inst_fetcherモジュールとcoreモジュールを接続する
topモジュールで、core_inst_ifをインスタンス化します。 (リスト19)。
▼リスト13.19: インターフェースの定義 (top.veryl) 差分をみる
inst i_membus_core: core_inst_if;
inst_fetcherモジュールをインスタンス化し、coreモジュールと接続します ( リスト20、 リスト21 )。
▼リスト13.20: inst_fetcherモジュールのインスタンス化 (top.veryl) 差分をみる
inst fetcher: inst_fetcher (
clk ,
rst ,
core_if: i_membus_core,
mem_if : i_membus ,
);
▼リスト13.21: インターフェースを変更する (top.veryl) 差分をみる
inst c: core (
clk ,
rst ,
i_membus: i_membus_core,
d_membus: d_membus_core,
led ,
);
inst_fetcherモジュールが64ビットのデータを32ビットの命令の列に変換してくれるようになったので、 d_membusとの調停のところで32ビットずつ選択する必要がなくなりました。 そのため、rdataをそのまま割り当てて、memarb_last_iaddr変数とビットの選択処理を削除します (リスト22、 リスト23、 リスト24)。
▼リスト13.22: 使用しない変数を削除する (top.veryl) 差分をみる
var memarb_last_i: logic;
var memarb_last_iaddr: Addr;
▼リスト13.23: 使用しない変数を削除する (top.veryl) 差分をみる
always_ff {
if_reset {
memarb_last_i = 0;
memarb_last_i = 0;
} else {
if mmio_membus.ready {
memarb_last_i = !d_membus.valid;
memarb_last_iaddr = i_membus.addr;
}
}
}
▼リスト13.24: ビットの選択処理を削除する (top.veryl) 差分をみる
always_comb {
i_membus.ready = mmio_membus.ready && !d_membus.valid;
i_membus.rvalid = mmio_membus.rvalid && memarb_last_i;
i_membus.rdata = mmio_membus.rdata;
16ビット境界に配置された32ビット幅の命令のサポート
inst_fetcherモジュールで、アドレスが2バイトの倍数の32ビット幅の命令をcoreモジュールに供給できるようにします。
アドレスの下位3ビット(issue_pc_offset)が6の場合、 issueとcoreの間に供給する命令のビット列はfetch_fifo_rdataの上位16ビットとfetch_fifoに格納されている次のデータの下位16ビットを結合したものになります。 このとき、fetch_fifo_rdataのデータの下位16ビットとアドレスを保存して、次のデータを読み出します。 fetch_fifoから次のデータを読み出せたら、保存していたデータと結合し、アドレスとともにissue_fifoに書き込みます。 issue_pc_offsetが0、2、4の場合、既存の処理との変更点はありません。
fetch_fifo_rdataのデータの下位16ビットとアドレスを保持する変数を作成します (リスト25)。
▼リスト13.25: データを一時保存するための変数の定義 (inst_fetcher.veryl) 差分をみる
var issue_is_rdata_saved: logic ;
var issue_saved_addr : Addr ;
var issue_saved_bits : logic<16>; // rdata[63:48]
issue_pc_offsetが6のとき、変数にデータを保存します (リスト26)。
▼リスト13.26: offsetが6のとき、変数に命令の下位16ビットとアドレスを保存する (inst_fetcher.veryl) 差分をみる
always_ff {
if_reset {
issue_pc_offset = 0;
issue_is_rdata_saved = 0;
issue_saved_addr = 0;
issue_saved_bits = 0;
} else {
if core_if.is_hazard {
issue_pc_offset = core_if.next_pc[2:0];
issue_is_rdata_saved = 0;
} else {
// offsetが6な32ビット命令の場合、
// アドレスと上位16ビットを保存してFIFOを読み進める
if issue_pc_offset == 6 && !issue_is_rdata_saved {
if fetch_fifo_rvalid {
issue_is_rdata_saved = 1;
issue_saved_addr = fetch_fifo_rdata.addr;
issue_saved_bits = fetch_fifo_rdata.bits[63:48];
}
} else {
if issue_fifo_wready && issue_fifo_wvalid {
issue_pc_offset += 4;
issue_is_rdata_saved = 0;
}
}
}
}
}
issue_pc_offsetが2、6の場合のissue_fifoへの書き込みを実装します (リスト27)。 6の場合、保存していた16ビットと新しく読み出した16ビットを結合した値、保存していたアドレスを書き込みます。
▼リスト13.27: issue_fifoにoffsetが2、6の命令を格納する (inst_fetcher.veryl) 差分をみる
if !core_if.is_hazard && fetch_fifo_rvalid {
if issue_fifo_wready {
if offset == 6 {
// offsetが6な32ビット命令の場合、
// 命令は{rdata_next[15:0], rdata[63:48}になる
if issue_is_rdata_saved {
issue_fifo_wvalid = 1;
issue_fifo_wdata.addr = {issue_saved_addr[msb:3], offset};
issue_fifo_wdata.bits = {rdata[15:0], issue_saved_bits};
} else {
// Read next 8 bytes
fetch_fifo_rready = 1;
}
} else {
fetch_fifo_rready = offset == 4;
issue_fifo_wvalid = 1;
issue_fifo_wdata.addr = {raddr[msb:3], offset};
issue_fifo_wdata.bits = case offset {
0 : rdata[31:0],
2 : rdata[47:16],
4 : rdata[63:32],
default: 0,
};
}
}
}
32ビット幅の命令の下位16ビットが既に保存されている(issue_is_rdata_savedが1)とき、 fetch_fifoから供給されるデータには、 32ビット幅の命令の上位16ビットを除いた残りの48ビットが含まれているので fetch_fifo_rreadyを1に設定しないことに注意してください。
RVC命令の変換
RVC命令フラグの実装
RVC命令を32ビット幅の命令に変換するモジュールを作る前に、 RVC命令かどうかを示すフラグを作成します。
まず、core_inst_ifインターフェースとInstCtrl構造体にis_rvcフラグを追加します (リスト28、 リスト29、 リスト30)。
▼リスト13.28: is_rvcフラグの定義 (core_inst_if.veryl) 差分をみる
var rdata : Inst ;
var is_rvc : logic;
var is_hazard: logic;
▼リスト13.29: modportにis_rvcを追加する (core_inst_if.veryl) 差分をみる
modport master {
rvalid : input ,
rready : output,
raddr : input ,
rdata : input ,
is_rvc : input ,
is_hazard: output, // control hazard
next_pc : output, // actual next pc
}
▼リスト13.30: InstCtrl型にis_rvcフラグを追加する (corectrl.veryl) 差分をみる
is_amo : logic , // AMO instruction
is_rvc : logic , // RVC instruction
funct3 : logic <3>, // 命令のfunct3フィールド
inst_fetcherモジュールで、is_rvcを0に設定してcoreモジュールに供給します (リスト31、 リスト32、 リスト33)。
▼リスト13.31: issue_fifo_type型にis_rvcフラグを追加する (inst_fetcher.veryl) 差分をみる
struct issue_fifo_type {
addr : Addr ,
bits : Inst ,
is_rvc: logic,
}
▼リスト13.32: is_rvcフラグを0に設定する (inst_fetcher.veryl) 差分をみる
if offset == 6 {
// offsetが6な32ビット命令の場合、
// 命令は{rdata_next[15:0], rdata[63:48}になる
if issue_is_rdata_saved {
issue_fifo_wvalid = 1;
issue_fifo_wdata.addr = {issue_saved_addr[msb:3], offset};
issue_fifo_wdata.bits = {rdata[15:0], issue_saved_bits};
issue_fifo_wdata.is_rvc = 0;
} else {
// Read next 8 bytes
fetch_fifo_rready = 1;
}
} else {
fetch_fifo_rready = offset == 4;
issue_fifo_wvalid = 1;
issue_fifo_wdata.addr = {raddr[msb:3], offset};
issue_fifo_wdata.bits = case offset {
0 : rdata[31:0],
2 : rdata[47:16],
4 : rdata[63:32],
default: 0,
};
issue_fifo_wdata.is_rvc = 0;
}
▼リスト13.33: is_rvcフラグを接続する (inst_fetcher.veryl) 差分をみる
always_comb {
issue_fifo_flush = core_if.is_hazard;
issue_fifo_rready = core_if.rready;
core_if.rvalid = issue_fifo_rvalid;
core_if.raddr = issue_fifo_rdata.addr;
core_if.rdata = issue_fifo_rdata.bits;
core_if.is_rvc = issue_fifo_rdata.is_rvc;
}
inst_decoderモジュールで、InstCtrl構造体のis_rvcフラグを設定します (リスト34、 リスト35、 リスト36)。 また、C拡張が無効なのにRVC命令が供給されたらvalidフラグを0に設定します。
▼リスト13.34: is_rvcフラグをポートに追加する (inst_decoder.veryl) 差分をみる
module inst_decoder (
bits : input Inst ,
is_rvc: input logic ,
valid : output logic ,
ctrl : output InstCtrl,
imm : output UIntX ,
) {
▼リスト13.35: InstCtrlにis_rvcフラグを設定する (inst_decoder.veryl) 差分をみる
default: {
InstType::X, F, F, F, F, F, F, F, F, F
},
}, is_rvc, f3, f7
};
▼リスト13.36: IALIGNが32ではないとき、不正な命令にする (inst_decoder.veryl) 差分をみる
OP_AMO : f3 == 3'b010 || f3 == 3'b011, // AMO
default : F,
} && (IALIGN == 16 || !is_rvc); // IALIGN == 32のとき、C拡張は無効
coreモジュールで、inst_decoderモジュールにis_rvcフラグを渡します (リスト37)。
▼リスト13.37: is_rvcフラグをinst_decoderに渡す (core.veryl) 差分をみる
inst decoder: inst_decoder (
bits : ids_inst_bits ,
is_rvc: i_membus.is_rvc,
valid : ids_inst_valid ,
ctrl : ids_ctrl ,
imm : ids_imm ,
);
ジャンプ命令でライトバックする値は次の命令のアドレスであるため、 RVC命令の場合はPCに2を足した値を設定します (リスト38)。
▼リスト13.38: 次の命令のアドレスを変える (core.veryl) 差分をみる
let wbs_wb_data: UIntX = switch {
wbs_ctrl.is_lui : wbs_imm,
wbs_ctrl.is_jump : wbs_pc + (if wbs_ctrl.is_rvc ? 2 : 4),
wbs_ctrl.is_load || wbs_ctrl.is_amo: wbq_rdata.mem_rdata,
wbs_ctrl.is_csr : wbq_rdata.csr_rdata,
default : wbq_rdata.alu_result
};
32ビット幅の命令に変換する
RVC命令のopcode、functなどのフィールドを読んで、 32ビット幅の命令を生成するrvc_converterモジュールを実装します。
その前に、命令のフィールドを引数に32ビット幅の命令を生成する関数を実装します。 src/inst_gen_pkg.verylを作成し、次のように記述します (リスト39)。 関数の名前は基本的に命令名と同じにしていますが、 Verylのキーワードと被るものはinst_をprefixにしています。
▼リスト13.39: 命令のビット列を生成する関数を定義する (inst_gen_pkg.veryl)
import eei::*;
package inst_gen_pkg {
function add (rd: input logic<5>, rs1: input logic<5>, rs2: input logic<5>) -> Inst {
return {7'b0000000, rs2, rs1, 3'b000, rd, OP_OP};
}
function addw (rd: input logic<5>, rs1: input logic<5>, rs2: input logic<5>) -> Inst {
return {7'b0000000, rs2, rs1, 3'b000, rd, OP_OP_32};
}
function addi (rd : input logic<5> , rs1: input logic<5> , imm: input logic<12>) -> Inst {
return {imm, rs1, 3'b000, rd, OP_OP_IMM};
}
function addiw (rd: input logic<5> ,rs1: input logic<5>, imm: input logic<12>) -> Inst {
return {imm, rs1, 3'b000, rd, OP_OP_IMM_32};
}
function sub (rd: input logic<5>,rs1: input logic<5>, rs2: input logic<5>) -> Inst {
return {7'b0100000, rs2, rs1, 3'b000, rd, OP_OP};
}
function subw (rd: input logic<5>, rs1: input logic<5>, rs2: input logic<5>) -> Inst {
return {7'b0100000, rs2, rs1, 3'b000, rd, OP_OP_32};
}
function inst_xor (rd: input logic<5>, rs1: input logic<5>, rs2: input logic<5>) -> Inst {
return {7'b0000000, rs2, rs1, 3'b100, rd, OP_OP};
}
function inst_or (rd: input logic<5>, rs1: input logic<5>, rs2: input logic<5>) -> Inst {
return {7'b0000000, rs2, rs1, 3'b110, rd, OP_OP};
}
function inst_and (rd: input logic<5>, rs1: input logic<5>, rs2: input logic<5>) -> Inst {
return {7'b0000000, rs2, rs1, 3'b111, rd, OP_OP};
}
function andi (rd: input logic<5> , rs1: input logic<5>, imm: input logic<12>) -> Inst {
return {imm, rs1, 3'b111, rd, OP_OP_IMM};
}
function slli (rd: input logic<5>, rs1: input logic<5>, shamt: input logic<6>) -> Inst {
return {6'b000000, shamt, rs1, 3'b001, rd, OP_OP_IMM};
}
function srli (rd: input logic<5>, rs1: input logic<5>, shamt: input logic<6>) -> Inst {
return {6'b000000, shamt, rs1, 3'b101, rd, OP_OP_IMM};
}
function srai (rd: input logic<5>, rs1: input logic<5>, shamt: input logic<6>) -> Inst {
return {6'b010000, shamt, rs1, 3'b101, rd, OP_OP_IMM};
}
function lui (rd: input logic<5>, imm: input logic<20>) -> Inst {
return {imm, rd, OP_LUI};
}
function load (rd: input logic<5> ,rs1: input logic<5>, imm: input logic<12>, funct3: input logic<3>) -> Inst {
return {imm, rs1, funct3, rd, OP_LOAD};
}
function store (rs1: input logic<5>, rs2: input logic<5>, imm: input logic<12>, funct3: input logic<3>) -> Inst {
return {imm[11:5], rs2, rs1, funct3, imm[4:0], OP_STORE};
}
function jal (rd : input logic<5>, imm: input logic<20>) -> Inst {
return {imm[19], imm[9:0], imm[10], imm[18:11], rd, OP_JAL};
}
function jalr (rd: input logic<5>, rs1: input logic<5>, imm: input logic<12>) -> Inst {
return {imm, rs1, 3'b000, rd, OP_JALR};
}
function beq (rs1: input logic<5>, rs2: input logic<5>, imm: input logic<12>) -> Inst {
return {imm[11], imm[9:4], rs2, rs1, 3'b000, imm[3:0], imm[10], OP_BRANCH};
}
function bne (rs1: input logic<5>, rs2: input logic<5>, imm: input logic<12>) -> Inst {
return {imm[11], imm[9:4], rs2, rs1, 3'b001, imm[3:0], imm[10], OP_BRANCH};
}
function ebreak () -> Inst {
return 32'h00100073;
}
}
rvc_conveterモジュールのポートを定義します。 src/rvc_converter.verylを作成し、次のように記述します (リスト40)。
▼リスト13.40: ポートの定義 (rvc_converter.veryl) 差分をみる
import eei::*;
import inst_gen_pkg::*;
module rvc_converter (
inst16: input logic<16>,
is_rvc: output logic ,
inst32: output Inst , // expanded inst16
) {
rvc_converterモジュールは、inst16で16ビットの値を受け取り、 それがRVC命令ならis_rvcを1にして、 inst32に同じ意味の32ビット幅の命令を出力する組み合わせ回路です。
inst16からソースレジスタ番号を生成します (リスト41)。 rs1d、rs2dの番号の範囲はx8からx15です。
▼リスト13.41: レジスタ番号の生成 (rvc_converter.veryl) 差分をみる
let rs1 : logic<5> = inst16[11:7];
let rs2 : logic<5> = inst16[6:2];
let rs1d: logic<5> = {2'b01, inst16[9:7]};
let rs2d: logic<5> = {2'b01, inst16[4:2]};
inst16から即値を生成します (リスト42)。
▼リスト13.42: 即値の生成 (rvc_converter.veryl) 差分をみる
let imm_i : logic<12> = {inst16[12] repeat 7, inst16[6:2]};
let imm_shamt: logic<6> = {inst16[12], inst16[6:2]};
let imm_j : logic<20> = {inst16[12] repeat 10, inst16[8], inst16[10:9], inst16[6], inst16[7], inst16[2], inst16[11], inst16[5:3]};
let imm_br : logic<12> = {inst16[12] repeat 5, inst16[6:5], inst16[2], inst16[11:10], inst16[4:3]};
let c0_mem_w : logic<12> = {5'b0, inst16[5], inst16[12:10], inst16[6], 2'b0}; // C.LW, C.SW
let c0_mem_d : logic<12> = {4'b0, inst16[6:5], inst16[12:10], 3'b0}; // C.LD, C.SD
inst16から32ビット幅の命令を生成します (リスト43)。 opcode(inst16[1:0])が2'b11以外なら16ビット幅の命令なので、 is_rvcに1を割り当てます。 inst32には、初期値として右にinst16を詰めてゼロで拡張した値を割り当てます。
32ビット幅の命令への変換はopcode、funct、レジスタ番号などで分岐して地道に実装します。 32ビット幅の命令に変換できないときinst32の値を更新しません。
inst16が不正なRVC命令のとき、 inst_decoderモジュールでデコードできない命令をcoreモジュールに供給してIllegal instruction例外を発生させ、 tvalに16ビット幅の不正な命令が設定されます。
▼リスト13.43: RVC命令を32ビット幅の命令に変換する (rvc_converter.veryl) 差分をみる
always_comb {
is_rvc = inst16[1:0] != 2'b11;
inst32 = {16'b0, inst16};
let funct3: logic<3> = inst16[15:13];
case inst16[1:0] { // opcode
2'b00: case funct3 { // C0
3'b000: if inst16 != 0 { // C.ADDI4SPN
let nzuimm: logic<10> = {inst16[10:7], inst16[12:11], inst16[5], inst16[6], 2'b0};
inst32 = addi(rs2d, 2, {2'b0, nzuimm});
}
3'b010: inst32 = load(rs2d, rs1d, c0_mem_w, 3'b010); // C.LW
3'b011: if XLEN >= 64 { // C.LD
inst32 = load(rs2d, rs1d, c0_mem_d, 3'b011);
}
3'b110: inst32 = store(rs1d, rs2d, c0_mem_w, 3'b010); // C.SW
3'b111: if XLEN >= 64 { // C.SD
inst32 = store(rs1d, rs2d, c0_mem_d, 3'b011);
}
default: {}
}
2'b01: case funct3 { // C1
3'b000: inst32 = addi(rs1, rs1, imm_i); // C.ADDI
3'b001: inst32 = if XLEN == 32 ? jal(1, imm_j) : addiw(rs1, rs1, imm_i); // C.JAL / C.ADDIW
3'b010: inst32 = addi(rs1, 0, imm_i); // C.LI
3'b011: if rs1 == 2 { // C.ADDI16SP
let imm : logic<10> = {inst16[12], inst16[4:3], inst16[5], inst16[2], inst16[6], 4'b0};
inst32 = addi(2, 2, {imm[msb] repeat 2, imm});
} else { // C.LUI
inst32 = lui(rs1, {imm_i[msb] repeat 8, imm_i});
}
3'b100: case inst16[11:10] { // funct2 or funct6[1:0]
2'b00: if !(XLEN == 32 && imm_shamt[msb] == 1) {
inst32 = srli(rs1d, rs1d, imm_shamt); // C.SRLI
}
2'b01: if !(XLEN == 32 && imm_shamt[msb] == 1) {
inst32 = srai(rs1d, rs1d, imm_shamt); // C.SRAI
}
2'b10: inst32 = andi(rs1d, rs1d, imm_i); // C.ADNI
2'b11: if inst16[12] == 0 {
case inst16[6:5] {
2'b00 : inst32 = sub(rs1d, rs1d, rs2d); // C.SUB
2'b01 : inst32 = inst_xor(rs1d, rs1d, rs2d); // C.XOR
2'b10 : inst32 = inst_or(rs1d, rs1d, rs2d); // C.OR
2'b11 : inst32 = inst_and(rs1d, rs1d, rs2d); // C.AND
default: {}
}
} else {
if XLEN >= 64 {
if inst16[6:5] == 2'b00 {
inst32 = subw(rs1d, rs1d, rs2d); // C.SUBW
} else if inst16[6:5] == 2'b01 {
inst32 = addw(rs1d, rs1d, rs2d); // C.ADDW
}
}
}
default: {}
}
3'b101 : inst32 = jal(0, imm_j); // C.J
3'b110 : inst32 = beq(rs1d, 0, imm_br); // C.BEQZ
3'b111 : inst32 = bne(rs1d, 0, imm_br); // C.BNEZ
default: {}
}
2'b10: case funct3 { // C2
3'b000: if !(XLEN == 32 && imm_shamt[msb] == 1) {
inst32 = slli(rs1, rs1, imm_shamt); // C.SLLI
}
3'b010: if rs1 != 0 { // C.LWSP
let offset: logic<8> = {inst16[3:2], inst16[12], inst16[6:4], 2'b0};
inst32 = load(rs1, 2, {4'b0, offset}, 3'b010);
}
3'b011: if XLEN >= 64 && rs1 != 0 { // C.LDSP
let offset: logic<9> = {inst16[4:2], inst16[12], inst16[6:5], 3'b0};
inst32 = load(rs1, 2, {3'b0, offset}, 3'b011);
}
3'b100: if inst16[12] == 0 {
inst32 = if rs2 == 0 ? jalr(0, rs1, 0) : addi(rs1, rs2, 0); // C.JR / C.MV
} else {
if rs2 == 0 {
inst32 = if rs1 == 0 ? ebreak() : jalr(1, rs1, 0); // C.EBREAK : C.JALR
} else {
inst32 = add(rs1, rs1, rs2); // C.ADD
}
}
3'b110: { // C.SWSP
let offset: logic<8> = {inst16[8:7], inst16[12:9], 2'b0};
inst32 = store(2, rs2, {4'b0, offset}, 3'b010);
}
3'b111: if XLEN >= 64 { // C.SDSP
let offset: logic<9> = {inst16[9:7], inst16[12:10], 3'b0};
inst32 = store(2, rs2, {3'b0, offset}, 3'b011);
}
default: {}
}
default: {}
}
}
RVC命令を発行する
inst_fetcherモジュールでrvc_converterモジュールをインスタンス化し、 RVC命令をcoreモジュールに供給します。
まず、rvc_converterモジュールをインスタンス化します (リスト44)。
▼リスト13.44: rvc_converterモジュールのインスタンス化 (inst_fetcher.veryl) 差分をみる
// instruction converter
var rvcc_inst16: logic<16>;
var rvcc_is_rvc: logic ;
var rvcc_inst32: Inst ;
inst rvcc: rvc_converter (
inst16: case issue_pc_offset {
0 : fetch_fifo_rdata.bits[15:0],
2 : fetch_fifo_rdata.bits[31:16],
4 : fetch_fifo_rdata.bits[47:32],
6 : fetch_fifo_rdata.bits[63:48],
default: 0,
},
is_rvc: rvcc_is_rvc,
inst32: rvcc_inst32,
);
RVC命令のとき、変換された32ビット幅の命令をissue_fifoに書き込み、 issue_pc_offsetを4ではなく2増やすようにします (リスト45、 リスト46)。
▼リスト13.45: RVC命令のときのオフセットの更新 (inst_fetcher.veryl) 差分をみる
// offsetが6な32ビット命令の場合、
// アドレスと上位16ビットを保存してFIFOを読み進める
if issue_pc_offset == 6 && !rvcc_is_rvc && !issue_is_rdata_saved {
if fetch_fifo_rvalid {
issue_is_rdata_saved = 1;
issue_saved_addr = fetch_fifo_rdata.addr;
issue_saved_bits = fetch_fifo_rdata.bits[63:48];
}
} else {
if issue_fifo_wready && issue_fifo_wvalid {
issue_pc_offset += if issue_is_rdata_saved || !rvcc_is_rvc ? 4 : 2;
issue_is_rdata_saved = 0;
}
}
▼リスト13.46: RVC命令のときのissue_fifoへの書き込み (inst_fetcher.veryl) 差分をみる
if !core_if.is_hazard && fetch_fifo_rvalid {
if issue_fifo_wready {
if offset == 6 {
// offsetが6な32ビット命令の場合、
// 命令は{rdata_next[15:0], rdata[63:48}になる
if issue_is_rdata_saved {
issue_fifo_wvalid = 1;
issue_fifo_wdata.addr = {issue_saved_addr[msb:3], offset};
issue_fifo_wdata.bits = {rdata[15:0], issue_saved_bits};
issue_fifo_wdata.is_rvc = 0;
} else {
fetch_fifo_rready = 1;
if rvcc_is_rvc {
issue_fifo_wvalid = 1;
issue_fifo_wdata.addr = {raddr[msb:3], offset};
issue_fifo_wdata.is_rvc = 1;
issue_fifo_wdata.bits = rvcc_inst32;
} else {
// Read next 8 bytes
}
}
} else {
fetch_fifo_rready = !rvcc_is_rvc && offset == 4;
issue_fifo_wvalid = 1;
issue_fifo_wdata.addr = {raddr[msb:3], offset};
if rvcc_is_rvc {
issue_fifo_wdata.bits = rvcc_inst32;
} else {
issue_fifo_wdata.bits = case offset {
0 : rdata[31:0],
2 : rdata[47:16],
4 : rdata[63:32],
default: 0,
};
}
issue_fifo_wdata.is_rvc = rvcc_is_rvc;
}
}
}
riscv-testsのrv64uc-p-から始まるテストを実行し、成功することを確認してください。
Zc*拡張の一部の命令は複数の命令になります ↩︎
ここで削除するコードは次の「13.4.3 inst_fetcherモジュールを作成する」で実装するコードと似通っているため、削除せずにコメントアウトしておくと少し楽に実装できます。 ↩︎