公開日:2023/02/25
title.png

24th Season


#24-7 万歩計をスマートに作ろう

 Arduino DUEで作った万歩計が有りました。

  「#21-8 万歩計を作ろう

  「#22-3 万歩計を完成させる

 Arduino DUEに、加速度センサ(MPU9250)OLEDディスプレイモバイルバッテリを組み合わせて作りました(下図)。

a222_expr.jpg

 写真には写っていませんが、USBケーブルの先にはモバイルバッテリが接続されていて、かなり大きな装置です。こんな物をぶら下げて外を歩きたくなかったので、家の中で歩き回って動作確認しました。実験中の様子はこちらです。

 この様な万歩計を、今回はM5StickC Plus(下図)を使って作ろうと思います。

a233_m5cp_pkg.jpg

 DUEの外部に必要だったモジュールや装置が全てオールインワンとなっている機体なので、スマートに実装できると思います。

MPU6886の制御

 万歩計を作成するに当たって、一番のキーモジュールである加速度センサから検討を始めます。

 M5StickC Plus内蔵の加速度センサはMPU6886と言って、DUEで万歩計を作った時に使ったMPU9250と同じ、InvenSense社製です。計測できるデータも、使い勝手も、ほぼ同じであると期待できます。

  MPU6886のデータシートを検索する(別ウインドウが開きます)

 DUEの時に歩行時のデータを取得し、歩行検知のモデリングと検出アルゴリズムを完成させているので(下図)、プログラムの主たる部分は、そのまま使えると思います。と、言うより、そうできるようにプログラムするべきでしょう。新たにM5StickC Plusでデータを取りなおして、歩行検出アルゴリズムの調整を繰り返すのはシンドイですから。

a218_walkacl.png


 MPU6886の制御に関しても楽ができるように、M5StickC Plusライブラリに備わったM5.Imuクラスを、そのまま使おうと思いました。初期化は、M5.Imu.Init()メソッドが用意されているのですが、これを使って初期化すると、色々意に沿わない設定値が書き込まれてしまって、宜しくありません……。
 MPU6886の初期設定は、自分でやらないとダメみたいです。MPU6886はI2C(Wire1)でESP32と接続されているので、Wire1クラスを使ってI2Cで制御できます。

 最初に、MPU6886モジュールのPWR_MGMT_1レジスタに対して、DEVICE_RESETビットに1を設定し、MPU6886のI/Oレジスタを初期設定に戻します。そこから、加速度センサの読み出しにデジタルLPFを適用しない設定値を書き込みます(ACCEL_CONFIG2レジスタ)。それ以外は初期値のまま使います。

  // MPU6886 setting
  // PWR_MGMT_1.DEVICE_RESET=1
  i2c1_write (i2c_MPU6886,0x6b,0x80);
  while (i2c1_read (i2c_MPU6886,0x6b)&0x80)
    ;
  // PWR_MGMT_1.GYRO_STANDBY=0, CLKSEL=0b001
  i2c1_write (i2c_MPU6886,0x6b,0x01);
  // ACCEL_CONFIG2=bypass LPF
  i2c1_write (i2c_MPU6886,0x1d,0x08);


 DUEで作った歩行検出アルゴリズムでは、10ms(=ped_msec_ITVL)毎にサンプリングした加速度の移動平均(ped_MAX_MVA=16区間)を取っています。移動平均もLPFに相当するため、MPU6886のLPF機能が活きていると、二重にLPFが適用されてしまって、歩行検出の閾値などの変更が必要になってしまいます。
 MPU9250は初期値がLPF無効だったのだけど、MPU6886は初期値がLPF有効なので、無効化設定(ACCEL_CONFIG2レジスタ)が必要となりました。
 これにて、MPU6886の動作設定と、MPU9250の動作設定が合いました。

 ここまで自分でI2C経由で直接MPU6886を制御したら、加速度データの読み出しも直接I2C読み出しでも良いのだけど、一応、M5.Imu.getAccelAdc()メソッドが使えます。

※折角のM5StickC Plusライブラリなのだけど、TFTディスプレイの制御関係以外、何だかんだで、思いのままに使えない気がするのは気のせいかしら?もう少し楽ができると期待して使い始めたのだけど……。汎用のライブラリなんて、所詮この程度なのかな?

共有リソースの管理

10ms毎のサンプリングと、歩行検出

 これをloop()関数内の実時間(millis()関数値)、10ms(=ped_msec_ITVL)間隔で実行しています。このサンプリング周期が乱れると、歩行検出のアルゴリズムが乱れ、うまく歩行が検出できなくなってしまいます。
 ところが、普通にTFTディスプレイ全体を描画させたところ、100ms近く必要(@240MHz)なことが判りました。loop()関数内でTFTディスプレイの表示を更新してしまうと、10ms毎のサンプリングが破綻してしまいます。

 解決策は色々あって、例えば、表示を複数回に分けて描画する様にして、1回当たりの描画時間がサンプリング周期(ped_msec_ITVL)未満に収まるように調整することもできます。ただし、この場合、CPUの動作周波数を変更すると10ms未満で描画可能な処理量も変化するため、一々調整が必要になります。

 今回は、FreeRTOSのスレッドを使った並列処理(下図c)でTFTディスプレイを更新することにしました。

