第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例外を実装しており、例外が発生したら次のように動作します。
- mcauseレジスタにトラップの発生原因を示す値(
11
)を書き込む - mepcレジスタにPCの値を書き込む
- 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)。
1: // 例外の情報を保存するための型 2: struct ExceptionInfo { 3: valid: logic , 4: cause: CsrCause, 5: }
EXステージ、MEMステージのFIFOのデータ型に構造体を追加します(リスト10.2、リスト10.3)。
1: struct exq_type { 2: addr: Addr , 3: bits: Inst , 4: ctrl: InstCtrl , 5: imm : UIntX , 6: expt: ExceptionInfo, 7: }
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)。
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)。
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)。
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> ,
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];
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)。
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;
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レジスタを読むことでどの例外が発生したかを判別できますが、その例外の詳しい情報を知りたいことがあります。

図10.1: mtvalレジスタ
RISC-Vには、例外が発生したときのソフトウェアによるハンドリングを補助するために、MXLENビットのmtvalレジスタが定義されています(図10.1)。例外が発生したとき、CPUはmtvalレジスタに例外に固有の情報を書き込みます。これ以降、例外に固有の情報のことをtvalと呼びます。
ExceptionInfo
構造体に例外に固有の情報を示すvalue
を追加します(リスト10.11)。
1: struct ExceptionInfo { 2: valid: logic , 3: cause: CsrCause, 4: value: UIntX , 5: }
ECALL命令はmtvalに書き込むような情報がないので0
に設定します(リスト10.12)。
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)。
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)。
1: const MTVAL_WMASK : UIntX = 'hffff_ffff_ffff_ffff;
1: var mtvec : UIntX; 2: var mepc : UIntX; 3: var mcause: UIntX; 4: var mtval : UIntX;
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: };
1: always_ff { 2: if_reset { 3: mtvec = 0; 4: mepc = 0; 5: mcause = 0; 6: mtval = 0; 7: led = 0;
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)。
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;
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)。
1: enum CsrCause: UIntX { 2: BREAKPOINT = 3, 3: ENVIRONMENT_CALL_FROM_M_MODE = 11, 4: }
IDステージでEBREAK命令を判定して、tvalにPCを設定します(リスト10.22)。
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)。
1: module inst_decoder ( 2: bits : input Inst , 3: valid: output logic , 4: ctrl : output InstCtrl, 5: imm : output UIntX , 6: ) {
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)。
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)。
1: const OP_MISC_MEM : logic<7> = 7'b0001111;
[*4] 基本編で実装するCPUはロードストア命令を直列に実行するため順序を保証する必要がありません。そのためFENCE命令は何もしない命令として扱います。
CsrCause
型にIllegal instruction例外のcauseを追加します(リスト10.27)。
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には、命令を右に詰めてゼロで拡張した値を設定します。
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に値が書き込まれるのは次のいずれかの場合です。読み書き可能なレジスタ内の読み込み専用のフィールドへの書き込みは例外を引き起こしません。
- CSRRW、CSRRWI命令である
- CSRRS命令でrs1が0番目のレジスタ以外である
- CSRRSI命令で即値が
0
以外である - CSRRC命令でrs1が0番目のレジスタ以外である
- CSRRCI命令で即値が
0
以外である
ソースレジスタの値が0
だとしても、0番目のレジスタではない場合にはCSRに書き込むと判断します。CSRに書き込むかどうかを正しく判定するために、csrunitモジュールのrs1
ポートをrs1_addr
とrs1_data
に分解します(リスト10.30、リスト10.29、リスト10.31)*5。また、causeを設定するためにcsrunitモジュールに命令のビット列を供給します。
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: ) {
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: );
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命令の判定を命令のビット列の比較に書き換えています。
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)。
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)。
1: struct wbq_type { 2: ... 3: csr_rdata : UIntX , 4: raise_trap: logic , 5: }
1: wbq_wdata.raise_trap = csru_raise_trap;
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)。
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は遷移先のアドレスになることに注意してください。
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はそれぞれ4
、6
で、tvalはアクセスするメモリのアドレスになります。
[*8] 例外を発生させず、そのようなメモリアクセスをサポートすることもできます。本書ではCPUを単純に実装するために例外とします。
CsrCause
型に例外のcauseを追加します(リスト10.39)。
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)。
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)。
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: );