Symmetric multiprocessing (SMP) with Raspberry Pi Pico (RP2040)

前回投稿のライブラリを改良して Raspberry Pi Pico 用のSMPタスクスイッチングライブラリを作成してみた。

世界最速のタスク・スイッチ・ライブラリ (Task Switch Library for ARM-G++ [M0/M0+/M1])

コアの排他制御処理の分だけスイッチング速度は少し遅くなってしまったが、それでも1.0usは楽に切っている。

旧: RP2040(125MHz): 82 cycles (0.656us)

※2022-02-08に追加したタスク情報の保存/復旧処理のためにサイクル数が若干増えたがなんとか1.0us未満に収まった。

新: RP2040(125MHz): 104 cycles (0.832us)

[SMPで動作しているところ]
※PicoのSMPでは2タスクが同時に実行状態になる。

前回投稿のライブラリを改良しただけなので実装は比較的楽だったのだがPico特有の落とし穴もあった。arm系にはSysTickと呼ばれるシステムクロックで動作するタイマーがありマイクロ秒以下の時間測定には必要なものなのだがなぜかSDKでは未サポートらしい。とりあえずSysTickを使えるようにしてみたところSMPでは誤動作してしまうという現象が...

SysTickはコア毎に存在しており、実行コア側のSysTickにしかアクセスできない。SMPではタスクが実行されるコアが不定=どのコアのSysTickにアクセスするかわからないということが原因だった。

例えば、次のコードではyield()実行前後で別のコアに切り替わることがある。micros()がSysTickで実装されていて各SysTickの同期がとれていないと仮定した場合、どのコアからアクセスされるかによりmicros()が返す時間情報が前後してしまうことになる。

この問題は、各SysTickを同時スタートすることで解決できる。

具体的な実装は次のメソッドのコードを見てほしい。ちなみに対策後に上の例のmicros()が返す時間情報が前後することがないか1日中ぶん回してみたが問題はなさそうだ。

void Task::begin([[maybe_unused]] bool multicore)
void Task::core1()

また、SysTickと同じくNVIC(割り込みコントローラ)もコア毎なので割り込み処理には特別な注意が必要となる。Picoだと割り込み処理を含むHWドライバー等をSMP対応するのは大変そうというか可能なら割り込みは使わない方が無難な気も...

ちなみに、優先度ベースのRTOSをSMP対応すると優先度が高いタスクの実行中に優先度が低いタスクが同時実行されることがありコア数が多くなるほど優先度ベースである意味が薄れてくることになる。力は技に勝るというべきか...(-_-;)

プログラムの書き込みは以前に投稿したUSBスイッチを使うとUSBの抜き差しなしで出来るので凄く便利だ。参考まで。

USBデバイスの電源を切るスイッチ

[pico_smp.cpp]

[timer_sample.cpp]

[update]
2022-02-18
PCEventクラスの仕様変更とチューニング、及び、Task::taskid()の型をvoid *からsize_tに変更。

2022-02-17
GPIOを監視するためのライブラリ(pcevent.h/pcevent.cpp)を追加。

PCEventクラスはGPIO割込を管理し割込ハンドラの呼び出しや指定のピン状態になるまでタスクを休止することができる。またキャプチャー処理のためのエッジ割込の時間(time_us_32)も取得可能だ。ピンの割込のみ管理しているので利用する前にピンを適切に初期化しておく必要がある。PCEventクラスを利用することでマルチタスク環境にてピンの監視処理を効率よく行うことが可能となる。

※Pico-SDKのGPIO割り込み機能とは排他的に利用可能であることに注意!

タイムアウト或いはピンがLOWになるまで休止する。既にLOWの場合は即座に戻る。

タイムアウト或いはピンがHIGHになるまで休止する。既にHIGHの場合は即座に戻る。

タイムアウト或いは次回のピンの立下りまで休止する。

タイムアウト或いは次回のピンの立上がりまで休止する。

タイムアウト或いは次回のピンの立下り或いは立上がりまで休止する。

※タイムアウトの指定値がゼロの場合は状態を見て即座に戻り、マイナス値の場合は無限に、ゼロより大きい場合は指定時間まで待つ。
※戻り値がSYNC_TIMEOUTEDならタイムアウト発生、それ以外はイベント待ち完了。

2022-02-08
全タスクが休止するときに誤動作するという致命的なバグがあったので修正。それとタスク固有の識別子やデータを管理できるようにしてみた。

タスク識別子の取得。

タスク固有データの取得。Task::start(void *data)で指定したデータが取得できる。

割当てされたメモリを解放しタスクを終了する。タスク関数からのリターン、或いは、この関数の呼び出しにより何時でもタスク終了することができる。

Task::start() … タスク固有データの指定に加えタスクのスタック領域のための静的メモリの指定もできるようにしてみた。

2022-02-07
Event/Sem/Mutexクラスのタイミング・バグがまだ残っていたので再改修。これで完璧...かも。(-_-;)

2022-02-06
Event/Sem/Mutexクラスのタイミング・バグがあまりに酷すぎたので全面的に改修。

2022-02-05
Eventクラスのバグを修正。おまけとしてSemaphoreクラスとMutexクラスを追加。但し、デバッグはしてないので注意すべし。

2022-02-04
シンプルかつ単機能なEventクラスを追加してみた。

Eventテンプレートクラス。マスク値(MASK)と待ち方(WAITALL)が指定可能。

イベント発生まで休止する。タイムアウト指定が可能。

イベント通知する。複数タスクが待っている場合は休止順に起床する。event引数には0-31までのビット番号を指定する。

2022-01-13
Alarmクラスの仕様変更とそれに伴う修正を行った。

2022-01-12
タイマークラスの仕様変更と、Sync::sleep()の戻り値でタイムアウト判定が出来るようにしてみた。

2022-01-11
全体的な最適化とsleep()のタイムアウト機能がタスクとの関連性がないというヘンな仕様だったので修正。

2022-01-10
Pico-SDKのタイマーライブラリはコア依存があり扱いが面倒であったため、コアに依然せずに使えるマルチコア対応のタイマーライブラリを作成してみた。これに伴い、Pico-SDKのタイマーライブラリを利用していたコードを削除するとともに時間指定する関数は全てマイクロ秒で統一してみた。
新たなタイマーライブラリはTimer::begin()を呼び出したコア側でのみ割り込み処理を行うが、実行コアを気にせずに使えることやアラーム数が無制限であること、及び、正確なリピート時間を特徴としている。

2021-12-14
SysTick::micros()を割り込み禁止状態からの呼び出しにも対応。

2021-12-10
タスク休止/起床方式のdelay()/delayMicroseconds()のオーバーヘッドが大きく時間が少し不正確な感じなので名称を変更しsleep()/sleepMicroseconds()として新たに関数を追加。今まで通りポーリング方式のdelay()/delayMicroseconds()も利用可能。待つ時間が長い場合はsleep、短い時間であればdelayのように使い分けするのが良さそうだ。

2021-12-09
Task::delay()/Task::dealyMicroseconds()をポーリングによる実装からPico-SDKのalarm機能を利用してタスク休止/起床する実装に改良してみた。おまけとしてget_alarm_pool()を追加。get_alarm_pool()によりSMP環境で実行コアに対して適切なalarm_poolを取得することができるようになる。それと、Sync::sleep()がコンパイラ最適化によりインライン展開されると誤動作するためインライン展開させないように修正。

[Library]