a244_multitsk.png

 サンプリングして歩行検出する処理(task1)と、TFTディスプレイの表示を更新する処理(task2)を並列処理することで、表示を複数回に分けて描画する様な「調整」作業が不要になる反面、共有リソースの管理が必要となります。

 各処理に於いて、どのようなリソースが必要となるのか?確認して行きましょう。

メインプロセス(歩行検出)

 メインプロセス(loop()関数)では、10ms(=ped_msec_ITVL)毎に加速度センサ(MPU6886)を読み出して、歩行検出を実行します。
 メインプロセスは以下のリソースを使います。

サブスレッド(表示の更新)

 TFTディスプレイの表示を司るサブスレッドを起動しておきます。
 このスレッドは500ms毎に変数thrd_upd_screenを監視して、thrd_upd_screen=trueを確認したら表示を更新します。更新が終わったらthrd_upd_screen=falseに戻します。

 サブスレッドは以下のリソースを使います。


 以上をまとめてみると、以下表の様になります。

リソース メインプロセス サブスレッド 共有/非共有
MPU6886 × 非共有
I2C(Wire1クラス) 共有
AXP192 × 非共有
TFTディスプレイ × 非共有
SPI × 非共有

 I2C(Wire1クラス)だけは、メインプロセスとサブスレッドの両方が使うため、共有リソースであると判りました。I2C(Wire1クラス)は、メインプロセスとサブスレッドのどちらかが排他的に使用しなければなりません。同時にI2C(Wire1クラス)を使用してしまうと、どこかで予期せぬ動作となって誤動作することになるからです。
 見かけ上、「MPU6886とAXP192を使う」とだけ考えてしまうと、I2Cが共有リソースであることを見落としてしまいそうですが、見落とさないように。

 なお、TFTディスプレイの表示は全てサブスレッドが担当する分業制にしたのですが、もし、メインプロセスでもTFTディスプレイを更新するようなプログラムにするなら、TFTディスプレイ(SPI)も、共有リソースとなってしまいます。
 並列処理を実装する場合、共有リソースは何と何なのか?十分吟味して、基本設計をしっかり実施しなくてはなりません。

セマフォ

 共有リソースの排他的使用を制御するのに、セマフォ🏁を使います。

 M5StickC Plusに搭載されているFreeRTOSには、セマフォAPI(関数)が整備されています。

 詳細な使い方については、FreeRTOS公式HPまたはESP-IDFリファレンスページを参照してください。次回完成させるプログラム(m5cp_pedometer.ino)を参照すると、具体的な使用例をご確認いただけます。sem_wire1()関数辺りで集中して使っています。

 基本的な処理の流れは以下の通りです。

処理の流れ 関連するAPI等
初期化 xSemaphoreCreateMutex()関数
 
開始(共有リソース処理)  
 
セマフォを取得する xSemaphoreTake()関数
 
共有リソースを使用する Wire1クラスを使ったI2Cアクセス等
 
セマフォを解放する xSemaphoreGive()関数
 
終了(次の処理)  

 セマフォが複数のスレッドに対して同時に許可されることはありませんので、上の様にセマフォを取得(take)してから、解放(give)するまでの間に共有リソース(I2C)を使用すれば、その共有リソースは必ず、唯一のスレッドだけが使用することになります。
 共有リソースを他のスレッドが使っていて、セマフォを保持している場合、セマフォを取得できない場合があります。必ず、xSemaphoreTake()関数でセマフォを取得できた事を確認してから、共有リソースを使用してください。

※セマフォを取得できなかった場合は、1)取得できるまでずっと待つ、か、2)一旦共有リソースの処理を保留して別な処理を実行後、改めてセマフォを取得する等の対応が必要です。

低消費電力化

 万歩計のプログラムでは歩行を検知するため、常に加速度センサを参照しながらCPUが処理する必要があり、動作中は電源を遮断できません。身に着けて使う物なのでバッテリ動作が基本となりますが、万一、動作中にバッテリ残量が無くなって電源が遮断されてしまうと、歩数などのデータは消えてしまいます。
 消費電力の削減は重要な課題なのです。

 と、言っても、出来る事と言えば以下くらいしか思い当たりませんでした。

 3)の5V電源以外の対策は実施しておこうと思います。しかし、バッテリ動作可能時間には、少し課題が残るかもしれませんね。

ログを残す

 バッテリ動作可能時間を評価するため、時間経過とバッテリレベルの推移をログに残し、シリアルモニタから読み出せるようにしておこうと思います。
 どうせログを残すなら、歩数の時間推移も一緒に残しておくと、後でExcel等のソフトウェアに読み込んで、時間帯毎に、歩行速さや歩行距離などのデータを計算できます。所望のグラフ作成なども実施できて良いだろうと考えました。

 万歩計の計測データから消費カロリまで算出するためには、歩数と歩行時間の2つの情報が必要です。

  ログ記録仕様:

 身長・体重などの情報はExcel等のソフトウェアで計算する時に設定できるので、上仕様のログデータは身体的特徴に依存しません。METsの変換テーブルなども用意しておけば、ログデータから消費カロリなどを算出可能となります。計算方法については「#22-3 万歩計を完成させる」を参照してください。


 次回、残りの設計を終わらせて、万歩計を完成させたいと思います。


btn_arduino.png

btn_prev.png btn_next.png

home