Skip to content

M-modeの実装 (2. 割り込みの実装)

概要

割り込みとは何か?

アプリケーションを記述するとき、キーボードやマウスの入力、時間の経過のようなイベントに起因して何らかのプログラムを実行したいことがあります。 例えばキーボードから入力を得たいとき、ポーリング(Polling)、または割り込み(Interrupt)という手法が利用されます。

ポーリングとは、定期的に問い合わせを行う方式のことです。 例えばキーボード入力の場合、定期的にキーボードデバイスにアクセスして入力があるかどうかを確かめます。 1秒ごとに入力の有無を確認する場合、キーボードの入力から検知までに最大1秒の遅延が発生します。 確認頻度をあげると遅延を減らせますが、 長時間キーボード入力が無い場合、 入力の有無の確認頻度が上がる分だけ何も入力が無いデバイスに対する確認処理が実行されることになります。 この問題は、CPUからデバイスに問い合わせをする方式では解決できません。

入力の理想的な確認タイミングは入力が確認できるようになってすぐであるため、 入力があったタイミングでデバイス側からCPUにイベントを通知すればいいです。 これを実現するのが割り込みです。

割り込みとは、何らかのイベントの通知によって実行中のプログラムを中断し、通知内容を処理する方式のことです。 割り込みを使うと、ポーリングのように無駄にデバイスにアクセスをすることなく、入力の処理が必要な時にだけ実行できます。

RISC-Vの割り込み

RISC-Vでは割り込み機能がCSRによって提供されます。 割り込みが発生するとトラップが発生します。 割り込みを発生させるようなイベントは外部割り込み、ソフトウェア割り込み、タイマ割り込みの3つに大別されます。

外部割り込み (External Interrupt)
コア外部のデバイスによって発生する割り込み。 複数の外部デバイスの割り込みは割り込みコントローラ(第19章「PLICの実装」)などによって調停(制御)されます。
ソフトウェア割り込み (Software Interrupt)
CPUで動くソフトウェアが発生させる割り込み。 CSR、もしくはメモリにマップされたレジスタ値の変更によって発生します。
タイマ割り込み (Timer Interrupt)
タイマ回路(デバイス)によって引き起こされる割り込み。 タイマの設定と時間経過によって発生します。

M-modeだけが実装されたRISC-VのCPUでは、次にような順序で割り込みが提供されます。 他に実装されている特権レベルがある場合については「16.9 割り込み条件の変更」「17.4 トラップの委譲」で解説します。

  1. 割り込みを発生させるようなイベントがデバイスで発生する
  2. 割り込み原因に対応したmipレジスタのビットが0から1になる
  3. 割り込み原因に対応したmieレジスタのビットが1であることを確認する (0なら割り込みは発生しない)
  4. mstatus.MIEが1であることを確認する (0なら割り込みは発生しない)
  5. (割り込み(トラップ)開始)
  6. mstatus.MPIEにmstatus.MIEを格納する
  7. mstatus.MIEに0を格納する
  8. mtvecレジスタの値にジャンプする

mip(Machine Interrupt Pending)レジスタは、割り込みを発生させるようなイベントが発生したことを通知するMXLENビットのCSRです。 mie(Machine Interrupt Enable)レジスタは割り込みを許可するかを原因ごとに制御するMXLENビットのCSRです。 mstatus.MIEはすべての割り込みを許可するかどうかを制御する1ビットのフィールドです。 mieとmstatus.MIEのことを割り込みイネーブル(許可)レジスタと呼び、 特にmstatus.MIEのようなすべての割り込みを制御するビットのことをグローバル割り込みイネーブルビットと呼びます

割り込みの発生時にmstatus.MIEを0にすることで、割り込みの処理中に割り込みが発生することを防いでいます。 また、トラップから戻る(MRET命令を実行する)ときは、mstatus.MPIEの値をmstatus.MIEに書き戻すことで割り込みの許可状態を戻します。

割り込みの優先順位

RISC-Vには外部割り込み、ソフトウェア割り込み、タイマ割り込みがそれぞれM-mode、S-mode向けに用意されています。 それぞれの割り込みには表1のような優先順位が定義されていて、 複数の割り込みを発生させられるときは優先順位が高い割り込みを発生させます。

表15.1: RISC-Vの割り込みの優先順位

cause説明優先順位
11Machine external interrupt高い
3Machine software Interrupt
7Machine timer interrupt
9Supervisor external interrupt
1Supervisor software interrupt
5Supervisor timer interrupt低い

割り込みの原因(cause)

それぞれの割り込みには原因を区別するための値(cause)が割り当てられています。 割り込みのcauseのMSBは1です。

CsrCause型に割り込みのcauseを追加します (リスト1)。

