Verylで作るCPU
Star

第10章
例外の実装

10.1 例外とは何か?

CPUがソフトウェアを実行するとき、処理を中断したり終了しなければならないような異常な状態*1が発生することがあります。例えば、実行環境(EEI)がサポートしていない、または実行を禁止しているような違法(illegal)*2な命令を実行しようとする場合です。このとき、CPUはどのような動作をすればいいのでしょうか?

[*1] 異常な状態(unusual condition)。予期しない(unexpected)事象と呼ぶ場合もあります。

[*2] 不正と呼ぶこともあります。逆に実行できる命令のことを合法(legal)な命令と呼びます

RISC-Vでは、命令によって引き起こされる異常な状態のことを例外(Exception)と呼び、例外が発生した場合にはトラップ(Trap)を引き起こします。トラップとは例外、または割り込み(Interrupt)*3によってCPUの状態、制御を変更することです。具体的にはPCをトラップベクタ(trap vector)に移動したり、CSRを変更します。

[*3] 割り込みは第15章「M-modeの実装 (2. 割り込みの実装)」で実装します。

本書では既にECALL命令の実行によって発生するEnvironment call from M-mode例外を実装しており、例外が発生したら次のように動作します。

  1. mcauseレジスタにトラップの発生原因を示す値(11)を書き込む
  2. mepcレジスタにPCの値を書き込む
  3. PCをmtvecレジスタの値に設定する

本章では、例外発生時に例外に固有の情報を書き込むmtvalレジスタと、現在の実装で発生する可能性がある例外を実装します。本書ではこれ以降、トラップの発生原因を示す値のことをcauseと呼びます。

10.2 例外情報の伝達

10.2.1 Environment call from M-mode例外をIFステージで処理する

今のところ、ECALL命令による例外はMEM(CSR)ステージのcsrunitモジュールで例外判定、処理されています。ECALL命令によって例外が発生するかは命令がECALLであるかどうかだけを判定すれば分かるため、命令をデコードする時点、つまりIDステージで判定できます。

本章で実装する例外にはMEMステージよりも前で発生する例外があるため、IDステージから順に次のステージに例外の有無、causeを受け渡していく仕組みを実装します。

まず、例外が発生するかどうか(valid)、例外のcause(cause)をまとめたExceptionInfo構造体を定義します(リスト10.1)。

リスト10.1: リスト10.1: ExceptionInfo構造体を定義する (corectrl.veryl)
1:     // 例外の情報を保存するための型
2:     struct ExceptionInfo {
3:         valid: logic   ,
4:         cause: CsrCause,
5:     }

EXステージ、MEMステージのFIFOのデータ型に構造体を追加します(リスト10.2リスト10.3)。

