第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 トラップの委譲」で解説します。
- 割り込みを発生させるようなイベントがデバイスで発生する
- 割り込み原因に対応したmipレジスタのビットが
0
から1
になる - 割り込み原因に対応したmieレジスタのビットが
1
であることを確認する (0
なら割り込みは発生しない) - mstatus.MIEが
1
であることを確認する (0
なら割り込みは発生しない) - (割り込み(トラップ)開始)
- mstatus.MPIEにmstatus.MIEを格納する
- mstatus.MIEに
0
を格納する - 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 | 説明 | 優先順位 |
---|---|---|
11 | Machine external interrupt | 高い |
3 | Machine software Interrupt | |
7 | Machine timer interrupt | |
9 | Supervisor external interrupt | |
1 | Supervisor software interrupt | |
5 | Supervisor timer interrupt | 低い |
15.1.4 割り込みの原因(cause)
それぞれの割り込みには原因を区別するための値(cause)が割り当てられています。割り込みのcauseのMSBは1
です。
CsrCause
型に割り込みのcauseを追加します(リスト15.1)。
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ビットに状態を通知します。

図15.1: ACLINTのメモリマップ
本書ではACLINTを図図15.1のようなメモリマップで実装します。本章ではMTIMER、MSWIデバイスを実装し、「17.5 ソフトウェア割り込みの実装 (SSWI)」でSSWIデバイスを実装します。デバイスの具体的な仕様については後で解説します。
メモリマップ用の定数をeeiパッケージに記述してください(リスト15.2)。
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)。インターフェースの中身は各デバイスの実装時に実装します。
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)。まだどのレジスタも実装していません。
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)。
1: enum Device { 2: UNKNOWN, 3: RAM, 4: ROM, 5: DEBUG, 6: ACLINT, 7: }
1: if MMAP_ACLINT_BEGIN <= addr && addr <= MMAP_ACLINT_END { 2: return Device::ACLINT; 3: }
ACLINTとのインターフェースを追加し、reset_all_device_masters関数にインターフェースをリセットするコードを追加します(リスト15.7、リスト15.8)。
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: ) {
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: }
ready
、rvalid
を取得する関数にACLINTを登録します(リスト15.9、リスト15.10)。
1: Device::ACLINT: return aclint_membus.ready;
1: Device::ACLINT: return aclint_membus.rvalid;
ACLINTのrvalid
、rdata
をreq_core
に割り当てます(リスト15.11)。
1: Device::ACLINT: req <> aclint_membus;
ACLINTのインターフェースに要求を割り当てます(リスト15.12)。
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)。
1: inst aclint_core_bus: aclint_if;
1: inst aclint_membus : Membus;
aclint_memoryモジュールをインスタンス化し、mmio_controllerモジュールと接続します(リスト15.15、リスト15.16)。
1: inst aclintm: aclint_memory ( 2: clk , 3: rst , 4: membus: aclint_membus , 5: aclint: aclint_core_bus, 6: );
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)。
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: ) {
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: );
1: minstret : input UInt64 , 2: led : output UIntX , 3: aclint : modport aclint_if::slave , 4: ) {
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デバイスのメモリマップ
オフセット | レジスタ |
---|---|
0000 | MSIP0 |
0004 | MSIP1 |
0008 | MSIP2 |
.. | .. |
3ff8 | MSIP4094 |
3ffc | 予約済み |
仕様上はmhartidとMSIPの後ろの数字(hartID)が一致する必要はありませんが、本書ではmhartidとhartIDが同じになるように実装します。他のACLINTのデバイスも同様に実装します。
15.3.1 MSIPレジスタを実装する

図15.2: MSIPレジスタ
ACLINTモジュールにMSIPレジスタを実装します(図15.2)。今のところCPUにはmhartidが0
のハードウェアスレッドしか存在しないため、MSIP0のみ実装します。
aclint_ifインターフェースにmsip
を追加します(リスト15.21)。
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)。
1: var msip0: logic;
1: always_ff { 2: if_reset { 3: membus.rvalid = 0; 4: membus.rdata = 0; 5: msip0 = 0;
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)。
1: always_comb { 2: aclint.msip = msip0; 3: }
15.3.2 mip、mieレジスタを実装する

図15.3: mipレジスタ

図15.4: mieレジスタ
mipレジスタのMSIPビット、mieレジスタのMSIEビットを実装します。mie.MSIEはMSIPビットによる割り込み待機を許可するかを制御するビットです。mip.MSIPとmie.MSIEは同じ位置のビットに配置されています。mip.MSIPに書き込むことはできません。
csrunitモジュールにmieレジスタを作成します(リスト15.26、リスト15.27)。
1: var mie : UIntX ;
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)。
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)。
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ビットも書き込めるようにしておきます。
1: const MIE_WMASK : UIntX = 'h0000_0000_0000_0088 as UIntX;
1: CsrAddr::MTVEC : MTVEC_WMASK, 2: CsrAddr::MIE : MIE_WMASK, 3: CsrAddr::MSCRATCH: MSCRATCH_WMASK,
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)。
1: const MSTATUS_WMASK : UIntX = 'h0000_0000_0000_0088 as UIntX;
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
を設定します。
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)。
1: rs1_data : input UIntX , 2: can_intr : input logic , 3: rdata : output UIntX ,
1: rs1_data : memq_rdata.rs1_data , 2: can_intr : mems_is_new , 3: rdata : csru_rdata ,
トラップが発生するときにmemunitモジュールを無効にします(リスト15.38)。今まではEXステージまでに例外が発生することが分かっていたら無効にしていましたが、csrunitモジュールからトラップが発生するかどうかの情報を直接得るようにします。
1: inst memu: memunit ( 2: clk , 3: rst , 4: valid : mems_valid && !csru_raise_trap,
memunitモジュールが無効(!valid
)なとき、state
をState::Init
にリセットします(リスト15.39)。
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)。
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)。本書では例外を優先します。
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)。
1: // Trap Return 2: assign trap_return = valid && is_mret && !raise_expt && !raise_interrupt;
トラップが発生するとき、例外の場合にのみmtvalレジスタに例外に固有の情報が書き込まれます。本書では例外を優先するので、raise_expt
が1
ならmtvalレジスタに書き込むようにします(リスト15.43)。
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)。
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モードの実装

