Verylで作るCPU
Star

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

15.1 概要

15.1.1 割り込みとは何か?

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

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

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

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

15.1.2 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に書き戻すことで割り込みの許可状態を戻します。

15.1.3 割り込みの優先順位

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

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

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

15.1.4 割り込みの原因(cause)

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

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

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

15.1.5 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のメモリマップ

図15.1: ACLINTのメモリマップ

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

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

リスト15.2: リスト15.2: メモリマップ用の定数の定義 (eei.veryl)
1:     // ACLINT
2:     const MMAP_ACLINT_BEGIN   : Addr = 'h200_0000 as Addr;
3:     const MMAP_ACLINT_MSIP    : Addr = 0;
4:     const MMAP_ACLINT_MTIMECMP: Addr = 'h4000 as Addr;
5:     const MMAP_ACLINT_MTIME   : Addr = 'h7ff8 as Addr;
6:     const MMAP_ACLINT_SETSSIP : Addr = 'h8000 as Addr;
7:     const MMAP_ACLINT_END     : Addr = MMAP_ACLINT_BEGIN + 'hbfff as Addr;

15.2 ACLINTモジュールの作成

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

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

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

リスト15.3: リスト15.3: aclint_if.veryl
1: interface aclint_if {
2:     modport master {
3:         // TODO
4:     }
5:     modport slave {
6:         ..converse(master)
7:     }
8: }

15.2.2 aclint_memoryモジュールを作成する

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

リスト15.4: リスト15.4: aclint_memory.veryl
1: import eei::*;
2: 
3: module aclint_memory (
4:     clk   : input   clock            ,
5:     rst   : input   reset            ,
6:     membus: modport Membus::slave    ,
7:     aclint: modport aclint_if::master,
8: ) {
9:     assign membus.ready = 1;
10:     always_ff {
11:         if_reset {
12:             membus.rvalid = 0;
13:             membus.rdata  = 0;
14:         } else {
15:             membus.rvalid = membus.valid;
16:         }
17:     }
18: }

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

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

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

リスト15.5: リスト15.5: Device型にACLINTを追加する (mmio_controller.veryl)
1:     enum Device {
2:         UNKNOWN,
3:         RAM,
4:         ROM,
5:         DEBUG,
6:         ACLINT,
7:     }
リスト15.6: リスト15.6: get_device関数でACLINTの範囲を定義する (mmio_controller.veryl)
1:     if MMAP_ACLINT_BEGIN <= addr && addr <= MMAP_ACLINT_END {
2:         return Device::ACLINT;
3:     }

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

