[modernAVR]シングルCPUマイコンで協調マルチタスク

実用性はともかく無いよりは出来たほうが良いじゃん？

- この記事は[Multix Zinnia Product SDK [*AVR] for Arduino IDE](https://github.com/askn37/askn37.github.io/wiki/InformationSDK)に含まれる[TaskChanger](https://github.com/askn37/askn37.github.io/wiki/TaskChanger) の技術解説だ。他の環境でそのまま動くようなものではないことに注意されたい。
- もっぱら AVR でインラインアセンブリを書く内容になっている。今どきそういうマシン語操作を詳説した文章も多くないだろうから、ノウハウサンプルとしてかなりの長文を割く。

## とっかかり

Arduino APIの`delay()`関数は内部で`yield()`関数がもし定義済あれば遅滞ループ中に繰り返し呼び出すよう設計されている。互換APIではそうなっていないものもあるが、仕組みが用意されているんだったら使ってみたくならないか？

一応その大元のAPIのそこがどうなっているか引用しておこう。ファイル名から判るが [Wiring Framework](http://wiring.org.co/reference/) が原典のようだ。`delay()`を呼ぶと1ミリ秒に1回`yield()`を呼ぶようになっていることが判る。

```c
/* hook.c */
static void __empty () { /* Empty */ }
void yield (void) __attribute__ ((weak, alias("__empty")));
```

```c
/* wiring.c */
void delay (unsigned long ms) {
  uint32_t start = micros();
  while (ms > 0) {
    yield();
    while ( ms > 0 && (micros() - start) >= 1000) {
      ms--;
      start += 1000;
    }
  }
}
```

`yield()`が宣言中に`weak`属性を付加しているのがミソで、これによって利用者が`yield()`関数を独自に同名のユーザー関数を書けば（エラーなく）置き換えてくれる。だから下記のような関数を自分のコード中に書けば`delay()`が呼ばれ続けるあいだだけ、Lチカさせることができる。

```c
void yield (void) {
  /* delayで待っているあいだは 100ms 間隔でLチカ */
  if ((millis() / 100) % 2 == 0)
    digitalWrite(LED_BUILTIN, LOW);
  else
    digitalWrite(LED_BUILTIN, HIGH);
}
```

この仕組みを発展させれば __協調マルチタスク__ を作れるんじゃないかと閃いたなら、アナタはきっとビギナーではない。
自分ならこういうスケッチコードが書けるようにしたかった。

```c
void setup (void) { /* あれこれ初期化 */ }
void loop (void) {
  /* なにかの処理 */

  /* 割と長く待つことがある */
  delay(5000);

  /* また何かの処理 */
}
/* 上の delay中にはこれが実行されて勝手に Lチカし続けて欲しい */
void subtask (void) {
  while (true) {
    digitalWrite(LED_BUILTIN, TOGGLE);
    delay(100); /* <-- この delay でメインループの delay に実行権が戻る */
  }
  /* このサブタスク自体は勝手に無限ループ */
  /* もしこのループを抜けてサブタスクが終わったなら、もう呼ばれないようでいて欲しい */
}
```

`delay()`が呼ばれたら次の待機タスクに実行権を渡す。それを順繰りラウンドロビンして一巡したら最初のタスクに実行権が戻ってくるという仕組みを作れたなら、このスケッチは目論見通り動くのではないだろうか。

## タスク切り替えフローを設計する

思いついたはいいものの、だいたいの人はそこでくじけるように思う。普通のシングルCPUマイコン用APIにそういう仕組は用意されてないし、自作するにしても普通のC/C++言語でとうてい記述できるものではないからだ。誰かが作ってくれないかなーと辺りを見回すに違いない。

一方、マシン語の知識とそれを書ける視点を持っているなら話は早い。ざっくりとこうすれば良さげと秒で発想する。

1. `yield()` が呼ばれたら、現在のレジスタファイルとシステムレジスタを漏れなく全部スタックに`PUSH`する。
1. そのスタックポインタを現在のタスク管理テーブルに記憶する。
1. 次に実行権を渡すべきタスク情報を、タスク管理テーブルから探す。
1. 対象タスクを見つけたら、そこにあるスタックポインタを復帰する。
1. システムレジスタと全レジスタファイルをそのスタックから`POP`して、`yield()` を終える。
1. 何事もなく切替先のタスクが進行し、また`yield()`が呼ばれたら1から繰り返す。

まあ本当の思案のしどころはここからなんですがね。つらつらと思考展開を書き出してゆくと；

1. 切替先のタスクは、初期化時点で個別かつ独自のスタック用メモリが用意されていなければならない。
1. そのスタックは最初に空であってはならない。前述の `yield()` が切替完了前にシステムレジスタと全レジスタを `POP` するからだ。
1. 更にタスクが終了したことが解らねばならず、タスク管理テーブルからタスクを削除するコードが実行されなければならない。これについてはスタックの先頭に削除コードへジャンプする番地が `PUSH` されていれば、スタックが空になる時にそこへ飛ぶはずではあると仮定する。そうでなければバグだ。
1. スタックオーバーフロー対策はどうしよう？これは率直に無視するものと妥協するしかない。それができるメモリ管理機能がマイコンにないから、API設計段階でマルチタスク対応を盛り込まなかったのだと想像できるからだ。タスク実行に必要十分なメモリを使用者が用意し、その過不足が生じないよう注意してコーディングする他にない。
1. スタック管理テーブルはどういうフォーマットが良いか。またどのくらいのタスク数に対応すればよいか。複雑な実装にはしたくないしそもそもメモリ量は限られているのだから、メインタスクを含め最大4個も使えればよいだろう。また8bit級マイコンではそのアドレス範囲の全部を SRAM領域に割り当てていることはなく、せいぜいその半分以下だ。だからスタックポインタもその全ビットを使っているわけではなく、必ず未使用ビットが余っているに違いない。実際 modernAVR系列の場合は 16bitアドレスのレジスタ中、SRAM番地は下位の 15bit以内に必ず納まる。故にMSBが立っていたらそれは SRAMひいてはスタック領域を指していないので「実行しなくて良いタスク」だと判定することにしよう。この想定に合わない MCUは対応外としてよい。

ここらでフローチャートを書いて検討するのが真面目というものだが、いきなりマシン語書き出してトライ＆エラーで煮詰めてゆく流儀もある。

## インラインアセンブリ構文

実際のコードは[ソースコード](https://github.com/askn37/MacroMicroAPI_lib/blob/24f77a4165d741e36ab1bcf6d72b598d468cfd5a/src/cpp/TaskChanger.cpp)を見たほうがよいが`yield()`実装周辺を引用するとこうだ。想定している AVRアーキテクチャは tinyAVR-0 以降の新世代系列で、ATmega328P のような旧世代 AVRは対象とはしていない。（何箇所か修正すれば使えるが）

```c
#include <avr/io.h>
#include <util/atomic.h>

struct TaskChanger_work_t {
  char* stack[4];
  uint8_t current;
};

namespace TaskChanger {
  volatile TaskChanger_work_t __worker = {0, (char*)-1, (char*)-1, (char*)-1, 0};

  __attribute__((naked,used)) void yield (void) {
    __asm__ __volatile__ ( R"#ASM#(
        PUSH    R1              ; enter function
        IN      R1, __SREG__    ; load SREG
        PUSH    R1              ; keep SREG
    ; 割込を禁止して残りの全レジスタを保存
        CLI                     ; change SREG
        PUSH    R0              ; keep all
        PUSH    R2              ;
        PUSH    R3              ;
(中略)
        PUSH    R29             ;
        PUSH    R30             ;
        PUSH    R31             ;
        IN      R24, __SP_L__   ; SPL 取得
        IN      R25, __SP_H__   ; SPH 取得
1:      LDI     YL, lo8(%0)     ; スタック管理先頭番地
        LDI     YH, hi8(%0)     ;
        LDD     XL, Y+%1        ; スタック管理指標
        CLR     XH              ;
        MOVW    ZL, YL          ; 作業ワードレジスタに管理先頭番地複製
        ADD     ZL, XL          ; オフセット加算
        ADC     ZL, XH          ;
        STD     Z+0, R24        ; SPL 保存
        STD     Z+1, R25        ; SPH 保存
2:      ADIW    XL, 2           ; 次の指標（線形探索）
        ANDI    XL, 7           ; 指標範囲制限（オフセット0,2,4,6のラウンドロビン）
        MOVW    ZL, YL          ; 作業ワードレジスタに管理先頭番地複製
        ADD     ZL, XL          ; オフセット加算
        ADC     ZH, XH          ;
        LDD     R25, Z+1        ; 保存されている SPH 取得
        SBRC    R25, 7          ; SPH の MSB をテスト
        RJMP    2b              ; MSB がセットならタスク無効なので次の指標をテスト
        STD     Y+%1, XL        ; 選択された指標を保存
        LDD     R24, Z+0        ; 管理に保存されている SPL 取得
        OUT     __SP_L__, R24   ; SPL 復帰
        OUT     __SP_H__, R25   ; SPH 復帰
    ; 復元したスタックから全レジスタを戻す
        POP     R31             ; restore all
        POP     R30             ;
        POP     R29             ;
(中略)
        POP     R3              ;
        POP     R2              ;
        POP     R0              ;
        POP     R1              ;
        OUT     __SREG__, R1    ; restore SREG
        POP     R1              ; restore work
        RET                     ; leave function
__TaskChanger_detach_task:
    ; 終了タスクをスタック管理テーブルから削除する
        LDI     R25, 0xFF       ; スタック未使用（MSB）をマーク
        RJMP    1b
    )#ASM#"
      ::"p" (_SFR_MEM_ADDR(TaskChanger::__worker.stack))
      , "n" (_SFR_MEM_ADDR(TaskChanger::__worker.current) - _SFR_MEM_ADDR(TaskChanger::__worker.stack))
      : "memory"
    );
  }
}

void yield (void) {
  TaskChanger::yield();
}
```

> 元のコードには`TIMEOUT_BLOCK`機能対応なども含まれているが、それについては論じないので省いた。

最初にタスク管理テーブルの構造体を定義し、それを`volatile`宣言付きで領域確保し、初期化する。これは4個分のスタックポインタと、1個の指標変数を備えている。4個のうち先頭のスタックポインタはメインタスク用である。これは必ず最初の `yield()` 実行で上書きされるから何が書かれてあっても関係はない。だが他はそうではないので、MSBがセットされた適当な値を「無効タスクマーク」として書き込んでおかなければならない。指標変数`current`にはメインタスクを現す 0を書いておく。

`yield()`関数には特別な属性を付加している。`naked`はこの関数全体がアセンブリで書かれることをコンパイラに教えるもので、これによって余分なコード（例えばreturn処理）をコードに付け加えさせない効果を持つ。`used`はこの関数がたとえ何処からも呼ばれざるとも最適化処理で除去させないようにし、必ず出力ファイルに書き出させる。

`__asm__ __volatile__(...)`はインラインアセンブリ構文で、毎度こう書く約束だと覚えておこう。単に`asm(...)`と書いてもよいのだが C/C++言語コンパイラの挙動が少し変わるので、書き方は統一しておいたほうが良い。説明する上では`asm`と略記する。

よくあるアセンブリコードは1行毎にダブルクォートで括って改行コードも埋め込む面倒な書き方をするが、ここでは今どきのコンパイラで使えるヒアドキュメント構文を使ってスッキリ書いた。それが使えないような古いAVR-GCCではそもそも、いま俎上にあげている新世代AVRに対応していないので下位互換性に配慮する必要はないからだ。

## asm構文の引数

その中身の説明をする前に`asm(...)`構文の引数について。これは丸括弧で括っているが関数ではない。引数は`:`でグループ化された構造になっている。

`asm(<アセンブリ>:<出力指定>:<入力指定>:<補助指定>)`

どのグループも必要に応じて省略可能だが、\<アセンブリ\>部はダブルクォート文字列で書く。\<出力指定\>はひとつだけ指定できる。\<入力指定\>と\<補助指定\>は`,`で区切って複数列挙できる。ここでは\<出力指定\>は使わないので、こういう記述になっている。

```c
      ::"p" (_SFR_MEM_ADDR(TaskChanger::__worker.stack))
      , "n" (_SFR_MEM_ADDR(TaskChanger::__worker.current) - _SFR_MEM_ADDR(TaskChanger::__worker.stack))
      : "memory"
```

`"指示子"(指定値)`がひとつの構文だ。指示子は指定値をコンパイラにどう解釈させるか（アセンブリコードに変換するか）を決める。`"p"`はポインタ定数の意味で指定値をポインタ（リンクプロセス時までは結果不明）であると解釈する。`"n"`は指定値を静的定数値（コンパイル中に結果は定まる）であると解釈する。ここではこれらが2文あり、\<アセンブリ\>部内では順番に`%0`と`%1`という疑似オペランドで参照することができる。

> この指示子関係の記述間違いが非常に厄介で、GCC/GASはエラーを吐けどもエラー内容は意味不明であるのが常、デバッグどうこう以前に何処の何が間違っているのかいくらも教えてくれないのだ。インラインアセンブリの習熟が極端に難しい一因である。対策はできるだけ小さな最小限の内容だけを書いた単体ファイルをコンパイルさせて見ることしかない。修練である。

`(指定値)`の部分は普通にC/C++の記述で、値をあれこれ`cast`して渡す。アセンブラは最終的に指定値をレジスタあるいは定数としてしか見ないので、その定数が指すのが IOレジスタ番地なのか、SRAMメモリ番地なのか、はたまた関数コードアドレスか、静的定数なのかを考えながら渡す。`_SFR_MEM_ADDR()`マクロはそういう作業を助けるマクロのひとつで、指定値を"データ領域内のメモリ番地"とみなせと指示してくれる。今回渡すのは SRAM内のタスク管理テーブル先頭のメモリ番地と、そこから指標変数`current`までのオフセット値（結果として静的定数になる）だけだ。

最後の\<補助指定\>である`"memory"`は、この`asm`構文がメモリ変更操作を行っていることをコンパイラに教える。意味はただそれだけでどういう最適化を施すかはコンパイル任せにするという意思を示しているに過ぎない。

> 意外かも知れないが GCCは`asm`構文内のコードも最適化対象にして書換えてしまうことがある。なぜなら C/C++言語から変換されたアセンブリコードと、使用者が書いたアセンブリコードとを平等に区別しないからだ。GCCを通す以上この挙動を止めることは難しいので、せめてここはこういうことをやっている、こうして欲しいという"プログラマの意思"を教えてやるように万全を期すしかない。

## レジスタ割り当て

|レジスタ名|別名|説明|
|-|-|-|
|R31:R30|ZH:ZL|Zポインタレジスタペア。__worker.stack 配列のインデックス。
|R29:R28|YH:YL|Yポインタレジスタペア。__worker.stack 配列の先頭アドレス。
|R27:R26|XH:XL|Xポインタレジスタペア。インデックスオフセットの増分に使う。
|R24:R24||タスク用スタックポインタ値の保存に使う。
|R1||一時的作業レジスタ。SREGの保存に使う。
|SREG||ステータスレジスタ。AVRではIOメモリ空間上にある。
|SP|SPH:SPL|SPレジスタ。システムスタックポインタ。AVRではIOメモリ空間上にある。

`X`、`Y`、`Z`の三つの16bitレジスタペアは、間接メモリ参照ができるポインタレジスタだ。AVRではこの三つしかポインタに使えないのだが、それを全部活用する。

## 説明しよう

さて、アセンブリコードで最初にやることは、システムレジスタ`SREG`の保存である。AVRでは困ったことにこれが普通の汎用レジスタではなくIOレジスタなので、直接それをスタックに`PUSH`することが出来ない。まず汎用作業レジスタ`R1`をひとつあけて、IOメモリ命令`IN`で読み出してからスタックに`PUSH`する。それがすめば`SREG`中の全体割込フラグ`I`（レジスタ内では MSB）を`CLI`命令でクリアして、割込禁止状態にできる。

```asm
        PUSH    R1              ; enter function
        IN      R1, __SREG__    ; load SREG
        PUSH    R1              ; keep SREG
    ; 割込を禁止して残りの全レジスタを保存
        CLI                     ; change SREG
```

> たったここまででももう、4命令を使った。CPUが 1MHzシステムクロック駆動だったら 4usも掛かっている。この間に多重割込なんぞが入ってきたらどうなるのかと心配になるが、AVRはそうならないように出来ているのが面白い。スタックポインタがインクリメント/デクリメントされるとそこから最大4システムクロックのあいだの割込が自動的に全部保留されるのである。それはこのルーチンが `CALL`される時からもう始まっているので、`CLI`命令が実行されるまで割込に邪魔されない時間的猶予がある。そもそもそういう設計でなければ、必ずワード単位で番地情報をスタック操作する `CALL/RET/RETI` のようなアトミック操作が分断され、スタックが壊されてしまう可能性があるからだろう。8bitデータバスCPUならではの事情である。

その次は残りの全レジスタ31個をスタックに`PUSH`する。当然31行もあるから中略。そうしておいてからスタックポインタペア`SP`を汎用レジスタに`IN`命令で読み出す。`SREG`もそうだがよく操作する特殊レジスタには`__`で括ったマクロシンボルが用意されているのでこれを使う。また`SP`は16bit幅だが、8bitCPUなので2回に分けて読まなければならない。なのでそのシンボルは`__SP_L__`と`__SP_H__`のふたつになる。ここでは`R25:R24`レジスタペアを保存用に使う。

```asm
        PUSH    R0              ; keep all
        PUSH    R2              ;
        PUSH    R3              ;
(中略)
        PUSH    R29             ;
        PUSH    R30             ;
        PUSH    R31             ;
        IN      R24, __SP_L__   ; SPL 取得
        IN      R25, __SP_H__   ; SPH 取得
```

次にタスク管理テーブルの中身を読むための準備をする。

```asm
1:      LDI     YL, lo8(%0)     ; スタック管理先頭番地
        LDI     YH, hi8(%0)     ;
```

`lo8()`と`hi8()`は avr-gcc 特有の擬似アセンブリ関数だ。これは 16bit「静的定数」引数の下位 8bit、あるいは上位 8bitを取り出す。16bit「レジスタ」引数の下位／上位を取り出すには`%A0`／`%B0`と書くので注意したい。[詳細はここに書かれている](https://gcc.gnu.org/wiki/avr-gcc#Assembly_Operand_Modifiers)が、ざっくり翻訳すると；

|修飾子|説明|
|-|-|
|lo8()|リンク時定数の 1 バイト目、ビット 0 ～ 7
|hi8()|リンク時定数の 2 バイト目、ビット 8 ～ 15
|hlo8()|リンク時定数の 3 バイト目、ビット 16 ～ 23
|hhi8()|リンク時定数の 4 バイト目、ビット 24 ～ 31
|hh8()|hlo8と同じ（3バイト目）
|pm_lo8()|リンク時定数の 2 で割った 1 番目のバイト、ビット 1 ～ 8
|pm_hi8()|リンク時定数を 2 で割った 2 番目のバイト、ビット 9 ～ 16
|pm_hh8()|リンク時定数を 2 で割った 3 番目のバイト、ビット 17 ～ 24
|pm()|lo8(pm(main)) のように、プログラムメモリ（ワード）アドレスを取得するためのリンク時定数を 2 で割ったもの。
|gs()|lo8(gs(main)) のように、プログラムメモリ（ワード）アドレスを取得するために関数アドレスを 2 で割る。必要に応じてスタブ（トランポリン）を生成する。これは`EICALL`で使用されることが想定される 128KiB を超えるプログラムメモリを備えたデバイス上の関数のアドレスを計算するときに必要。

`pm()`と`gs()`はプログラムメモリ空間（AVRxmアーキテクチャでは最大24bitワード）をポイントするために使う。それはワード表現メモリなので常にアドレスも偶数であるから、最下位1ビットをけちって「2で割る」操作が必要なのを助けてくれる。結果的に16bitデータレジスタ幅で 128KiBのフラッシュ空間内のどこにでも実行位置をジャンプできる。普通のノイマン型CPUではちょっとお目に掛かれない設計だ。

> プログラムアドレスはリンク作業完了までどういう絶対値に定まるか分からないので、これらの擬似関数の使いこなしは必須。

`Y`ポインタレジスタが初期化できたところで、そこから`current`に入っている現在のタスク管理番号を`XL`レジスタにロードする。ペアになる`XH`レジスタはゼロクリアしたいので、`CLR XH`を実行する。これが現在の`R25:R24`レジスタペアを保存したい`stack`ワード型配列内のバイト型インデックスだ。

```asm
        LDD     XL, Y+%1        ; スタック管理指標
        CLR     XH              ;
```

`X`レジスタペアを作業用の`Z`レジスタペアに複製する。`MOVW`命令はこれ一つで16bitレジスタペアを複製してくれる便利な命令だが、オペランドに下位レジスタ名だけを書くので、直感的には分かりづらい。（ペアになっている`ZH`にも`YH`がコピーされる）

```asm
        MOVW    ZL, YL          ; 作業ワードレジスタに管理先頭番地複製
```

次は`Z`レジスタペアに`X`レジスタペア（`stack`テーブル先頭）を足しこむ。するとそれが`R25:R24`レジスタペアを保存するべき`stack`ワード型配列内のアドレスだ。1命令でレジスタペアをメモリに保存するような便利なオペコードはないので、二つの`STD Z+`命令を使用する。

```asm
        ADD     ZL, XL          ; オフセット加算
        ADC     ZL, XH          ;
        STD     Z+0, R24        ; SPL 保存
        STD     Z+1, R25        ; SPH 保存
```

ここからがようやく本題だ。次に実行権を与えるべき登録タスクを、スタック管理テーブルの中から探す。`X`レジスタペアに定数`2`（1ワード分のオフセット）を足して`7`で論理和をとった値が、次のタスクの管理テーブルアドレスだ。16bit定数加算は`ADIW`、8bit定数ANDは`ANDI`命令を使う。結果として `X`レジスタペアは四通りの値しか持たない。

```asm
2:      ADIW    XL, 2           ; 次の指標（線形探索）
        ANDI    XL, 7           ; 指標範囲制限（オフセット0,2,4,6のラウンドロビン）
```

`Y`レジスタペアを`Z`レジスタペアに複製して、`X`レジスタペア値を足しこむ。16bitアドレスレジスタペア同士の加算は1命令ではできないので、`ADD`と`ADC`の2命令を使う。

```asm
        MOVW    ZL, YL          ; 作業ワードレジスタに管理先頭番地複製
        ADD     ZL, XL          ; オフセット加算
        ADC     ZH, XH          ;
```

長い道のりを経て、`Z`レジスタペアには「次のタスクのスタック」が保存してあるアドレス値が格納されている。そのスタック値の上位バイト`SPH`の最上位 MSBは管理フラグなので、`SPH`を`R25`に読み出して`SBRC`命令で第7ビット（MSB）をテストする。このテスト命令は結果が真なら直後の命令を実行、偽なら実行しないでスキップさせる。Z80などでは「条件付きジャンプ」が多用されるが、AVRではこの「条件付き次命令スキップ」がよく使われる。ともあれテストが真なら、直後の相対ジャンプ`RJMP`が実行される。その飛び先は`2b`つまり上（back）方向にある一番近い`2:`ラベルまで PCカウンタが戻される。

```asm
        LDD     R25, Z+1        ; 保存されている SPH 取得
        SBRC    R25, 7          ; SPH の MSB をテスト
        RJMP    2b              ; MSB がセットならタスク無効なので次の指標をテスト
```

> 下（forward）方向へのジャンプは`1f`などと書く。数値を使った仮ラベルは何度同じ番号を宣言しても良い。直近への指定番号を探す仕組みなので、複数のラベルを跨ぐには違う番号で宣言する。

テストが済むとそのスタックを持つタスクは実行可能だ。現在の指標を示している`XL`を`current`メモリに保存し、`Z`ポインタで`SPL`を`R24`に読み込む。完成した`SPL`と`SPH`を真の SPポインタに書き込む。

```asm
        STD     Y+%1, XL        ; 選択された指標を保存
        LDD     R24, Z+0        ; 管理に保存されている SPL 取得
        OUT     __SP_L__, R24   ; SPL 復帰
        OUT     __SP_H__, R25   ; SPH 復帰
```

お疲れ様でした。あとは保存してあるすべてのレジスタファイルを読み戻し、割り込みフラグをステータスレジスタに復帰して`RET`すれば、新たなタスク実行へと復帰する。前述したように`PUSH/POP`直後の4システムクロックは無条件に割込禁止なので、正しく割込許可が再開されるのは`RET`命令実行完了後まで保留される。

```asm
        POP     R31             ; restore all
        POP     R30             ;
        POP     R29             ;
(中略)
        POP     R3              ;
        POP     R2              ;
        POP     R0              ;
        POP     R1              ;
        OUT     __SREG__, R1    ; restore SREG
        POP     R1              ; restore work
        RET                     ; leave function
```

最後に、小さなスニペットコードがある。ここには終了したタスクが最後に実行するコードが書かれていて、次の生きているタスクを探すよう`1:`ラベルへ実行処理を飛ばすようになっている。

```asm
__TaskChanger_detach_task:
    ; 終了タスクをスタック管理テーブルから削除する
        LDI     R25, 0xFF       ; スタック未使用（MSB）をマーク
        RJMP    1b
```

## タスク登録

タスク登録をする関数は、タスク管理テーブルへの登録とともに、タスク別ローカルスタックの初期化もする。与えられる引数はすべてリンク時定数であるから、`assert`を使ってコンパイル時チェックした上で受け入れる。

そしてもちろん、作業は全体割込禁止状態でなければならない。`ATOMIC_BLOCK(ATOMIC_RESTORESTATE){...}`ブロックを使うと`SREG`フラグレジスタを自動で保存／復帰してくれるので便利だ。（`#include <util/atomic.h>`で使えるようになる）

```c
  void attach_task (uint8_t __task_index, volatile char __local_stack[], size_t __local_stack_size, void (*__start_task)()) {
    assert(1 <= __task_index && __task_index <= 3);
    assert(64 <= __local_stack_size);
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      /* ローカルスタックを初期化する */
      /* スタック最上位は 終わったタスクを片付ける処理の番地（BigEndian） */
      uint16_t __temp;
      __asm__ __volatile__ ( R"#ASM#(
          LDI     %A0, lo8(pm(__TaskChanger_detach_task)) ;
          LDI     %B0, hi8(pm(__TaskChanger_detach_task)) ;
        )#ASM#"
        : "=r" (__temp)
      );
      __local_stack[__local_stack_size - 1] = __temp;
      __local_stack[__local_stack_size - 2] = __temp >> 8;
      /* 実行開始番地 */
      __local_stack[__local_stack_size - 3] = (uint16_t)__start_task;
      __local_stack[__local_stack_size - 4] = (uint16_t)__start_task >> 8;
      /* R1 の初期値 */
      __local_stack[__local_stack_size - 5] = 0; // R1 == __zero_reg__
      /* SREG の初期値 */
      __local_stack[__local_stack_size - 6] = 0x80; // local SREG == sei()
      /* SP を計算 */
      __worker.stack[__task_index] = (char*)__local_stack + __local_stack_size - (32 + 6);
    }
  }
```

コード中の小さなアセンブリは、前述したスニペットコード`__TaskChanger_detach_task`関数アドレスを変数`__temp`にロードするためのものだ。ここで`pm()`、`lo8()`、`hi8()`の擬似アセンブリ関数が活躍する。アセンブリ内で宣言したラベル（のアドレス）は`export`（アセンブリ内では`.GLOBAL`擬似命令）宣言しない限り C/C++言語側からは全く見えないが、それだとグローバル公開になってしまうためローカル関数のままにしておきたい場合は都合が悪い。そこでこのようなスニペットコードを使って、通常の C/C++言語変数へ関数アドレス値をロードするのだ。

タスク別ローカルスタックの初期化はそのメモリ配列末尾から逆順に埋めてゆくが、`RET`命令で PCレジスタへ取り出される実行関数アドレスのバイト並びはビッグエンディアンでなければならない。C/C++言語のデータ格納順や 16bit以上の幅の I/Oレジスタファイルがリトルエンディアンなのに、これだけはビッグエンディアンなので失念しやすい。AVRの古くからある CPU設計の名残なので、そういうものだと覚えておこう。最後に全レジスタ分だけ減数したスタックポインタ値を初期値として管理テーブルに書き込んでいる。

> `__start_task`関数アドレスのメモリ格納は C/C++言語が自動的に`pm()`相当の「2で割る」処理をするため、小細工する必要はない。素直にメモリに格納する。

## 制約

- ここで書かれた`yield()`タスク切り替えは少なくとも 140システムクロックを消費する。これは CPUが 1MHz 動作の場合は 140usの、20MHz動作なら 7us の遅延に相当する。この間は他の割込実行は待たされる。遅延が許されないタイムクリティカルな処理があるなら、その間はタスク切換が発生しないように配慮しなければならない。（`ATOMIC_BLOCK`や、[`<TimeoutTimer.h>`](https://github.com/askn37/askn37.github.io/wiki/TimeoutTimer) の `TIMEOUT_BLOCK` などで必要処理をガードする）
- 各タスクには登録時に個別の専用ローカルスタックメモリを必ず割り当てなければならない。これは該当タスク実行中に不足するようなことが絶対にあってはならない。スタックメモリ不足は スタック・オーバーフロー を発生せしめ、CPU動作を不正な状態に招く。
- 異なるタスク間の変数共有は`volatile`宣言を伴っていなければ保証されない。C/C++言語からはタスク切り替えの動向を把握できないからだ。面倒なら必要な変数は全部`struct`構造体の中に入れておくと良い。avr-gcc でのそれは`volatile`と同じ効果を持つ。

## まとめ

- アセンブリ記述の解説は、やたら長くなって疲れるね。(;´д｀)

## 関連リンク

- [TaskChanger ツールリファレンス](https://github.com/askn37/askn37.github.io/wiki/TaskChanger)
- [TaskChanger ソースコード](https://github.com/askn37/MacroMicroAPI_lib/blob/24f77a4165d741e36ab1bcf6d72b598d468cfd5a/src/cpp/TaskChanger.cpp)
- [TackChanger.ino 実演スケッチ]()
- [avr-gcc wiki](https://gcc.gnu.org/wiki/avr-gcc)
- [AVR-LIBC(日本語訳)](https://cega.jp/avr-libc-jp/)
- [Wiring Framework](http://wiring.org.co/reference/)

## Copyright and Contact

Twitter(X): [@askn37](https://twitter.com/askn37) \
BlueSky Social: [@multix.jp](https://bsky.app/profile/multix.jp) \
GitHub: [https://github.com/askn37/](https://github.com/askn37/) \
Product: [https://askn37.github.io/](https://askn37.github.io/)

Copyright (c) askn (K.Sato) multix.jp \
Released under the MIT license \
[https://opensource.org/licenses/mit-license.php](https://opensource.org/licenses/mit-license.php) \
[https://www.oshwa.org/](https://www.oshwa.org/)