不確定特異点

広く深く、ところどころ超深く

コンテキスト切り替えの仕組み

Cortex-M コアのコンテキスト切り替えについて、uros に実装した仕組みについてまとめておきます。(自分でもいつか忘れそうなので…)

コンテキスト情報とは?

タスクのスタック領域は full-descending スタック(スタックポインタが最後に push したデータを指し、push と共にアドレスの下位方向に成長するスタック)として、静的に確保しており、通常は自動変数や関数呼び出しに使用されてます。しかし、タスクのコンテキストが退避されている状況では、そのスタック上に下表のようなレイアウトでそのタスクのレジスタ値を格納しておきます。また、タスク管理構造体(TCB)には context というメンバを用意しておき、このスタックポインタ(下図の context + 0 にあたる番地)を保持しておきます。つまり、ここでいうタスクのコンテキスト情報とは、「スタックに積んだ全レジスタ値」+「スタックポインタ」ということになります。(あくまで uros の実装での話です。)

番地 スタックの内容 初期値
context + 0 R11 -
context + 1 R10 -
context + 2 R9 -
context + 3 R8 -
context + 4 R7 -
context + 5 R6 -
context + 6 R5 -
context + 7 R4 -
context + 8 R0 -
context + 9 R1 -
context + 10 R2 -
context + 11 R3 -
context + 12 R12 -
context + 13 LR -
context + 14 Return Address エントリポイント
context + 15 xPSR 0x01000000

コンテキスト切り替えは PendSV 例外を契機として行っており、PendSV ハンドラでは現在のコンテキストの退避と、次のコンテキストの復帰を行います。

コンテキストの退避処理

xPSR 〜 R0 は PendSV 例外に突入したときに HW により自動的にタスクのスタックに push されます。残りの R4 〜 R11 は以下のように SW により継続して push します。

asm("mrs   r0, PSP;"
    "stmdb r0!, {r4-r11};"
    "str   r0, [%0];"
    :
    : "r" (&taskp->context)
    : "r0");

例外ハンドラでは MSP のスタックに切り替わっているため、単純に push 命令を実行するのではなく、PSP が指す先のメモリにストアすることに注意が必要になります。R4 〜 R11 をひとしきり PSP が指すスタックに格納したら、そのスタックポインタを TCB の context メンバ(taskp->context)に格納してます。

コンテキストの復帰処理

asm("ldmia %0!, {r4-r11};"
    "msr   PSP, %0;"
    "orr   lr, #0xD;"             /* Return back to user mode (0xFFFFFFFD) */
    "bx    lr;"
    :
    : "r" (taskp->context));

コンテキストの復帰は先ほどと逆の処理をするだけです。つまり、TCB の context メンバ(taskp->context)が指すスタックから R4 〜 R11 をロードします。ハンドラモードから非特権のスレッドモード(ユーザモード)に復帰するため、最後に LR レジスタの下位 4 bit を 0xD に修正して、bx lr を実行します。これにより、xPSR 〜 R0 が HW により自動的に pop されて例外から復帰します。

最初のタスクのディスパッチ

カーネルの初期化処理が完了して、最初のタスクを起動するときは、カーネルモードからユーザモードに切り替える必要があります。CONTROL レジスタを直接変更することでも可能ですが、例外を経由してコンテキスト切り替えのコードを利用した方がスマートにいけます。

カーネルモードにおいて最初のデフォルトタスクのスタックを上記のようなレイアウトで構築した後、PSP を context + 8 の位置にあらかじめセットしておきます。PendSV を発行して割り込みを許可すると、PendSV ハンドラに突入し、そこで上記のようなコンテキストの退避・復帰処理がなされ、ユーザモードに入ることができます。PendSV 例外の前に PSP を context + 8 の位置に設定しておく、というのがポイントで、PendSV ハンドラでは、あたかもデフォルトタスクがユーザモードから HW によるレジスタ退避が完了した状態でハンドラに入ってきているように見えるため、通常のコンテキストスイッチと最初のタスクディスパッチを一緒くたにして同じ切り替えコードで対応できることになります。(ただし、この方法は、PendSV 例外発生時の HW による自動スタッキングにより、 MSP が指すカーネルスタックには xPSR 〜 R0 の 8 word 分が積まれたままになります。)