Wio TerminalのFreeRTOSの構造
Wio TerminalのFreeRTOSの構造
この記事は Wio Terminal Advent Calendar の18日目の記事です。
Wio TerminalのFreeRTOSは、現時点で割り込み周りの扱いに問題があるので、原因と対策について説明します。
Wio TerminalのFreeRTOS
Wio TerminalのFreeRTOSは、 Seeed_Arduino_FreeRTOS
ライブラリとして分離されています。
元々は Seeed_Arduino_FreeRTOS
は Wio Terminal向けのArduino core には含まれておらず、個別にzipファイルをダウンロードしてインストールする必要がありました。
ただし、現在では Arduino core に 含まれている ため、個別にインストールする必要はありません。
FreeRTOSの使い方
元々 Seeed_Arduino_FreeRTOS
は、Wio TerminalのWi-Fi/BLE通信用のライブラリ Seeed_Arduino_atUnified
や Seeed_Arduino_rpcUnified
で使うことを目的として作られています。
アプリケーション自体の処理と平行して効率よく無線通信処理を行うためには、FreeRTOSなどのRTOSが合った方が便利だからです。
上記の Seeed_Arduino_atUnified
や Seeed_Arduino_rpcUnified
を使う場合は、これらのライブラリ側でFreeRTOSの初期化処理を行うので、アプリケーションの開発者は特に何もする必要はなく、そのままFreeRTOSのAPIを使えます。
逆に、Seeed_Arduino_atUnified
や Seeed_Arduino_rpcUnified
を使わない場合は、アプリケーション開発者自身でFreeRTOSの初期化処理を記述する必要があります。
このためのサンプルが Seeed_Arduino_FreeRTOS
の Basic_RTOS_Example
サンプル・スケッチです。
FreeRTOSの初期化処理
FreeRTOSのスケジューラーを実行するだけであれば、 vTaskStartScheduler()
を呼び出すだけで終わりですが、Arduino環境で使うためには、その前に loop()
関数を実行するためのタスクを起動しないといけません。
元々はFreeRTOSのIdleタスク実行時に loop()
が呼ばれる実装だったようですが、ソースコード上のコメントにある通り、この方法だと setup()
が呼ばれる前に loop()
が呼ばれてしまうバグがあったようで、現在ではコメントアウトされています。
よって、vTaskStartScheduler
を呼び出す前に、 xTaskCreate
を呼び出して loop()
を実行するタスクを作る必要があります。
xTaskCreate
に渡すタスクの処理の関数は、void*
型の引き数を一つとる、 void (*)(void*)
型の関数ポインタであり、タスクが生存している限り関数から抜けないようになっている必要があります。
このため、 loop()
を呼び出し続けるラッパーとして loopTask
関数を定義して xTaskCreate
に渡しています。
xTaskCreate
の第3引き数はタスクのスタック・サイズを指定します。 loop()
で実行する処理で必要十分な量を割り当てないと、スタック破壊による割と分かりづらいバグとなりますので注意が必要です。
今回はとりあえず 2048
バイトにしています。
static TaskHandle_t task = nullptr;
void loop();
static void loopTask(void*)
{
for(;;) {
loop();
vTaskDelay(0);
}
}
void setup() {
xTaskCreate(loopTask, "loop", 2048, nullptr, 0, &task);
vTaskStartScheduler();
}
void loop() {
// ...
}
FreeRTOSで割り込みを使う
RTOSを使っている場合、割り込み処理中で必要最低限の処理を行い、残りの処理はキューに入れて高優先度のタスクで実行することが多いと思います。
この場合、FreeRTOSのAPIを割り込みハンドラから呼び出すことになりますが、FreeRTOSには割り込みハンドラから呼び出す専用のAPIが用意されています。
これらのAPIは関数名の末尾に FromISR
が付いています。
例えばキューに値を入れるAPIは xQueueSend
ですが、ISRから呼び出す場合は xQueueSendFromISR
となります。
ただし、FromISR
が付いているAPIであっても、一定以上の優先度の割り込みハンドラからは呼び出せないようになっています。
このときの優先度の最大値は configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
として、 FreeRTOSConfig.h
に定義されています。執筆時点の Seeed_Arduino_FreeRTOS
での実装では 5
です。
よって、割り込みを使う場合は、configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
以上の値を指定して
NVIC_SetPriority
を呼び出して、割り込みの優先度を下げておく必要があります。 (※Cortex-Mの割り込み優先度は大きい値ほど優先度が低い)
以下の例では、TC3の割り込み優先度をFreeRTOS APIを呼び出せる優先度の最大値に設定します。
NVIC_SetPriority(TC3_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY);
現時点での問題
前述の通りFreeRTOSを使い、割り込みハンドラからFreeRTOS APIを呼び出すアプリケーションを作成すると、アプリケーションがクラッシュします。
実際にどこでクラッシュしているのかデバッガを使って追いかけると、FreeRTOS内部のアサーションに引っかかっていることが分かります。
このアサーションは、APIを呼び出した割り込みハンドラの優先度が、前述の優先度より低いかどうか (優先度の数値が configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
以上かどうか) をチェックしています。
よって、アサーションに引っかかったということは、割り込みの優先度が configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
より高くなっているということです。
しかし、前述の通り、NVIC_SetPriority(..., configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY)
を呼び出して、割り込みの優先度を configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
に設定しているため、このアサーションに引っかかるのは、何かおかしいということになります。
configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
このアサーションの式のうち、 ucCurrentPriority
には、APIを呼び出した割り込みの優先度の値が入っています。また、 ucMaxSysCallPriority
はAPIを呼び出せる割り込み優先度の最大値が入っています。
デバッガでそれぞれの値を見ると、 ucCurrentPriority = 64
、 ucMaxSysCallPriority = 126
となっています。
ここで、WioTerminalで使われているSoC ATSAMD51P19A
のCPUコア Cortex-M4F
の割り込み優先度レジスタ IPR
の仕様を確認すると、8bitのうちCPUの構成に応じた上位数ビットのみが実際に優先度として使用できることが分かります。(詳しくは Cortex-M4 Devices Generic User Guide の p.4-7 を参照)
IPR
の使用可能なビット数は、Arduino coreに付属しているツールセットに含まれる CMSIS-Atmel
パッケージ内のヘッダ atsamd51p19a.h
に __NVIC_BITS
として定義されています。現在のバージョンでは #define __NVIC_BITS 2
として定義されています。
NVIC_SetPriority
は内部的には __NVIC_BITS
を用いて IPR
レジスタに
priority << (8u - __NVIC_PRIO_BITS)
の値を計算して設定します。これにより、 上位 __NVIC_PRIO_BITS
に priority
の値が設定されます。
デバッガで確認した ucCurrentPriority
や ucMaxSysCallPriority
の値は、同様に 8 - __NVIC_PRIO_BITS
つまり、6ビット左シフトした値になっています。
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << 6 == 320
なので、320となるはずですが、優先度レジスタは8bit幅のため、実際には 320 & 255 == 64
となっています。10進数の 5
を表現するためには、最低3ビット必要なのに、 __NVIC_PRIO_BITS
が2となっているため、上位1ビットが無くなってしまっています。
一方、FreeRTOS側で定義している定数 ucMaxSysCallPriority
の値は 126 == 5 << (8 - 3)
となっています。
これは、Seeed_Arduino_FreeRTOS
の FreeRTOSConfig.h
の内部で __NVIC_PRIO_BITS
を #undef
して、本来 __NVIC_PRIO_BITS
と同じ値に定義されるはずの configPRIO_BITS
の値を 3
として定義しているためです。
/* Cortex-M specific definitions. */
#undef __NVIC_PRIO_BITS
#ifdef __NVIC_PRIO_BITS
/* __BVIC_PRIO_BITS will be specified when CMSIS is being used. */
#define configPRIO_BITS __NVIC_PRIO_BITS
#else
#define configPRIO_BITS 3 /* 8 priority levels */
#endif
では、なぜこのような定義をしているのかというと、このように定義しなければ、別のアサーションに引っかかるためです。
この問題の根本原因は、CMSIS-Atmel
の __NVIC_PRIO_BITS
の定義が間違っていることです。
ATSAMD51P19Aのデータシート (DS60001507F) p.63 によると、ATSAMD51P19Aの割り込みは8つの優先度レベルがあるとの記載があるので、正しくは __NVIC_PRIO_BITS
は 3
のはずです。
実際、新しいバージョンの CMSIS-Atmel
では __NVIC_PRIO_BITS
は 3
として定義されています。
現状、アプリケーション開発者として取れる対策は、優先度が上位2ビットのみ使用可能である前提で NVIC_Priority
を呼び出すことです。たとえば、 NVIC_Priority(..., 3)
とすれば、現時点で設定可能な優先度を指定できますが、実質的にこの優先度しか使えません。
より柔軟な優先度設定を使えるようにするためには、Seeed公式によるArduino core内の __NVIC_PRIO_BITS
の定義の修正を待つ必要があります。
割り込み中のFreeRTOS API呼び出しの動作確認
上記の対策にてFreeRTOS APIが割り込みハンドラから呼び出せることを確認するサンプルは次の通りです。
(GitHubのリポジトリにも置いてあります)
#include <FreeRTOS.h>
#include <queue.h>
#include <cstdint>
#include <sam.h>
#include "SAMD51_TC.h"
static TaskHandle_t task = nullptr;
static QueueHandle_t queue = nullptr;
// ISR handler called when the TC3 period has elapsed.
static void isrCallback()
{
// Post counter value to the loop task.
static std::uint8_t value = 0;
xQueueSendFromISR(queue, &value, nullptr);
value += 1;
}
void loop();
static void loopTask(void*)
{
for(;;) {
loop();
vTaskDelay(0);
}
}
void setup() {
queue = xQueueCreate( 32, 1 );
configASSERT(queue);
TimerTC3.initialize();
TimerTC3.setPriority(3);
// Uncomment if you want to reproduce the Seeed_Arduino_FreeRTOS priority issue.
//TimerTC3.setPriority(configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY);
TimerTC3.attachInterrupt(isrCallback);
Serial.begin(115200);
Serial.println("start");
xTaskCreate(loopTask, "loop", 2048, nullptr, 0, &task);
vTaskStartScheduler();
}
void loop() {
std::uint8_t value;
// Receive the value sent from the TC3 ISR.
xQueueReceive(queue, &value, portMAX_DELAY);
Serial.println(value);
}
タイマー TC3
を使って1秒間隔で割り込みを発生させ、割り込みハンドラから isrCallback
を呼び出しています。
タイマー周りの初期化処理は SAMD51_TC.h
を使っています。 TimerTC3.attachInterrupt
内部で TimerTC3.setPriority()
に指定した値をそのまま NVIC_SetPriority
に渡して呼び出しています。
元のコードのまま実行すると、Wio TerminalのUSBシリアル経由で0~255までの値が1秒ごとに出力されます。
コード中の
// TimerTC3.setPriority(configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY);
をコメントアウトすると、優先度として configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
を指定することになるので、前述の通りアサーションに引っかかって動作しなくなります。
実際にアサーションに引っかかったかどうかはデバッガを接続しないと確認しにくいですが、デバッガをお持ちの方は試してみてください。
おわりに
以上で、プライオリティに関するワークアラウンドを適用すれば、とりあえずWio TerminalでFreeRTOSを使った割り込み処理ができることがわかると思います。
修正方法自体もわかっていますが、修正できるのはSeeedの中の人だけなので、一応報告をしておこうかと思います。
2020/12/22 追記: 次のリリース時に修正が適用されるようです。
Discussion