2週目
1週目で学んだことを活かして、組み合わせてみましょう。
注釈
一部古い記述がふくまれています。Arduino IDE → arduino-cli におきかえてください。
複数人で書いたプログラムを統合する
Arduino IDEでは、プログラムを複数のファイル(たとえば、main.ino / sub.ino / hoge.cpp / hoge.h ) に分割して記述することができます。複数ファイルに分割することで、関数定義を機能別にまとめることができ、管理しやすくなります。
Arduino IDEでは、プログラムを構成する、複数のファイル(スケッチブック)を、1つのフォルダに入れて管理します。(スケッチブックのことを、他のIDEでは「プロジェクト」と呼ぶ場合もある)
スケッチブックに別のファイル(hogehoge.ino) を追加するには、右上にあるシリアルモニタをひらくアイコンの、下の「▼」ボタンからメニューをひらき、「新規タブ」を選択し、ファイル名(拡張子.ino をのぞいた、hogehogeの部分のみ)を入力します。
スケッチブックをコピーしたいときは、内包するフォルダごとコピーしてください。その際、フォルダ名と、メインのソースコードファイル名(拡張子以外の部分)は、一致している必要があります。
複数のファイルを置いたときの挙動について:
***.ino
ファイルの内容は、単純にメインのタブ(フォルダ名と同じinoファイル)にマージされます。***.cpp
や***.c
という拡張子でファイルを作成した場合は、***.h
を作成する必要があります。参考:Properly using separate tabs with Arduino IDEGit を利用すると、複数人で作業したファイルを統合しやすいです。
タスク
複数の機能を1つの loop()
にまとめようとすると、プログラムが複雑になります。タスクを用いると、 loop()
に相当する関数を複数定義し、並列に動作させることができます。
リスト 26 に、タスクを利用する例を示します。3つの異なるタスクを作成し、それぞれの関数内部で setup()
と loop()
に相当する処理を記述しています。
引数の詳細については、非公式日本語リファレンス を参照してください。
ここの例では、1つのファイルに記述していますが、タスクごとに別のファイルにすることもできます。 以下の例ではタスク生成時に、タスクハンドル TaskHandle_t
を設定しています。タスクハンドルは、タスクの一時停止(サスペンド)や、再開(レジューム)、削除のときにタスクを特定するために必要となります。
1#include <M5Unified.h>
2
3int interval_msec[] = { 333, 1000, 2000 }; // Led, Lcd, Beep
4TaskHandle_t tH[3];
5
6void ledTask(void *pvParam) {
7 /** setup をここに書く **/
8 portTickType lastTime;
9 int PIN = 10;
10 pinMode(PIN, OUTPUT); // PINのモード設定
11 int highOrLow = 0;
12 for (;;) {
13 /** loop をここに書く **/
14 lastTime = xTaskGetTickCount();
15 vTaskDelayUntil(&lastTime, interval_msec[0] ); // 第2引数に、実行間隔ミリ秒を指定
16 digitalWrite(PIN, highOrLow); // HIGH = 1, LOW = 0
17 highOrLow = 1 - highOrLow; // HIGH <=> LOW を切り替える
18 }
19}
20
21void lcdTask(void *pvParam) {
22 /** setup をここに書く **/
23 portTickType lastTime;
24 M5.Display.setRotation(0);
25 M5.Display.fillScreen(GREEN);
26 M5.Display.setTextColor(WHITE, OLIVE);
27 M5.Display.setTextSize(2);
28 M5.Display.setCursor(0, 0);
29 int count = 0;
30 for (;;) {
31 /** loop をここに書く **/
32 M5.Display.printf("count=%d\n", count);
33 count++;
34 lastTime = xTaskGetTickCount();
35 vTaskDelayUntil(&lastTime, interval_msec[1] ); // 第2引数に、実行間隔ミリ秒を指定
36 if (count % 10 == 0) {
37 M5.Display.fillScreen(GREEN);
38 M5.Display.setCursor(0, 0);
39 }
40 }
41}
42
43void beepTask(void *pvParam) {
44 /** setup をここに書く **/
45 portTickType lastTime;
46 int f[8] = { 262, 294, 330, 349, 392, 440, 494, 524 };
47 int note = 0;
48 for (;;) {
49 /** loop をここに書く **/
50 lastTime = xTaskGetTickCount(); // ここでの時刻を変数に保存
51 tone(GPIO_NUM_2, f[note], 500); M5.Display.setBrightness(255);
52 M5.delay(500); //0.5秒鳴らす
53 note = (note+1)%8;
54 vTaskDelayUntil(&lastTime, interval_msec[2] );
55 // 途中の処理やdelayは含まず、「保存」時刻の2秒後まで待つ。
56 }
57}
58
59void setup() {
60 auto cfg = M5.config();
61 cfg.serial_baudrate = 115200;
62 M5.begin(cfg);
63
64 xTaskCreatePinnedToCore(ledTask , "LedT", 4096, NULL, 1, &tH[0], 1/*<= CoreNo.*/ );
65 xTaskCreatePinnedToCore(lcdTask , "LcdT", 4096, NULL, 1, &tH[1], 1/*<= CoreNo.*/ );
66 xTaskCreatePinnedToCore(beepTask,"BeepT", 4096, NULL, 1, &tH[2], 0/*<= CoreNo.*/ );
67}
68
69void loop() {
70 delay(10);
71}
注釈
タスク生成時に、引数を渡すこともできます。リスト 27 に、引数を渡す例を示します。(次で述べるミューテックスを使って、排他制御もしています。)
1#include <M5Unified.h>
2
3xSemaphoreHandle mutex; //ミューテックス(排他制御用)
4
5void withArgTask(void *pvParam) {
6 int num = *(int*) pvParam; // 引数は、グローバル変数のアドレスをポインタで渡す
7 BaseType_t mStatus;
8 char* tsknm = pcTaskGetTaskName(NULL); //自タスク名を取得するならNULL、他タスク名を取得するならタスクハンドルを引数に指定する。
9 while (1) {
10 mStatus = xSemaphoreTake(mutex, 500); // ミューテックスを取得
11 if (mStatus == pdPASS) {
12 Serial.println("----");
13 Serial.printf("[%s] ", tsknm );
14 M5.Display.println("----");
15 M5.Display.printf("[%s] ", tsknm );
16 delay(500);
17 for (int i = 1; i < 6; i++) {
18 Serial.printf("%d ", i * num);
19 M5.Display.printf("%d ", i * num);
20 delay(300);
21 }
22 delay(300);
23 Serial.printf("done \n");
24 M5.Display.printf("done \n");
25 delay(300);
26 xSemaphoreGive(mutex); // ミューテックスを解放
27 vTaskDelete(NULL); // 自タスクを削除する
28 } else {
29 delay(random(10,100)); //ミューテックスがとれなかったらランダムに待つ
30 }
31 }
32}
33
34int arg[] = {2, 3, 5, 7, 11} ;
35char tskname[5];
36
37void setup() {
38 auto cfg = M5.config();
39 cfg.serial_baudrate = 115200;
40 M5.begin(cfg);
41 M5.Display.setRotation(3); //横向き
42 M5.Display.setFont(&fonts::lgfxJapanGothic_16);
43 M5.Display.println("シリアルにも出力します");
44 M5.Display.setTextScroll(true);
45
46 mutex = xSemaphoreCreateMutex(); // ミューテックス作成
47}
48
49void loop() {
50 Serial.println("-------");
51 if (mutex != NULL) { // ミューテックスの作成に成功していたら
52 for (int i = 0; i < 5; i++) {
53 sprintf(tskname, "x%d", arg[i]);
54 xTaskCreatePinnedToCore(withArgTask, tskname, 4096, &arg[i], 1, NULL , 1);
55 }
56 }
57 M5.delay(20 * 1000); // 次のタスクの仕込みまで、20秒待つ
58}
警告
引数をアドレスで渡すとき、関数内で宣言したローカル変数は使えません。グローバルな変数を使用する必要があります。
ミューテックスとセマフォ
複数のタスクを並列動作させると、リソース(入出力や、メモリ)に複数のタスクが同時アクセスすることで意図しない動作を引き起こすことがあります。 ミューテックス(mutex: mutual exclusion)を用いると、リソースに同時にアクセスするタスクを1つに限定することができます。これを「排他制御」と呼びます。
リスト 27 の例では、ミューテックスをつかって、1つのタスクの動作(シリアルコンソールへの書き込み)が終わるまで、他のタスクが待つ例です。 このように、常時動いているタスクに対して、一時停止したり、処理を制限したりするのがミューテックスの使い方です。 (参考:ESP32のFreeRTOS入門 その6 セマフォとミューテックス )
これに対して、セマフォは、基本的には待機・一時停止状態にあるタスクに対して、動作許可を与える用途で使用されます。 動作許可を与える主体・タイミングとしては、他のタスクや、「割り込み(ISR: interrupt service routine)」からになります。 ただし、バイナリセマフォを用いるより、RTOS Task Notifications を用いたほうが、高速かつメモリ使用量を削減できるようです。
バイナリセマフォは、タスク間またはタスクと割り込み間の同期に適しています。
バイナリセマフォとミューテックスは似ていますが、いくつか本質的な違いがあります。ミューテックスは優先度を継承する機構が備わっていますが、バイナリセマフォには備わっていません。
注釈
キュー
FreeRTOSと、その他のRTOS
今回の実験では、Arduino IDEでのM5StickCPlusプログラミングを行ってきました。これまでArduino IDEでM5StickCPlusに書き込んできたプログラムは、FreeRTOS (RealTime OS) というRTOSの仕組みをつかって動作しています。 いいかえると、ESP32プロセッサ上で、FreeRTOS プログラミングをしていたことになります。ちなみに、M5StickCPlusのような画面やブザー、センサがついていない ESP32プロセッサ開発ボード も販売されています。
MCU RTOS習得(2020年版) http://happytech.jp/bRTOS.html
その他のRTOSには、T-Kernel や ThreadX があります。こちらも、限られた資源(メモリやプロセッサ)で、厳密な処理時間管理が行える仕組みが備わっています。