▼リスト15.1: 割り込みの原因の定義 (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,
    SUPERVISOR_SOFTWARE_INTERRUPT = 'h8000_0000_0000_0001,
    MACHINE_SOFTWARE_INTERRUPT = 'h8000_0000_0000_0003,
    SUPERVISOR_TIMER_INTERRUPT = 'h8000_0000_0000_0005,
    MACHINE_TIMER_INTERRUPT = 'h8000_0000_0000_0007,
    SUPERVISOR_EXTERNAL_INTERRUPT = 'h8000_0000_0000_0009,
    MACHINE_EXTERNAL_INTERRUPT = 'h8000_0000_0000_000b,
}

ACLINT (Advanced Core Local Interruptor)

RISC-Vにはソフトウェア割り込みとタイマ割り込みを実現するデバイスの仕様であるACLINTが用意されています。 ACLINTは、SiFive社が開発したCLINT(Core-Local Interruptor)デバイスが基になった仕様です。

ACLINTにはMTIMER、MSWI、SSWIの3つのデバイスが定義されています。 MTIMERデバイスはタイマ割り込み、MSWIとSSWIデバイスはソフトウェア割り込み向けのデバイスで、 それぞれmipレジスタのMTIP、MSIP、SSIPビットに状態を通知します。

ACLINTのメモリマップ 本書ではACLINTを図図1のようなメモリマップで実装します。 本章ではMTIMER、MSWIデバイスを実装し、「17.5 ソフトウェア割り込みの実装 (SSWI)」でSSWIデバイスを実装します。 デバイスの具体的な仕様については後で解説します。

メモリマップ用の定数をeeiパッケージに記述してください (リスト2)。

▼リスト15.2: メモリマップ用の定数の定義 (eei.veryl) 差分をみる

veryl
// ACLINT
const MMAP_ACLINT_BEGIN   : Addr = 'h200_0000 as Addr;
const MMAP_ACLINT_MSIP    : Addr = 0;
const MMAP_ACLINT_MTIMECMP: Addr = 'h4000 as Addr;
const MMAP_ACLINT_MTIME   : Addr = 'h7ff8 as Addr;
const MMAP_ACLINT_SETSSIP : Addr = 'h8000 as Addr;
const MMAP_ACLINT_END     : Addr = MMAP_ACLINT_BEGIN + 'hbfff as Addr;

ACLINTモジュールの作成

本章では、ACLINTのデバイスをaclint_memoryモジュールに実装します。 aclint_memoryモジュールは割り込みを起こすためにcsrunitモジュールと接続します。

インターフェースを作成する

まず、ACLINTのデバイスとcsrunitモジュールを接続するためのインターフェースを作成します。 src/aclint_if.verylを作成し、次のように記述します (リスト3)。 インターフェースの中身は各デバイスの実装時に実装します。

▼リスト15.3: aclint_if.veryl 差分をみる

veryl
interface aclint_if {
    modport master {
        // TODO
    }
    modport slave {
        ..converse(master)
    }
}

aclint_memoryモジュールを作成する

ACLINTのデバイスを実装するモジュールを作成します。 src/aclint_memory.verylを作成し、次のように記述します (リスト4)。 まだどのレジスタも実装していません。

▼リスト15.4: aclint_memory.veryl 差分をみる

veryl
import eei::*;

module aclint_memory (
    clk   : input   clock            ,
    rst   : input   reset            ,
    membus: modport Membus::slave    ,
    aclint: modport aclint_if::master,
) {
    assign membus.ready = 1;
    always_ff {
        if_reset {
            membus.rvalid = 0;
            membus.rdata  = 0;
        } else {
            membus.rvalid = membus.valid;
        }
    }
}

mmio_controllerモジュールにACLINTを追加する

mmio_controllerモジュールにACLINTデバイスを追加して、 aclint_memoryモジュールにアクセスできるようにします。

Device型にACLINTを追加して、ACLINTのデバイスをアドレスにマップします ( リスト5、 リスト6 )。

▼リスト15.5: Device型にACLINTを追加する (mmio_controller.veryl) 差分をみる

veryl
enum Device {
    UNKNOWN,
    RAM,
    ROM,
    DEBUG,
    ACLINT,
}

▼リスト15.6: get_device関数でACLINTの範囲を定義する (mmio_controller.veryl) 差分をみる

veryl
if MMAP_ACLINT_BEGIN <= addr && addr <= MMAP_ACLINT_END {
    return Device::ACLINT;
}

ACLINTとのインターフェースを追加し、 reset_all_device_masters関数にインターフェースをリセットするコードを追加します ( リスト7、 リスト8 )。

▼リスト15.7: ポートにACLINTのインターフェースを追加する (mmio_controller.veryl) 差分をみる

