2022年5月29日日曜日

FreeRTOSのマルチタスクが固まる場合はstack sizeを増やすと解決することがある


背景

stm32l5でFreeRTOSのマルチタスクでlcdを扱うと固まる現象に遭遇しましたが、表題の通り下記のようにstack sizeを増やすと解決しました。
xTaskCreate(ThreadLcd, NULL, configMINIMAL_STACK_SIZE * 2, NULL, 2, NULL);
参考: frFifoDataLogger.ino#L163

解決方法が分かれば何ということは無いのですが、10時間ほど時間を取られたので、備忘録を兼ねて関連情報を記事に残します。

使ったもの

  • stm32l5開発ボード
    nucleo-l552ze-qを利用しました。
    ピン情報などはデータシートを見てください。
    STM32L552xx datasheet (PDF)
    nucleo144 mb1361 manual (PDF)
  • LCD
    320x240 3.2inchのものを利用しました。
  • ブレッドボード + ジャンパワイヤ
    LCDと開発ボードの接続に利用しました。
  • arduinoの開発環境
    PlatformIOを利用しました。
  • FreeRTOSとLCDのライブラリ
    下記のライブラリを利用しました。

    STM32duino FreeRTOS
    Adafruit ILI9341
    Adafruit GFX Library
    Adafruit BusIO

    PlatformIOの場合は、platformio.iniのlib_depsに記述すれば使えます。
    Arduino IDEを使う場合は、それぞれインストールしてください。
    lib_deps =
    STM32duino FreeRTOS
    Adafruit ILI9341
    Adafruit GFX Library
    Adafruit BusIO

回路構成

STM32L5のSPIでLCDを制御する回路です。
LCDstm32l5
3V33V3
GNDGND
CSD10 / PD14
RESETD8 / PF12
DCD9 / PD15
MOSID11 / PA7
SCKD13 / PA5
LED3V3
MISOD12 / PA6

組むとこうなりました。


動くプログラム

問題なく動くプロジェクトのplatform.iniとmain.cppです。
FreeRTOSのblinkプログラムを参考にしつつ、LCD機能を追加しました。
(参考にしたのが軽めのタスクでstackサイズが最小だったために、LCDのタスクを書くとstackが足りなくなりました。)

LED2(青)の点滅とLCDの更新を別のスレッドで行います。
platformio.ini
[env:nucleo_l552ze_q]
platform = ststm32
board = nucleo_l552ze_q
framework = arduino
monitor_speed = 115200
lib_deps =
STM32duino FreeRTOS
Adafruit ILI9341
Adafruit GFX Library
Adafruit BusIO

src/main.cpp
#include <Adafruit_ILI9341.h>
#include <Arduino.h>
#include <STM32FreeRTOS.h>
#include <Wire.h>

#define PIN_SPI_CS PD14
#define PIN_SPI_DC PD15
#define PIN_SPI_RST PF12
Adafruit_ILI9341 lcd(PIN_SPI_CS, PIN_SPI_DC, PIN_SPI_RST);
#define PIN_LCD_LED

#define PIN_LED LED2

static void ThreadLedBlink(void* arg) {
UNUSED(arg);
pinMode(PIN_LED, OUTPUT);

while (true) {
digitalWrite(PIN_LED, HIGH);
// Sleep for 200 milliseconds.
vTaskDelay((100L * configTICK_RATE_HZ) / 1000L);
digitalWrite(PIN_LED, LOW);
// Sleep for 200 milliseconds.
vTaskDelay((100L * configTICK_RATE_HZ) / 1000L);
}
}

static void ThreadLcd(void* arg) {
UNUSED(arg);
lcd.begin();
lcd.fillScreen(ILI9341_BLACK);
lcd.setTextColor(ILI9341_WHITE, ILI9341_BLACK);

while (true) {
auto now = millis();
String str = "hello " + String(now);
lcd.setCursor(0, 0);
lcd.println(str);
lcd.flush();
// Sleep for 10 milliseconds.
vTaskDelay((10L * configTICK_RATE_HZ) / 1000L);
}
}

void setup() {
Serial.begin(115200);
auto s1 = xTaskCreate(ThreadLedBlink, NULL, configMINIMAL_STACK_SIZE, NULL, 2,
NULL);
auto s2 =
xTaskCreate(ThreadLcd, NULL, configMINIMAL_STACK_SIZE * 2, NULL, 2, NULL);

if (s1 != pdPASS || s2 != pdPASS) {
while (true) {
Serial.println(F("Creation problem"));
delay(2000);
}
}

vTaskStartScheduler();
Serial.println("Insufficient memory");
while (true) delay(1);
}

void loop() {
// Not used.
}