リスト10.2: リスト10.2: EXステージのFIFOにExceptionInfoを追加する (core.veryl)
1:     struct exq_type {
2:         addr: Addr         ,
3:         bits: Inst         ,
4:         ctrl: InstCtrl     ,
5:         imm : UIntX        ,
6:         expt: ExceptionInfo,
7:     }
リスト10.3: リスト10.3: MEMステージのFIFOにExceptionInfoを追加する (core.veryl)
1:     struct memq_type {
2:         addr      : Addr            ,
3:         bits      : Inst            ,
4:         ctrl      : InstCtrl        ,
5:         imm       : UIntX           ,
6:         expt      : ExceptionInfo   ,
7:         alu_result: UIntX           ,
8:         rs1_addr  : logic        <5>,

IDステージからEXステージに命令を渡すとき、命令がECALL命令なら例外が発生することを伝えます(リスト10.4)。

リスト10.4: リスト10.4: IDステージでECALL命令を判定する (core.veryl)
1:     always_comb {
2:         // ID -> EX
3:         if_fifo_rready = exq_wready;
4:         exq_wvalid     = if_fifo_rvalid;
5:         exq_wdata.addr = if_fifo_rdata.addr;
6:         exq_wdata.bits = if_fifo_rdata.bits;
7:         exq_wdata.ctrl = ids_ctrl;
8:         exq_wdata.imm  = ids_imm;
9:         // exception
10:         exq_wdata.expt.valid = ids_inst_bits == 32'h00000073; // ECALL
11:         exq_wdata.expt.cause = CsrCause::ENVIRONMENT_CALL_FROM_M_MODE;
12:     }

EXステージで例外は発生しないので、例外情報をそのままMEMステージに渡します(リスト10.5)。

リスト10.5: リスト10.5: EXステージからMEMステージに例外情報を渡す (core.veryl)
1:     always_comb {
2:         // EX -> MEM
3:         exq_rready            = memq_wready && !exs_stall;
4:         ...
5:         memq_wdata.jump_addr  = if inst_is_br(exs_ctrl) ? exs_pc + exs_imm : exs_alu_result & ~1;
6:         memq_wdata.expt       = exq_rdata.expt;
7:     }

csrunitモジュールを変更します。expt_infoポートを追加して、MEMステージ以前の例外情報を受け取ります(リスト10.6リスト10.7リスト10.8)。

リスト10.6: リスト10.6: csrunitモジュールに例外情報を受け取るためのポートを追加する (csrunit.veryl)
1: module csrunit (
2:     clk        : input  clock            ,
3:     rst        : input  reset            ,
4:     valid      : input  logic            ,
5:     pc         : input  Addr             ,
6:     ctrl       : input  InstCtrl         ,
7:     expt_info  : input  ExceptionInfo    ,
8:     rd_addr    : input  logic        <5> ,
リスト10.7: リスト10.7: MEMステージの例外情報の変数を作成する (core.veryl)
1:     ///////////////////////////////// MEM Stage /////////////////////////////////
2:     var mems_is_new   : logic           ;
3:     let mems_valid    : logic            = memq_rvalid;
4:     let mems_pc       : Addr             = memq_rdata.addr;
5:     let mems_inst_bits: Inst             = memq_rdata.bits;
6:     let mems_ctrl     : InstCtrl         = memq_rdata.ctrl;
7:     let mems_expt     : ExceptionInfo    = memq_rdata.expt;
8:     let mems_rd_addr  : logic        <5> = mems_inst_bits[11:7];
リスト10.8: リスト10.8: csrunitモジュールに例外情報を供給する (core.veryl)
1:     inst csru: csrunit (
2:         clk                             ,
3:         rst                             ,
4:         valid    : mems_valid           ,
5:         pc       : mems_pc              ,
6:         ctrl     : mems_ctrl            ,
7:         expt_info: mems_expt            ,
8:         rd_addr  : mems_rd_addr         ,

ECALL命令かどうかを判定するis_ecall変数を削除して、例外の発生条件、例外の種類を示す値を変更します(リスト10.9リスト10.10)。

リスト10.9: リスト10.9: csrunitモジュールでのECALL命令の判定を削除する (csrunit.veryl)
1:     // CSRR(W|S|C)[I]命令かどうか
2:     let is_wsc: logic = ctrl.is_csr && ctrl.funct3[1:0] != 0;
3:     // ECALL命令かどうか
4:     let is_ecall: logic = ctrl.is_csr && csr_addr == 0 && rs1[4:0] == 0 && ctrl.funct3 == 0 && rd_addr == 0;
リスト10.10: リスト10.10: ExceptionInfoを使って例外を起こす (csrunit.veryl)
1:     // Exception
2:     let raise_expt: logic = valid && expt_info.valid;
3:     let expt_cause: UIntX = expt_info.cause;

10.2.2 mtvalレジスタを実装する

例外が発生すると、CPUはトラップベクタにジャンプして例外を処理します。mcauseレジスタを読むことでどの例外が発生したかを判別できますが、その例外の詳しい情報を知りたいことがあります。

mtvalレジスタ

図10.1: mtvalレジスタ

RISC-Vには、例外が発生したときのソフトウェアによるハンドリングを補助するために、MXLENビットのmtvalレジスタが定義されています(図10.1)。例外が発生したとき、CPUはmtvalレジスタに例外に固有の情報を書き込みます。これ以降、例外に固有の情報のことをtvalと呼びます。

ExceptionInfo構造体に例外に固有の情報を示すvalueを追加します(リスト10.11)。

リスト10.11: リスト10.11: tvalをExceptionInfoに追加する (corectrl.veryl)
1:     struct ExceptionInfo {
2:         valid: logic   ,
3:         cause: CsrCause,
4:         value: UIntX   ,
5:     }

ECALL命令はmtvalに書き込むような情報がないので0に設定します(リスト10.12)。

リスト10.12: リスト10.12: ECALL命令のtvalを設定する (corectrl.veryl)
1:         // exception
2:         exq_wdata.expt.valid = ids_inst_bits == 32'h00000073; // ECALL
3:         exq_wdata.expt.cause = CsrCause::ENVIRONMENT_CALL_FROM_M_MODE;
4:         exq_wdata.expt.value = 0;

CsrAddr型にmtvalレジスタのアドレスを追加します(リスト10.13)。

リスト10.13: リスト10.13: mtvalのアドレスを定義する (eei.veryl)
1:     enum CsrAddr: logic<12> {
2:         MTVEC = 12'h305,
3:         MEPC = 12'h341,
4:         MCAUSE = 12'h342,
5:         MTVAL = 12'h343,
6:         LED = 12'h800,
7:     }

mtvalレジスタを実装して、書き込み、読み込みできるようにします(リスト10.14リスト10.15リスト10.16リスト10.17リスト10.18)。

リスト10.14: リスト10.14: mtvalの書き込みマスクを定義する (csrunit.veryl)
1:     const MTVAL_WMASK : UIntX = 'hffff_ffff_ffff_ffff;
リスト10.15: リスト10.15: mtvalレジスタを作成する (csrunit.veryl)
1:     var mtvec : UIntX;
2:     var mepc  : UIntX;
3:     var mcause: UIntX;
4:     var mtval : UIntX;
リスト10.16: リスト10.16: mtvalの読み込みデータ、書き込みマスクを設定する (csrunit.veryl)
1:     always_comb {
2:         // read
3:         rdata = case csr_addr {
4:             ...
5:             CsrAddr::MTVAL : mtval,
6:             ...
7:         };
8:         // write
9:         wmask = case csr_addr {
10:             ...
11:             CsrAddr::MTVAL : MTVAL_WMASK,
12:             ...
13:         };
リスト10.17: リスト10.17: mtvalレジスタをリセットする (csrunit.veryl)
1:     always_ff {
2:         if_reset {
3:             mtvec  = 0;
4:             mepc   = 0;
5:             mcause = 0;
6:             mtval  = 0;
7:             led    = 0;
リスト10.18: リスト10.18: mtvalに書き込めるようにする (csrunit.veryl)
1:     } else {
2:         if is_wsc {
3:             case csr_addr {
4:                 ...
5:                 CsrAddr::MTVAL : mtval  = wdata;
6:                 ...
7:             }
8:         }
9:     }

例外が発生するとき、mtvalレジスタにexpt_info.valueを書き込むようにします(リスト10.19リスト10.20)。

リスト10.19: リスト10.19: tvalを変数に割り当てる (csrunit.veryl)
1:     let raise_expt : logic = valid && expt_info.valid;
2:     let expt_cause : UIntX = expt_info.cause;
3:     let expt_value : UIntX = expt_info.value;
リスト10.20: リスト10.20: 例外が発生するとき、mtvalにtvalを書き込む (csrunit.veryl)
1:     if valid {
2:         if raise_trap {
3:             if raise_expt {
4:                 mepc   = pc;
5:                 mcause = trap_cause;
6:                 mtval  = expt_value;
7:             }

10.3 Breakpoint例外の実装

Breakpoint例外は、EBREAK命令によって引き起こされる例外です。EBREAK命令はデバッガがプログラムを中断させる場合などに利用されます。EBREAK命令はECALL命令と同様に例外を発生させるだけで、ほかに操作を行いません。causeは3で、tvalは例外が発生した命令のアドレスになります。

CsrCause型にBreakpoint例外のcauseを追加します(リスト10.21)。

リスト10.21: リスト10.21: Breakpoint例外のcauseを定義する (eei.veryl)
1:     enum CsrCause: UIntX {
2:         BREAKPOINT = 3,
3:         ENVIRONMENT_CALL_FROM_M_MODE = 11,
4:     }

IDステージでEBREAK命令を判定して、tvalにPCを設定します(リスト10.22)。

リスト10.22: リスト10.22: IDステージでEBREAK命令を判定する (core.veryl)
1:         exq_wdata.expt = 0;
2:         if ids_inst_bits == 32'h00000073 {
3:             // ECALL
4:             exq_wdata.expt.valid = 1;
5:             exq_wdata.expt.cause = CsrCause::ENVIRONMENT_CALL_FROM_M_MODE;
6:             exq_wdata.expt.value = 0;
7:         } else if ids_inst_bits == 32'h00100073 {
8:             // EBREAK
9:             exq_wdata.expt.valid = 1;
10:             exq_wdata.expt.cause = CsrCause::BREAKPOINT;
11:             exq_wdata.expt.value = ids_pc;
12:         }

10.4 Illegal instruction例外の実装

Illegal instruction例外は、現在の環境で実行できない命令を実行しようとしたときに発生する例外です。causeは2で、tvalは例外が発生した命令のビット列になります。

本章では、EEIが認識できない不正な命令ビット列を実行しようとした場合と、読み込み専用のCSRに書き込もうとした場合の2つの状況で例外を発生させます。

10.4.1 不正な命令ビット列で例外を起こす

CPUに実装していない命令、つまりデコードできない命令を実行しようとするとき、Illegal instruction例外が発生します。

今のところ未知の命令は何もしない命令として実行しています。ここで、inst_decoderモジュールを、未知の命令であることを報告するように変更します。

inst_decoderモジュールに、命令が有効かどうかを示すvalidポートを追加します(リスト10.23リスト10.24)。

リスト10.23: リスト10.23: validポートを追加する (inst_decoder.veryl)
1: module inst_decoder (
2:     bits : input  Inst    ,
3:     valid: output logic   ,
4:     ctrl : output InstCtrl,
5:     imm  : output UIntX   ,
6: ) {
リスト10.24: リスト10.24: inst_decoderモジュールのvalidポートと変数を接続する (core.veryl)
1:     let ids_valid     : logic    = if_fifo_rvalid;
2:     let ids_pc        : Addr     = if_fifo_rdata.addr;
3:     let ids_inst_bits : Inst     = if_fifo_rdata.bits;
4:     var ids_inst_valid: logic   ;
5:     var ids_ctrl      : InstCtrl;
6:     var ids_imm       : UIntX   ;
7: 
8:     inst decoder: inst_decoder (
9:         bits : ids_inst_bits ,
10:         valid: ids_inst_valid,
11:         ctrl : ids_ctrl      ,
12:         imm  : ids_imm       ,
13:     );

今のところ実装してある命令を有効な命令として判定する処理をalways_combブロックに記述します(リスト10.25)。

リスト10.25: リスト10.25: 命令の有効判定を行う (inst_decoder.veryl)
1: valid = case op {
2:     OP_LUI, OP_AUIPC, OP_JAL, OP_JALR: T,
3:     OP_BRANCH                        : f3 != 3'b010 && f3 != 3'b011,
4:     OP_LOAD                          : f3 != 3'b111,
5:     OP_STORE                         : f3[2] == 1'b0,
6:     OP_OP                            : case f7 {
7:         7'b0000000                       : T, // RV32I
8:         7'b0100000                       : f3 == 3'b000 || f3 == 3'b101, // SUB, SRA
9:         7'b0000001                       : T, // RV32M
10:         default                          : F,
11:     },
12:     OP_OP_IMM: case f3 {
13:         3'b001   : f7[6:1] == 6'b000000, // SLLI (RV64I)
14:         3'b101   : f7[6:1] == 6'b000000 || f7[6:1] == 6'b010000, // SRLI, SRAI (RV64I)
15:         default  : T,
16:     },
17:     OP_OP_32  : case f7 {
18:         7'b0000001: f3 == 3'b000 || f3[2] == 1'b1, // RV64M
19:         7'b0000000: f3 == 3'b000 || f3 == 3'b001 || f3 == 3'b101, // ADDW, SLLW, SRLW
20:         7'b0100000: f3 == 3'b000 || f3 == 3'b101, // SUBW, SRAW
21:         default   : F,
22:     },
23:     OP_OP_IMM_32: case f3 {
24:         3'b000      : T, // ADDIW
25:         3'b001      : f7 == 7'b0000000, // SLLIW
26:         3'b101      : f7 == 7'b0000000 || f7 == 7'b0100000, // SRLIW, SRAIW
27:         default     : F,
28:     },
29:     OP_SYSTEM: f3 != 3'b000 && f3 != 3'b100 || // CSRR(W|S|C)[I]
30:      bits == 32'h00000073 || // ECALL
31:      bits == 32'h00100073 || // EBREAK
32:      bits == 32'h30200073, //MRET
33:     OP_MISC_MEM: T, // FENCE
34:     default    : F,
35: };

riscv-testsでメモリ読み書きの順序を保証するFENCE命令*4を使用しているため、opcodeがOP-MISCである命令を合法な命令として取り扱っています。OP-MISCのopcode(7'b0001111)をeeiパッケージに定義してください(リスト10.26)。

リスト10.26: リスト10.26: OP-MISCのビット列を定義する (eei.veryl)
1:     const OP_MISC_MEM : logic<7> = 7'b0001111;

[*4] 基本編で実装するCPUはロードストア命令を直列に実行するため順序を保証する必要がありません。そのためFENCE命令は何もしない命令として扱います。

CsrCause型にIllegal instruction例外のcauseを追加します(リスト10.27)。

リスト10.27: リスト10.27: Illegal instruction例外のcauseを定義する (eei.veryl)
1:     enum CsrCause: UIntX {
2:         ILLEGAL_INSTRUCTION = 2,
3:         BREAKPOINT = 3,
4:         ENVIRONMENT_CALL_FROM_M_MODE = 11,
5:     }

validフラグを利用して、IDステージでIllegal instruction例外を発生させます(リスト10.28)。tvalには、命令を右に詰めてゼロで拡張した値を設定します。

リスト10.28: リスト10.28: 不正な命令のとき、例外を発生させる (core.veryl)
1:         exq_wdata.expt = 0;
2:         if !ids_inst_valid {
3:             // illegal instruction
4:             exq_wdata.expt.valid = 1;
5:             exq_wdata.expt.cause = CsrCause::ILLEGAL_INSTRUCTION;
6:             exq_wdata.expt.value = {1'b0 repeat XLEN - ILEN, ids_inst_bits};
7:         } else if ids_inst_bits == 32'h00000073 {

10.4.2 読み込み専用のCSRへの書き込みで例外を起こす

RISC-VのCSRには読み込み専用のレジスタが存在しており、アドレスの上位2ビットが2'b11のCSRが読み込み専用として定義されています。読み込み専用のCSRに書き込みを行おうとするとIllegal instruction例外が発生します。

CSRに値が書き込まれるのは次のいずれかの場合です。読み書き可能なレジスタ内の読み込み専用のフィールドへの書き込みは例外を引き起こしません。

  1. CSRRW、CSRRWI命令である
  2. CSRRS命令でrs1が0番目のレジスタ以外である
  3. CSRRSI命令で即値が0以外である
  4. CSRRC命令でrs1が0番目のレジスタ以外である
  5. CSRRCI命令で即値が0以外である

ソースレジスタの値が0だとしても、0番目のレジスタではない場合にはCSRに書き込むと判断します。CSRに書き込むかどうかを正しく判定するために、csrunitモジュールのrs1ポートをrs1_addrrs1_dataに分解します(リスト10.30リスト10.29リスト10.31)*5。また、causeを設定するためにcsrunitモジュールに命令のビット列を供給します。

リスト10.29: リスト10.29: csrunitモジュールのポート定義を変更する (csrunit.veryl)
1: module csrunit (
2:     clk        : input  clock            ,
3:     rst        : input  reset            ,
4:     valid      : input  logic            ,
5:     pc         : input  Addr             ,
6:     inst_bits  : input  Inst             ,
7:     ctrl       : input  InstCtrl         ,
8:     expt_info  : input  ExceptionInfo    ,
9:     rd_addr    : input  logic        <5> ,
10:     csr_addr   : input  logic        <12>,
11:     rs1_addr   : input  logic        <5> ,
12:     rs1_data   : input  UIntX            ,
13:     rdata      : output UIntX            ,
14:     raise_trap : output logic            ,
15:     trap_vector: output Addr             ,
16:     led        : output UIntX            ,
17: ) {
リスト10.30: リスト10.30: csrunitモジュールのポート定義を変更する (core.veryl)
1:     inst csru: csrunit (
2:         clk                               ,
3:         rst                               ,
4:         valid      : mems_valid           ,
5:         pc         : mems_pc              ,
6:         inst_bits  : mems_inst_bits       ,
7:         ctrl       : mems_ctrl            ,
8:         expt_info  : mems_expt            ,
9:         rd_addr    : mems_rd_addr         ,
10:         csr_addr   : mems_inst_bits[31:20],
11:         rs1_addr   : memq_rdata.rs1_addr  ,
12:         rs1_data   : memq_rdata.rs1_data  ,
13:         rdata      : csru_rdata           ,
14:         raise_trap : csru_raise_trap      ,
15:         trap_vector: csru_trap_vector     ,
16:         led                               ,
17:     );
リスト10.31: リスト10.31: rs1の変更に対応する (csrunit.veryl)
1:     let wsource: UIntX = if ctrl.funct3[2] ? {1'b0 repeat XLEN - 5, rs1_addr} : rs1_data;
2:     wdata   = case ctrl.funct3[1:0] {
3:         2'b01  : wsource,
4:         2'b10  : rdata | wsource,
5:         2'b11  : rdata & ~wsource,
6:         default: 'x,
7:     } & wmask | (rdata & ~wmask);

[*5] 基本編 第1部の初版のwdataの生成ロジックに間違いがあったので訂正してあります。

命令のfunct3とrs1のアドレスを利用して、書き込み先が読み込み専用レジスタかどうかを判定します*6(リスト10.32)。また、命令のビット列を利用できるようになったので、MRET命令の判定を命令のビット列の比較に書き換えています。

リスト10.32: リスト10.32: 読み込み専用CSRへの書き込みが発生するか判定する (csrunit.veryl)
1:     // CSRR(W|S|C)[I]命令かどうか
2:     let is_wsc: logic = ctrl.is_csr && ctrl.funct3[1:0] != 0;
3:     // MRET命令かどうか
4:     let is_mret: logic = inst_bits == 32'h30200073;
5: 
6:     // Check CSR access
7:     let will_not_write_csr     : logic = (ctrl.funct3[1:0] == 2 || ctrl.funct3[1:0] == 3) && rs1_addr == 0; // set/clear with source = 0
8:     let expt_write_readonly_csr: logic = is_wsc && !will_not_write_csr && csr_addr[11:10] == 2'b11; // attempt to write read-only CSR

[*6] IDステージで判定することもできます。

例外が発生するとき、causeとtvalを設定します(リスト10.33)。

リスト10.33: リスト10.33: 読み込み専用CSRの書き込みで例外を発生させる (csrunit.veryl)
1:     let raise_expt: logic = valid && (expt_info.valid || expt_write_readonly_csr);
2:     let expt_cause: UIntX = switch {
3:         expt_info.valid        : expt_info.cause,
4:         expt_write_readonly_csr: CsrCause::ILLEGAL_INSTRUCTION,
5:         default                : 0,
6:     };
7:     let expt_value: UIntX = switch {
8:         expt_info.valid                            : expt_info.value,
9:         expt_cause == CsrCause::ILLEGAL_INSTRUCTION: {1'b0 repeat XLEN - $bits(Inst), inst_bits},
10:         default                                    : 0
11:     };

この変更により、レジスタにライトバックするようにデコードされた命令がcsrunitモジュールでトラップを起こすようになりました。トラップが発生するときにWBステージでライトバックしないように変更します(リスト10.34リスト10.35リスト10.36)。

リスト10.34: リスト10.34: トラップが発生したかを示すlogicをwbq_typeに追加する (core.veryl)
1:     struct wbq_type {
2:         ...
3:         csr_rdata : UIntX   ,
4:         raise_trap: logic   ,
5:     }
リスト10.35: リスト10.35: トラップが発生したかをWBステージに伝える (core.veryl)
1:     wbq_wdata.raise_trap = csru_raise_trap;
リスト10.36: リスト10.36: トラップが発生しているとき、レジスタにデータを書き込まないようにする (core.veryl)
1:     always_ff {
2:         if wbs_valid && wbs_ctrl.rwb_en && !wbq_rdata.raise_trap {
3:             regfile[wbs_rd_addr] = wbs_wb_data;
4:         }
5:     }

10.5 命令アドレスのミスアライン例外

RISC-Vでは、命令アドレスがIALIGNビット境界に整列されていない場合にInstruction address misaligned例外が発生します。causeは0で、tvalは命令のアドレスになります。

第13章「C拡張の実装」で実装するC拡張が実装されていない場合、IALIGNは32と定義されています。C拡張が定義されている場合は16になります。

IALIGNビット境界に整列されていない命令アドレスになるのはジャンプ命令、分岐命令を実行する場合です*7。PCの遷移先が整列されていない場合に例外が発生します。分岐命令の場合、分岐が成立する場合にしか例外は発生しません。

[*7] mepc、mtvecはIALIGNビットに整列されたアドレスしか書き込めないため、遷移先のアドレスは常に整列されています。

CsrCause型にInstruction address misaligned例外のcauseを追加します(リスト10.37)。

リスト10.37: リスト10.37: Instruction address misaligned例外のcauseを定義する (eei.veryl)
1:     enum CsrCause: UIntX {
2:         INSTRUCTION_ADDRESS_MISALIGNED = 0,
3:         ILLEGAL_INSTRUCTION = 2,
4:         BREAKPOINT = 3,
5:         ENVIRONMENT_CALL_FROM_M_MODE = 11,
6:     }

EXステージでアドレスを確認して例外を判定します(リスト10.38)。tvalは遷移先のアドレスになることに注意してください。

リスト10.38: リスト10.38: EXステージでInstruction address misaligned例外の判定を行う (core.veryl)
1:         memq_wdata.jump_addr  = if inst_is_br(exs_ctrl) ? exs_pc + exs_imm : exs_alu_result & ~1;
2:         // exception
3:         let instruction_address_misaligned: logic = memq_wdata.br_taken && memq_wdata.jump_addr[1:0] != 2'b00;
4:         memq_wdata.expt                = exq_rdata.expt;
5:         if !memq_rdata.expt.valid {
6:             if instruction_address_misaligned {
7:                 memq_wdata.expt.valid = 1;
8:                 memq_wdata.expt.cause = CsrCause::INSTRUCTION_ADDRESS_MISALIGNED;
9:                 memq_wdata.expt.value = memq_wdata.jump_addr;
10:             }
11:         }

10.6 ロードストア命令のミスアライン例外

RISC-Vでは、ロード、ストア命令でアクセスするメモリのアドレスが、ロード、ストアするビット幅に整列されていない場合に、それぞれLoad address misaligned例外、Store/AMO address misaligned例外が発生します*8。例えばLW命令は4バイトに整列されたアドレス、LD命令は8バイトに整列されたアドレスにしかアクセスできません。causeはそれぞれ46で、tvalはアクセスするメモリのアドレスになります。

[*8] 例外を発生させず、そのようなメモリアクセスをサポートすることもできます。本書ではCPUを単純に実装するために例外とします。

CsrCause型に例外のcauseを追加します(リスト10.39)。

リスト10.39: リスト10.39: 例外のcauseを定義する (eei.veryl)
1:     enum CsrCause: UIntX {
2:         INSTRUCTION_ADDRESS_MISALIGNED = 0,
3:         ILLEGAL_INSTRUCTION = 2,
4:         BREAKPOINT = 3,
5:         LOAD_ADDRESS_MISALIGNED = 4,
6:         STORE_AMO_ADDRESS_MISALIGNED = 6,
7:         ENVIRONMENT_CALL_FROM_M_MODE = 11,
8:     }

EXステージでアドレスを確認して例外を判定します(リスト10.40)。

リスト10.40: リスト10.40: EXステージで例外の判定を行う (core.veryl)
1:         let instruction_address_misaligned: logic = memq_wdata.br_taken && memq_wdata.jump_addr[1:0] != 2'b00;
2:         let loadstore_address_misaligned  : logic = inst_is_memop(exs_ctrl) && case exs_ctrl.funct3[1:0] {
3:             2'b00  : 0, // B
4:             2'b01  : exs_alu_result[0] != 1'b0, // H
5:             2'b10  : exs_alu_result[1:0] != 2'b0, // W
6:             2'b11  : exs_alu_result[2:0] != 3'b0, // D
7:             default: 0,
8:         };
9:         memq_wdata.expt = exq_rdata.expt;
10:         if !memq_rdata.expt.valid {
11:             if instruction_address_misaligned {
12:                 memq_wdata.expt.valid = 1;
13:                 memq_wdata.expt.cause = CsrCause::INSTRUCTION_ADDRESS_MISALIGNED;
14:                 memq_wdata.expt.value = memq_wdata.jump_addr;
15:             } else if loadstore_address_misaligned {
16:                 memq_wdata.expt.valid = 1;
17:                 memq_wdata.expt.cause = if exs_ctrl.is_load ? CsrCause::LOAD_ADDRESS_MISALIGNED : CsrCause::STORE_AMO_ADDRESS_MISALIGNED;
18:                 memq_wdata.expt.value = exs_alu_result;
19:             }
20:         }

例外が発生するときにmemunitモジュールが動作しないようにします(リスト10.41)。

リスト10.41: リスト10.41: 例外が発生するとき、memunitのvalidを0にする (core.veryl)
1:     inst memu: memunit (
2:         clk                                   ,
3:         rst                                   ,
4:         valid : mems_valid && !mems_expt.valid,
5:         is_new: mems_is_new                   ,
6:         ctrl  : mems_ctrl                     ,
7:         addr  : memq_rdata.alu_result         ,
8:         rs2   : memq_rdata.rs2_data           ,
9:         rdata : memu_rdata                    ,
10:         stall : memu_stall                    ,
11:         membus: d_membus                      ,
12:     );