nRF max30102 hartrate


前回、 MAX30102をnRFで使えるように、ライブラリー化したので、心拍数が測定できるように  SparkFun Electronicsのhttps://github.com/sparkfun/MAX30105_Breakoutに提供されているheartRateを使ってみる。前回の続きで、Timer、NRF_LOG、TWIが使える状態が前提。

heartRate.cppファイルをcompornents/drivers_ext/max30102と前回作ったフォルダにコピーし、拡張子を.cに変更する。heartRate.hはそのままコピーをとる。フォルダへの紐づけは前回しているのでそのままだがしていない場合はする。

【heartRate.cの改変】

cppファイルであるが、クラスファイルを定義している訳でもなく中身はC言語なので拡張子だけの違い。

改変は本来いらないはずだが、まあそのままだとちゃんとデータを表示してくれなかった。

改変ポイントだけ記載するが、ArduinoIDEとかのシリアルモニタ、シリアルプロットを使って基本的には一つひとつ図示していき確認しながら進める(後でその辺も)

まず一番癖のある関数から改変する。

int16_t averageDCEstimator(int32_t *p, uint16_t x)
{
*p += ((((long) x << 15) - *p) >> 4);
return (*p >> 15);
}

このDC成分を検出する関数。
やっている事の解説。第二引数のx(ダイオードで受けた光検出量が入ってくる)、これを15ビット左にシフト。これは2の15乗(32768)を掛け算する事と同じ、それから現在の*pの値を引く。この*pは基本物理的には、今のAD変換されてくる値の32768倍の値が常に格納されている。引いたものを右側に今度は4ビットシフト。これは2の4乗(16)で割り算する。その割り算した値を*pに足して新な*pとしている。これまでの値に、1/16だけ新しく取得したデータを反映させる処理をしている。
と動作はわかる・・・が、このx uint16で定義しているが、そもそもAD変換されて出てくるのは18bit。つまり最大で262,144の値が入ってくる。一方16bitで示せる整数は65,536・・・。intで返すと半分の値までしか示せない。値の小さなうちはよいがオーバーフローすると正しく表示されない。どうしてこれでDC成分を除去できるのか?わからない。ってか動かない。

それで、他のやり方があるかもしれないが安直に、pのメモリ領域を確保している幅を広げてやる。まず変数定義から広げる必要があるので70行目あたりにある変数定義で

int32_t ir_avg_reg = 0;

とあるのを

int64_t ir_avg_reg = 0;

に編集。

関数そのものも、

uint32_t averageDCEstimator(int64_t *p, uint32_t x)
{
*p += ((((int64_t)x<<15) - *p) >>4);
return (*p>>15);
}

ちなみにこの15、この32768倍する15ビットのシフトの数を小さくすると応答は早くなるが、平均化の効果は薄くなるのでグラフを見ながら最後は調整する。

これに合わせて、heartRate.h内の関数定義も変更してやる。
これで大きな整数も扱う事ができるようになる。

あと変更した点は、上でDC成分(IR_Average_Estimatedという変数に入る)を測定した値から引いてAC成分を出している所で、lowPassFIRFilterの関数を呼び出しているが、なまりすぎ・・・と判断して、フィルタを入れずに単に引き算に変更。
このあたりは、グラフに表示しながら調整している。(下の方参照)

  //IR_AC_Signal_Current = lowPassFIRFilter(sample -IR_Average_Estimated);
  IR_AC_Signal_Current = sample -IR_Average_Estimated;

その他の改変点は、
    if ((IR_AC_Max - IR_AC_Min) > 100 & (IR_AC_Max - IR_AC_Min) < 4000)
の部分で最大最小の差が20より大きく、かつ1000より小さいという部分、ここが結構AC成分の振幅を見ていると最大最小で3000ぐらいある時があるので、4000としている。
これもグラフを表示させての調整

あと、値が負の値から正に切り替わるタイミングでbeatDetected をtureにして、その時間間隔を測定する事で心拍数を計算しているが、波形を見た時に、正から負の方に切り替わるタイミングの方が急峻に見えたので、そちらがに変更した。このあたりは下の解説で補足。

    if ((IR_AC_Max - IR_AC_Min) > 100 & (IR_AC_Max - IR_AC_Min) < 4000)
    {
      //Heart beat!!!
      beatDetected = true;
    }

