この記事について
早くコード見たい人用 ←ただし実験用だよ!!!
コンテキストスイッチをしたいです(唐突)。setjmp、longjmpで行うコンテキストスイッチは記事が山のようにありそれはそれで便利なのですが割り込まれることを考慮してなかったりr0からr3までのレジスタ退避を行わなければならない関係上どうしてもコンテキストスイッチを行う側にコンテキストスイッチを明示的に記述する必要がありました。そこで割り込み(システムコールやPendSV)でコンテキストスイッチを行うと割り込まれたタイミングでr0からr3(とlr、pc、r12)が自動的にH/Wによって退避が行われるため大変便利です。しかしながらこのH/Wによる自動退避の関係上コンテキストスイッチする際のスタックがどのようになっているのかが大変複雑かつ通常の割り込みハンドラとは異なり復帰する際も特別な手順を必要とするという大変面倒なものとなっています。で、解説記事がない。だから作ってやろうとそう言うわけです。
コンテキストスイッチとは
端的に言うとコンテキストスイッチとはあるメソッド(一続きの関数や処理の塊のこと)の実行途中で他のメソッドを実行させて、また他のメソッドを...という感じで実行するメソッドを切り替える機能です。
で、何が嬉しいの?
簡単に言うとパソコンのように複数の関数を「たくさん」並行実行できます。並列実行はマルチコアでないと厳しく、またRaspberry Pi Pico界隈ではマルチコアによる並列処理が主流ですが困ったことにRP2040はCortex-M0+コアが2個しかありません(時間のかかる処理を他のコアに投げればいいと言う意味で単純かもしれませんが)これではネットワーク関係の処理などたくさん並行処理したい関数があった時に困ります。待機してるだけで何も処理をしない(正確には一定タイミングで完了してるかどうかを確認したいだけの)関数があった時確認するタイミングの隙間に別のことをしたりスリープすることで電力をより有意義に使えます。
本題(コード)
#include <hardware/exception.h>
//このコードは実験目的で作ったコードだよ!色々サボって実装したから実用には耐えないよ!
enum struct exc_return : uint32_t {
handler_MSP = 0xFFFFFFF1,
thread_MSP = 0xFFFFFFF9,
thread_PSP = 0xFFFFFFFD,
};
struct exception_stack {
uint32_t r0, r1, r2, r3, r12;
void (*lr)(), (*pc)();
uintptr_t xPSR = (1 << 24);
};
struct context {
exception_stack return_stack;
uint32_t r4 = 0, r5 = 0, r6 = 0, r7 = 0, r8 = 0, r9 = 0, r10 = 0, r11 = 0;
void const *stack_start;
void *sp;
exec_mode mode = exec_mode::thread_MSP;
context(method method, size_t arg0, size_t arg1, size_t arg2, size_t arg3,
size_t stack_size = 1024)
: stack_start{malloc(stack_size)} {
return_stack.pc = (void (*)())method;
return_stack.r0 = arg0;
return_stack.r1 = arg1;
return_stack.r2 = arg2;
return_stack.r3 = arg3;
return_stack.lr = trap;
return_stack.xPSR = (1 << 24);
if (stack_start != nullptr)
sp = (void *)(((uintptr_t)stack_start + stack_size - 1) & ~0xfL);
else
;
};
~context() {
if (stack_start != nullptr)
free((void *)stack_start);
};
};
void pendsv_handler() {
//Warninng!!! r4からr11とSPの退避および復帰をサボっています!!!
//インラインアセンブリとかでなんとかして!
//これそのまま移しても間違いなく動かないよ!!!だってスタック普通にぶっ壊してるもん!!!
void *sp = target_context.sp;
//↓PSPにコンテキストのスタックポインタを書き込み
asm volatile("msr PSP,%0" ::"r"(sp));
//↑WARNING!!!実行するメソッドをMSPで実行するためにここをMSPにすると現在のSPの値も変わってしまいます!
//これはCortex-m0+の仕様でハンドラモード中は絶対MSPを使うためで回避不可能だよ!!!(汚染しなければいい話かもしれないけどね!)
//やるならこれ以降の処理で復帰用のスタックフレームを汚染しないように
//(大人しくPSP使っとけ!!!な!!!)!!!
exception_stack *stack;
asm volatile("mrs %0, PSP" : "=r"(stack));
*stack = target_context.return_stack;
//↑ここで復帰用スタックフレームの設定してしまってるけど本当はいけないよ!
//コンテキストを構築するときにスタックも同時に構築しなきゃダメよ!!!
exc_return return_code = exc_return::thread_PSP;
asm volatile("bx %0" ::"r"(return_code));
}
void main(){
exception_set_exclusive_handler(PENDSV_EXCEPTION,pendsv_handler);//PendSVを登録(hardware_exceptionのリンクとhardware/exception.hのインクルードが必要)
*(uintptr_t *)(PPB_BASE + M0PLUS_ICSR_OFFSET) |= 1 << 28;//PendSV発動!!!
}
解説
コードコメントにあるようにこのコードは実験目的でとりあえずコンテキストスイッチできればいいよってコードです。このコードを元にコンテキストスイッチに必要なことを解説していきます。
大事な部分
このコードで大切な部分は
enum struct exc_return : uint32_t {
handler_MSP = 0xFFFFFFF1,
thread_MSP = 0xFFFFFFF9,
thread_PSP = 0xFFFFFFFD,
};
struct exception_stack {
uint32_t r0, r1, r2, r3, r12;
void (*lr)(), (*pc)();
uintptr_t xPSR = (1 << 24);
};
void pendsv_handler() {
...
exception_stack *stack;
asm volatile("mrs %0, PSP" : "=r"(stack));
*stack = target_context.return_stack;
//↑ここで復帰用スタックフレームの設定してしまってるけど本当はいけないよ!
//コンテキストを構築するときにスタックも同時に構築しなきゃダメよ!!!
...
exc_return return_code = exc_return::thread_PSP;
asm volatile("bx %0" ::"r"(return_code));
}
ここです。この1つのenumと1つのstruct、あと5行ほどの処理が中核です。あとはセットアップに過ぎません。つまりお好みでってことです。
割り込みが実行される流れ
割り込みがかかったら割り込みがかかる前の状態に対応してまずlrに次の3通りの値がセットされます。
enum struct exc_return : uint32_t {
handler_MSP = 0xFFFFFFF1,//割り込み前ハンドラーモード(割り込み中)の時
thread_MSP = 0xFFFFFFF9,//割り込み前スレッドモード(通常時)でMSPを使ってる時
thread_PSP = 0xFFFFFFFD,//割り込み前スレッドモードでPSPを使ってる時
};
そうです。大事な1つのenumの値です。割り込み前も割り込みと書かれていて混乱するかもしれませんが例えばシステムコールも一つの割り込みなのでシステムコール中にタイマー割り込みがかかったなどの場合が考えられます。普通にmain関数などが実行されているときはスレッドモードです。このスレッドモードの時、Cortex-M0+はMSPとPSPと言う2つのスタックポインタのうち、どちらかをr13(sp)として使用します(これはCortex-m0+のCONTROLレジスタの値やジャンプ先のexc_returnの値(※後述します)などでも切り替え可能です。)
次に、現在のsp(これはMSPかもしれないし、PSPかもしれません。割り込み前の状態に依存します)を0x20減算したあと、spが示すアドレスに次のような構造体を保存します。
struct exception_stack {
uint32_t r0, r1, r2, r3, r12;
void (*lr)(), (*pc)();
uintptr_t xPSR = (1 << 24);
//xPSRの24bit目を1にしておかないと復帰時HardFault例外(割り込み)が発生。
};
そうです。大切な構造体です。(本当はCortex-m0+のDevices Generic User Guideに書いてあるのですが引っ張るのがめんどくさいですすいません。)。ここでは割り込み前のr0~r3,r12,lr,pc(レジスタ名)が保存されています。
(新しいコンテキストを作成する際は)xPSRに代入してある初期値には復帰した時Thumbモードで実行するために24bit目を1にする効果があります。Cortex-Mシリーズはほとんどの場合ThumbかThumb2命令セットのみ対応のためこれをしておかないと対応していないARM命令セットに切り替えられたと判定されHardFaultが発動し、そのままフリーズします(HardFault用の割り込みハンドラもあるのでそれによって検出することももしかしたらできるかもしれません)。
Cortex-M0+はARM命令セットに対応してません。またxPSRレジスタの24bit目はARMモードかThumbモードかの判定に使用されます。このためCortex-M0+ではxPSRの24bit目は常に1になっていることが要求されます。(また、コンパイル時にarmv6-mを指定しないと常にThumbモードになるようコードを生成しないためまずいことになります)
そしてようやく割り込みベクターテーブルに保存されている割り込みハンドラのエントリーポイントを読み出し割り込みハンドラの実行を開始します。割り込みハンドラがリターンする際、lrには先ほど述べた特別な値が入っていて、Cortex-M0+はハンドラーモード中これらのアドレスをmov命令かbx命令によってPCに書き込まれた場合アドレスと対応したスタックから32バイトを読み出し(基本先程のハドウェアによって退避されたレジスタが保存されている領域に相当)てハードウェアによってレジスタが復帰されたあと、割り込み前(とされている状態。新規コンテキスト作成時はこれを再現します)の状態に復帰します。
余談ですがこのように割り込みが発生するたびにスタックを積み上げて、割り込み中に割り込みがネストされるような仕組みはすごく上手だと思いました。
コンテキストの作成時に気をつけること
- 新規コンテキストの作成時には新規コンテキストが使用するSP(大抵の場合PSPだが、PSPにコンテキストのSPを突っ込む前に、本来の開始時点より32バイト手前へ割り込み復帰用の情報を書き込んで本来の開始時点より32バイト手前をPSPは刺すようにする
- 新規コンテキストのpcにエントリポイント。lrにreturnした時呼び出す関数(exitみたいなやつがいいでしょう)のエントリポイントを入れておく。
- コンテキストのエントリが引数を要求する場合、スタックのr0~r3に相当する領域へ書き込んどくとエントリへ引数を渡せます
SVCのようなユーザーから発動可能で引数および戻り値をもつ割り込みを使用する際引数は割り込み発生後対応するスタック領域を読み出せばいいです。戻り値は逆に書き込みをやっておくことで渡すことができます。
終わり!
皆さんのRaspberry Pi picoやその他Cortex-Mマイコンでの開発の助けとなれば幸いです。
参考資料
Cortex-M0+ Device Generic User Guide←全部これに書いてある
STM32 Nucleo Boardでマルチタスクしたい←僕が試作した時大変お世話になりました。この場を借りてお礼申し上げます。