こんにちは。MiNTsという会社の白井です。
以前、勉強用として作成していたRISC-V CPUを、Linuxが動くところまで育てたいと思い、最近また実装を進めています。
参考にしているのはこちらのサイトです。
CPU側のCSRや割り込み回路を実装しながら、SBIやLinuxなどのソフトウェア側も確認していると、OSとRISC-Vのレイヤーを行き来することになります。
最近は、この境界部分を理解していくのが結構楽しいなと思う今日この頃です。
突然ですが、RISC-VのACLINTをご存じでしょうか。
ACLINTは、RISC-Vのhartに対してタイマー割り込みやソフトウェア割り込みを提供する、MMIOデバイス群です。
大きく分けると、次の3つの機能が定義されています。
MTIMER
→ タイマー機能
MSWI
→ M-mode向けのソフトウェア割り込み
SSWI
→ S-mode向けのソフトウェア割り込み
参照しているACLINT Version 1.0の文書は、2026年7月23日時点ではDraft状態と明記されています。
そのため、ratified済みのRISC-V ISA拡張というよりは、RISC-V Internationalで策定されていたMMIOデバイス仕様として捉えるのがよさそうです。
そんなACLINTの実装を読みながら、OSの世界とCPUの世界を行き来していると、次のようなレジスタが出てきました。
SETSSIP
MSIPはあるのに、SETMSIPはありません。
一方、S-mode向けにはSSIPとは別に、SETSSIPというMMIOレジスタがあります。
最初は、
- なぜ
SSIPへ直接書かないのか -
SETSSIPは何を保持するレジスタなのか - hartを切り替えるための機能なのか
- Linuxでは何に使われるのか
という点が分かりづらく感じました。
この記事では、SETSSIPの役割を中心に整理します。
※質問しまくっている内容をAIによってまとめさせているので、必要のない部分はどんどん飛ばして読んでいただくのが良いかと思います!
まず結論
SETSSIPは、対象hartのSupervisor Software Interruptをpendingにするための、MMIOの「ドアベル」です。
たとえば、hart 0からhart 1へ通知する場合は、概念的に次のようになります。
hart 0
↓ SETSSIP1へ1を書く
SSWI
↓ hart 1へset要求を送る
hart 1のmip.SSIPが1になる
↓
Supervisor Software Interruptがpendingになる
ただし、hart 0からhart 1へ実行主体が切り替わるわけではありません。
hart 0
→ SETSSIP1へ書いた後も処理を続ける
hart 1
→ 割り込みを受けて、自分のhandlerへ入る
2つのhartは独立して動作しています。
ここで、そもそもhartとは何なのかを一度整理しておきます。
SETSSIPを理解するには、
hartがそれぞれ独立して動いている
という点が重要だからです。
hartとは何か
hartは、RISC-Vにおけるハードウェア上の命令実行主体です。
正式には、Hardware Threadを略した言葉です。
1つのhartは、少なくとも概念的には、次のような状態を持ちます。
PC
x0〜x31
現在のPrivilege Mode
mstatus
mepc
mcause
mip
mie
その他のCSR
つまりhartは、
1本の命令列を、独立した状態を持って実行できるハードウェア上の作業者
と考えると分かりやすいと思います。
たとえば、2 hart構成のCPUシステムでは、次のようになります。
hart 0
├─ PC
├─ x0〜x31
├─ CSR
└─ 実行中の命令列
hart 1
├─ 別のPC
├─ 別のx0〜x31
├─ 別のCSR
└─ 別の命令列
時間軸で表すと、2つのhartは並行して動作できます。
hart 0: ── 命令A ── 命令B ── 命令C ── 命令D ──
hart 1: ── 命令X ── 命令Y ── 命令Z ── 命令W ──
単純なCPU設計では、
1 core = 1 hart
となることが多いです。
その場合、2 core構成であれば2 hart存在します。
core 0 = hart 0
core 1 = hart 1
一方で、1つのcoreが複数のhardware threadを持つ設計では、
1 core = 複数hart
となる場合もあります。
したがって、厳密にはhart数とcore数は必ずしも同じではありません。
ただし、私が現在作成しているような比較的シンプルなCPUでは、CPU coreモジュールを複数配置し、それぞれを1 hartとして扱う構成が分かりやすそうです。
cpu_core #(
.HART_ID(0)
) u_hart0 (
.clk,
.rst
);
cpu_core #(
.HART_ID(1)
) u_hart1 (
.clk,
.rst
);
各hartは、自分のhart番号をmhartid CSRから確認できます。
hart 0 → mhartid = 0
hart 1 → mhartid = 1
hartとprocessは別物
hartを作業者と考えた場合、processはその作業者に割り当てられる仕事に相当します。
hart
= ハードウェア上の作業者
process
= OSが管理する仕事
Linuxでは、多数のprocessやthreadを、利用可能なhartへ割り当てながら実行します。
たとえば、2 hartと4つのprocessがある場合は、次のようになります。
時刻A:
hart 0 → process A
hart 1 → process B
しばらくすると、OSが実行対象を変更します。
時刻B:
hart 0 → process C
hart 1 → process D
どのhartで、どのprocessやthreadを、いつ実行するかを決めるのがスケジューリングです。
そして、現在動かしているprocessから別のprocessへ、レジスタなどの実行状態を入れ替える処理がコンテキストスイッチです。
scheduler
→ 次に実行するprocessを決める
context switch
→ 実際に実行状態を切り替える
したがって、SETSSIPが直接processを切り替えるわけではありません。
SETSSIPが通知する対象は、processではなくhartです。
独立して動くhart同士は、どうやって連携するのか
複数のhartは、それぞれ独立して命令を実行しています。
この独立性によって、複数の処理を同時に進められます。
一方で、独立しているからこそ、別のhartに何かを依頼したい場面が生まれます。
たとえばhart 0上で動作しているOSが、hart 1に対して次のような要求を出したい場合があります。
スケジュールを見直してほしい
TLBをflushしてほしい
特定の関数を実行してほしい
CPUを停止してほしい
共有状態を確認してほしい
しかし、hart 0はhart 1のPCやCSRを直接操作できません。
hart 0
─×→ hart 1のPCを直接変更
hart 0
─×→ hart 1のCSRを直接書き換える
また、共有メモリへ要求を書くだけでは、hart 1がいつそれに気付くか分かりません。
hart 0
↓
共有メモリへ要求を書く
hart 1
↓
いつ確認するのか分からない
通知手段がなければ、hart 1は共有メモリを繰り返し確認する必要があります。
hart 1:
要求はある?
要求はある?
要求はある?
これはポーリングであり、無駄な処理が増えます。
そこで、別hartに対して、
共有メモリに要求を書いたので、確認してください
と伝えるための仕組みが必要になります。
そのために使えるのが、hart間割り込みであるIPIです。
そして、Supervisor-levelのIPIを実現するためのMMIOドアベルがSETSSIPです。
hart 0
↓ 共有メモリへ要求を書く
↓ SETSSIP1へ1を書く
hart 1
↓ SSIPを受ける
↓ 共有メモリを確認する
↓ 要求を処理する
つまり、複数のhartは独立して動作しつつ、必要なときだけ割り込みを使って互いに連携します。
ACLINTの構成
改めて、SETSSIPが定義されているACLINTの構成を確認します。
ACLINTでは、主に次の3種類のデバイスが定義されています。
ACLINT
├─ MTIMER
│ ├─ MTIME
│ └─ MTIMECMP
│
├─ MSWI
│ └─ MSIP
│
└─ SSWI
└─ SETSSIP
それぞれの役割は次のとおりです。
| デバイス | レジスタ | 役割 |
|---|---|---|
| MTIMER |
MTIME / MTIMECMP
|
Machine Timer Interrupt |
| MSWI | MSIP |
Machine Software Interrupt / IPI |
| SSWI | SETSSIP |
Supervisor Software Interrupt / IPI |
この記事で扱うSETSSIPは、SSWI、つまりSupervisor-level Software Interrupt Deviceが持つMMIOレジスタです。
SSWIは、マウスやUARTのような外部周辺機器という意味ではありません。
CPUシステム内部に配置される、Supervisor Software Interruptを発生させるための小さなMMIOデバイスです。
SSWI
├─ SETSSIP0
├─ SETSSIP1
└─ SETSSIP2
SSIPとは
SSIPは、Supervisor Software Interrupt Pendingを表すbitです。
RISC-Vでは、mip CSRの中にSSIPがあります。
mip.SSIP = 0
→ Supervisor Software Interruptはpendingではない
mip.SSIP = 1
→ Supervisor Software Interruptがpending
S-modeからは、sip.SSIPとして見えます。
概念的には、次のような関係です。
sip = mip & mideleg;
ただし、実際のCSR実装では単純なANDだけではなく、各bitの実装可否や書き込み属性なども考慮する必要があります。
必要な割り込み許可条件がそろっていれば、SSIPがpendingになったhartは、S-modeのtrap handlerへ入ります。
sip.SSIP = 1
sie.SSIE = 1
割り込みがS-modeで有効
↓
Supervisor Software Interrupt
↓
PC = stvec
つまり、SSIPはSupervisor Software Interruptの要求が来ており、まだ処理されていないことを示すpending bitです。
SETSSIPとは
SETSSIPは、対象hartのSSIPを1にするためのMMIOレジスタです。
hartごとに対応するアドレスがあります。
SETSSIP0 → hart 0向け
SETSSIP1 → hart 1向け
SETSSIP2 → hart 2向け
たとえばhart 0がSETSSIP1へ1を書き込むと、hart 1のSSIPをsetする要求が送られます。
hart 0
↓ MMIO store
SETSSIP1 = 1
↓
hart 1のmip.SSIP = 1
SETSSIPのbit 0は、次のように動作します。
1を書き込む
→ 対象hartのSSIPをsetする
0を書き込む
→ 何もしない
読み出す
→ 0
そのため、SETSSIPは通常の状態保持レジスタではありません。
書き込みをきっかけに、対象hartへset要求を送るための書き込み口です。
なぜSSIPへ直接書かないのか
CSR命令で操作できるのは、現在その命令を実行しているhart自身のCSRです。
たとえば、hart 0でSSIPをsetするCSR操作を行った場合、その対象になるのはhart 0自身のSSIPです。
hart 0がCSR命令を実行
↓
hart 0のSSIPを変更
hart 1のCSRを直接指定して書き換えることはできません。
hart 0から
hart 1のsip.SSIPへ直接CSR write
という命令はありません。
RISC-VのCSR命令では、CSR番号を指定することはできますが、
どのhartのCSRを操作するか
を指定することはできません。
CSRは、常に現在その命令を実行しているhartのものです。
そこで、hartごとに異なるMMIOアドレスを用意します。
SETSSIP0
SETSSIP1
SETSSIP2
hart 0がSETSSIP1へstoreすれば、アドレスによって通知先がhart 1であると判断できます。
hart 0
↓ SETSSIP1のアドレスへstore
SSWI
↓
hart 1のSSIPをset
つまりSETSSIPは、
別hartのSSIPを、MMIOアドレスによって指定してsetするための入口
です。
SETSSIPはSSIPの値を保持しない
SETSSIP自身が、割り込みのpending状態を保持しているわけではありません。
SETSSIP
→ set要求を送る
mip.SSIP
→ pending状態を保持する
イメージとしては次のとおりです。
SETSSIP
= ベルのボタン
mip.SSIP
= ベルが押され、まだ要求が処理されていない状態
hart 0がSETSSIP1へ1を書き込むと、SSWIはhart 1へ一時的なset要求を送ります。
SETSSIP1へのwrite
↓
hart 1向けset信号
↓
hart 1のCSR回路がSSIPを1にして保持
SystemVerilogで実装するなら、概念的には次のような処理になります。
if (write_valid &&
write_addr == SETSSIP1_ADDR &&
write_data[0]) begin
setssip[1] = 1'b1;
end
そしてhart 1側のCSR回路が、この信号を受けてmip.SSIPをsetします。
if (setssip_i) begin
mip_ssip <= 1'b1;
end
したがって、SSWIは複雑なデバイスではありません。
SETSSIPへのMMIO writeを検出し、対応するhartへset信号を送る回路
です。
SystemVerilogの構成としては、sswi.svという独立したモジュールとして実装してもよいですし、ACLINT全体を扱うモジュールの中に含めてもよいと思います。
aclint.sv
├─ MTIMER相当の処理
├─ MSWI相当の処理
└─ SSWI相当の処理
重要なのはファイルの分け方ではなく、
SETSSIPへのMMIO write
↓
対象hartへのset要求
↓
対象hartのSSIPがpending
という動作を実現することです。
MSIPとの違い
MSIPとSETSSIPは、似た用途を持っていますが、レジスタの性質が異なります。
MSIP
MSIPは、Machine Software Interruptのpending状態そのものを保持します。
MSIPへ1を書く
→ Machine Software Interruptをpendingにする
MSIPへ0を書く
→ pendingを解除する
SETSSIP
SETSSIPは、Supervisor Software Interruptをsetする要求だけを送ります。
SETSSIPへ1を書く
→ SSIPをsetする
SETSSIPへ0を書く
→ 何もしない
整理すると次のようになります。
MSIP
= pending状態を保持する
SETSSIP
= SSIPをsetするための書き込み口
mip.SSIP
= 実際のpending状態を保持する
SETMSIPが存在しないのは、MSIP自身へ1を書けばsetでき、0を書けばclearできるためです。
一方、SETSSIPはSSIPをsetすることだけに特化しています。
では、なぜSSIP側もMSIPと同じように、MMIOレジスタ自身へ0または1を書き込む形式ではないのでしょうか。
SSIPは、mipやsipというCSR側にpending状態があります。
そのためSSWI側では、その状態をもう一度MMIOレジスタとして保持するのではなく、
SSIPをsetしてください
という要求だけを送る構造になっています。
SETSSIPで送れるのは通知だけ
SETSSIPへ書き込んでも、「何をしてほしいか」という情報までは送れません。
送れるのは、
hart 1に何らかの要求がある
という通知だけです。
実際の要求内容は、共有メモリなどに置きます。
共有メモリ:
hart 1への要求 = RESCHEDULE
その後、hart 0がSETSSIP1へ1を書きます。
hart 0
↓ 共有メモリへ要求を書く
↓ SETSSIP1へ1を書く
hart 1
↓ SSIPを受ける
↓ 共有メモリを確認する
↓ RESCHEDULE要求を処理する
この構造は、データ本体と通知を分けています。
共有メモリ
= 要求内容
SETSSIP
= 要求があることを知らせるベル
これは、hart間通知を考えるうえで重要な点だと思います。
SETSSIPのレジスタ幅が大きくても、そこへ要求内容を詰め込んで送るわけではありません。
あくまで、
通知が来た
という事実だけを対象hartへ伝えます。
対象hartは、割り込みハンドラの中で共有状態を確認し、何を求められているのかを判断します。
Linuxでは何に使われるのか
multi-hart Linuxでは、あるhartから別のhartへ処理を依頼したい場面があります。
代表的な例は次のとおりです。
reschedule
TLB shootdown
別hart上での関数実行
CPU stop
reschedule
hart 0上のLinuxが、hart 1で現在動いている処理よりも、優先度の高いprocessやthreadを実行してほしいと判断する場合があります。
その場合、hart 1に対して、
スケジュールを見直してください
と通知します。
hart 0上のLinux
↓ hart 1向けの要求を共有領域へ記録
↓ SETSSIP1へ1を書く
hart 1
↓ Supervisor Software Interrupt
↓ IPI handler
↓ 要求内容を確認
↓ 必要ならschedulerを実行
TLB shootdown
あるhartがページテーブルを変更した場合、別hartのTLBに古い変換情報が残っている可能性があります。
そのため、対象hartに対して、
対応するTLBエントリを無効化してください
と通知する必要があります。
hart 0
↓ ページテーブルを変更
↓ hart 1向けのTLB flush要求を記録
↓ SETSSIP1
hart 1
↓ IPI handler
↓ sfence.vmaなどを実行
ここでも、SETSSIP自身がTLBを無効化するわけではありません。
SETSSIPは、hart 1に処理のきっかけを与えるだけです。
SETSSIP
= hartへ通知する
Linux
= 通知内容を判断する
scheduler
= 次に動かすprocessやthreadを決める
context switch
= 実際に実行状態を切り替える
hartが切り替わるわけではない
hart 0がSETSSIP1へ書いたとき、hart 0の実行がhart 1へ移動するわけではありません。
hart 0:
通常処理
↓
SETSSIP1へwrite
↓
通常処理を継続
hart 1:
通常処理
↓
SSIPを受理
↓
trap handlerへ入る
時間軸で表すと次のようになります。
hart 0: ── 処理 ── SETSSIP1 write ── 処理を継続 ──
hart 1: ── 処理 ─────────────── SSIP ── handler ──
SETSSIPはhartの切り替え機能ではなく、独立して動作している別hartへの割り込み通知です。
ただし、hart 1が割り込みを受けた結果、Linuxのschedulerが動き、processのコンテキストスイッチが発生することはあります。
hart 0
↓ SETSSIP1
hart 1
↓ IPI handler
↓ scheduler
↓ process Aからprocess Bへ切り替え
この場合でも、
SETSSIPがhartを切り替えた
わけではありません。
SETSSIP
→ hart 1へ通知
Linux
→ 通知内容を判断
scheduler
→ 次のprocessを決定
context switch
→ processを切り替え
という別々の処理です。
SSIPを受けたhartはどうやって解除するのか
SETSSIPはSSIPをsetするための書き込み口です。
そのため、処理が終わった後にSETSSIPへ0を書いても、pending状態は解除されません。
SETSSIPへ0を書く
→ 何もしない
SSIPのpending状態は、対象hart側にあります。
hart 1のmip.SSIP
したがって、通知を受け取ったhart 1が、自分のSSIPをclearします。
hart 1
↓ IPI handlerへ入る
↓ 要求内容を確認
↓ 自分のSSIPをclear
↓ trapから復帰
整理すると、送信側と受信側の役割は次のようになります。
送信側hart
→ SETSSIPへ1を書いて通知する
受信側hart
→ SSIPを確認する
→ 要求を処理する
→ SSIPをclearする
送信側が対象hartのpending状態を直接clearするのではなく、受信側が自分で処理済みにします。
SETSSIPはLinuxに必須なのか
Linuxで複数hartを利用する場合、hart間通知であるIPIの仕組みは重要です。
ただし、その実現方法が必ずSETSSIPである必要はありません。
たとえば、S-modeのLinuxがSBIを使ってM-mode firmwareへIPI送信を依頼し、M-mode側がMSIPを操作する方式もあります。
S-mode Linux
↓ SBI send_ipi
M-mode firmware
↓ 対象hartのMSIPをset
対象hart
↓ Machine Software Interrupt
↓ firmwareがS-modeへ通知
この構成では、LinuxがSSWIのSETSSIPへ直接書き込むのではなく、M-mode firmwareにIPI送信を依頼します。
つまり、
multi-hart Linux
→ IPI機能は必要
SETSSIP
→ IPIを実現する方法の1つ
です。
single-hart構成では通知先となる別hartが存在しないため、hart間IPIとしてのSETSSIPは基本的に必要ありません。
ただし、single-hartでも、M-modeからS-modeへSupervisor Software Interruptを発生させるような独自設計で利用することはできます。
そのため、
single-hartでは絶対に不要
というよりは、
hart間通知としての必要性がない
と表現する方が正確だと思います。
自作CPUでは何を実装すればよいのか
自作CPUでSETSSIPを実装する場合、大きく分けると次の2か所が必要になります。
1. SSWI側
SETSSIPのMMIOアドレスへの書き込みを検出し、対象hart向けのset信号を生成します。
always_comb begin
setssip = '0;
if (write_valid && write_data[0]) begin
case (write_addr)
SETSSIP0_ADDR: setssip[0] = 1'b1;
SETSSIP1_ADDR: setssip[1] = 1'b1;
SETSSIP2_ADDR: setssip[2] = 1'b1;
default: setssip = '0;
endcase
end
end
2. 各hartのCSR側
自分向けのsetssip信号を受けたら、mip.SSIPを1にします。
always_ff @(posedge clk) begin
if (rst) begin
mip_ssip <= 1'b0;
end else begin
if (setssip_i) begin
mip_ssip <= 1'b1;
end
if (ssip_clear_i) begin
mip_ssip <= 1'b0;
end
end
end
2 hart構成なら、概念的な接続は次のようになります。
┌───────────────┐
hart 0 bus ───────→│ │── setssip[0] ─→ hart 0 CSR
hart 1 bus ───────→│ SSWI │
│ │── setssip[1] ─→ hart 1 CSR
└───────────────┘
複数のhartが同じSSWIのMMIO領域へアクセスするため、実際のSoCでは、その前段にバスやインターコネクト、アービタなども必要になります。
hart 0 ─┐
├─ bus interconnect ─→ SSWI
hart 1 ─┘
SETSSIP自体の回路は単純ですが、multi-hart化すると、共有メモリやバスアクセスの調停など、CPU全体の構成も考える必要が出てきます。
まとめ
SETSSIPは、対象hartのSupervisor Software InterruptをpendingにするためのMMIOドアベルです。
hart 0
↓ SETSSIP1へ1を書く
hart 1のmip.SSIPが1になる
↓
hart 1がSupervisor Software Interruptを受ける
重要な点は次のとおりです。
SETSSIP
= set要求を送るMMIOレジスタ
mip.SSIP
= 割り込みのpending状態を保持するbit
共有メモリ
= 実際の要求内容を保持する場所
また、SETSSIPはhartを切り替える機能ではありません。
hart 0
→ 通知後も処理を続ける
hart 1
→ 通知を受けて自分のhandlerへ入る
hartは、それぞれ独立したハードウェア上の命令実行主体です。
複数のhartを持つことで複数の命令列を並行して実行できますが、独立しているため、別hartへ何かを依頼したい場合には明示的な通知が必要になります。
複数hart
→ 独立して処理できる
独立している
→ 別hartの状態を直接操作できない
別hartへ要求したい
→ 共有メモリへ内容を書く
すぐに気付いてほしい
→ SETSSIPで通知する
最終的には、次の一文で整理できます。
SETSSIPは、独立して動作している別hartへ「処理してほしい要求があります」と伝えるための、Supervisor-level Software Interrupt用MMIOドアベルである。