Rustで割り込み処理 (連載19)
今回は、
SOLID-OS上で割り込み処理をRustで書くために必要なこと、
について調べます。
ベアメタルでの割り込み処理を書いていくわけではないため、ARMコアの割り込みコントローラや割り込みベクタを直接操作したりすることは行いません。
リアルタイムOSカーネルの作法にしたがって、対応する割り込みをイネーブルにし、割り込みハンドラ関数を実装&登録します。
今回は、
SOLID-OS上で割り込み処理をRustで書くために必要なこと、
について調べます。
Rustはコンパイル言語なので、タスク処理だけででなく、割り込みハンドラも実装できてしまいます。
特に、今回は RTOSを使っているので、Rustで記述したハンドラ処理をコールバック登録するだけなので比較的簡単に実装でき、Rustで記述したコードが Cで書いた割り込みハンドラと同じように、割り込みステートで動作します。
実装は、RTOSカーネルとして動作しているSOLID-OSの作法に従い行います。
SOLID-OSは一般的なリアルタイムOSカーネルと同じ作法ですが、Raspberry Pi 4用に無償で公開されているカーネルのみ特殊な事情があります。
例えば割り込みハンドラ関数の登録方法に特殊事情があります。
一般的には、カーネルをコンフィグ&ビルドすることで割り込みハンドラ関数を登録しますが、今回のRaspberry Pi 4用SOLID-OSではカーネルビルドは許可されていません。
代わりに、専用の操作をもって同等処理を行うことになります。
今回発生させたい割り込みは、SPI受信・送信時の割り込みです。
では、見ていきましょう!
1.割り込みを登録する操作
割り込みコントローラへの設定時に、アプリケーションから直接I/Oレジスタをアクセスするわけにはいきません。なぜなら、割り込みはカーネルの責任範疇だからです。カーネル上で動作するアプリケーションが勝手に割り込みを操作して、秩序を乱すことは避けるべきです。
代わりに、カーネル側からアプリケーション側に対し、割り込みイネーブル用のAPIがあるはずです。
そのAPI群は、以下のURLに記載されています。
http://solid.kmckk.com/doc/skit/current/os/cs/intc.html
それではまず、割り込みを登録する方法について見ていきましょう。
「登録」とは、カーネルに対して、以下を申請することです。
・使いたい割り込み
・その割り込みの優先順位
・割り込み発生時に呼び出してほしい関数
・その関数を呼び出すときに、一緒に欲しい値
これらは、SOLID-OSでは「割り込みハンドラ登録用構造体」として定義されています。
typedef struct _SOLID_INTC_HANDLER_ {
int intno;
int priority;
int config;
int (*func)(void*, SOLID_CPU_CONTEXT*);
void* param;
} SOLID_INTC_HANDLER;
この構造体の値を設定し、以下関数をコールすることにより、割り込みが登録できます。
シングルコア用:
int SOLID_INTC_Register(SOLID_INTC_HANDLER *pHandler)
マルチコア用:
int SOLID_INTC_RegisterWithTargetProcess(SOLID_INTC_HANDLER *pHandler, char mask)
「割り込みハンドラ登録用構造体」を準備し、Registerする、という流れですね。
今回のRaspberry Pi 4 SOLID-OSは、2つのCPU上で動作していますので、マルチコア用のAPIを使うことになりそうです。
2.割り込みを許可する操作
次に、登録した割り込みを許可する方法について調べていきます。
2.1 割り込みを発生させるハードウェアの初期化
SPI受信・送信時に割り込みが発生できるよう、ハードウェアを初期化する必要があります。
BCM2711のperipheral manualを参考にして、具体的な手順を調べます。
https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf
・RX FIFOにデータがシフトインされたことを示す割り込みが発生できるようにする。
⇒CSレジスタのINTRビットを1にする
・DONEビットが1になったことを示す割り込みが発生できるようにする。
⇒CSレジスタのINTDビットを1にする
この二つのビットを設定しておけばよさそうです。
I/Oレジスタにアクセスし、これらの設定をしていけばOKですね。
2.2 割り込みコントローラにより対応する割り込みをイネーブルにする
先程の、割り込みイネーブル用のAPIに戻ります。
http://solid.kmckk.com/doc/skit/current/os/cs/intc.html
登録した割り込みの通知を、許可状態にするAPIがあります。
シングルコア用:
int SOLID_INTC_Enable(int intno)
マルチコア用:
int SOLID_INTC_EnableM(int intno)
このAPIをコールすればOKです。
intnoには、先程設定した割り込みの番号を設定します。
3.C++で実際に見てみる
では次に、サンプルを用いて実際に書き方を見ていきましょう。
以下GitHubにタイマ割り込みを発生させるサンプルがあります。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/cpp-blinky-cs/cpp-blinky-cs/main.cpp
33行目、
SOLID_TIMER_HANDLER g_timer;
おっと。ちょっと違った。
タイマ割り込みハンドラ登録用構造体、を定義しています。
タイマは、一般的な割り込みの実装ではなく、特殊な実装になっているのか。
多分、想像だけど、一般的な割り込み処理設定を行うためのAPIの上に、タイマ独自の実装が追加されているAPIがあるんだろう。。。
ありました。これですね。
http://solid.kmckk.com/doc/skit/current/os/cs/timer.html
まぁともかく、これはタイマ割り込み専用のサンプルですが、そうはいっても一般的な割り込みに対しても参考になるはず。
次、進みます。
ここでは割り込みの登録をするところだけ見ることにします。
という事で、先ほど「構造体」を定義したので、次、具体的に値を設定し、それをRegisterするところを見てみましょう。
extern "C" void slo_main()
{
SOLID_LOG_printf("Starting LED blinker\n");
// Configure the LED port
green_led::init();
// Initialize the timer object
g_timer.type = SOLID_TIMER_TYPE_INTERVAL;
g_timer.time = 200'000;
g_timer.func = [] (void *, SOLID_CPU_CONTEXT *) {
// Determine the next LED state
g_led_state = !g_led_state;
// Toggle the LED
green_led::update(g_led_state);
};
g_timer.param = NULL;
// Start the timer
int ret = SOLID_TIMER_RegisterTimer(&g_timer);
solid_cs_assert(ret == SOLID_ERR_OK);
}
g_timerに具体的な値を設定し、
SOLID_TIMER_RegisterTimer関数で登録しています。
一般的な割り込みも、同じ流れで登録できそうです。
4.Rustで実際に見てみる
では次に、Rustで見ていきましょう。
Rustで記述されたタイマサンプルはこちらです。
タイマオブジェクトをnewしているところがあります。
// Construct a timer object on a global variable
let mut timer = pin_singleton!(: Timer<_> = timer::Timer::new(
timer::Schedule::Interval(timer::Usecs32(200_000)),
move |_: CpuCx<'_>| {
// Determine the next LED state
state = !state;
// Toggle the LED
green_led::update(state);
},
))
.unwrap();
どうやら他のクレートで、タイマオブジェクトをnewしていて、そこでカーネルに対してタイマ割り込みを登録しているようですね。
タイマインターバルと、ハンドラを渡していますね。
見つけました。
2行目のsolidクレートに“timer”定義があります。これですね。
use solid::{singleton::pin_singleton, thread::CpuCx, timer};
ではsolidクレートのtimerに関する実装を見てみましょう。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/common/solid/src/timer.rs
impl Timer {
/// Construct a stopped `Timer`.
#[inline]
pub const fn new(schedule: Schedule, handler: T) -> Self {
let mut inner = abi::SOLID_TIMER_HANDLER {
pNext: null_mut(),
pCallQ: null_mut(),
globalTick: 0,
ty: 0,
time: 0,
func: Self::handler_trampoline as _,
param: null_mut(), // set later
};
schedule.update_sys(&mut inner.ty, &mut inner.time, &mut inner.globalTick);
Self {
inner: UnsafeCell::new(inner),
owning_processor_id_p1: AtomicUsize::new(STOPPED),
handler: UnsafeCell::new(handler),
_pin: core::marker::PhantomPinned,
}
}
渡されたインターバルと、ハンドラを用いて、SOLID_TIMER_HANDLER構造体を初期化しています。
この流れですね。
では、solidクレートの一般割り込みに関する実装を見てみましょう。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/common/solid/src/interrupt.rs
タイマ割り込みの場合は、
・newすることでSOLID_TIMER_HANDLER構造体を準備
・.startでSOLID_TIMER_RegisterTimer関数をコールし登録を済ませる
今回は、
・newでSOLID_INTC_HANDLER構造体を準備
・.registerでSOLID_INTC_RegisterWithTargetProcess関数をコールして登録を済ませる
という流れになっているようです。
5.SPI割り込み登録用構造体
では、具体的に、割り込み登録用構造体には何を設定しましょうか。
typedef struct _SOLID_INTC_HANDLER_ {
int intno;
int priority;
int config;
int (*func)(void*, SOLID_CPU_CONTEXT*);
void* param;
} SOLID_INTC_HANDLER;
今回、送信・受信と2つの割り込みを使用するので、割り込み登録用構造体が2つ必要になると思いきや、違うようです。
説明します。
Raspberry Pi 3では、独自の割り込みコントローラが搭載されていますが、
Raspberry Pi 4のBCM2711では、加えて、GIC-400コントローラも搭載されました。
何が言いたいかというと、Raspberry Pi 4では、
・レガシーコントローラ(独自のもの)
・GIC-400コントローラ(Cortex標準のもの)
の二つの割り込みコントローラが存在します。
かといって、二つ共存するわけではありません。
BCM2711マニュアルのP.85に書かれてありますが、
BCM2711 ARM Peripherals
初期値ではGIC-400が選択されており、変更することが可能です。
SOLID-OSでは、GIC-400を使います。
intnoにはGIC-400の割り込み番号を設定することになります。
次に、今回対象のSPI割り込みが、GIC-400のどの割り込み番号に対応するか、を調べます。
SPI割り込みはすべてORされ、VideoCoreのIRQ54にマップされているようです。
BCM2711マニュアルのP.87に書かれてあります。
では、VideoCoreのIRQ54はGIC-400では何番に割り振られるのでしょうか。
BCM2711マニュアルのP.89にオフセット値が書かれてあります。
ややこしいのですが、右側に記載されている"SPI"は、Shared Peripheral Interruptの略で、今回対象の通信用SPI(Serial Peripheral Interface)とは異なります。
VideoCoreのPeripheral IRQsは、GIC-400の96番以降に割り振られています。
したがって、SPI割り込みは 96 + 54 = 150になります(と思います。違ったら次回訂正します。)
という事で、割り込み番号150に対する割り込み登録用構造体を一つ作成すればよさそうです。
intno (割り込み番号):150
priority(優先順位):許可されている最大値としましょう。SOLID_INTC_GetPriorityLevel() で取得できます。
config:
割り込み設定(ICFGR) 2bitのみ有効 -1を指定した場合はconfigを変更しません
SPI(Serial Peripheral Interface)割り込みは、SPI(Shared Peripheral Interrupt)なので、実装依存ですね。
-1を設定すれば良いのかな?ま、やってみつつ考えましょう。
param:
割り込み発生時に関数に引き渡される第一引数ですが、今回は別に使わないのかな。。。
では次、割り込みハンドラについて考えてみます。
6.割り込みハンドラですべきこと
以下二つのケースについて処理を行います。
・送信時の割り込み
割り込み要因:
DONEビットが1になったことを示す割り込みに対する処理
行う処理:
次のデータをTX FIFOに書きます。送るべきデータがなければ、TAビットを0に落とします。
・受信時の割り込み
割り込み要因:
RXRビットが1になったことを示す割り込みに対する処理
行う処理:
RX FIFOからデータを読みます。RXDビットが0になるまで読みます。
これら二つの処理を持つ割り込みハンドラを実装すればよさそうです。
以上、
・割り込みの登録方法
・割り込みの許可方法
・割り込みハンドラの実装検討
を行いました。
これで次回、割り込み処理を実装できそうです。