【main.cの改変】

ArduinoのExample5_HeartRateを基本的には、そのまま移植する。
whileの外で、setupで初期設定を送り込んでやることと、使う変数の定義をする。

  //configration of Max30102
    MAX30102_setup(0x2F,4,2,200,69,4096);
   
    const uint8_t RATE_SIZE = 4; //Increase this for more averaging. 4 is good.
    uint8_t rates[RATE_SIZE]; //Array of heart rates
    uint8_t rateSpot = 0;
    uint32_t lastBeat = 0; //Time at which the last beat occurred

    float beatsPerMinute;
    int beatAvg;

whileの中は、

  long irValue = particleSensor.getIR();
の所を
     uint32_t irValue = MAX30102_getRed();
と自分で作った関数に変更する。MAX30102_getRedとするか?MAX30102_getIRとするかは試してみる。

millis()の関数部分は、”app_timer_cnt_get()/32.768”
で置換する。
sdk_configのAPP_TIMERの周波数設定の所が32768になっているかを確認する。

  Serial.print("IR=");
  Serial.print(irValue);

などは、

NRF_LOG_RAW_INFO("IR:%d, ",irValue);
NRF_LOG_FLUSH();

と置換する。=でなくて:にしておくとArduinoのシリアルプロッターでラベルが張られる。NRF_LOG_RAW_INFOは改行コードは手動で送り込む必要があるので、

パラメータを吐き出したら最後に”¥n"を送ってあげる。

【簡単な動作の説明】

averageDCEstimatorという関数は、その時間までのデータの平均値に新たに得たデータを1/16だけ更新していく


緑が実際に取得してるデータの時系列で、青がIR_Average_Estimatedの変数をプロットしたもので、時間おくれがあるものの、緑に漸近していく。
完全に漸近しきり、ArduinoIDEのプロッターを使っていると自動で縦軸が拡大されていき上のように心拍の振動する曲線とその平均が随時敵に青のようにしめされる。DCと言っても完全には平になる訳ではい。次に緑と青を引いてAC成分だけを取り出す。
すると上のようなゼロを中心にして振動するグラフが得られる。
(テストする際は、適宜NRF_LOGをコメントアウト、\nの挿入や削除をして)

オリジナルのプログラム上は、負から正に切り替わる所を検出して、前回切り替わった所からの時間を計測する事で心拍数を出しているが、立ち上がりは階段のような形になるのと立下りの方が急峻なので、立下り側を検出するようにした。

ポイントは上のようにグラフに表示して一つ一つ確認し、ローパスフィルタを入れるべきか?入れないべきか?を確認する。


mainの
    //NRF_LOG_RAW_INFO("IR:%d, ",irValue);
    //NRF_LOG_FLUSH();
センサ取得の値をプロットするところをコメントアウト、heartRate.c内で、(heartRate.c
にも必要なインクルードファイルは読み込む)

    NRF_LOG_RAW_INFO("AC:%d, ",IR_AC_Signal_Current);
    NRF_LOG_FLUSH();
とAC成分をプロットするようにしてあげて表示。
正から負に切り替わる所で、checkがtrueになりゼロをまたぐ点を検出している事が確認できる(上の例は、切り替わりで*****checkと表示を入れているが、そのままでもAvg BPMの数字が更新されるタイミングで判断できる。

最後に測定値をOLEDに表示してみた。
  SSD1306_GotoXY (0,0);
    sprintf(OLED_buffer,"BPM=%03d",beatAvg);
    SSD1306_Puts (OLED_buffer, &Font_11x18, 1);
    SSD1306_UpdateScreen();
をwhileのNRF_LOG_RAW_INFOの後に加えてやる。

スマートウォッチの計測と比べてみたがまあまあの一致。
測定準備の時間(DC成分が収集データに漸近するまでの時間)は準備中などと表示するとかとかは後ほど。

ソースコード一式はここに。




コメント

このブログの人気の投稿

Attiny85とAQM0802A(LCD)のI2C接続

Attiny85 FuseRest

HS101 STM32の自作お手軽オシロスコープ