🐭

SpresenseでNuttXのpthreadsを使用しマルチスレッドを学ぶ #1 〜スレッドの起動・終了待ち〜

2022/02/28に公開

これは何?

Spresense SDKはリアルタイムOS NuttXをベースに動いています。NuttXはpthreadsインターフェース[1]を使えます。
私が書籍 並行プログラミング入門の学習を開始したこと・マルチスレッドプログラミング経験不足なためSpresenseでマルチスレッドプログラミングを学習することにしました。

今回のテーマ

今回の学習はpthreadsのスレッド起動・終了待ちのAPIです。

  • pthread_create:スレッドの起動
  • pthread_join:スレッドの終了待ち

確認環境

先日の技術書典で書いた本と同じで次の環境で動作確認をおこないます。

ハードウェア

  • Spresenseメインボード
  • Spresense拡張ボード
  • APS学習ボード

ソフトウェア

WebマニュアルではSpresense SDKかArduino IDEが開発手法として紹介されています。
今回はSpresense SDKで開発します。

テストコード説明

今回のテストコードはGitHub[2]におきました。
ディレクトリ・実行ファイル名称はpthread_first_test_appです。

スレッドの起動

スレッドの起動はpthread_createを使います。
APS学習ボードUSER_LED1、USER_LED2のそれぞれのLEDにアクセスするように2つのスレッドを生成しています。
スレッドの引数には操作対象LEDを示す値(USER_LED1 = 0, USER_LED2 = 1)を設定します。

pthread_first_test_app_main.c main関数 // 該当部分を抽出し記載
#define THREAD_NUM (2)  // スレッドの数

int main(int argc, FAR char *argv[])
{
  pthread_t thread[THREAD_NUM];
  int ret[THREAD_NUM];
  int i;

  for (i = 0; i < THREAD_NUM; i++) {
    if(pthread_create(&thread[i], NULL, blink_led, (void*)i) != 0) {
      printf("Error; pthread_create[%d].\n", i);
    }
  }  

}

スレッドの動作

blink_led関数をスレッドで動かします。
スレッドは1秒もしくは2秒間隔(スレッドの引数で決まります)でLEDを点滅します。
APS学習ボードのタクトスイッチ(SW1, SW2)押下を検出するとスレッドを終了します。
スレッドは引数と同じ値をリターンします。

pthread_first_test_app_main.c blink_led関数
// APS学習ボードピンアサイン
#define SWITCH_1    (39)  // 拡張ボード JP13-1 SPI3_CS1_X Spresense SDKピン番号39
#define SWITCH_2    (29)  // 拡張ボード JP13-4 SPI2_MOSI Spresense SDKピン番号29
#define USER_LED_1  (46)  // 拡張ボード JP13-2 PIN_PWM0 Spresense SDKピン番号46
#define USER_LED_2  (47)  // 拡張ボード JP13-3 PIN_PWM1 Spresense SDKピン番号47

const int aps_board_led_pin[2] = {
  USER_LED_1,
  USER_LED_2
};

const int aps_board_switch_pin[2] = {
  SWITCH_1,
  SWITCH_2
};

void* blink_led(void* arg) {
  uint32_t led_index = (uint32_t)arg;
  uint32_t switch_index = (uint32_t)arg;
  unsigned int sleep_sec = 1 + (uint32_t)arg; // sleepする秒数
  int led_value = 1;

  volatile int switch_status = board_gpio_read(aps_board_switch_pin[switch_index]);

  printf("start blink_led[%ld] thread ID = %d switch_status = %d.\n", led_index, pthread_self(), switch_status);

  while (switch_status == 1) {  // スイッチ押下されたらループから抜ける
    board_gpio_write(aps_board_led_pin[led_index], led_value);
    sleep(sleep_sec);
    led_value ^= 1;
    switch_status = board_gpio_read(aps_board_switch_pin[switch_index]);
    printf("blink_led[%ld] thread ID = %d led_value = %d switch_status = %d.\n", led_index, pthread_self(), led_value, switch_status);
  }

  printf("end blink_led[%ld] thread ID = %d.\n", led_index, pthread_self());

  return (void*)led_index;
}

スレッド終了待ち

スレッドの終了待ちはpthread_joinでおこないます。
スレッドの戻り値をpthread_joinの第2引数に格納します。

pthread_first_test_app_main.c main関数 // 該当部分を抽出し記載
int main(int argc, FAR char *argv[])
{
  pthread_t thread[THREAD_NUM];
  int ret[THREAD_NUM];
  int i;


  for (i = 0; i < THREAD_NUM; i++){
    if (pthread_join(thread[i], (void**)&ret[i]) != 0) {
      printf("Error; pthread_join[%d].\n", i);
    }
    printf("exit thread[%d] thread ID = %d return value = %d\n", i, pthread_self(), ret[i]);
  }

  printf("Bye.\n");

  return 0;
}

動作確認結果

ソフトウェアを書き込み、シェル(nsh)に接続後にアプリケーションpthread_first_test_appを実行します。
次のメッセージが出力されます。