図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)。
1: const MTVEC_WMASK : UIntX = 'hffff_ffff_ffff_fffd;
割り込みのトラップベクタをMODEとcauseに応じて変更します(リスト15.46)。
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
に割り当てます。
1: let expt_vector: Addr = {mtvec[msb:2], 2'b0};
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デバイスのメモリマップ
オフセット | レジスタ |
---|---|
0000 | MTIMECMP0 |
0008 | MTIMECMP1 |
.. | .. |
7ff0 | MTIMECMP4094 |
7ff8 | MTIME |
MTIMEレジスタは、固定された周波数でのクロックサイクル毎にインクリメントするレジスタです。リセット時に0
になります。
MTIMERデバイスは、それに対応するハードウェアスレッドのmip.MTIPと接続されており、MTIMEがMTIMECMPを上回ったときmip.MTIPを1
にします。これにより、指定した時間に割り込みを発生させることが可能になります。
15.5.2 MTIME、MTIMECMPレジスタを実装する
ACLINTモジュールにMTIME、MTIMECMPレジスタを実装します。今のところmhartidが0
のハードウェアスレッドしか存在しないため、MTIMECMP0のみ実装します。
mtime
、mtimecmp0
レジスタを作成し、読み書きできるようにします(リスト15.49、リスト15.50、リスト15.51)。mtime
レジスタはクロック毎にインクリメントします。
1: var msip0 : logic ; 2: var mtime : UInt64; 3: var mtimecmp0: UInt64;
1: always_ff { 2: if_reset { 3: membus.rvalid = 0; 4: membus.rdata = 0; 5: msip0 = 0; 6: mtime = 0; 7: mtimecmp0 = 0;
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)。
1: var msip: logic; 2: var mtip: logic; 3: modport master { 4: msip: output, 5: mtip: output, 6: }
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)。
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)。
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)。
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)。
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)。
1: let is_wfi: logic = inst_bits == 32'h10500073;
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)。
1: // Unprivileged Counter/Timers 2: CYCLE = 12'hC00, 3: TIME = 12'hC01, 4: INSTRET = 12'hC02,
mtimeレジスタの値をACLINTモジュールからcsrunitに渡します(リスト15.61、リスト15.62)。
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: }
1: always_comb { 2: aclint.msip = msip0; 3: aclint.mtip = mtime >= mtimecmp0; 4: aclint.mtime = mtime; 5: }
time、instret、cycleレジスタを読み込めるようにします(リスト15.63)。
1: CsrAddr::CYCLE : mcycle, 2: CsrAddr::TIME : aclint.mtime, 3: CsrAddr::INSTRET : minstret,