M5Stack IMUとタッチパネルを別タスクで使うためMutexで排他制御

カテゴリー: ESP32  タグ:

M5Stack Core2でIMU(6軸センサ)のサンプルプログラムを応用して、アプリを構築しようとしています。

M5Stack Core2のスイッチはタッチパネル(以降TP)の一部を使っています。IMUと共通I2Cです。マルチタスクで読みだす際に、アクセス競合防止にMutexで排他制御した覚書です

ESP32の開発では、「ESP-IDF環境でないとFreeRTOSが使えない」と勝手に誤解して永らくESP-IDFを使っていましたが、今回の解決でArudino環境でも使えるのが大々的に判明したので、嬉しくなっての投稿でもあります。

IMUとTPは同じI2C

M5Stack Core2の前面パネルのスイッチは、TPの一部を使用しています。

M5Stack Core2の回路図と見るとTPのSDA/SCLとIMU(MPU-6886)のSDA/SCLは同じポートと使っている(具体的にはG21とG22、ライブラリソース内では「Wire1」として使われている)ので、同時アクセスはできないことになります。

シングルタスクのシーケンシャルアクセスでは何の問題もない

通常のシングルタスクでは、以下のコードは問題なく動作します。

  M5.update();        // ボタン状態更新
  M5.IMU.getAccelData(&accX,&accY,&accZ); // 加速度データ取得

IMU読出しを別タスクにしたら、スイッチが読めなくなった

今回IMUを2ms定周期で読み出したいのですが、時間のかかるLCD描画もあるので、IMU読出しは周期割り込みから別タスクで動かすこととし、タスク生成は「Lang-ship」さんのサイトを参考に「xTaskCreateUniversal」を使い2ndコアタスクにしました。いつも大変お世話になっております。

無事コンパイルも通り、いざRUN!。なんとTP入力ができない!。

そこで、調べていくうちに前述の競合が判明。

マルチスレッドで一つのシリアルポートをアクセスする時などによく使う、Mutexで排他制御することにします。

読出しをバックグラウンド処理で割り込みやDMAなんか使ってると通信の終了を待たなければいけない場合がありますが、ソースコードを見る限りそうでもないようなので、単純に排他処理でよさそうです。

そういうわけで、抜粋コードです

QueueHandle_t       xQueue;         // キュー
TaskHandle_t        taskHandle;     // タイマー処理用タスク(IMU読出し)
TaskHandle_t        looptaskHandle; // mainloop用タスク(TP読出し)
SemaphoreHandle_t   semaphore;


void event() {  // 周期割り込みハンドラ
  int8_t data=0;  // トリガとしてqueを使うので送信データの内容はなんでも良い
  xQueueSendFromISR(xQueue, &data, 0);  // 8bitデータをqueに格納
}

void task(void *pvParameters) {  // IMU読出しタスク
  int8_t data;
  while (1) {
    xQueueReceive(xQueue, &data, portMAX_DELAY);    // タイマー割込からque待ち
    xSemaphoreTake(semaphore, portMAX_DELAY);       // MutexでWire1の資源を獲得
    M5.IMU.getAccelData(&accX,&accY,&accZ);         // 加速度データ取得
    xSemaphoreGive(semaphore);                      // MutexでWire1を解放
        ・
        ・
  }
}

void setup(){
  Serial.begin(115200); //115200bpsでシリアルポートを開く
  timer = timerBegin(0, getApbFrequency() / 1000000, true);
  timerAttachInterrupt(timer, &event, true);        // タイマー割り込み設定
  timerAlarmWrite(timer, 2000, true);               // 2ms周期
  xQueue = xQueueCreate(QUEUE_LENGTH, sizeof(int8_t));  // queを生成
  xTaskCreateUniversal(task,"task",8192,NULL,5,&taskHandle,APP_CPU_NUM);     // CPU1タスク
  xTaskCreateUniversal(looptask,"looptask",8192,NULL,1,&looptaskHandle,CONFIG_ARDUINO_RUNNING_CORE);    //CPU0の基本タスク
  semaphore = xSemaphoreCreateMutex();  // セマフォはMutexとして定義
  M5.begin();                           // 本体初期化
  M5.IMU.Init();                        // IMU初期化
  timerAlarmEnable(timer);                          // タイマー開始
}

// メインloop
void looptask(void *pvParameters) {
  while(1) {
    xSemaphoreTake(semaphore, portMAX_DELAY);   // MutexでWire1の資源を獲得
    M5.update();        // ボタン状態更新
    xSemaphoreGive(semaphore);                  // MutexでWire1を解放
    if (M5.BtnA.wasReleased()) {
      // ボタンAが押された処理
    }
    if (M5.BtnB.wasReleased()) {
      // ボタンBが押された処理
    }
    if (M5.BtnC.wasReleased()) {
      // ボタンCが押された処理
    }
  }
}

void loop() {delay(1);}   // MainLoopもtaskにしたので、loop関数は未使用

簡単な説明

メインloopもタスクにしました。TPはここで「M5.Update」で読んでいます。コードは省略してますが、このループ内でLCD描画(30msほどかかる)も行います。

IMUを2msで読み出す手順は、2msタイマー周期割り込み内でキューに適当なデータを格納し、2ndタスク内では、キューになんかデータが入ったらIMUを読出し、再びque待ち、を繰り返す動作です。

ローエンドマイコンなら、割り込み内で全部やってしまいたいところですが、ESP32では第二コアを別タスクとして使えるので、多少オーバーヘッドが増えてもスッキリ記述できました。

この時に、メインタスクと2ndタスクとでセマフォtakeとgiveでI2C資源の空き待ちをし合っているわけですが、この2つのタスク以外からアクセスしなければロックがかかることはないので問題ありません。

ポートに信号をだしてオシロで目視する限りでは目立つジッタもなく、IMU読出しはわりと正確に2ms定周期で読みだせていたのでOKとしました。

お気軽にコメントをどうぞ。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)