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」として使われている)ので、同時アクセスはできないことになります。
シングルタスクのシーケンシャルアクセスでは何の問題もない
通常のシングルタスクでは、以下のコードは問題なく動作します。
2 | M5.IMU.getAccelData(&accX,&accY,&accZ); // 加速度データ取得 |
IMU読出しを別タスクにしたら、スイッチが読めなくなった
今回IMUを2ms定周期で読み出したいのですが、時間のかかるLCD描画もあるので、IMU読出しは周期割り込みから別タスクで動かすこととし、タスク生成は「Lang-ship」さんのサイトを参考に「xTaskCreateUniversal」を使い2ndコアタスクにしました。いつも大変お世話になっております。
無事コンパイルも通り、いざRUN!。なんとTP入力ができない!。
そこで、調べていくうちに前述の競合が判明。
マルチスレッドで一つのシリアルポートをアクセスする時などによく使う、Mutexで排他制御することにします。
読出しをバックグラウンド処理で割り込みやDMAなんか使ってると通信の終了を待たなければいけない場合がありますが、ソースコードを見る限りそうでもないようなので、単純に排他処理でよさそうです。
そういうわけで、抜粋コードです
2 | TaskHandle_t taskHandle; |
3 | TaskHandle_t looptaskHandle; |
4 | SemaphoreHandle_t semaphore; |
9 | xQueueSendFromISR(xQueue, &data, 0); |
12 | void task( void *pvParameters) { |
15 | xQueueReceive(xQueue, &data, portMAX_DELAY); |
16 | xSemaphoreTake(semaphore, portMAX_DELAY); |
17 | M5.IMU.getAccelData(&accX,&accY,&accZ); |
18 | xSemaphoreGive(semaphore); |
26 | timer = timerBegin(0, getApbFrequency() / 1000000, true ); |
27 | timerAttachInterrupt(timer, &event, true ); |
28 | timerAlarmWrite(timer, 2000, true ); |
29 | xQueue = xQueueCreate(QUEUE_LENGTH, sizeof ( int8_t )); |
30 | xTaskCreateUniversal(task, "task" ,8192,NULL,5,&taskHandle,APP_CPU_NUM); |
31 | xTaskCreateUniversal(looptask, "looptask" ,8192,NULL,1,&looptaskHandle,CONFIG_ARDUINO_RUNNING_CORE); |
32 | semaphore = xSemaphoreCreateMutex(); |
35 | timerAlarmEnable(timer); |
39 | void looptask( void *pvParameters) { |
41 | xSemaphoreTake(semaphore, portMAX_DELAY); |
43 | xSemaphoreGive(semaphore); |
44 | if (M5.BtnA.wasReleased()) { |
47 | if (M5.BtnB.wasReleased()) { |
50 | if (M5.BtnC.wasReleased()) { |
56 | void loop() {delay(1);} |
簡単な説明
メインloopもタスクにしました。TPはここで「M5.Update」で読んでいます。コードは省略してますが、このループ内でLCD描画(30msほどかかる)も行います。
IMUを2msで読み出す手順は、2msタイマー周期割り込み内でキューに適当なデータを格納し、2ndタスク内では、キューになんかデータが入ったらIMUを読出し、再びque待ち、を繰り返す動作です。
ローエンドマイコンなら、割り込み内で全部やってしまいたいところですが、ESP32では第二コアを別タスクとして使えるので、多少オーバーヘッドが増えてもスッキリ記述できました。
この時に、メインタスクと2ndタスクとでセマフォtakeとgiveでI2C資源の空き待ちをし合っているわけですが、この2つのタスク以外からアクセスしなければロックがかかることはないので問題ありません。
ポートに信号をだしてオシロで目視する限りでは目立つジッタもなく、IMU読出しはわりと正確に2ms定周期で読みだせていたのでOKとしました。