veryl
module mmio_controller (
    clk          : input   clock         ,
    rst          : input   reset         ,
    DBG_ADDR     : input   Addr          ,
    req_core     : modport Membus::slave ,
    ram_membus   : modport Membus::master,
    rom_membus   : modport Membus::master,
    dbg_membus   : modport Membus::master,
    aclint_membus: modport Membus::master,
) {

▼リスト15.8: インターフェースの要求部分をリセットする (mmio_controller.veryl) 差分をみる

veryl
function reset_all_device_masters () {
    reset_membus_master(ram_membus);
    reset_membus_master(rom_membus);
    reset_membus_master(dbg_membus);
    reset_membus_master(aclint_membus);
}

readyrvalidを取得する関数にACLINTを登録します ( リスト9、 リスト10 )。

▼リスト15.9: get_device_ready関数にACLINTのreadyを追加 (mmio_controller.veryl) 差分をみる

veryl
Device::ACLINT: return aclint_membus.ready;

▼リスト15.10: get_device_rvalid関数にACLINTのrvalidを追加 (mmio_controller.veryl) 差分をみる

veryl
Device::ACLINT: return aclint_membus.rvalid;

ACLINTのrvalidrdatareq_coreに割り当てます ( リスト11 )。

▼リスト15.11: ACLINTへのアクセス結果をreqに割り当てる (mmio_controller.veryl) 差分をみる

veryl
Device::ACLINT: req <> aclint_membus;

ACLINTのインターフェースに要求を割り当てます ( リスト12 )。

▼リスト15.12: ACLINTにreqを割り当ててアクセス要求する (mmio_controller.veryl) 差分をみる

veryl
Device::ACLINT: {
    aclint_membus      <> req;
    aclint_membus.addr -= MMAP_ACLINT_BEGIN;
}

ACLINTとmmio_controller、csrunitモジュールを接続する

aclint_ifインターフェース(aclint_core_bus)、 aclint_memoryモジュールとmmio_controllerモジュールを接続するインターフェース(aclint_membus)をインスタンス化します ( リスト13、 リスト14 )。

▼リスト15.13: aclint_ifインターフェースのインスタンス化 (top.veryl) 差分をみる

veryl
inst aclint_core_bus: aclint_if;

▼リスト15.14: mmio_controllerモジュールと接続するインターフェースのインスタンス化 (top.veryl) 差分をみる

veryl
inst aclint_membus  : Membus;

aclint_memoryモジュールをインスタンス化し、 mmio_controllerモジュールと接続します ( リスト15、 リスト16 )。

▼リスト15.15: aclint_memoryモジュールをインスタンス化する (top.veryl) 差分をみる

veryl
inst aclintm: aclint_memory (
    clk                    ,
    rst                    ,
    membus: aclint_membus  ,
    aclint: aclint_core_bus,
);

▼リスト15.16: mmio_controllerモジュールと接続する (top.veryl) 差分をみる

veryl
inst mmioc: mmio_controller (
    clk                           ,
    rst                           ,
    DBG_ADDR     : MMAP_DBG_ADDR  ,
    req_core     : mmio_membus    ,
    ram_membus   : mmio_ram_membus,
    rom_membus   : mmio_rom_membus,
    dbg_membus                    ,
    aclint_membus                 ,
);

core、csrunitモジュールにaclint_ifポートを追加し、 csrunitモジュールとaclint_memoryモジュールを接続します ( リスト17、 リスト18、 リスト19、 リスト20 )。

▼リスト15.17: coreモジュールにACLINTのデバイスとのインターフェースを追加する (core.veryl) 差分をみる

veryl
module core (
    clk     : input   clock               ,
    rst     : input   reset               ,
    i_membus: modport core_inst_if::master,
    d_membus: modport core_data_if::master,
    led     : output  UIntX               ,
    aclint  : modport aclint_if::slave    ,
) {

▼リスト15.18: coreモジュールにaclint_ifインターフェースを接続する (top.veryl) 差分をみる

veryl
inst c: core (
    clk                      ,
    rst                      ,
    i_membus: i_membus_core  ,
    d_membus: d_membus_core  ,
    led                      ,
    aclint  : aclint_core_bus,
);

▼リスト15.19: csrunitモジュールACLINTデバイスとのインターフェースを追加する (csrunit.veryl) 差分をみる

veryl
    minstret   : input   UInt64              ,
    led        : output  UIntX               ,
    aclint     : modport aclint_if::slave    ,
) {

▼リスト15.20: csrunitモジュールのインスタンスにインターフェースを接続する (core.veryl) 差分をみる

veryl
    minstret                          ,
    led                               ,
    aclint                            ,
);

ソフトウェア割り込みの実装 (MSWI)

MSWIデバイスはソフトウェア割り込み(machine software interrupt)を提供するためのデバイスです。 MSWIデバイスにはハードウェアスレッド毎に4バイトのMSIPレジスタが用意されています(表2)。 MSIPレジスタの上位31ビットは読み込み専用の0であり、最下位ビットのみ変更できます。 各MSIPレジスタは、それに対応するハードウェアスレッドのmip.MSIPと接続されています。

表15.2: MSWIデバイスのメモリマップ

オフセットレジスタ
0000MSIP0
0004MSIP1
0008MSIP2
....
3ff8MSIP4094
3ffc予約済み
仕様上はmhartidとMSIPの後ろの数字(hartID)が一致する必要はありませんが、 本書ではmhartidとhartIDが同じになるように実装します。 他のACLINTのデバイスも同様に実装します。

MSIPレジスタを実装する

MSIPレジスタ ACLINTモジュールにMSIPレジスタを実装します(図2)。 今のところCPUにはmhartidが0のハードウェアスレッドしか存在しないため、MSIP0のみ実装します。

aclint_ifインターフェースにmsipを追加します (リスト21)。

▼リスト15.21: mispビットをインターフェースに追加する (aclint_if.veryl) 差分をみる

veryl
interface aclint_if {
    var msip: logic;
    modport master {
        msip: output,
    }
    modport slave {
        ..converse(master)
    }
}

aclint_memoryモジュールにmsip0レジスタを作成し、読み書きできるようにします ( リスト22、 リスト23、 リスト24 )。

▼リスト15.22: msip0レジスタの定義 (aclint_memory.veryl) 差分をみる

veryl
var msip0: logic;

▼リスト15.23: msip0レジスタを0でリセットする (aclint_memory.veryl) 差分をみる

veryl
always_ff {
    if_reset {
        membus.rvalid = 0;
        membus.rdata  = 0;
        msip0         = 0;

▼リスト15.24: msip0レジスタの書き込み、読み込み (aclint_memory.veryl) 差分をみる

veryl
if membus.valid {
    let addr: Addr = {membus.addr[XLEN - 1:3], 3'b0};
    if membus.wen {
        let M: logic<MEMBUS_DATA_WIDTH> = membus.wmask_expand();
        let D: logic<MEMBUS_DATA_WIDTH> = membus.wdata & M;
        case addr {
            MMAP_ACLINT_MSIP: msip0 = D[0] | msip0 & ~M[0];
            default         : {}
        }
    } else {
        membus.rdata = case addr {
            MMAP_ACLINT_MSIP: {63'b0, msip0},
            default         : 0,
        };
    }
}

msip0レジスタとインターフェースのmsipを接続します ( リスト25 )。

▼リスト15.25: インターフェースのmsipとmsip0レジスタを接続する (aclint_memory.veryl) 差分をみる

veryl
always_comb {
    aclint.msip = msip0;
}

mip、mieレジスタを実装する

mipレジスタmieレジスタ mipレジスタのMSIPビット、mieレジスタのMSIEビットを実装します。 mie.MSIEはMSIPビットによる割り込み待機を許可するかを制御するビットです。 mip.MSIPとmie.MSIEは同じ位置のビットに配置されています。 mip.MSIPに書き込むことはできません。

csrunitモジュールにmieレジスタを作成します ( リスト26、 リスト27 )。

▼リスト15.26: mieレジスタの定義 (csrunit.veryl) 差分をみる

veryl
var mie     : UIntX ;

▼リスト15.27: mieレジスタを0でリセットする (csrunit.veryl) 差分をみる

veryl
if_reset {
    mode     = PrivMode::M;
    mstatus  = 0;
    mtvec    = 0;
    mie      = 0;
    mscratch = 0;

mipレジスタを作成します。 MSIPビットをMSWIデバイスのMSIP0レジスタと接続し、 それ以外のビットは0に設定します (リスト28)。

▼リスト15.28: mipレジスタの定義 (csrunit.veryl) 差分をみる

veryl
let mip: UIntX = {
    1'b0 repeat XLEN - 12, // 0, LCOFIP
    1'b0, // MEIP
    1'b0, // 0
    1'b0, // SEIP
    1'b0, // 0
    1'b0, // MTIP
    1'b0, // 0
    1'b0, // STIP
    1'b0, // 0
    aclint.msip, // MSIP
    1'b0, // 0
    1'b0, // SSIP
    1'b0, // 0
};

mie、mipレジスタの値を読み込めるようにします (リスト29)。

▼リスト15.29: rdataにmip、mieレジスタの値を割り当てる (csrunit.veryl) 差分をみる

veryl
CsrAddr::MTVEC   : mtvec,
CsrAddr::MIP     : mip,
CsrAddr::MIE     : mie,
CsrAddr::MCYCLE  : mcycle,

mieレジスタの書き込みマスクを設定して、MSIEビットを書き込めるようにします ( リスト30、 リスト31、 リスト32 )。 あとでMTIMEデバイスを実装するときにMTIEビットを使うため、 ここでMTIEビットも書き込めるようにしておきます。

▼リスト15.30: mieレジスタの書き込みマスクの定義 (csrunit.veryl) 差分をみる

veryl
const MIE_WMASK     : UIntX = 'h0000_0000_0000_0088 as UIntX;

▼リスト15.31: wmaskに書き込みマスクを設定する (csrunit.veryl) 差分をみる

veryl
CsrAddr::MTVEC   : MTVEC_WMASK,
CsrAddr::MIE     : MIE_WMASK,
CsrAddr::MSCRATCH: MSCRATCH_WMASK,

▼リスト15.32: mieレジスタの書き込み (csrunit.veryl) 差分をみる

veryl
if is_wsc {
    case csr_addr {
        CsrAddr::MSTATUS : mstatus  = wdata;
        CsrAddr::MTVEC   : mtvec    = wdata;
        CsrAddr::MIE     : mie      = wdata;
        CsrAddr::MSCRATCH: mscratch = wdata;

mstatusのMIE、MPIEビットを実装する

mstatus.MIE、MPIEを変更できるようにします ( リスト33、 リスト34 )。

▼リスト15.33: 書き込みマスクを変更する (csrunit.veryl) 差分をみる

veryl
const MSTATUS_WMASK : UIntX = 'h0000_0000_0000_0088 as UIntX;

▼リスト15.34: レジスタの場所を変数に割り当てる (csrunit.veryl) 差分をみる

veryl
// mstatus bits
let mstatus_mpie: logic = mstatus[7];
let mstatus_mie : logic = mstatus[3];

トラップが発生するとき、mstatus.MPIEにmstatus.MIE、mstatus.MIEに0を設定します ( リスト35 )。 また、MRET命令でmstatus.MIEにmstatus.MPIE、mstatus.MPIEに0を設定します。

▼リスト15.35: トラップ、MRET命令の動作の実装 (csrunit.veryl) 差分をみる

veryl
if raise_trap {
    if raise_expt {
        mepc   = pc;
        mcause = trap_cause;
        mtval  = expt_value;
        // save mstatus.mie to mstatus.mpie
        // and set mstatus.mie = 0
        mstatus[7] = mstatus[3];
        mstatus[3] = 0;
    } else if trap_return {
        // set mstatus.mie = mstatus.mpie
        //     mstatus.mpie = 0
        mstatus[3] = mstatus[7];
        mstatus[7] = 0;
    }

これによりトラップで割り込みを無効化して、 トラップから戻るときにmstatus.MIEを元に戻す、 という動作が実現されます。

割り込み処理の実装

必要なレジスタを実装できたので、割り込みを起こす処理を実装します。 割り込みはmip、mieの両方のビット、mstatus.MIEビットが立っているときに発生します。

割り込みのタイミング

割り込みでトラップを発生させるとき、 トラップが発生した時点の命令のアドレスが必要なため、 csrunitモジュールに有効な命令が供給されている必要があります。

割り込みが発生したときにcsrunitモジュールに供給されていた命令は実行されません。 ここで、割り込みを起こすタイミングに注意が必要です。 今のところ、CSRの処理はMEMステージと同時に行っているため、 例えばストア命令をmemunitモジュールで実行している途中に割り込みを発生させてしまうと、 ストア命令の結果がメモリに反映されるにもかかわらず、 mepcレジスタにストア命令のアドレスを書き込んでしまいます。

それならば、単純に次の命令のアドレスをmepcレジスタに格納するようにすればいいと思うかもしれませんが、 そもそも実行中のストア命令が本来は最終的に例外を発生させるものかもしれません。

本章ではこの問題に対処するために、 割り込みはMEM(CSR)ステージに新しく命令が供給されたクロックでしか起こせなくして、 トラップが発生するならばmemunitモジュールを無効化します。

割り込みを発生させられるかを示すフラグ(can_intr)をcsrunitモジュールに定義し、 mems_is_newフラグを割り当てます ( リスト36、 リスト37 )。

▼リスト15.36: csrunitモジュールにcan_intrを追加する (csrunit.veryl) 差分をみる

veryl
rs1_data   : input   UIntX               ,
can_intr   : input   logic               ,
rdata      : output  UIntX               ,

▼リスト15.37: mem_is_newをcan_intrに割り当てる (core.veryl) 差分をみる

veryl
rs1_data   : memq_rdata.rs1_data  ,
can_intr   : mems_is_new          ,
rdata      : csru_rdata           ,

トラップが発生するときにmemunitモジュールを無効にします ( リスト38 )。 今まではEXステージまでに例外が発生することが分かっていたら無効にしていましたが、 csrunitモジュールからトラップが発生するかどうかの情報を直接得るようにします。

▼リスト15.38: validの条件を変更する (core.veryl) 差分をみる

veryl
inst memu: memunit (
    clk                                   ,
    rst                                   ,
    valid : mems_valid && !csru_raise_trap,

memunitモジュールが無効(!valid)なとき、 stateState::Initにリセットします (リスト39)。

▼リスト15.39: validではないとき、stateをInitにリセットする (core.veryl) 差分をみる

veryl
} else {
    if !valid {
        state = State::Init;
    } else {
        case state {
            State::Init: if is_new & inst_is_memop(ctrl) {

割り込みの判定

割り込みを起こすかどうか(raise_intrrupt)、 割り込みのcause(intrrupt_cause)、 トラップベクタ(interrupt_vector)を示す変数を作成します (リスト40)。

▼リスト15.40: 割り込みを判定する (csrunit.veryl) 差分をみる

veryl
// Interrupt
let raise_interrupt : logic = valid && can_intr && mstatus_mie && (mip & mie) != 0;
let interrupt_cause : UIntX = CsrCause::MACHINE_SOFTWARE_INTERRUPT;
let interrupt_vector: Addr  = mtvec;

トラップ情報の変数に、割り込みの情報を割り当てます (リスト41)。 本書では例外を優先します。

▼リスト15.41: トラップを制御する変数に割り込みの値を割り当てる (csrunit.veryl) 差分をみる

veryl
assign raise_trap = raise_expt || raise_interrupt || trap_return;
let trap_cause: UIntX = switch {
    raise_expt     : expt_cause,
    raise_interrupt: interrupt_cause,
    default        : 0,
};
assign trap_vector = switch {
    raise_expt     : mtvec,
    raise_interrupt: interrupt_vector,
    trap_return    : mepc,
    default        : 0,
};

割り込みの時にMRET命令の判定が0になるようにします (リスト42)。

▼リスト15.42: 割り込みが発生するとき、trap_returnを0にする (csrunit.veryl) 差分をみる

veryl
// Trap Return
assign trap_return = valid && is_mret && !raise_expt && !raise_interrupt;

トラップが発生するとき、 例外の場合にのみmtvalレジスタに例外に固有の情報が書き込まれます。 本書では例外を優先するので、 raise_expt1ならmtvalレジスタに書き込むようにします (リスト43)。

▼リスト15.43: 例外が発生したときにのみmtvalレジスタに書き込む (csrunit.veryl) 差分をみる

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

ソフトウェア割り込みをテストする

ソフトウェア割り込みが正しく動くことを確認します。

test/mswi.cを作成し、次のように記述します (リスト44)。

▼リスト15.44: test/mswi.c 差分をみる

c
#define MSIP0 ((volatile unsigned int *)0x2000000)
#define DEBUG_REG ((volatile unsigned long long*)0x40000000)
#define MIE_MSIE (1 << 3)
#define MSTATUS_MIE (1 << 3)

void interrupt_handler(void);

void w_mtvec(unsigned long long x) {
    asm volatile("csrw mtvec, %0" : : "r" (x));
}

void w_mie(unsigned long long x) {
    asm volatile("csrw mie, %0" : : "r" (x));
}

void w_mstatus(unsigned long long x) {
    asm volatile("csrw mstatus, %0" : : "r" (x));
}

void main(void) {
    w_mtvec((unsigned long long)interrupt_handler);
    w_mie(MIE_MSIE);
    w_mstatus(MSTATUS_MIE);
    *MSIP0 = 1;
    while (1) *DEBUG_REG = 3; // fail
}

void interrupt_handler(void) {
    *DEBUG_REG = 1; // success
}

プログラムでは、 mtvecにinterrupt_handler関数のアドレスを書き込み、 mstatus.MIE、mie.MSIEを1に設定して割り込みを許可してから MSIP0レジスタに1を書き込んでいます。

プログラムをコンパイルして実行[1]すると、 ソフトウェア割り込みが発生することでinterrupt_handlerにジャンプし、 デバッグ用のデバイスに1を書き込んで終了することを確認できます。

mtvecのVectoredモードの実装

mtvecレジスタ mtvecレジスタにはMODEフィールドがあり、 割り込みが発生するときのジャンプ先の決定方法を制御できます(図5)。

MODEがDirect(2'b00)のとき、mtvec.BASE << 2のアドレスにトラップします。 Vectored(2'b01)のとき、(mtvec.BASE << 2) + 4 * causeのアドレスにトラップします。 ここでcauseは割り込みのcauseのMSBを除いた値です。 例えばmachine software interruptの場合、(mtvec.BASE << 2) + 4 * 3がジャンプ先になります。

例外のトラップベクタは、常にMODEがDirectとして計算します。

下位1ビットに書き込めるようにすることで、 mtvec.MODEにVectoredを書き込めるようにします (リスト45)。

▼リスト15.45: 書き込みマスクを変更する (csrunit.veryl) 差分をみる

veryl
const MTVEC_WMASK   : UIntX = 'hffff_ffff_ffff_fffd;

割り込みのトラップベクタをMODEとcauseに応じて変更します (リスト46)。

▼リスト15.46: 割り込みのトラップベクタを求める (csrunit.veryl) 差分をみる

veryl
let interrupt_vector: Addr  = if mtvec[0] == 0 ? {mtvec[msb:2], 2'b0} : // Direct
 {mtvec[msb:2] + interrupt_cause[msb - 2:0], 2'b0}; // Vectored

例外のトラップベクタを、mtvecレジスタの下位2ビットを0にしたアドレス(Direct)に変更します ( リスト47、 リスト48 )。 新しくexpt_vectorを定義し、trap_vectorに割り当てます。

▼リスト15.47: 例外のトラップベクタ (csrunit.veryl) 差分をみる

veryl
let expt_vector: Addr = {mtvec[msb:2], 2'b0};

▼リスト15.48: expt_vectorをtrap_vectorに割り当てる (csrunit.veryl) 差分をみる

veryl
assign trap_vector = switch {
    raise_expt     : expt_vector,
    raise_interrupt: interrupt_vector,
    trap_return    : mepc,
    default        : 0,
};

タイマ割り込みの実装 (MTIMER)

タイマ割り込み

MTIMERデバイスは、タイマ割り込み(machine timer interrupt)を提供するためのデバイスです。 MTIMERデバイスには1つの8バイトのMTIMEレジスタ、 ハードウェアスレッド毎に8バイトのMTIMECMPレジスタが用意されています。 本書ではMTIMECMPの後ろにMTIMEを配置します(表3)。

表15.3: 本書のMTIMERデバイスのメモリマップ

オフセットレジスタ
0000MTIMECMP0
0008MTIMECMP1
....
7ff0MTIMECMP4094
7ff8MTIME
MTIMEレジスタは、固定された周波数でのクロックサイクル毎にインクリメントするレジスタです。 リセット時に`0`になります。

MTIMERデバイスは、それに対応するハードウェアスレッドのmip.MTIPと接続されており、 MTIMEがMTIMECMPを上回ったときmip.MTIPを1にします。 これにより、指定した時間に割り込みを発生させることが可能になります。

MTIME、MTIMECMPレジスタを実装する

ACLINTモジュールにMTIME、MTIMECMPレジスタを実装します。 今のところmhartidが0のハードウェアスレッドしか存在しないため、MTIMECMP0のみ実装します。

mtimemtimecmp0レジスタを作成し、読み書きできるようにします ( リスト49、 リスト50、 リスト51 )。 mtimeレジスタはクロック毎にインクリメントします。

▼リスト15.49: mtime、mtimecmpレジスタの定義 (aclint_memory.veryl)

veryl
var msip0    : logic ;
var mtime    : UInt64;
var mtimecmp0: UInt64;

▼リスト15.50: レジスタを0でリセットする (aclint_memory.veryl) 差分をみる

veryl
always_ff {
    if_reset {
        membus.rvalid = 0;
        membus.rdata  = 0;
        msip0         = 0;
        mtime         = 0;
        mtimecmp0     = 0;

▼リスト15.51: mtime、mtimecmpの書き込み、読み込み (aclint_memory.veryl) 差分をみる

veryl
if membus.wen {
    let M: logic<MEMBUS_DATA_WIDTH> = membus.wmask_expand();
    let D: logic<MEMBUS_DATA_WIDTH> = membus.wdata & M;
    case addr {
        MMAP_ACLINT_MSIP    : msip0     = D[0] | msip0 & ~M[0];
        MMAP_ACLINT_MTIME   : mtime     = D | mtime & ~M;
        MMAP_ACLINT_MTIMECMP: mtimecmp0 = D | mtimecmp0 & ~M;
        default             : {}
    }
} else {
    membus.rdata = case addr {
        MMAP_ACLINT_MSIP    : {63'b0, msip0},
        MMAP_ACLINT_MTIME   : mtime,
        MMAP_ACLINT_MTIMECMP: mtimecmp0,
        default             : 0,
    };
}

aclint_ifインターフェースにmtipを作成し、タイマ割り込みが発生する条件を設定します ( リスト52、 リスト53 )。

▼リスト15.52: mtipをインターフェースに追加する (aclint_if.veryl) 差分をみる

veryl
var msip: logic;
var mtip: logic;
modport master {
    msip: output,
    mtip: output,
}

▼リスト15.53: mtipにタイマ割り込みが発生する条件を設定する (aclint_memory.veryl) 差分をみる

veryl
always_comb {
    aclint.msip = msip0;
    aclint.mtip = mtime >= mtimecmp0;
}

mip.MTIP、割り込み原因を設定する

mipレジスタのMTIPビットにaclint_ifインターフェースのmtipを接続します (リスト54)。

▼リスト15.54: mip.MTIPにインターフェースのmtipを割り当てる (csrunit.veryl) 差分をみる

veryl
let mip: UIntX = {
    1'b0 repeat XLEN - 12, // 0, LCOFIP
    1'b0, // MEIP
    1'b0, // 0
    1'b0, // SEIP
    1'b0, // 0
    aclint.mtip, // MTIP
    1'b0, // 0
    1'b0, // STIP
    1'b0, // 0
    aclint.msip, // MSIP
    1'b0, // 0
    1'b0, // SSIP
    1'b0, // 0
};

割り込み原因を優先順位に応じて設定します。 タイマ割り込みはソフトウェア割り込みよりも優先順位が低いため、 ソフトウェア割り込みの下で原因を設定します (リスト55)。

▼リスト15.55: タイマ割り込みのcauseを設定する (csrunit.veryl) 差分をみる

veryl
let interrupt_pending: UIntX = mip & mie;
let raise_interrupt  : logic = valid && can_intr && mstatus_mie && interrupt_pending != 0;
let interrupt_cause  : UIntX = switch {
    interrupt_pending[3]: CsrCause::MACHINE_SOFTWARE_INTERRUPT,
    interrupt_pending[7]: CsrCause::MACHINE_TIMER_INTERRUPT,
    default             : 0,
};
let interrupt_vector: Addr = if mtvec[0] == 0 ? {mtvec[msb:2], 2'b0} : // Direct
 {mtvec[msb:2] + interrupt_cause[msb - 2:0], 2'b0}; // Vectored

タイマ割り込みをテストする

タイマ割り込みが正しく動くことを確認します。

test/mtime.cを作成し、次のように記述します (リスト56)。

▼リスト15.56: test/mtime.c 差分をみる

c
#define MTIMECMP0 ((volatile unsigned int *)0x2004000)
#define MTIME     ((volatile unsigned int *)0x2007ff8)
#define DEBUG_REG ((volatile unsigned long long*)0x40000000)
#define MIE_MTIE (1 << 7)
#define MSTATUS_MIE (1 << 3)

void interrupt_handler(void);

void w_mtvec(unsigned long long x) {
    asm volatile("csrw mtvec, %0" : : "r" (x));
}

void w_mie(unsigned long long x) {
    asm volatile("csrw mie, %0" : : "r" (x));
}

void w_mstatus(unsigned long long x) {
    asm volatile("csrw mstatus, %0" : : "r" (x));
}

void main(void) {
    w_mtvec((unsigned long long)interrupt_handler);
    *MTIMECMP0 = *MTIME + 1000000; // この数値は適当に調整する
    w_mie(MIE_MTIE);
    w_mstatus(MSTATUS_MIE);
    while (1);
    *DEBUG_REG = 3; // fail
}

void interrupt_handler(void) {
    *DEBUG_REG = 1; // success
}

プログラムでは、 mtvecにinterrupt_handler関数のアドレスを設定し、 mtimeに10000000を足した値をmtimecmp0に設定した後、 mstatus.MIE、mie.MTIEを1に設定して割り込みを許可しています。 タイマ割り込みが発生するまでwhile文で無限ループします。

プログラムをコンパイルして実行すると、 時間経過によってmain関数からinterrupt_handler関数にトラップしてテストが終了します。 mtimecmp0に設定する値を変えることで、 タイマ割り込みが発生するまでの時間が変わることを確認してください。

WFI命令の実装

WFI命令は、割り込みが発生するまでCPUをストールさせる命令です。 ただし、グローバル割り込みイネーブルビットは考慮せず、 ある割り込みの待機(pending)ビットと許可(enable)ビットの両方が立っているときに実行を再開します。 また、それ以外の自由な理由で実行を再開させてもいいです。 WFI命令で割り込みが発生するとき、WFI命令の次のアドレスの命令で割り込みが起こったことになります。

本書ではWFI命令を何もしない命令として実装します。

inst_decoderモジュールでWFI命令をデコードできるようにします (リスト57)。

▼リスト15.57: WFI命令のデコード (inst_decoder.veryl) 差分をみる

veryl
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
 bits == 32'h10500073, // WFI
OP_MISC_MEM: T, // FENCE

WFI命令で割り込みが発生するとき、mepcレジスタにpc + 4を書き込むようにします ( リスト58、 リスト59 )。

▼リスト15.58: WFI命令の判定 (csrunit.veryl) 差分をみる

veryl
let is_wfi: logic = inst_bits == 32'h10500073;

▼リスト15.59: WFI命令のとき、mepcをpc+4にする (csrunit.veryl) 差分をみる

veryl
if raise_expt || raise_interrupt {
    mepc = if raise_expt ? pc : // exception
    if raise_interrupt && is_wfi ? pc + 4 : pc; // interrupt when wfi / interrupt
    mcause = trap_cause;

time、instret、cycleレジスタの実装

RISC-Vにはtime、instret、cycleという読み込み専用のCSRが定義されており、 それぞれmtime、minstret、mcycleレジスタと同じ値をとります[2]

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

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

veryl
// Unprivileged Counter/Timers
CYCLE = 12'hC00,
TIME = 12'hC01,
INSTRET = 12'hC02,

mtimeレジスタの値をACLINTモジュールからcsrunitに渡します ( リスト61、 リスト62 )。

▼リスト15.61: mtimeをインターフェースに追加する (aclint_if.veryl) 差分をみる

veryl
import eei::*;

interface aclint_if {
    var msip : logic ;
    var mtip : logic ;
    var mtime: UInt64;
    modport master {
        msip : output,
        mtip : output,
        mtime: output,
    }

▼リスト15.62: mtimeをインターフェースに割り当てる (aclint_memory.veryl) 差分をみる

veryl
always_comb {
    aclint.msip  = msip0;
    aclint.mtip  = mtime >= mtimecmp0;
    aclint.mtime = mtime;
}

time、instret、cycleレジスタを読み込めるようにします (リスト63)。

▼リスト15.63: rdataにインターフェースのmtimeを割り当てる (csrunit.veryl) 差分をみる

veryl
CsrAddr::CYCLE   : mcycle,
CsrAddr::TIME    : aclint.mtime,
CsrAddr::INSTRET : minstret,

  1. コンパイル、実行方法は「11.6.4 出力をテストする」を参考にしてください。 ↩︎

  2. mhpmcounterレジスタと同じ値をとるhpmcounterレジスタもありますが、mhpmcounterレジスタを実装していないので実装しません。 ↩︎