Skip to content

例外の実装

例外とは何か?

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

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

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

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

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

例外情報の伝達

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

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

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

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

▼リスト10.1: ExceptionInfo構造体を定義する (corectrl.veryl) 差分をみる

veryl
// 例外の情報を保存するための型
struct ExceptionInfo {
    valid: logic   ,
    cause: CsrCause,
}

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

▼リスト10.2: EXステージのFIFOにExceptionInfoを追加する (core.veryl) 差分をみる

veryl
struct exq_type {
    addr: Addr         ,
    bits: Inst         ,
    ctrl: InstCtrl     ,
    imm : UIntX        ,
    expt: ExceptionInfo,
}

▼リスト10.3: MEMステージのFIFOにExceptionInfoを追加する (core.veryl) 差分をみる

veryl
struct memq_type {
    addr      : Addr            ,
    bits      : Inst            ,
    ctrl      : InstCtrl        ,
    imm       : UIntX           ,
    expt      : ExceptionInfo   ,
    alu_result: UIntX           ,
    rs1_addr  : logic        <5>,

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

▼リスト10.4: IDステージでECALL命令を判定する (core.veryl) 差分をみる

veryl
always_comb {
    // ID -> EX
    if_fifo_rready = exq_wready;
    exq_wvalid     = if_fifo_rvalid;
    exq_wdata.addr = if_fifo_rdata.addr;
    exq_wdata.bits = if_fifo_rdata.bits;
    exq_wdata.ctrl = ids_ctrl;
    exq_wdata.imm  = ids_imm;
    // exception
    exq_wdata.expt.valid = ids_inst_bits == 32'h00000073; // ECALL
    exq_wdata.expt.cause = CsrCause::ENVIRONMENT_CALL_FROM_M_MODE;
}

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

▼リスト10.5: EXステージからMEMステージに例外情報を渡す (core.veryl) 差分をみる

veryl
always_comb {
    // EX -> MEM
    exq_rready            = memq_wready && !exs_stall;
    memq_wvalid           = exq_rvalid && !exs_stall;
    memq_wdata.addr       = exq_rdata.addr;
    memq_wdata.bits       = exq_rdata.bits;
    memq_wdata.ctrl       = exq_rdata.ctrl;
    memq_wdata.imm        = exq_rdata.imm;
    memq_wdata.rs1_addr   = exs_rs1_addr;
    memq_wdata.rs1_data   = exs_rs1_data;
    memq_wdata.rs2_data   = exs_rs2_data;
    memq_wdata.alu_result = if exs_ctrl.is_muldiv ? exs_muldiv_result : exs_alu_result;
    memq_wdata.br_taken   = exs_ctrl.is_jump || inst_is_br(exs_ctrl) && exs_brunit_take;
    memq_wdata.jump_addr  = if inst_is_br(exs_ctrl) ? exs_pc + exs_imm : exs_alu_result & ~1;
    memq_wdata.expt       = exq_rdata.expt;
}

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

▼リスト10.6: csrunitモジュールに例外情報を受け取るためのポートを追加する (csrunit.veryl) 差分をみる

veryl
module csrunit (
    clk        : input  clock            ,
    rst        : input  reset            ,
    valid      : input  logic            ,
    pc         : input  Addr             ,
    ctrl       : input  InstCtrl         ,
    expt_info  : input  ExceptionInfo    ,
    rd_addr    : input  logic        <5> ,

▼リスト10.7: MEMステージの例外情報の変数を作成する (core.veryl) 差分をみる

veryl
///////////////////////////////// MEM Stage /////////////////////////////////
var mems_is_new   : logic           ;
let mems_valid    : logic            = memq_rvalid;
let mems_pc       : Addr             = memq_rdata.addr;
let mems_inst_bits: Inst             = memq_rdata.bits;
let mems_ctrl     : InstCtrl         = memq_rdata.ctrl;
let mems_expt     : ExceptionInfo    = memq_rdata.expt;
let mems_rd_addr  : logic        <5> = mems_inst_bits[11:7];

▼リスト10.8: csrunitモジュールに例外情報を供給する (core.veryl) 差分をみる

veryl
inst csru: csrunit (
    clk                             ,
    rst                             ,
    valid    : mems_valid           ,
    pc       : mems_pc              ,
    ctrl     : mems_ctrl            ,
    expt_info: mems_expt            ,
    rd_addr  : mems_rd_addr         ,

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

▼リスト10.9: csrunitモジュールでのECALL命令の判定を削除する (csrunit.veryl) 差分をみる

veryl
// CSRR(W|S|C)[I]命令かどうか
let is_wsc: logic = ctrl.is_csr && ctrl.funct3[1:0] != 0;
// ECALL命令かどうか
let is_ecall: logic = ctrl.is_csr && csr_addr == 0 && rs1[4:0] == 0 && ctrl.funct3 == 0 && rd_addr == 0;

▼リスト10.10: ExceptionInfoを使って例外を起こす (csrunit.veryl) 差分をみる

veryl
// Exception
let raise_expt: logic = valid && expt_info.valid;
let expt_cause: UIntX = expt_info.cause;

mtvalレジスタを実装する

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

mtvalレジスタ

▲図1: mtvalレジスタ

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

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

▼リスト10.11: tvalをExceptionInfoに追加する (corectrl.veryl) 差分をみる

veryl
struct ExceptionInfo {
    valid: logic   ,
    cause: CsrCause,
    value: UIntX   ,
}

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

▼リスト10.12: ECALL命令のtvalを設定する (corectrl.veryl) 差分をみる

veryl
// exception
exq_wdata.expt.valid = ids_inst_bits == 32'h00000073; // ECALL
exq_wdata.expt.cause = CsrCause::ENVIRONMENT_CALL_FROM_M_MODE;
exq_wdata.expt.value = 0;

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

▼リスト10.13: mtvalのアドレスを定義する (eei.veryl) 差分をみる

veryl
enum CsrAddr: logic<12> {
    MTVEC = 12'h305,
    MEPC = 12'h341,
    MCAUSE = 12'h342,
    MTVAL = 12'h343,
    LED = 12'h800,
}

mtvalレジスタを実装して、書き込み、読み込みできるようにします ( リスト14、 リスト15、 リスト16、 リスト17、 リスト18 )。

▼リスト10.14: mtvalの書き込みマスクを定義する (csrunit.veryl) 差分をみる

veryl
const MTVAL_WMASK : UIntX = 'hffff_ffff_ffff_ffff;

▼リスト10.15: mtvalレジスタを作成する (csrunit.veryl) 差分をみる

veryl
var mtvec : UIntX;
var mepc  : UIntX;
var mcause: UIntX;
var mtval : UIntX;

▼リスト10.16: mtvalの読み込みデータ、書き込みマスクを設定する (csrunit.veryl) 差分をみる

veryl
always_comb {
    // read
    rdata = case csr_addr {
        CsrAddr::MTVEC : mtvec,
        CsrAddr::MEPC  : mepc,
        CsrAddr::MCAUSE: mcause,
        CsrAddr::MTVAL : mtval,
        CsrAddr::LED   : led,
        default        : 'x,
    };
    // write
    wmask = case csr_addr {
        CsrAddr::MTVEC : MTVEC_WMASK,
        CsrAddr::MEPC  : MEPC_WMASK,
        CsrAddr::MCAUSE: MCAUSE_WMASK,
        CsrAddr::MTVAL : MTVAL_WMASK,
        CsrAddr::LED   : LED_WMASK,
        default        : 0,
    };

▼リスト10.17: mtvalレジスタをリセットする (csrunit.veryl) 差分をみる

veryl
always_ff {
    if_reset {
        mtvec  = 0;
        mepc   = 0;
        mcause = 0;
        mtval  = 0;
        led    = 0;

▼リスト10.18: mtvalに書き込めるようにする (csrunit.veryl) 差分をみる

veryl
} else {
    if is_wsc {
        case csr_addr {
            CsrAddr::MTVEC : mtvec  = wdata;
            CsrAddr::MEPC  : mepc   = wdata;
            CsrAddr::MCAUSE: mcause = wdata;
            CsrAddr::MTVAL : mtval  = wdata;
            CsrAddr::LED   : led    = wdata;
            default        : {}
        }
    }
}

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

▼リスト10.19: tvalを変数に割り当てる (csrunit.veryl) 差分をみる

veryl
let raise_expt: logic = valid && expt_info.valid;
let expt_cause: UIntX = expt_info.cause;
let expt_value: UIntX = expt_info.value;

▼リスト10.20: 例外が発生するとき、mtvalにtvalを書き込む (csrunit.veryl) 差分をみる

veryl
if valid {
    if raise_trap {
        if raise_expt {
            mepc   = pc;
            mcause = trap_cause;
            mtval  = expt_value;
        }

Breakpoint例外の実装

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

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

▼リスト10.21: Breakpoint例外のcauseを定義する (eei.veryl) 差分をみる

veryl
enum CsrCause: UIntX {
    BREAKPOINT = 3,
    ENVIRONMENT_CALL_FROM_M_MODE = 11,
}

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

▼リスト10.22: IDステージでEBREAK命令を判定する (core.veryl) 差分をみる

veryl
exq_wdata.expt = 0;
if ids_inst_bits == 32'h00000073 {
    // ECALL
    exq_wdata.expt.valid = 1;
    exq_wdata.expt.cause = CsrCause::ENVIRONMENT_CALL_FROM_M_MODE;
    exq_wdata.expt.value = 0;
} else if ids_inst_bits == 32'h00100073 {
    // EBREAK
    exq_wdata.expt.valid = 1;
    exq_wdata.expt.cause = CsrCause::BREAKPOINT;
    exq_wdata.expt.value = ids_pc;
}

Illegal instruction例外の実装

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

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

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

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

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

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

▼リスト10.23: validポートを追加する (inst_decoder.veryl) 差分をみる

veryl
module inst_decoder (
    bits : input  Inst    ,
    valid: output logic   ,
    ctrl : output InstCtrl,
    imm  : output UIntX   ,
) {

▼リスト10.24: inst_decoderモジュールのvalidポートと変数を接続する (core.veryl) 差分をみる

veryl
let ids_valid     : logic    = if_fifo_rvalid;
let ids_pc        : Addr     = if_fifo_rdata.addr;
let ids_inst_bits : Inst     = if_fifo_rdata.bits;
var ids_inst_valid: logic   ;
var ids_ctrl      : InstCtrl;
var ids_imm       : UIntX   ;

inst decoder: inst_decoder (
    bits : ids_inst_bits ,
    valid: ids_inst_valid,
    ctrl : ids_ctrl      ,
    imm  : ids_imm       ,
);

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

▼リスト10.25: 命令の有効判定を行う (inst_decoder.veryl) 差分をみる

veryl
valid = case op {
    OP_LUI, OP_AUIPC, OP_JAL, OP_JALR: T,
    OP_BRANCH                        : f3 != 3'b010 && f3 != 3'b011,
    OP_LOAD                          : f3 != 3'b111,
    OP_STORE                         : f3[2] == 1'b0,
    OP_OP                            : case f7 {
        7'b0000000: T, // RV32I
        7'b0100000: f3 == 3'b000 || f3 == 3'b101, // SUB, SRA
        7'b0000001: T, // RV32M
        default   : F,
    },
    OP_OP_IMM: case f3 {
        3'b001 : f7[6:1] == 6'b000000, // SLLI (RV64I)
        3'b101 : f7[6:1] == 6'b000000 || f7[6:1] == 6'b010000, // SRLI, SRAI (RV64I)
        default: T,
    },
    OP_OP_32: case f7 {
        7'b0000001: f3 == 3'b000 || f3[2] == 1'b1, // RV64M
        7'b0000000: f3 == 3'b000 || f3 == 3'b001 || f3 == 3'b101, // ADDW, SLLW, SRLW
        7'b0100000: f3 == 3'b000 || f3 == 3'b101, // SUBW, SRAW
        default   : F,
    },
    OP_OP_IMM_32: case f3 {
        3'b000 : T, // ADDIW
        3'b001 : f7 == 7'b0000000, // SLLIW
        3'b101 : f7 == 7'b0000000 || f7 == 7'b0100000, // SRLIW, SRAIW
        default: F,
    },
    OP_SYSTEM: f3 != 3'b000 && f3 != 3'b100 || // CSRR(W|S|C)[I]
     bits == 32'h00000073 || // ECALL
     bits == 32'h00100073 || // EBREAK
     bits == 32'h30200073, //MRET
    OP_MISC_MEM: T, // FENCE
    default    : F,
};

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

▼リスト10.26: OP-MISCのビット列を定義する (eei.veryl) 差分をみる

veryl
const OP_MISC_MEM : logic<7> = 7'b0001111;

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

▼リスト10.27: Illegal instruction例外のcauseを定義する (eei.veryl) 差分をみる

veryl
enum CsrCause: UIntX {
    ILLEGAL_INSTRUCTION = 2,
    BREAKPOINT = 3,
    ENVIRONMENT_CALL_FROM_M_MODE = 11,
}

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

▼リスト10.28: 不正な命令のとき、例外を発生させる (core.veryl) 差分をみる

veryl
exq_wdata.expt = 0;
if !ids_inst_valid {
    // illegal instruction
    exq_wdata.expt.valid = 1;
    exq_wdata.expt.cause = CsrCause::ILLEGAL_INSTRUCTION;
    exq_wdata.expt.value = {1'b0 repeat XLEN - ILEN, ids_inst_bits};
} else if ids_inst_bits == 32'h00000073 {

読み込み専用の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に分解します ( リスト30、 リスト29、 リスト31 )[5]。 また、causeを設定するためにcsrunitモジュールに命令のビット列を供給します。

▼リスト10.29: csrunitモジュールのポート定義を変更する (csrunit.veryl) 差分をみる

veryl
module csrunit (
    clk        : input  clock            ,
    rst        : input  reset            ,
    valid      : input  logic            ,
    pc         : input  Addr             ,
    inst_bits  : input  Inst             ,
    ctrl       : input  InstCtrl         ,
    expt_info  : input  ExceptionInfo    ,
    rd_addr    : input  logic        <5> ,
    csr_addr   : input  logic        <12>,
    rs1_addr   : input  logic        <5> ,
    rs1_data   : input  UIntX            ,
    rdata      : output UIntX            ,
    raise_trap : output logic            ,
    trap_vector: output Addr             ,
    led        : output UIntX            ,
) {

▼リスト10.30: csrunitモジュールのポート定義を変更する (core.veryl) 差分をみる

veryl
inst csru: csrunit (
    clk                               ,
    rst                               ,
    valid      : mems_valid           ,
    pc         : mems_pc              ,
    inst_bits  : mems_inst_bits       ,
    ctrl       : mems_ctrl            ,
    expt_info  : mems_expt            ,
    rd_addr    : mems_rd_addr         ,
    csr_addr   : mems_inst_bits[31:20],
    rs1_addr   : memq_rdata.rs1_addr  ,
    rs1_data   : memq_rdata.rs1_data  ,
    rdata      : csru_rdata           ,
    raise_trap : csru_raise_trap      ,
    trap_vector: csru_trap_vector     ,
    led                               ,
);

▼リスト10.31: rs1の変更に対応する (csrunit.veryl) 差分をみる

veryl
let wsource: UIntX = if ctrl.funct3[2] ? {1'b0 repeat XLEN - 5, rs1_addr} : rs1_data;
wdata   = case ctrl.funct3[1:0] {
    2'b01  : wsource,
    2'b10  : rdata | wsource,
    2'b11  : rdata & ~wsource,
    default: 'x,
} & wmask | (rdata & ~wmask);

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

▼リスト10.32: 読み込み専用CSRへの書き込みが発生するか判定する (csrunit.veryl) 差分をみる

veryl
// CSRR(W|S|C)[I]命令かどうか
let is_wsc: logic = ctrl.is_csr && ctrl.funct3[1:0] != 0;
// MRET命令かどうか
let is_mret: logic = inst_bits == 32'h30200073;

// Check CSR access
let will_not_write_csr     : logic = (ctrl.funct3[1:0] == 2 || ctrl.funct3[1:0] == 3) && rs1_addr == 0; // set/clear with source = 0
let expt_write_readonly_csr: logic = is_wsc && !will_not_write_csr && csr_addr[11:10] == 2'b11; // attempt to write read-only CSR

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

▼リスト10.33: 読み込み専用CSRの書き込みで例外を発生させる (csrunit.veryl) 差分をみる

veryl
let raise_expt: logic = valid && (expt_info.valid || expt_write_readonly_csr);
let expt_cause: UIntX = switch {
    expt_info.valid        : expt_info.cause,
    expt_write_readonly_csr: CsrCause::ILLEGAL_INSTRUCTION,
    default                : 0,
};
let expt_value: UIntX = switch {
    expt_info.valid                            : expt_info.value,
    expt_cause == CsrCause::ILLEGAL_INSTRUCTION: {1'b0 repeat XLEN - $bits(Inst), inst_bits},
    default                                    : 0
};

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

▼リスト10.34: トラップが発生したかを示すlogicをwbq_typeに追加する (core.veryl) 差分をみる

veryl
struct wbq_type {
    addr      : Addr    ,
    bits      : Inst    ,
    ctrl      : InstCtrl,
    imm       : UIntX   ,
    alu_result: UIntX   ,
    mem_rdata : UIntX   ,
    csr_rdata : UIntX   ,
    raise_trap: logic   ,
}

▼リスト10.35: トラップが発生したかをWBステージに伝える (core.veryl) 差分をみる

veryl
wbq_wdata.raise_trap = csru_raise_trap;

▼リスト10.36: トラップが発生しているとき、レジスタにデータを書き込まないようにする (core.veryl) 差分をみる

veryl
always_ff {
    if wbs_valid && wbs_ctrl.rwb_en && !wbq_rdata.raise_trap {
        regfile[wbs_rd_addr] = wbs_wb_data;
    }
}

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

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

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

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

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

▼リスト10.37: Instruction address misaligned例外のcauseを定義する (eei.veryl) 差分をみる

veryl
enum CsrCause: UIntX {
    INSTRUCTION_ADDRESS_MISALIGNED = 0,
    ILLEGAL_INSTRUCTION = 2,
    BREAKPOINT = 3,
    ENVIRONMENT_CALL_FROM_M_MODE = 11,
}

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

▼リスト10.38: EXステージでInstruction address misaligned例外の判定を行う (core.veryl) 差分をみる

veryl
memq_wdata.jump_addr  = if inst_is_br(exs_ctrl) ? exs_pc + exs_imm : exs_alu_result & ~1;
// exception
let instruction_address_misaligned: logic = memq_wdata.br_taken && memq_wdata.jump_addr[1:0] != 2'b00;
memq_wdata.expt                = exq_rdata.expt;
if !memq_rdata.expt.valid {
    if instruction_address_misaligned {
        memq_wdata.expt.valid = 1;
        memq_wdata.expt.cause = CsrCause::INSTRUCTION_ADDRESS_MISALIGNED;
        memq_wdata.expt.value = memq_wdata.jump_addr;
    }
}

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

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

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

▼リスト10.39: 例外のcauseを定義する (eei.veryl) 差分をみる

veryl
enum CsrCause: UIntX {
    INSTRUCTION_ADDRESS_MISALIGNED = 0,
    ILLEGAL_INSTRUCTION = 2,
    BREAKPOINT = 3,
    LOAD_ADDRESS_MISALIGNED = 4,
    STORE_AMO_ADDRESS_MISALIGNED = 6,
    ENVIRONMENT_CALL_FROM_M_MODE = 11,
}

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

▼リスト10.40: EXステージで例外の判定を行う (core.veryl) 差分をみる

veryl
let instruction_address_misaligned: logic = memq_wdata.br_taken && memq_wdata.jump_addr[1:0] != 2'b00;
let loadstore_address_misaligned  : logic = inst_is_memop(exs_ctrl) && case exs_ctrl.funct3[1:0] {
    2'b00  : 0, // B
    2'b01  : exs_alu_result[0] != 1'b0, // H
    2'b10  : exs_alu_result[1:0] != 2'b0, // W
    2'b11  : exs_alu_result[2:0] != 3'b0, // D
    default: 0,
};
memq_wdata.expt = exq_rdata.expt;
if !memq_rdata.expt.valid {
    if instruction_address_misaligned {
        memq_wdata.expt.valid = 1;
        memq_wdata.expt.cause = CsrCause::INSTRUCTION_ADDRESS_MISALIGNED;
        memq_wdata.expt.value = memq_wdata.jump_addr;
    } else if loadstore_address_misaligned {
        memq_wdata.expt.valid = 1;
        memq_wdata.expt.cause = if exs_ctrl.is_load ? CsrCause::LOAD_ADDRESS_MISALIGNED : CsrCause::STORE_AMO_ADDRESS_MISALIGNED;
        memq_wdata.expt.value = exs_alu_result;
    }
}

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

▼リスト10.41: 例外が発生するとき、memunitのvalidを0にする (core.veryl) 差分をみる

veryl
inst memu: memunit (
    clk                                   ,
    rst                                   ,
    valid : mems_valid && !mems_expt.valid,
    is_new: mems_is_new                   ,
    ctrl  : mems_ctrl                     ,
    addr  : memq_rdata.alu_result         ,
    rs2   : memq_rdata.rs2_data           ,
    rdata : memu_rdata                    ,
    stall : memu_stall                    ,
    membus: d_membus                      ,
);

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

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

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

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

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

  6. IDステージで判定することもできます。 ↩︎

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

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