書き込むとLED2(青)の点滅とLCDの更新が期待通りに行われると思います。


動かないプログラム

先ほどのプログラムのタスク登録時のstackを最小にすると、フリーズして緑色のLEDが点滅します。

動く記述
  auto s2 =
xTaskCreate(ThreadLcd, NULL, configMINIMAL_STACK_SIZE * 2, NULL, 2, NULL);

動かない記述
  auto s2 =
xTaskCreate(ThreadLcd, NULL, configMINIMAL_STACK_SIZE, NULL, 2, NULL);

動かない記述でプログラムを書き込んで動作させると、下記の状態になります。
  • LCDが更新されない
    下記の画像ではhelloのhの縦棒だけ描画して止まっています。
  • LED2(青)が点滅しない
    LCDのタスクが止まると、別のタスクも止まるようです。
  • LED1(青の横の緑)が4回点滅と休止を繰り返す
    LED1で内部異常が通知されるようです。


LED1を光らせるプログラムを書いてないのにLED1が何故か光っている場合は、stackが不足して動作停止しているかもしれません。

stackとは?タスク内の関数の深さや変数の数などで必要量が異なるメモリ領域

FreeRTOSのxTaskCreateの説明書きで紹介されていたどのくらいのStackが必要?という解説によると、下記の要素によって必要な量が異なるタスク用に確保するメモリ領域のようです。
  • 関数呼び出し階層の深さ
  • 関数内の変数の数
  • 関数の引数の数
  • プロセッサの回路
  • コンパイラ
  • 割り込み処理実行に必要な領域

uxTaskGetStackHighWaterMarkという関数でstackの残量が分かるので、今回のタスクの残量を見てみます。
参考: FreeRTOS理解その10(スタックサイズ)

タスク紐付け用のTaskHandle_tと残量表示用のタスクを追加し、それに合わせてsetupを書き換えました。
TaskHandle_t xTaskLcd, xTaskLed, xTaskPrint;

static void ThreadPrintWaterMark(void* arg) {
UNUSED(arg);

while (1) {
char buf[4];
Serial.println("minimal stack " + String(configMINIMAL_STACK_SIZE));
Serial.println("left stacks");
sprintf(buf, "%d", uxTaskGetStackHighWaterMark(xTaskLcd));
Serial.print("taskLcd ");
Serial.println(buf);
sprintf(buf, "%d", uxTaskGetStackHighWaterMark(xTaskLed));
Serial.print("taskLed ");
Serial.println(buf);
sprintf(buf, "%d", uxTaskGetStackHighWaterMark(xTaskPrint));
Serial.print("taskPrint ");
Serial.println(buf);
Serial.println();
vTaskDelay((1000L * configTICK_RATE_HZ) / 1000L);
}
}

void setup() {
Serial.begin(115200);
auto s1 = xTaskCreate(ThreadLedBlink, NULL, configMINIMAL_STACK_SIZE, NULL, 2,
&xTaskLed);
auto s2 = xTaskCreate(ThreadLcd, NULL, configMINIMAL_STACK_SIZE * 2, NULL, 2,
&xTaskLcd);
auto s3 = xTaskCreate(ThreadPrintWaterMark, NULL,
configMINIMAL_STACK_SIZE * 2, NULL, 2, &xTaskPrint);

if (s1 != pdPASS || s2 != pdPASS || s3 != pdPASS) {
while (1) {
Serial.println(F("Creation problem"));
delay(2000);
}
}

vTaskStartScheduler();
Serial.println("Insufficient memory");
while (1) delay(1);
}

上記の変更を行ったプログラムを実行してシリアルモニタで出力を確認すると、下記の情報を得られました。
minimal stack 128
left stacks
taskLcd 98
taskLed 106
taskPrint 146

taskLcdには minimal stack x2のstackを割り当てているので、128*2-98 = 158のstackを利用していると分かります。minimal stackではstackが不足していたため動作を停止したようです。
taskLedは128-106=22のstackを利用していました。

利用しているstack数と余り
taskLcd: 158 余り98
taskLed: 22 余り106

終わり

FreeRTOSのタスクに割り当てるstack数を増やしたことで、固まる処理が固まらずに動くようになりました。
どの程度余裕があれば良いかは自分にはまだ分かりませんが、stackを減らすのはメモリが足りなくなってからで良い気がするので、開発時は最小スタックの8倍ほどを割り当てて開発しようと思いました。

参考

nucleo-l552ze-q
STM32L552xx datasheet (PDF)
nucleo144 mb1361 manual (PDF)
FreeRTOSのblinkプログラム
xTaskCreate
どのくらいのStackが必要?(英語)
FreeRTOS理解その10(スタックサイズ)

0 件のコメント :