FUJITSU アドベントカレンダー7日目の記事です
掲載内容は私個人の見解であり, 富士通グループを代表するものではありません。
また, 本稿執筆にあたり, 細心の注意を払っておりますが, これらの情報の正確性, 十分性, 有用性, 確実性, 完全性等について一切保証いたしません。
本稿に記載された情報の誤り, 不完全性またはその使用・不使用等によって生ずるいかなる損害, ダウンロード等によって生じたいかなる障害等についても, 賠償その他一切の責任を負いません。
元組込みエンジニアです。
今回は, RISC-Vの外部割込みコントローラの制御方法について紹介させていただきます。
RISC-Vとは
RISC-Vとは, 米国カリフォルニア大学バークレイ校発祥のオープン標準の命令セット・アーキテクチャ (Instruction Set Architecture - ISA コンピュータに命令を与える際の命令体系) の名称です。
RISC-Vの命令体系は, 大きく以下の3つの命令体系に分類されます。
- Unprivileged Specification アプリケーションを記述するための命令体系です。
- Privileged Specification 周辺デバイスなどのハードウエアを制御するための命令体系です。
- Debug Specification デバッガなどの開発支援ツールを記述するための命令体系です。
各命令セット仕様の最新版は, https://riscv.org/specifications/ で公開されています。
今回ご紹介する外部割込みコントローラに関する仕様は, 上記のうちPrivileged Specificationに記載されています。
割込みとは
割込みとは第3世代コンピュータの開発時に周辺装置への入出力処理(以下I/O処理)とCPUの演算処理とを並列に実行させることで計算資源の利用効率を向上させる仕組みとして編み出された考え方です。
CPUがI/O処理の完了を制御する代わりに周辺装置側からCPUにI/O処理の完了や後続のI/O処理の受付が可能となったことを通知する仕組みを導入することで,I/O処理中にCPUが他の処理を実行することが可能になります。
以降, 周辺装置側からCPUにI/O処理の完了などを通知することを割込みと呼びます。
典型的な組込みシステムには複数の周辺装置(e.g., GPIO, タイマ, シリアル, LAN など)が接続されています。組込みシステムの用途によってどの周辺装置からの通知を優先的に扱うかが変わってきますので, 典型的な割込み制御機構では, 割込みに対して優先度を設定することが可能となっています。
あくまで例ですが, IoT機器などでGPIOからの入力を受け付けその内容を記録し, 記録した内容を後からまとめてLAN回線を通してサーバに引き渡すような場合は, GPIOからの割込みに優先的に処理し, GPIOからの入力がないときにLAN経由で記録した内容を送信するといった構成をとることも考えられます (実際の設計においては, 既存資産の流用時に問題が発生しないか?, 記録先のメモリは十分にあるか?, LAN上のトラブルにどう対応するか?など様々な観点から慎重に設計の妥当性を判断する必要があります)。
RISC-Vでの割込み処理の流れ
割込みを処理するための要素は大きく次の2つです。
-
割込み番号 周辺装置からの割込みを一意に識別する識別子です。 RISC-Vの場合は, ハードウエア構成によって決定される1以上の整数値が割り当てられます。
-
割込み優先度 周辺装置からの割込みを処理する順番を順序付けするための数値です。RISC-Vの場合は, 1以上の整数値によって割込みの処理順序をソフトウエアから指定することが可能です。RISC-Vの場合は, 数値が大きい順(降順)に割込みを処理します。
RISC-Vでの割込み処理の概要は以下の通りです。
上記に従って割込みを処理する手順を以下に示します。
- 割込み発生元となる周辺装置から割込みコントローラに割込み要求を発行する**[周辺装置の処理]**
- 周辺装置から通知された割込みと各割込みに設定された割込み優先度を元に次に処理する割込みの割込み番号を算出する**[割込みコントローラの処理]**
- 割込み発生を通知する宛先となるCPUを算出する**[割込みコントローラの処理]**
- CPUに割込み発生を通知する**[割込みコントローラの処理]**
- 割込みを受け付ける**[CPUの処理]**
- CPUに対する割込み通知の抑止する**[CPUの処理]**
- CPUが割込み処理の開始アドレス(割込みベクタ)に処理を移行する**[CPUの処理]**
- 割込み元アドレス(RISC-Vの場合, mepcレジスタまたはsepcレジスタの値), 割込み発生時のCPUの状態(RISC-Vの場合, mstatusレジスタまたはsstatusレジスタの値), 割込み発生時のスタックポインタ(RISC-Vの場合, spレジスタの値)をメモリ中に保存する**[ソフトウエア(OS)の処理]**
- 割込みコントローラから処理対象の割込みの割込み番号を取得する**[ソフトウエア(OS)の処理]**
- 処理対象の割込みの割込み番号の割込みの受け付けを抑止する(同一割込み番号からの割込みをマスクする)[ソフトウエア(OS)の処理]1
- 現在の割込み優先度(受け付けを抑止している割込み優先度)を割込みコントローラから取得する**[ソフトウエア(OS)の処理]**1
- 処理対象の割込みの割込み番号に設定された割込み優先度以下の割込みを抑止する(以下割込み優先度マスクと呼びます)[ソフトウエア(OS)の処理]1
- CPUが周辺装置からの割込みを受け付けたことを割込みコントローラに通知する**[ソフトウエア(OS)の処理]**
- 次に処理する割込みの割込み番号を算出する**[割込みコントローラの処理]**
- CPUに対する割込み通知を再開する**[ソフトウエア(OS)の処理]**
- 処理対象の割込みに応じて割込みを処理する**[デバイスドライバの処理]**
- CPUに対する割込み通知を抑止する**[ソフトウエア(OS)の処理]**
- 割込み優先度単位での割込みマスクを設定前の割込み優先度に設定する**[ソフトウエア(OS)の処理]**1
- 処理対象の割込みの割込み番号の割込みの受け付けを再開する(同一割込み番号からの割込みを受け付け可能にする)[ソフトウエア(OS)の処理]1
x64やAArch64に慣れた方の場合は, 割込み元アドレス, 割込み発生時のCPUの状態, 割込み発生時のスタックポインタをOSがメモリ中に保存する必要があることに驚かれるかもしれません。RISC-Vの場合, スーパーバイザモードやマシンモード(タイマ割込みやプロセッサ間割込みを処理するモード)処理用の一時レジスタが用意されており, これを活用することでソフトウエアで割込み元アドレスなどを保存する処理を記述します。
いずれにせよ, 箇条書きにすると複雑に見えますが, 全般に割込みコントローラが処理の重要な役割を担っていることは感じていただけるのではないでしょうか?
Platform-Level Interrupt Controller (PLIC)による割込み操作
RISC-Vには, Platform-Level Interrupt Controller (PLIC)という割込みコントローラが搭載されています。 PLICは, メモリ空間中にマップされ, OSなどの割込み制御機構を実現する場合は, メモリマップトIO(MMIO)として対象のメモリ領域を読み書きすることによってレジスタを制御します。
PLICのレジスタ構成とアクセス方法
PLICは, OSが割込みを制御するためのレジスタとして以下のレジスタ群を提供しています。
レジスタ名 | オフセットアドレス(単位:バイト) | 意味 |
---|---|---|
優先度設定レジスタ | 0x0 | 割込みごとに割込み優先度を設定する |
保留割込みレジスタ | 0x1000 | 割込み通知が保留されていることを確認する。 |
スーパーバイザモード割込み許可レジスタ | 0x201000 | スーパバイザ(OS)への割込み通知を割込み番号単位で許可・禁止(抑止)する。 |
スーパーバイザモード割込み優先度レジスタ | 0x201000 | スーパバイザ(OS)への割込み通知を割込み優先度単位で許可・禁止(抑止)する。 |
スーパーバイザモード割込みクレームレジスタ | 0x201004 | スーパバイザ(OS)が次に処理する割込みの割込み番号を返却する。 |
QEmuのRISC-V64システムシミュレーション機能(qemu-system-riscv64)では, PLICは, 物理アドレス0x0C000000
にマップされています。そのため, C言語からは, 以下のようなコードによってPLICを操作することが可能です。また, qemu-system-riscv64のvirtターゲットの場合, 1から7までの割込み優先度を設定可能です。
PLICを操作するためのマクロ定義例を以下に示します。
#include <stdint.h>
typedef uint32_t irq_no; /**< 割込み番号 */
typedef uint32_t irq_prio; /**< 割込み優先度 */
typedef uint32_t plic_reg; /**< PLICのレジスタ */
typedef volatile uint32_t * plic_reg_ref; /**< PLICのレジスタ参照 */
#define PLIC_PRIO_DIS (0) /**< 割込み無効時の割込み優先度 */
#define PLIC_PRIO_THRES_ALL (0) /**< 割込みマスク無効時の割込み優先度 */
#define PLIC_PRIO_MIN (1) /**< 最小割込み優先度 */
#define PLIC_PRIO_MAX (7) /**< 最大割込み優先度 */
/**< 割込みコントローラレジスタ長(単位:バイト) */
#define PLIC_REGSIZE (4)
/** QEMU Platform-Level Interrupt Controller (PLIC) レジスタ物理アドレス */
#define RV64_PLIC (0x0C000000)
/**< 優先度設定レジスタオフセット(単位:バイト) */
#define PLIC_PRIO_OFFSET (0x0)
/**< 保留割込みレジスタオフセット(単位:バイト) */
#define PLIC_PEND_OFFSET (0x1000)
/**< スーパーバイザモード割込み許可レジスタオフセット(単位:バイト) */
#define PLIC_SENABLE_OFFSET (0x2080)
/**< hart単位での割込み許可レジスタ長(単位:バイト) */
#define PLIC_ENABLE_PER_HART (0x100)
/**< スーパーバイザモード割込み優先度レジスタオフセット(単位:バイト) */
#define PLIC_SPRIO_OFFSET (0x201000)
/**< hart単位での割込み優先度レジスタ長(単位:バイト) */
#define PLIC_PRIO_PER_HART (0x2000)
/**< スーパーバイザモード割込みクレームレジスタオフセット(単位:バイト) */
#define PLIC_SCLAIM_OFFSET (0x201004)
/**< hart単位での割込みクレームレジスタ長(単位:バイト) */
#define PLIC_CLAIM_PER_HART (0x2000)
/**
PLIC優先度レジスタ取得
@param[in] _irq 割込み番号
@return PLIC優先度レジスタアドレス
*/
#define PLIC_PRIO_REG(_irq) \
((plic_reg_ref)(RV64_PLIC + PLIC_PRIO_OFFSET + ( (_irq) * PLIC_REGSIZE ) ))
/**
PLIC割込みペンディングレジスタ取得
@param[in] _irq 割込み番号
@return PLIC割込みペンディングレジスタアドレス
*/
#define PLIC_PEND_REG(_irq) \
((plic_reg_ref)(RV64_PLIC + PLIC_PEND_OFFSET \
+ ( (_irq) / ( PLIC_REGSIZE * BITS_PER_BYTE ) ) ) )
/**
スーパーバイザモード割込み許可レジスタ取得
@param[in] _hart 物理プロセッサID (hart番号)
@return スーパーバイザモード割込み許可レジスタアドレス
*/
#define PLIC_SENABLE_REG(_hart) \
((plic_reg_ref)(RV64_PLIC + PLIC_SENABLE_OFFSET + ( (_hart) * PLIC_ENABLE_PER_HART) ) )
/**
スーパーバイザモード割込み優先度レジスタ取得
@param[in] _hart 物理プロセッサID (hart番号)
@return スーパーバイザモード割込み優先度レジスタアドレス
*/
#define PLIC_SPRIO_REG(_hart) \
((plic_reg_ref)(RV64_PLIC + PLIC_SPRIO_OFFSET + ( (_hart) * PLIC_PRIO_PER_HART) ) )
/**
スーパーバイザモード割込みクレームレジスタ取得
@param[in] _hart 物理プロセッサID (hart番号)
@return スーパーバイザモード割込みクレームレジスタアドレス
*/
#define PLIC_SCLAIM_REG(_hart) \
((plic_reg_ref)(RV64_PLIC + PLIC_SCLAIM_OFFSET + ( (_hart) * PLIC_CLAIM_PER_HART) ) )
前節で説明した割込み処理における割込みコントローラ操作をRISC-V上で実現する場合の例を示します。
割込みコントローラから処理対象の割込みの割込み番号を取得する
RISC-V64では, スーパーバイザモード割込みクレームレジスタ(claim)から値を読み込むことで, 割込みコントローラから処理対象の割込みの割込み番号を取得することができます。
RISC-V64では, CPUのコア(RISC-Vでは, hardware threads(hart)と呼びます)毎にスーパーバイザモード割込みクレームレジスタが用意されています。
前節のマクロを使用して, 以下のようなコードを記載することで, 処理対象の割込み番号を得ることができます。
/**
Platform-Level Interrupt Controller割込み検出処理
@return 処理対象の割込みの割込み番号
*/
irq_no
plic_irq_is_pending(void){
/* hart0の処理対象の割込み番号をコントローラから取得 */
return *(plic_reg_ref)PLIC_SCLAIM_REG(0);
}
割込み番号単位で割込みの受け付けを抑止する
RISC-V64では, スーパーバイザモード割込み許可レジスタ(enable)に値を書き込むことで, 割込みコントローラからCPUコアへの割込みの通知許可を割込み番号単位で制御します。RISC-V64では, hart毎にスーパーバイザモード割込み許可レジスタが用意されています。
レジスタの各ビットが割込み番号に対応しており, 1にセットすると割込みを受け付け, 0の場合は割込みの受け付けを抑止します。
前節のマクロを使用すると, 以下のようなコードで割込みの受け付けを抑止することができます。
/**
Platform-Level Interrupt Controller割込み無効化処理
@param[in] irq 割込み番号
*/
void
plic_disable_irq(irq_no irq){
plic_reg_ref reg;
reg = (plic_reg_ref)PLIC_SENABLE_REG(0); /* 割込みマスクレジスタを取得 */
*reg &= ~(1 << irq); /* 指定された割込みの受け付けを抑止する */
}
現在の割込み優先度を割込みコントローラから取得する
RISC-V64では, 優先度設定レジスタ(threshold)から値を読み込むことで, 割込みコントローラにその時点で設定されている割込み優先度マスク(受け付けを抑止している割込みの優先度の最大値)を取得することができます。
前節のマクロを使用すると, 以下のようなコードで現在設定されている割込み優先度マスクを割込みコントローラから取得することができます。
/**
Platform-Level Interrupt Controller割込み優先度獲得処理
@param[out] prio 割込み優先度返却領域
*/
void
plic_get_priority(irq_prio *prio){
plic_reg_ref reg;
uint32_t cur_prio;
reg = (plic_reg_ref)PLIC_SPRIO_REG(0); /* 割込み優先度レジスタ参照 */
cur_prio = *reg; /* 割込み優先度レジスタの設定値を獲得 */
*prio = cur_prio; /* 優先度を返却 */
return ;
}
割込み優先度マスクを割込みコントローラに設定する
優先度設定レジスタに設定する割込み優先度マスク値を書き込むことで割込み優先度マスクを設定します。
前節のマクロを使用すると, 以下のようなコードで割込み優先度マスクを設定することができます。
/**
Platform-Level Interrupt Controllerに自hartの割込み優先度を設定する
@param[in] prio 割込み優先度
@retval 設定前の割込み優先度
*/
irq_prio
rv64_plic_set_priority_mask(irq_prio prio){
plic_reg_ref reg;
irq_prio old_prio;
reg = (plic_reg_ref)PLIC_SPRIO_REG(0); /* 割込み優先度レジスタ参照 */
old_prio = *reg; /* 現在の割込み優先度マスク */
*reg = prio; /* 割込み優先度レジスタに設定 */
return old_prio; /* 設定前の値を返却 */
}
割込みの受け付けを通知する
前述のスーパーバイザモード割込みクレームレジスタ(claim)に受け付けた割込みの割込み番号を書き込むことで, 割込みの受け付けを割込みコントローラに通知します。
前節のマクロを使用して, 以下のようなコードを記載することで, 受け付けた割込みの割込み番号を割込みコントローラに通知します。
/**
Platform-Level Interrupt Controller割込み受け付け通知
@param[in] irq 割込み番号
*/
void
plic_eoi(irq_no irq){
plic_reg_ref reg;
reg = (plic_reg_ref)PLIC_SCLAIM_REG(0); /* Supervisor claimレジスタを参照 */
*reg = irq; /* 受け付けた割込み番号を通知 */
}
割込みコントローラは, CPUからの割込み受け付け通知を受け取ると, 次に処理する割込みを再計算し, 未処理の割込みがあればCPUに対して割込みを通知をします。
処理対象の割込みの割込み番号の割込みの受け付けを許可(再開)する
前述のとおり, スーパーバイザモード割込み許可レジスタに値を書き込むことで, 割込みコントローラからCPUコアへの割込みの通知を許可します。
前節のマクロを使用すると, 以下のようなコードで割込みの受け付けを許可することができます。
/**
Platform-Level Interrupt Controller割込み受け付け許可処理
@param[in] irq 割込み番号
*/
void
plic_enable_irq(irq_no irq){
plic_reg_ref reg;
reg = (plic_reg_ref)PLIC_SENABLE_REG(0); /* 割込みマスクレジスタを取得 */
*reg = (1 << irq); /* 指定された割込みの通知を受け付ける */
}
最後に
RISC-Vの割込みコントローラの構成とその制御方法の概要について解説しました。
実際に組込みOSなどを作成する場合には, 他にも割込みベクタの設定方法や割込みコンテキストの退避・復元, タスクコンテキストの退避・復元, タイマ割込みの設定, 受け付けなどの知識が必要となりますが, 既存の組み込みソフトウエアをRISC-V環境に移植する際の参考になれば幸いです。
最後に, FUJITSU Advent Calendarへの参加のお誘いをくださった富士通 五島康文氏に感謝の意を表します。
参考文献
- [The RISC-V Instruction Set Manual Volume II: Privileged Architecture] (https://riscv.org/specifications/privileged-isa/)
- [SiFive FU540-C000 Manual] (https://static.dev.sifive.com/FU540-C000-v1.0.pdf)
- QEmu RISC-V64シミュレータのソースコード