リスト15.7: リスト15.7: ポートにACLINTのインターフェースを追加する (mmio_controller.veryl)
1: module mmio_controller (
2:     clk          : input   clock         ,
3:     rst          : input   reset         ,
4:     DBG_ADDR     : input   Addr          ,
5:     req_core     : modport Membus::slave ,
6:     ram_membus   : modport Membus::master,
7:     rom_membus   : modport Membus::master,
8:     dbg_membus   : modport Membus::master,
9:     aclint_membus: modport Membus::master,
10: ) {
リスト15.8: リスト15.8: インターフェースの要求部分をリセットする (mmio_controller.veryl)
1:     function reset_all_device_masters () {
2:         reset_membus_master(ram_membus);
3:         reset_membus_master(rom_membus);
4:         reset_membus_master(dbg_membus);
5:         reset_membus_master(aclint_membus);
6:     }

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

リスト15.9: リスト15.9: get_device_ready関数にACLINTのreadyを追加 (mmio_controller.veryl)
1:     Device::ACLINT: return aclint_membus.ready;
リスト15.10: リスト15.10: get_device_rvalid関数にACLINTのrvalidを追加 (mmio_controller.veryl)
1:     Device::ACLINT: return aclint_membus.rvalid;

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

リスト15.11: リスト15.11: ACLINTへのアクセス結果をreqに割り当てる (mmio_controller.veryl)
1:     Device::ACLINT: req <> aclint_membus;

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

リスト15.12: リスト15.12: ACLINTにreqを割り当ててアクセス要求する (mmio_controller.veryl)
1:     Device::ACLINT: {
2:         aclint_membus      <> req;
3:         aclint_membus.addr -= MMAP_ACLINT_BEGIN;
4:     }

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

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

リスト15.13: リスト15.13: aclint_ifインターフェースのインスタンス化 (top.veryl)
1:     inst aclint_core_bus: aclint_if;
リスト15.14: リスト15.14: mmio_controllerモジュールと接続するインターフェースのインスタンス化 (top.veryl)
1:     inst aclint_membus  : Membus;

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

リスト15.15: リスト15.15: aclint_memoryモジュールをインスタンス化する (top.veryl)
1:     inst aclintm: aclint_memory (
2:         clk                    ,
3:         rst                    ,
4:         membus: aclint_membus  ,
5:         aclint: aclint_core_bus,
6:     );
リスト15.16: リスト15.16: mmio_controllerモジュールと接続する (top.veryl)
1:     inst mmioc: mmio_controller (
2:         clk                           ,
3:         rst                           ,
4:         DBG_ADDR     : MMAP_DBG_ADDR  ,
5:         req_core     : mmio_membus    ,
6:         ram_membus   : mmio_ram_membus,
7:         rom_membus   : mmio_rom_membus,
8:         dbg_membus                    ,
9:         aclint_membus                 ,
10:     );

core、csrunitモジュールにaclint_ifポートを追加し、csrunitモジュールとaclint_memoryモジュールを接続します(リスト15.17リスト15.18リスト15.19リスト15.20)。

リスト15.17: リスト15.17: coreモジュールにACLINTのデバイスとのインターフェースを追加する (core.veryl)
1: module core (
2:     clk     : input   clock               ,
3:     rst     : input   reset               ,
4:     i_membus: modport core_inst_if::master,
5:     d_membus: modport core_data_if::master,
6:     led     : output  UIntX               ,
7:     aclint  : modport aclint_if::slave    ,
8: ) {
リスト15.18: リスト15.18: coreモジュールにaclint_ifインターフェースを接続する (top.veryl)
1:     inst c: core (
2:         clk                      ,
3:         rst                      ,
4:         i_membus: i_membus_core  ,
5:         d_membus: d_membus_core  ,
6:         led                      ,
7:         aclint  : aclint_core_bus,
8:     );
リスト15.19: リスト15.19: csrunitモジュールACLINTデバイスとのインターフェースを追加する (csrunit.veryl)
1:     minstret   : input   UInt64              ,
2:     led        : output  UIntX               ,
3:     aclint     : modport aclint_if::slave    ,
4: ) {
リスト15.20: リスト15.20: csrunitモジュールのインスタンスにインターフェースを接続する (core.veryl)
1:         minstret                          ,
2:         led                               ,
3:         aclint                            ,
4:     );

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

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

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

オフセットレジスタ
0000MSIP0
0004MSIP1
0008MSIP2
....
3ff8MSIP4094
3ffc予約済み

仕様上はmhartidとMSIPの後ろの数字(hartID)が一致する必要はありませんが、本書ではmhartidとhartIDが同じになるように実装します。他のACLINTのデバイスも同様に実装します。

15.3.1 MSIPレジスタを実装する

MSIPレジスタ

図15.2: MSIPレジスタ

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

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

リスト15.21: リスト15.21: mispビットをインターフェースに追加する (aclint_if.veryl)
1: interface aclint_if {
2:     var msip: logic;
3:     modport master {
4:         msip: output,
5:     }
6:     modport slave {
7:         ..converse(master)
8:     }
9: }

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

リスト15.22: リスト15.22: msip0レジスタの定義 (aclint_memory.veryl)
1: var msip0: logic;
リスト15.23: リスト15.23: msip0レジスタを0でリセットする (aclint_memory.veryl)
1: always_ff {
2:     if_reset {
3:         membus.rvalid = 0;
4:         membus.rdata  = 0;
5:         msip0         = 0;
リスト15.24: リスト15.24: msip0レジスタの書き込み、読み込み (aclint_memory.veryl)
1: if membus.valid {
2:     let addr: Addr = {membus.addr[XLEN - 1:3], 3'b0};
3:     if membus.wen {
4:         let M: logic<MEMBUS_DATA_WIDTH> = membus.wmask_expand();
5:         let D: logic<MEMBUS_DATA_WIDTH> = membus.wdata & M;
6:         case addr {
7:             MMAP_ACLINT_MSIP: msip0 = D[0] | msip0 & ~M[0];
8:             default         : {}
9:         }
10:     } else {
11:         membus.rdata = case addr {
12:             MMAP_ACLINT_MSIP: {63'b0, msip0},
13:             default         : 0,
14:         };
15:     }
16: }

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

リスト15.25: リスト15.25: インターフェースのmsipとmsip0レジスタを接続する (aclint_memory.veryl)
1: always_comb {
2:     aclint.msip = msip0;
3: }

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

mipレジスタ

図15.3: mipレジスタ

mieレジスタ

図15.4: mieレジスタ

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

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

リスト15.26: リスト15.26: mieレジスタの定義 (csrunit.veryl)
1:     var mie     : UIntX ;
リスト15.27: リスト15.27: mieレジスタを0でリセットする (csrunit.veryl)
1: if_reset {
2:     mode     = PrivMode::M;
3:     mstatus  = 0;
4:     mtvec    = 0;
5:     mie      = 0;
6:     mscratch = 0;

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

リスト15.28: リスト15.28: mipレジスタの定義 (csrunit.veryl)
1:     let mip: UIntX = {
2:         1'b0 repeat XLEN - 12, // 0
3:         1'b0, // MEIP
4:         1'b0, // 0
5:         1'b0, // SEIP
6:         1'b0, // 0
7:         1'b0, // MTIP
8:         1'b0, // 0
9:         1'b0, // STIP
10:         1'b0, // 0
11:         aclint.msip, // MSIP
12:         1'b0, // 0
13:         1'b0, // SSIP
14:         1'b0, // 0
15:     };

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

リスト15.29: リスト15.29: rdataにmip、mieレジスタの値を割り当てる (csrunit.veryl)
1: CsrAddr::MTVEC   : mtvec,
2: CsrAddr::MIP     : mip,
3: CsrAddr::MIE     : mie,
4: CsrAddr::MCYCLE  : mcycle,

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

リスト15.30: リスト15.30: mieレジスタの書き込みマスクの定義 (csrunit.veryl)
1:     const MIE_WMASK     : UIntX = 'h0000_0000_0000_0088 as UIntX;
リスト15.31: リスト15.31: wmaskに書き込みマスクを設定する (csrunit.veryl)
1: CsrAddr::MTVEC   : MTVEC_WMASK,
2: CsrAddr::MIE     : MIE_WMASK,
3: CsrAddr::MSCRATCH: MSCRATCH_WMASK,
リスト15.32: リスト15.32: mieレジスタの書き込み (csrunit.veryl)
1: if is_wsc {
2:     case csr_addr {
3:         CsrAddr::MSTATUS : mstatus  = wdata;
4:         CsrAddr::MTVEC   : mtvec    = wdata;
5:         CsrAddr::MIE     : mie      = wdata;
6:         CsrAddr::MSCRATCH: mscratch = wdata;

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

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

リスト15.33: リスト15.33: 書き込みマスクを変更する (csrunit.veryl)
1:     const MSTATUS_WMASK : UIntX = 'h0000_0000_0000_0088 as UIntX;
リスト15.34: リスト15.34: レジスタの場所を変数に割り当てる (csrunit.veryl)
1:     // mstatus bits
2:     let mstatus_mpie: logic = mstatus[7];
3:     let mstatus_mie : logic = mstatus[3];

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

リスト15.35: リスト15.35: トラップ、MRET命令の動作の実装 (csrunit.veryl)
1: if raise_trap {
2:     if raise_expt {
3:         mepc   = pc;
4:         mcause = trap_cause;
5:         mtval  = expt_value;
6:         // save mstatus.mie to mstatus.mpie
7:         // and set mstatus.mie = 0
8:         mstatus[7] = mstatus[3];
9:         mstatus[3] = 0;
10:     } else if trap_return {
11:         // set mstatus.mie = mstatus.mpie
12:         //     mstatus.mpie = 0
13:         mstatus[3] = mstatus[7];
14:         mstatus[7] = 0;
15:     }

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

15.3.4 割り込み処理の実装

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

割り込みのタイミング

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

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

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

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

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

リスト15.36: リスト15.36: csrunitモジュールにcan_intrを追加する (csrunit.veryl)
1:     rs1_data   : input   UIntX               ,
2:     can_intr   : input   logic               ,
3:     rdata      : output  UIntX               ,
リスト15.37: リスト15.37: mem_is_newをcan_intrに割り当てる (core.veryl)
1:     rs1_data   : memq_rdata.rs1_data  ,
2:     can_intr   : mems_is_new          ,
3:     rdata      : csru_rdata           ,

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

リスト15.38: リスト15.38: validの条件を変更する (core.veryl)
1:     inst memu: memunit (
2:         clk                                   ,
3:         rst                                   ,
4:         valid : mems_valid && !csru_raise_trap,

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

リスト15.39: リスト15.39: validではないとき、stateをInitにリセットする (core.veryl)
1:     } else {
2:         if !valid {
3:             state = State::Init;
4:         } else {
5:             case state {
6:                 State::Init: if is_new & inst_is_memop(ctrl) {

割り込みの判定

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

リスト15.40: リスト15.40: 割り込みを判定する (csrunit.veryl)
1:     // Interrupt
2:     let raise_interrupt : logic = valid && can_intr && mstatus_mie && (mip & mie) != 0;
3:     let interrupt_cause : UIntX = CsrCause::MACHINE_SOFTWARE_INTERRUPT;
4:     let interrupt_vector: Addr  = mtvec;

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

リスト15.41: リスト15.41: トラップを制御する変数に割り込みの値を割り当てる (csrunit.veryl)
1:     assign raise_trap = raise_expt || raise_interrupt || trap_return;
2:     let trap_cause: UIntX = switch {
3:         raise_expt     : expt_cause,
4:         raise_interrupt: interrupt_cause,
5:         default        : 0,
6:     };
7:     assign trap_vector = switch {
8:         raise_expt     : mtvec,
9:         raise_interrupt: interrupt_vector,
10:         trap_return    : mepc,
11:         default        : 0,
12:     };

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

リスト15.42: リスト15.42: 割り込みが発生するとき、trap_returnを0にする (csrunit.veryl)
1:     // Trap Return
2:     assign trap_return = valid && is_mret && !raise_expt && !raise_interrupt;

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

リスト15.43: リスト15.43: 例外が発生したときにのみmtvalレジスタに書き込む (csrunit.veryl)
1:     if raise_trap {
2:         if raise_expt || raise_interrupt {
3:             mepc   = pc;
4:             mcause = trap_cause;
5:             if raise_expt {
6:                 mtval = expt_value;
7:             }

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

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

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

リスト15.44: リスト15.44: test/mswi.c
1: #define MSIP0 ((volatile unsigned int *)0x2000000)
2: #define DEBUG_REG ((volatile unsigned long long*)0x40000000)
3: #define MIE_MSIE (1 << 3)
4: #define MSTATUS_MIE (1 << 3)
5: 
6: void interrupt_handler(void);
7: 
8: void w_mtvec(unsigned long long x) {
9:     asm volatile("csrw mtvec, %0" : : "r" (x));
10: }
11: 
12: void w_mie(unsigned long long x) {
13:     asm volatile("csrw mie, %0" : : "r" (x));
14: }
15: 
16: void w_mstatus(unsigned long long x) {
17:     asm volatile("csrw mstatus, %0" : : "r" (x));
18: }
19: 
20: void main(void) {
21:     w_mtvec((unsigned long long)interrupt_handler);
22:     w_mie(MIE_MSIE);
23:     w_mstatus(MSTATUS_MIE);
24:     *MSIP0 = 1;
25:     while (1) *DEBUG_REG = 3; // fail
26: }
27: 
28: void interrupt_handler(void) {
29:     *DEBUG_REG = 1; // success
30: }

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

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

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

15.4 mtvecのVectoredモードの実装

mtvecレジスタ

図15.5: mtvecレジスタ

mtvecレジスタにはMODEフィールドがあり、割り込みが発生するときのジャンプ先の決定方法を制御できます(図15.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を書き込めるようにします(リスト15.45)。

リスト15.45: リスト15.45: 書き込みマスクを変更する (csrunit.veryl)
1:     const MTVEC_WMASK   : UIntX = 'hffff_ffff_ffff_fffd;

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

リスト15.46: リスト15.46: 割り込みのトラップベクタを求める (csrunit.veryl)
1:     let interrupt_vector: Addr  = if mtvec[0] == 0 ? {mtvec[msb:2], 2'b0} : // Direct
2:      {mtvec[msb:2] + interrupt_cause[msb - 2:0], 2'b0}; // Vectored

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

リスト15.47: リスト15.47: 例外のトラップベクタ (csrunit.veryl)
1:     let expt_vector: Addr = {mtvec[msb:2], 2'b0};
リスト15.48: リスト15.48: expt_vectorをtrap_vectorに割り当てる (csrunit.veryl)
1:     assign trap_vector = switch {
2:         raise_expt     : expt_vector,
3:         raise_interrupt: interrupt_vector,
4:         trap_return    : mepc,
5:         default        : 0,
6:     };

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

15.5.1 タイマ割り込み

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

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

オフセットレジスタ
0000MTIMECMP0
0008MTIMECMP1
....
7ff0MTIMECMP4094
7ff8MTIME

MTIMEレジスタは、固定された周波数でのクロックサイクル毎にインクリメントするレジスタです。リセット時に0になります。

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

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

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

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

リスト15.49: リスト15.49: mtime、mtimecmpレジスタの定義 (aclint_memory.veryl)
1:     var msip0    : logic ;
2:     var mtime    : UInt64;
3:     var mtimecmp0: UInt64;
リスト15.50: リスト15.50: レジスタを0でリセットする (aclint_memory.veryl)
1:     always_ff {
2:         if_reset {
3:             membus.rvalid = 0;
4:             membus.rdata  = 0;
5:             msip0         = 0;
6:             mtime         = 0;
7:             mtimecmp0     = 0;
リスト15.51: リスト15.51: mtime、mtimecmpの書き込み、読み込み (aclint_memory.veryl)
1:     if membus.wen {
2:         let M: logic<MEMBUS_DATA_WIDTH> = membus.wmask_expand();
3:         let D: logic<MEMBUS_DATA_WIDTH> = membus.wdata & M;
4:         case addr {
5:             MMAP_ACLINT_MSIP    : msip0     = D[0] | msip0 & ~M[0];
6:             MMAP_ACLINT_MTIME   : mtime     = D | mtime & ~M;
7:             MMAP_ACLINT_MTIMECMP: mtimecmp0 = D | mtimecmp0 & ~M;
8:             default             : {}
9:         }
10:     } else {
11:         membus.rdata = case addr {
12:             MMAP_ACLINT_MSIP    : {63'b0, msip0},
13:             MMAP_ACLINT_MTIME   : mtime,
14:             MMAP_ACLINT_MTIMECMP: mtimecmp0,
15:             default             : 0,
16:         };
17:     }

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

リスト15.52: リスト15.52: mtipをインターフェースに追加する (aclint_if.veryl)
1:     var msip: logic;
2:     var mtip: logic;
3:     modport master {
4:         msip: output,
5:         mtip: output,
6:     }
リスト15.53: リスト15.53: mtipにタイマ割り込みが発生する条件を設定する (aclint_memory.veryl)
1:     always_comb {
2:         aclint.msip = msip0;
3:         aclint.mtip = mtime >= mtimecmp0;
4:     }

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

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

リスト15.54: リスト15.54: mip.MTIPにインターフェースのmtipを割り当てる (csrunit.veryl)
1:     let mip: UIntX = {
2:         1'b0 repeat XLEN - 12, // 0, LCOFIP
3:         1'b0, // MEIP
4:         1'b0, // 0
5:         1'b0, // SEIP
6:         1'b0, // 0
7:         aclint.mtip, // MTIP
8:         1'b0, // 0
9:         1'b0, // STIP
10:         1'b0, // 0
11:         aclint.msip, // MSIP
12:         1'b0, // 0
13:         1'b0, // SSIP
14:         1'b0, // 0
15:     };

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

リスト15.55: リスト15.55: タイマ割り込みのcauseを設定する (csrunit.veryl)
1:     let interrupt_pending: UIntX = mip & mie;
2:     let raise_interrupt  : logic = valid && can_intr && mstatus_mie && interrupt_pending != 0;
3:     let interrupt_cause  : UIntX = switch {
4:         interrupt_pending[3]: CsrCause::MACHINE_SOFTWARE_INTERRUPT,
5:         interrupt_pending[7]: CsrCause::MACHINE_TIMER_INTERRUPT,
6:         default             : 0,
7:     };
8:     let interrupt_vector: Addr = if mtvec[0] == 0 ? {mtvec[msb:2], 2'b0} : // Direct
9:      {mtvec[msb:2] + interrupt_cause[msb - 2:0], 2'b0}; // Vectored

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

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

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

リスト15.56: リスト15.56: test/mtime.c
1: #define MTIMECMP0 ((volatile unsigned int *)0x2004000)
2: #define MTIME     ((volatile unsigned int *)0x2007ff8)
3: #define DEBUG_REG ((volatile unsigned long long*)0x40000000)
4: #define MIE_MTIE (1 << 7)
5: #define MSTATUS_MIE (1 << 3)
6: 
7: void interrupt_handler(void);
8: 
9: void w_mtvec(unsigned long long x) {
10:     asm volatile("csrw mtvec, %0" : : "r" (x));
11: }
12: 
13: void w_mie(unsigned long long x) {
14:     asm volatile("csrw mie, %0" : : "r" (x));
15: }
16: 
17: void w_mstatus(unsigned long long x) {
18:     asm volatile("csrw mstatus, %0" : : "r" (x));
19: }
20: 
21: void main(void) {
22:     w_mtvec((unsigned long long)interrupt_handler);
23:     *MTIMECMP0 = *MTIME + 1000000; // この数値は適当に調整する
24:     w_mie(MIE_MTIE);
25:     w_mstatus(MSTATUS_MIE);
26:     while (1);
27:     *DEBUG_REG = 3; // fail
28: }
29: 
30: void interrupt_handler(void) {
31:     *DEBUG_REG = 1; // success
32: }

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

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

15.6 WFI命令の実装

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

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

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

リスト15.57: リスト15.57: WFI命令のデコード (inst_decoder.veryl)
1:     OP_SYSTEM: f3 != 3'b000 && f3 != 3'b100 || // CSRR(W|S|C)[I]
2:      bits == 32'h00000073 || // ECALL
3:      bits == 32'h00100073 || // EBREAK
4:      bits == 32'h30200073 || //MRET
5:      bits == 32'h10500073, // WFI
6:     OP_MISC_MEM: T, // FENCE

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

リスト15.58: リスト15.58: WFI命令の判定 (csrunit.veryl)
1:     let is_wfi: logic = inst_bits == 32'h10500073;
リスト15.59: リスト15.59: WFI命令のとき、mepcをpc+4にする (csrunit.veryl)
1:     if raise_expt || raise_interrupt {
2:         mepc = if raise_expt ? pc : // exception
3:          if raise_interrupt && is_wfi ? pc + 4 : pc; // interrupt when wfi / interrupt
4:         mcause = trap_cause;

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

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

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

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

リスト15.60: リスト15.60: アドレスの定義 (eei.veryl)
1:     // Unprivileged Counter/Timers
2:     CYCLE = 12'hC00,
3:     TIME = 12'hC01,
4:     INSTRET = 12'hC02,

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

リスト15.61: リスト15.61: mtimeをインターフェースに追加する (aclint_if.veryl)
1: import eei::*;
2: 
3: interface aclint_if {
4:     var msip : logic ;
5:     var mtip : logic ;
6:     var mtime: UInt64;
7:     modport master {
8:         msip : output,
9:         mtip : output,
10:         mtime: output,
11:     }
リスト15.62: リスト15.62: mtimeをインターフェースに割り当てる (aclint_memory.veryl)
1:     always_comb {
2:         aclint.msip  = msip0;
3:         aclint.mtip  = mtime >= mtimecmp0;
4:         aclint.mtime = mtime;
5:     }

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

リスト15.63: リスト15.63: rdataにインターフェースのmtimeを割り当てる (csrunit.veryl)
1:     CsrAddr::CYCLE   : mcycle,
2:     CsrAddr::TIME    : aclint.mtime,
3:     CsrAddr::INSTRET : minstret,