前回投稿したAVR用のタスク・スイッチ・ライブラリをARM(M0/M0+/M1)向けにポーティングしてみた。
世界最速のタスク・スイッチ・ライブラリ (Task Switch Library for AVR-G++)
8ビットのAVRではポインターのRWが2命令になってしまうが、16ビット以上のCPUなら1命令で済むので効率が良い。また、AVRと同じくARMは演算のみなら1サイクルで動作するうえにCPUクロックも高いので非常に高速に動作する。ただ、CPUとBUSのクロックの違いによりタイミングによってはBUS経由で実行する命令が待たされてしまうことがあるのが難点だ。理解不足なので間違ってるのかもしれないがメモリやI/Oなどの[BUS経由で実行する命令]~[BUS経由で実行する命令]までの実行命令サイクル数が(一般的には)偶数になるように命令をうまく配置できたら待たされることがなくなるのではないかと思う。但し、割り込みやDMAなども考慮するとどれだけ改善できるのかは定かではないが...
関数呼び出しのオーバーヘッドを含めたスイッチング時間は次のようになった。これ以上の高速化は不可能だろう。たぶん。
65 cycles (ATSAMD21(48MHz)=1.354us,
RP2040(133MHz)=0.489us)
※計算上でのサイクル数。状況によりバス・ウェイトがときどき入るので現実的には2割増程度になると思った方が良い。かも。
Seeeduino XIAO(ATSAMD21)で試験してて、まだRP2040(Raspberry Pi Pico)は試してみてないけどマイクロ秒は楽に切れそうだ。(/・ω・)/
タスクスイッチングについてSVC命令を使うべきという意見も聞こえてきそうだがオーバーヘッドが大きくかなり遅くなってしまうので、そこまで拘る必要がない小規模開発向けとして最速を目指してみた。
それとM0/M0+/M1のみ考えていたのでM4以降で対応されているFPUについては何も考慮してないが特に気にする必要はないのだろうか?とか、M3は動くのかな?とか、M3以降をOS無しで素で使ってる人などいないからそもそも考える必要がないのかな?とか...疑問は尽きない。
あと、インライン・アセンブリの仕様が良くわからなくて色々試していたら、たまたま解決策が見つかったのはいいんだけど、かなりイレギュラーっぽい書き方なのが若干不安。普通に書くと余計な命令が追加されてしまうのが嫌だったのと使われるレジスタを指定したくて★のような記述をしてみたら狙い通りになったのはいいがこういう書き方ってあり?なし?どっち?
1 2 3 4 5 |
★ASM("" :: "r" (&_queue) : "r0", "r1", "r3"); // ldr r2, =_queue 21b8: 4a09 ldr r2, [pc, #36] ; (21e0 <_ZN4Task5yieldEv+0x50>) ... 21e0: 20000284 .word 0x20000284 |
しかし、まだarmアセンブラ2度目の初心者ではあるが、armの命令はなんとなく覚えずらい気がする...って、もしかして、命令の問題じゃなくて脳の老化現象のほうが問題か?(-_-;)
特に(説明が)理解しずらかったのがpush/pop命令。レジスタ・リストを指定することができて便利そうなのだがドキュメントの説明を読んだときに同じレジスタ・リストを指定したpush/popでは元に戻らないのか?と勘違いしてしまった。そんな命令使いものにならんと思っていたところコンパイラの吐き出したコードを眺めていたらある関数が次のようなコードにコンパイルされていた。
1 2 3 |
push {r4-r6, lr} ... pop {r4-r6, pc} |
あれ?これができるということは使えるんじゃん?と思って次のようなコードで試験してみたところ正しくpush/popされていることが確認できた。ググッてみたら私のように勘違いして説明してるサイトも見受けられるので注意したほうが良いだろう。
1 2 3 4 5 6 7 8 9 10 |
mov r0, #0 mov r1, #1 mov r2, #2 mov r3, #3 push {r0-r3} pop {r0-r3} // r0 -> 0 // r1 -> 1 // r2 -> 2 // r3 -> 3 |
【概要】
1 |
bool Task::start(uint16_t size, void (&func)(void *id)) |
新しいタスクを登録する。sizeにはスタック領域のバイト数、funcには実行する関数を指定する。登録したタスクはyield呼び出しにより順番に実行される。malloc()で割り当てられたメモリ・アドレスがfuncの引数として渡され、funcからリターンするとタスク終了する。
1 |
void Task::yield(void) |
タスクを切り替える。実行待ちのタスクがなければ何もせずに戻る。
1 |
bool Task::empty(void); |
実行待ちのタスクがなければtrue、あればfalseを返す。
1 |
void Task::delay(uint32_t ms); |
[Arduinoのみ] 指定時間(ミリ秒)待つ。待っている間は他タスクを実行し続ける。
1 |
void Task::delayMicroseconds(uint32_t us); |
[Arduinoのみ] 指定時間(マイクロ秒)待つ。待っている間は他タスクを実行し続ける。
1 |
void Sync::sleep(void); |
自タスクを起床待ちリストに追加し休止する。
1 |
void Sync::wakeup(bool prepend = false, bool all = false); |
タスクを起床する。prepend=falseのときは実行待ちタスクリストの最後に、prepend=trueのときは実行待ちタスクリスト先頭に追加する。all=falseのときは起床待ちの先頭タスクのみ起床し、all=trueのときは起床待ちの全タスクを起床する。
1 |
bool Sync::empty(void); |
起床待ちのタスクがなければtrue、あればfalseを返す。
【サンプル・スケッチ】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include "task.h" void task1(void *id) { (void)id; while (1) Task::yield(); /* return to terminate task */ } void task2(void *id) { (void)id; while (1) Task::yield(); /* return to terminate task */ } void setup(void) { Task::start(256, task1); Task::start(256, task2); } void loop(void) { Task::yield(); } |
【修正履歴】
2021-10-24
割り込み処理にも対応できるよう割り込み禁止処理を追加。この影響によりスイッチングが1サイクル遅くなってしまった。あと、疑似的に実行優先度を変更できるよう起床するタスクを実行待ちタスクリスト先頭にも追加できるように仕様変更。
2021-10-16
スタック・ポインタは8バイト境界にする必要があるとの記述を見つけたので修正。
2021-10-15
おまけで追加したSync::sleep()がスタックやレジスタを壊すというバグがあったので修正。ついでに関数からリターンしたときにタスク終了するようにしてみたり、スタック領域としてmallocで割り当てたメモリ領域のアドレスを関数引数として渡すように改良。引数はタスク固有の識別情報として利用したり、余裕を持ったスタック領域の割り当てによりタスク固有の情報などが格納できるようになる。
2021-10-14
ipレジスタの保存復旧を削除しスタック構成を最適化した結果さらに高速化できた。これ以上は無理っぽい。
おまけとしてタスク同期のためにタスクを休止/起床するだけのクラス(Sync)も作ってみた。興味のある方はそれを利用してevent/semaphore/mutexなどの同期機能を作ることが出来るはずなので挑戦してみてほしい。なお、mutexの実装に必要なタスク識別情報がこのライブラリには存在しないので重複しないIDを各タスクに割当するなどの工夫が必要となる。
2021-10-13
壊してはいけない一部のレジスタを壊していたので修正。
【ライブラリ】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
/* task.h - Task Switching Library for ARM-G++ (M0/M0+/M1) Copyright (c) 2021 Sasapea's Lab. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef __TASK_H #define __TASK_H #include <stdint.h> #include <stdbool.h> class Task { public: static bool start(uint16_t size, void (&func)(void *id)); static void yield(void) __attribute__ ((naked)); #if defined(ARDUINO) static void delay(uint32_t ms); static void delayMicroseconds(uint32_t us); #endif protected: typedef size_t reg_t; typedef struct context { reg_t _r8; reg_t _r9; reg_t _sl; reg_t _fp; struct context *_next; // r2 reg_t _mask; // r3 reg_t _r4; reg_t _r5; reg_t _r6; reg_t _r7; reg_t _lr; } context_t; typedef struct { context_t *head; context_t *tail; } queue_t; static void invoke(void) __attribute__ ((naked)); static void dispatch(void) __attribute__ ((naked)); static void enqueue(context_t *head, context_t *tail, bool prepend); static bool empty(void); private: static queue_t _queue; }; class Sync : protected Task { public: Sync(void) : _queue({0, 0}) {} virtual ~Sync(void) { wakeup(false, true); } void sleep(void); void wakeup(bool prepend = false, bool all = false); protected: bool empty(void); private: queue_t _queue; }; #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
/* task.cpp - Task Switching Library for ARM-G++ (M0/M0+/M1) Copyright (c) 2021 Sasapea's Lab. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include <stdlib.h> #include "task.h" #if defined(ARDUINO) #include "Arduino.h" #endif Task::queue_t Task::_queue; #define DECLARE_CRITICAL register uint32_t __primask__ #define ENTER_CRITICAL() \ ASM("mrs %0, PRIMASK" : "=r" (__primask__)); \ ASM("cpsid i") #define LEAVE_CRITICAL() \ ASM("msr PRIMASK, %0" :: "r" (__primask__)) #define ASM __asm__ volatile #define PUSHA() \ ASM("mov r2, #0" ::: "r2"); \ ASM("mrs r3, PRIMASK" ::: "r3"); \ ASM("push {r2-r7, lr}"); \ ASM("mov r0, r8" ::: "r0"); \ ASM("mov r1, r9" ::: "r1"); \ ASM("mov r2, sl" ::: "r2"); \ ASM("mov r3, fp" ::: "r3"); \ ASM("push {r0-r3}") #define POPA() \ ASM("ldr r0, [sp, %0]" :: "I" (offsetof(context_t, _mask))); \ ASM("msr PRIMASK, r0"); \ ASM("pop {r0-r3}"); \ ASM("mov r8, r0"); \ ASM("mov r9, r1"); \ ASM("mov sl, r2"); \ ASM("mov fp, r3"); \ ASM("pop {r2-r7, pc}") bool Task::start(uint16_t size, void (&func)(void *id)) { DECLARE_CRITICAL; // // allocate new task stack area // register uint8_t *sv, *sp, *id = (uint8_t *)malloc(size); if (id == 0) return false; sp = (uint8_t *)((reg_t)(id + size) & ~7); // 8 byte alignment // // save task parameter // ASM("mov %0, sp" : "=r" (sv)); ASM("mov sp, %0" :: "r" (sp)); ASM("push {%0}" :: "r" (func)); // r1 ASM("push {%0}" :: "r" (id)); // r0 ASM("mov lr, %0" :: "r" (invoke) : "lr"); // // save context // PUSHA(); ASM("mov %0, sp" : "=r" (sp)); ASM("mov sp, %0" :: "r" (sv)); // // append tail link // ENTER_CRITICAL(); _queue.tail = *(_queue.tail ? &_queue.tail->_next : &_queue.head) = (context_t *)sp; LEAVE_CRITICAL(); return true; } void Task::invoke(void) { // // load task parameter // ASM("pop {r0-r1}"); // // call task method // ASM("mov r4, r0"); ASM("blx r1"); // // free() // ASM("mov r0, r4"); ASM("bl free"); // // dispatch next task // dispatch(); } // // Number of cycles from function call to return // // 65 cycles (ATSAMD21(48MHz)=1.354us, RP2040(133MHz)=0.489us) // void Task::yield(void) // (3) { // // save context (21) // PUSHA(); // // load tail link (7) // ASM("cpsid i"); ASM("" :: "r" (&_queue) : "r0", "r1", "r2"); // ldr r3, =_queue ASM("ldr r2, [r3, %0]" :: "I" (offsetof(queue_t, tail))); ASM("tst r2, r2"); ASM("beq 9f"); // --> no task // // append tail link (5) // ASM("mov r0, sp"); ASM("str r0, [r2, %0]" :: "I" (offsetof(context_t, _next))); ASM("str r0, [r3, %0]" :: "I" (offsetof(queue_t, tail))); // // remove head link (7) // ASM("ldr r2, [r3, %0]" :: "I" (offsetof(queue_t, head))); ASM("ldr r0, [r2, %0]" :: "I" (offsetof(context_t, _next))); ASM("str r0, [r3, %0]" :: "I" (offsetof(queue_t, head))); ASM("mov sp, r2"); // // restore context (22) // ASM("9:"); POPA(); } void Task::dispatch(void) { // // task empty wait // ASM("" :: "r" (&_queue) : "r0", "r1", "r2"); // ldr r3, =_queue ASM("1:"); ASM("cpsid i"); ASM("ldr r2, [r3, %0]" :: "I" (offsetof(queue_t, head))); ASM("tst r2, r2"); ASM("bne 2f"); ASM("cpsie i"); ASM("yield"); // M0/M0+/M1 -> NOP ASM("b 1b"); // // remove head link // ASM("2:"); ASM("mov sp, r2"); ASM("ldr r0, [r2, %0]" :: "I" (offsetof(context_t, _next))); ASM("str r0, [r3, %0]" :: "I" (offsetof(queue_t, head))); ASM("tst r0, r0"); ASM("bne 3f"); ASM("str r0, [r3, %0]" :: "I" (offsetof(queue_t, tail))); // // restore context // ASM("3:"); POPA(); } void Task::enqueue(context_t *head, context_t *tail, bool prepend) { if (prepend) { if ((tail->_next = _queue.head) == nullptr) _queue.tail = tail; _queue.head = head; } else { *(_queue.tail ? &_queue.tail->_next : &_queue.head) = head; (_queue.tail = tail)->_next = nullptr; } } bool Task::empty(void) { return !_queue.head; } #if defined(ARDUINO) void Task::delay(uint32_t ms) { uint32_t t = millis(); while(millis() - t < ms) yield(); } void Task::delayMicroseconds(uint32_t us) { uint32_t t = micros(); while(micros() - t < us) yield(); } #endif void Sync::sleep(void) { // // save context // register uint32_t ret; ASM("ldr %0, =9f + 1" : "=r" (ret)); ASM("mov lr, %0" :: "r" (ret): "lr"); PUSHA(); // // append tail link // register context_t *ctx; ASM("mov %0, sp" : "=r" (ctx)); ASM("cpsid i"); _queue.tail = *(_queue.tail ? &_queue.tail->_next : &_queue.head) = ctx; // // dispatch next task // dispatch(); // // execute prologue code // ASM("9:"); } void Sync::wakeup(bool prepend, bool all) { DECLARE_CRITICAL; ENTER_CRITICAL(); context_t *head = _queue.head; if (head) { if (all) { context_t *tail = _queue.tail; _queue.head = _queue.tail = nullptr; enqueue(head, tail, prepend); } else { if ((_queue.head = head->_next) == nullptr) _queue.tail = nullptr; enqueue(head, head, prepend); } } LEAVE_CRITICAL(); } bool Sync::empty(void) { return !_queue.head; } |