nsh> pthread_first_test_app
start blink_led[0] thread ID = 9 switch_status = 1.
start blink_led[1] thread ID = 10 switch_status = 1.
blink_led[0] thread ID = 9 led_value = 0 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 0 switch_status = 1.
blink_led[0] thread ID = 9 led_value = 1 switch_status = 1.
blink_led[0] thread ID = 9 led_value = 0 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 1 switch_status = 1.
blink_led[0] thread ID = 9 led_value = 1 switch_status = 1.
blink_led[0] thread ID = 9 led_value = 0 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 0 switch_status = 1.
blink_led[0] thread ID = 9 led_value = 1 switch_status = 1.
blink_led[0] thread ID = 9 led_value = 0 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 1 switch_status = 1.
end blink_led[0] thread ID = 9.
exit thread[0] thread ID = 8 return value = 0
blink_led[1] thread ID = 10 led_value = 1 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 0 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 1 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 0 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 1 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 0 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 1 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 0 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 1 switch_status = 1.
blink_led[1] thread ID = 10 led_value = 0 switch_status = 0.
end blink_led[1] thread ID = 10.
exit thread[1] thread ID = 8 return value = 1
Bye.

スレッドの生成

スレッド0(blink_led[0] thread ID = 9)は1秒間隔、スレッド1(blink_led[1] thread ID = 10)は2秒間隔でLEDを点滅・printfします。
スレッド生成時に指定した引数により期待する時間sleepしていることが確認できました。

スレッドの終了待ち

APS学習ボードのタクトスイッチ(SW1・SW2)を押下するとスレッドが終了します。SW1押下でスレッド0、SW2押下でスレッド1が終了します。
スイッチ押下後にmain関数のスレッドが終了した旨のprintfが表示され、pthread_join関数によるスレッド終了待ちが正常に動作していることが確認できました。また、スレッドの戻り値も期待値とおりであることが確認できました(スレッド0は0、スレッド1は1)。

動作確認方法

※こちらのリンク開発ツールのセットアップは完了している前提とします。

ソースコードの配置

GitHub[2:1]からCode -> Download ZIPを選択し、ZIPファイルをダウンロードします。
ダウンロードしたファイルを解凍し、Spresense開発環境をインストールしたディレクトリに置きます。Macの場合だと次のディレクトリになります。

  • /Users/ユーザ名/spresense/

作業ディレクトリへ移動

【spresense/sdk】ディレクトリに移動します。

インストールしたツールを使用可能にするコマンド実行

次のコマンドを実行します。

source ~/spresenseenv/setup

コンフィグレーション

コンフィグレーションは次のコマンドを実行します。

tools/config.py default

make

makeしバイナリイメージ(nuttx.spk)を作成します。
-jオプションでパラレル(並列)ビルドが可能です[3]

make -j

書き込み

次のコマンドでシリアルポート名を確認します。

$ ls /dev/cu.usb*
/dev/cu.usbserial-14140

書き込みコマンドにシリアルポート名、makeで作成されたバイナリイメージ名を指定し実行します。

$ tools/flash.sh -c /dev/cu.usbserial-14140 nuttx.spk
>>> Install files ...
install -b 115200
Install nuttx.spk
|0%-----------------------------50%------------------------------100%|
######################################################################

156272 bytes loaded.
Package validation is OK.
Saving package to "nuttx"
updater# sync
updater# Restarting the board ...
reboot

シェルへ接続

書き込みが終わったらシェルに接続します。
シリアルターミナルからボーレート115200bpsで接続します。
minicomの場合は次のコマンドを実行します。

$ minicom -D /dev/cu.usbserial-14140 -b 115200

動作確認

シェルに接続すると次の表示になります。

Welcome to minicom 2.8

OPTIONS: 
Compiled on Jan  4 2021, 00:04:46.
Port /dev/cu.usbserial-14140, 12:02:52

Press Meta-Z for help on special keys


NuttShell (NSH) NuttX-10.1.0
nsh> 

helpコマンドを実行しmakeしたアプリケーションが登録されているか確認します。

nsh> help
help usage:  help [-v] [<cmd>]

  .          cmp        false      ls         nslookup   sleep      usleep     
  [          dirname    free       mkdir      poweroff   source     xd         
  ?          date       help       mkfatfs    ps         test       
  basename   dd         hexdump    mkfifo     pwd        time       
  break      df         ifconfig   mkrd       reboot     true       
  cat        echo       ifdown     mksmartfs  rm         uname      
  cd         exec       ifup       mount      rmdir      umount     
  cp         exit       kill       mv         set        unset      

Builtin Apps:
  nsh                     pthread_first_test_app  sh                      

【Builtin Apps:】に【pthread_first_test_app】が登録されています。
シェルから【pthread_first_test_app】をタイプするとアプリケーションを実行できます。

今回の感想

SpresenseのRTOS NuttXのpthreadsインターフェースでマルチスレッドが確認できました。PCと同じようにpthreadsを使えて次のことを感じました。

  • PCで動作確認したコードをSpresenseに割と簡単に移植できそう
  • 動作実績のあるpthreadsのコードをSpresenseに割と簡単に移植できそう

NuttX pthreadsインターフェース[1:1]に記載があるように各スレッドで同期・排他制御の仕組みも提供されています。

  • pthread Mutexes.(ミューテックス)
  • Condition Variables.(条件変数)
  • Barriers.(バリア)

Linuxに比べて軽量なNuttX、ラズパイに比べて低速なSpresenseでpthreadsインターフェースを使えることは興味深いと感じました[4]
今後もSpresenseでマルチスレッドプログラミングを確認していきたいと考えています。

脚注
  1. NuttX Pthread Interfaces ↩︎ ↩︎

  2. GitHubリンク ↩︎ ↩︎

  3. ビルド手順 ↩︎

  4. こちらのリンクにArduino Uno、Spresense、Raspberry Pi 3Bのハードウェアを比較したページがあります。適材適所でハードウェアを選択したいですね ↩︎

Discussion