Arduinoで脈拍を測る

デジタルグッズ

脈拍を調べてみたくなったので、Amazonで評判がよかった安いセンサーを購入。

動かすボードはArduinoです。

Arduino IDEの「ライブラリー管理」の検索窓に”PulseSensor”と入れてライブラリーをインストールします。

するとArduino IDEの「ファイル」> 「スケッチ例」> 「PulseSensor Playground」にさまざまなサンプルがあります。
(PulseSensor…は私の場合、リストのかなり下でしたので、よく探してください。)

Githubにインストール、サンプルの解説がありました。
PulseSensorPlayGround

さて私の場合、Two PulseSensors_On_OneArduinoをやりたいので、コードをロードします。
中をよく見てみることにします。



/*
   Arduino Sketch to detect pulses from two PulseSensors.

   Here is a link to the tutorial
   https://pulsesensor.com/pages/two-or-more-pulse-sensors

   Copyright World Famous Electronics LLC - see LICENSE
   Contributors:
     Joel Murphy, https://pulsesensor.com
     Yury Gitman, https://pulsesensor.com
     Bradford Needham, @bneedhamia, https://bluepapertech.com

   Licensed under the MIT License, a copy of which
   should have been included with this software.

   This software is not intended for medical use.
*/
/* ハートのマーク付きのPulseSensor専用のようですね。医療用じゃないよ、と。*/
/*
   Every Sketch that uses the PulseSensor Playground must
   define USE_ARDUINO_INTERRUPTS before including PulseSensorPlayground.h.
   Here, #define USE_ARDUINO_INTERRUPTS false tells the library to
   not use interrupts to read data from the PulseSensor.

   If you want to use interrupts, simply change the line below
   to read:
     #define USE_ARDUINO_INTERRUPTS true

   Set US_PS_INTERRUPTS to false if either
   1) Your Arduino platform's interrupts aren't yet supported
   by PulseSensor Playground, or
   2) You don't wish to use interrupts because of the side effects.

   NOTE: if US_PS_INTERRUPTS is false, your Sketch must
   call pulse.sawNewSample() at least once every 2 milliseconds
   to accurately read the PulseSensor signal.
*/
/*
ライブラリーを使うためには、
USE_ARDUINO_INTERRUPTをPulseSensorPlaygroundライブラリーの前に定義しなくてはいけない、ということ。
普通はUSE_...にはtrueを設定。falseを設定する場合とは、
1) PulseSensor Playgroundがまだ割り込みをサポートしていなかったバージョンの時か、
2) 割り込みを使いたくない時
に限る。割り込みを使いたくないなら、ないなら、pulse.sawNewSample()を最低でも2ミリ秒ごとに
呼び出さないと正確なデータは取れないよ、とのこと。
*/
#define USE_ARDUINO_INTERRUPTS true
#include <PulseSensorPlayground.h>


/*
   The format of our output.

   Set this to PROCESSING_VISUALIZER if you're going to run
    the multi-sensor Processing Visualizer Sketch.
    See https://github.com/WorldFamousElectronics/PulseSensorAmped_2_Sensors

   Set this to SERIAL_PLOTTER if you're going to run
    the Arduino IDE's Serial Plotter.
*/
/*
出力形式は PROCESSING_VISUALIZERをセットすると、Visualizerのスケッチとなる
詳細はhttps://github.com/WorldFamousElectronics/PulseSensorAmped_2_Sensors
SERIAL_PLOTTERをセットすると、Arduino IDEのシリアルプロッターを使う。

*/
const int OUTPUT_TYPE = SERIAL_PLOTTER;

/*
   Number of PulseSensor devices we're reading from.
*/
const int PULSE_SENSOR_COUNT = 2;

/*
     PULSE_POWERx = the output pin that the red (power) pin of
      the first PulseSensor will be connected to. PulseSensor only
      draws about 4mA, so almost any micro can power it from a GPIO.
      If you don't want to use pins to power the PulseSensors, you can remove
      the code dealing with PULSE_POWER0 and PULSE_POWER1.
     PULSE_INPUTx = Analog Input. Connected to the pulse sensor
      purple (signal) wire.
     PULSE_BLINKx = digital Output. Connected to an LED (must have at least
      470 ohm resistor) that will flash on each detected pulse.
     PULSE_FADEx = digital Output. PWM pin onnected to an LED (must have
      at least 470 ohm resistor) that will smoothly fade with each pulse.

     NOTE: PULSE_FADEx must be pins that support PWM.
       If USE_INTERRUPTS is true, Do not use pin 9 or 10 for PULSE_FADEx
       because those pins' PWM interferes with the sample timer.
*/
/*
  PULSE_POSERxは使われていないようだ。
 PULSE_INPUTxはセンサーのアナログ入力ポートを指定する。
 PULSE_BLINKxはデジタル出力で、LEDに接続することを想定している。LEDは検知した脈拍に応じて点滅する。(470オーム程度の抵抗をLEDにつけること)
  PULSE_FADExはデジタル出力でPWMにより、やわらかい点滅をする。
  注意:PULSE_FADExはPWMをサポートしているピンでなければならない。つまりUSE_INTERRUPTS=trueで9pin, 10pinは使えないのでそれ以外となる。
*/
const int PULSE_INPUT0 = A0;
const int PULSE_BLINK0 = 11;    // changed.
//const int PULSE_FADE0 = 5;    // 使わなし

const int PULSE_INPUT1 = A1;
const int PULSE_BLINK1 = 12;
//const int PULSE_FADE1 = 11;  // 使わない

const int THRESHOLD = 550;   // Adjust this number to avoid noise when idle

/*
   All the PulseSensor Playground functions.
   We tell it how many PulseSensors we're using.
*/
PulseSensorPlayground pulseSensor(PULSE_SENSOR_COUNT);

void setup() {
  /*
     Use 250000 baud because that's what the Processing Sketch expects to read,
     and because that speed provides about 25 bytes per millisecond,
     or 50 characters per PulseSensor sample period of 2 milliseconds.

     If we used a slower baud rate, we'd likely write bytes faster than
     they can be transmitted, which would mess up the timing
     of readSensor() calls, which would make the pulse measurement
     not work properly.
  */
 /*
     250000ボーレートを使うべき。なぜならば1ミリ秒あたりに25バイトの速度であるから。
   2ミリ秒だと50文字が表示できる。
   それより遅いと、正しく表示されない可能性がある。
  */
  Serial.begin(250000);

  /*
     Configure the PulseSensor manager,
     telling it which PulseSensor (0 or 1)
     we're configuring.
  */

  pulseSensor.analogInput(PULSE_INPUT0, 0);
  pulseSensor.blinkOnPulse(PULSE_BLINK0, 0);
  // pulseSensor.fadeOnPulse(PULSE_FADE0, 0);

  pulseSensor.analogInput(PULSE_INPUT1, 1);
  pulseSensor.blinkOnPulse(PULSE_BLINK1, 1);
  // pulseSensor.fadeOnPulse(PULSE_FADE1, 1);

  pulseSensor.setSerial(Serial);
  pulseSensor.setOutputType(OUTPUT_TYPE);
  pulseSensor.setThreshold(THRESHOLD);


  // Now that everything is ready, start reading the PulseSensor signal.
  if (!pulseSensor.begin()) {
    /*
       PulseSensor initialization failed,
       likely because our Arduino platform interrupts
       aren't supported yet.

       If your Sketch hangs here, try changing USE_ARDUINO_INTERRUPTS to false.
    */
    for (;;) {
      // Flash the led to show things didn't work.
      digitalWrite(PULSE_BLINK0, LOW);
      delay(50);
      digitalWrite(PULSE_BLINK0, HIGH);
      delay(50);
    }
  }
}

void loop() {

  /*
     Wait a bit.
     We don't output every sample, because our baud rate
     won't support that much I/O.
  */
  delay(20);

  // write the latest sample to Serial.
  pulseSensor.outputSample();

  /*
     If a beat has happened on a given PulseSensor
     since we last checked, write the per-beat information
     about that PulseSensor to Serial.
  */
  for (int i = 0; i < PULSE_SENSOR_COUNT; ++i) {
    if (pulseSensor.sawStartOfBeat(i)) {
      pulseSensor.outputBeat(i);
    }
  }
}

現場写真

センサーに指をあてる時、強すぎず弱すぎず調整が必要です。LEDの点滅を参考にしながら調整します。
Arduiono IDEのプロッターのボーレートを(250000bps)にすることを忘れずに。

 

以下はライブラリーがどういうアルゴリズムでセンシングデータを処理しているかの説明です。
Pulse Senser Ampedから

Arduino Code V1.2 をみていく

細かい話に入る前に、信号と処理を行うためのテクニックについて知っておくべきことがいくつかあります。

私たちが作る脈拍センサーは本質的にphotoplethysmographs(光反射型検知:と呼ぶ)であり、心拍数モニタリングによく使用される機器です。光反射型検知は、血中酸素レベル (SpO2) を測定する場合としない場合があります。光反射型検知から出力されるパルス信号は電圧のアナログ変動であり、図 1 に示すような予測可能な波形をしています。
パルス波の描写は、PhotoPlethysmoGraphs (PPG) と呼ばれます。当社のハードウェアである Pulse Sensor Amped は、Pulse Sensor の生信号を増幅し、V/2 (電圧の中間点) 付近で脈波を正規化します。 Pulse Sensor Amped は、光強度の相対的な変化に反応します。センサーに入射する光の量が一定のままである場合、信号値は 512 (ADC 範囲の中間点) に (またはそれに近い) ままになります。多くの光だと信号値が上がります。少ない光だと下がります。反射してセンサーに戻る緑色の LED からの光はパルスごとに変化します。

(訳注:Dichrotic notch(ダイクロイックノッチ)とは心臓の動脈弁が閉じた時に起きる、特有の低下ということです。興味深いですね!)

私たちの目標は、瞬間的な心拍の連続した瞬間を見つけ、その間の時間を測定することです。これを心拍間間隔 (IBI) と呼びます。 PPG 波の予測可能なパターンを利用することで、それを行うことができます。

私たちは心臓の研究者ではありませんが、私たちにとって妥当と思われる他の人々の調査に基づいています (以下の参照)。心臓が体中に血液を送り出すとき、鼓動のたびに脈波 (衝撃波のようなもの) が発生し、動脈に沿って毛細血管組織の末端まで伝わります。実際の血液は、脈波が伝わる速度よりもはるかにゆっくりと体内を循環します。以下の 図のPPG のポイント ‘T’ から進行するイベントをたどってみましょう。脈波がセンサーの下を通過する際に信号値が急激に上昇し、その後信号は正常点に向かって下降します。場合によっては、ダイクロイックノッチ (下向きのスパイク) が他のものよりも顕著に現れますが、通常、信号は次の脈波が始まる前にはバックグラウンド ノイズに落ち着きます。波は繰り返され、予測可能であるため、ほぼすべての認識可能な特徴、たとえばピークを基準点として選択し、各ピーク間の時間を計算して心拍数を測定できます。
一部の心臓研究者は、信号が振幅の 25% になったとき、ある人は振幅の 50% になったとき、ある人は上昇イベント中に勾配が最も急になったときだと言います。このパルス センサー コードのバージョン 1.1 は、信号が急速な上昇中に波の振幅の 50% を横切る瞬間間のタイミングによって IBI を測定するように設計されています。 BPM は、前の 10 回の IBI 時間の平均から毎拍導き出されます。

まず、各ビート間のタイミングを確実に測定するには、十分に高い解像度を持つ通常のサンプル レートを使用することが重要です。これを行うために、ATmega328 (UNO) 上の 8 ビット ハードウェア タイマーである Timer2 をセットアップして、1 ミリ秒ごとに割り込みを起こすようにします。これにより、500Hz のサンプル レートと 2mS のビート間のタイミング分解能が得られます (注 2)。ただし、Timer2が使うため、ピン 3 と 11 の PWM 出力は使えません。同様にtone() コマンドも無効になります。このコードは、Arduino UNO、Arduino PRO、Arduino Pro Mini 5V、または ATmega328 と 16MHz クロックで動作する Arduino で動作します。

void interruptSetup(){ 
TCCR2A = 0x02; 
TCCR2B = 0x06; 
OCR2A = 0x7C; 
TIMSK2 = 0x02; 
sei();
}

上記のレジスタ設定は、Timer2にCTCモードに入り、124 (0x7C) までカウントするように指示します。 256 のプリスケーラを使用して適切なタイミングを取得し、124 までカウントするのに 2 ミリ秒かかります。Timer2 が 124 に達するたびに割り込みフラグが設定され、作成した割り込みサービス ルーチン (ISR) と呼ばれる特別な関数が実行されます。 プログラムの残りの部分が何をしていても、次の可能な瞬間に。 sei() は、グローバル割り込みが有効であることを確認します。 タイミングが重要! 別の Arduino または Arduino 互換デバイスを使用している場合は、この関数を変更する必要があります。

FIO または LillyPad Arduino または Arduino Pro Mini 3V または Arduino SimpleSnap または ATmega168 または ATmega328 を 8MHz 発振器で搭載したその他の Arduino を使用している場合は、行 TCCR2B = 0x06 を TCCR2B = 0x05 に変更します。

Arduino Leonardo、Adafruit の Flora、Arduino Micro、または 16MHz で動作する ATmega32u4 を搭載したその他の Arduino を使用している場合は

 void interruptSetup(){
TCCR1A = 0x00;
TCCR1B = 0x0C; 
OCR1A = 0x7C; 
TIMSK1 = 0x02; 
sei();
}

Arduino に電源が投入され、Pulse Sensor Amped がアナログ ピン 0 に接続された状態で実行されている場合、Arduino は常に (2 ミリ秒ごとに) センサー値を読み取り、心拍を探します。 このような割り込み処理のコードとなるでしょう。

ISR(TIMER2_COMPA_vect){
Signal = analogRead(pulsePin); 
sampleCounter += 2; 
int N = sampleCounter - lastBeatTime;

この割り込み処理は 2 ミリ秒ごとに呼び出されます。 最初に行うことは、脈拍センサーのアナログ読み取りを行うことです。 次に、変数 sampleCounter をインクリメントします。 sampleCounter 変数は、時間を追跡するために使用するものです。 変数 N は、後でノイズを回避するのに役立ちます。

次に、振幅の正確な測定値を得るために、PPG 波の最高値と最低値を追跡します。

if(Signal < thresh && N > (IBI/5)*3){
  if (Signal < T){
    T = Signal;
  }
}
if(Signal > thresh && Signal > P){
  P = Signal;
}

変数 P と T は、それぞれピーク値とトラフ値を保持します。 thresh 変数は 512 (アナログ範囲の中間) で初期化され、ランタイム中に変化して、後で説明するように振幅の 50% でポイントを追跡します。 ダイクロイック ノッチからのノイズや誤った読み取りを回避する方法として、T が更新される前に 3/5 IBI の期間が経過する必要があります。

では、脈拍があるか確認してみましょう。

if (N > 250){
  if ( (Signal > thresh) && (Pulse == false) && (N > ((IBI/5)*3) ){
    Pulse = true;
    digitalWrite(pulsePin,HIGH);
    IBI = sampleCounter - lastBeatTime;
    lastBeatTime = sampleCounter;

心拍を探すことを検討する前に、最小限の時間が経過する必要があります。 これにより、高周波ノイズを回避できます。 最小 250 ミリ秒の N は、上限を 240 BPM に設定します。 より高い BPM が予想される場合は、それに応じてこれを調整します。 波形がしきい値を超えて上昇し、最後の IBI の 3/5 が経過すると、パルスが発生します! パルス フラグを設定し、pulsePin LED をオンにする時間です。 (注: ピン 13 で何か他のことをしたい場合は、この行と後の行もコメントアウトしてください)。 次に、最後のビートから IBI を取得するまでの時間を計算し、lastBeatTime を更新します。

次のビットは、起動時に現実的な BPM 値で開始することを確認するために使用されます。

if(secondBeat){
  secondBeat = false;
  for(int i=0; i<=9; i++){ 
    rate[i] = IBI;
  }
}

if(firstBeat){
  firstBeat = false; 
  secondBeat = true;
  sei():
  return; 
}

BooleanのFirstBeatはTrueとして初期化され、SecondBeatは起動時にFalseとして初期化されるため、初めてビートを見つけてISRでここに到達すると、FirstBeatはfalseとなり最初のIBIを読み取ることになります。 2回目は、IBIを(多かれ少なかれ)IBIを信頼し、より正確なBPMから始めるためにレート[]アレイをシードするために使用することができます。 BPMは、最後の10のIBI値の平均から派生しています。

次にBPMを計算しましょう!

word runningTotal = 0;
for(int i=0; i<=8; i++){
rate[i] = rate[i+1];
runningTotal += rate[i];
}
rate[9] = IBI;
runningTotal += rate[9];
runningTotal /= 10;
BPM = 60000/runningTotal;
QS = true;
}
} 

まず、大きな変数 runningTotal を取得して IBI を収集します。次に、rate[] の内容をシフトして runnungTotal に追加します。 最も古い IBI (11 ビート前) は位置 0 から外れ、新しい IBI は位置 9 に配置されます。その後、配列を平均して BPM を計算します。 最後に行うことは、QS フラグを設定することです (Quantified Self の略です)、プログラムの残りの部分がビートを見つけたことを認識します。 ビートを見つけたときにやるべきことは以上です。

他にもいくつかのルーズエンドがあります。たとえば、ビートのないものを見つけるなどです。

 if (Signal < thresh && Pulse == true){ 
digitalWrite(13,LOW);
Pulse = false;
amp = P - T;
thresh = amp/2 + T;
P = thresh;
T = thresh;
}

上記のビートを見つけたとき、パルスセンサー信号の上昇中にパルスが真であると宣言されたため、信号が下降してスレッシュ値を横切ると、パルスが終了したことがわかります。 pulsePin と Pulse ブール値をクリアします。次に、通過したばかりの波の振幅が測定され、スレッシュが新しい 50% マークで更新されます。 P と T は新しいスレッシュにリセットされます。 次のビートを見つける準備が整いました。

ISR が完了する前に、もう 1 つ考慮事項があります。 ビートがない場合はどうなりますか?

if (N > 2500){ 
thresh = 512;
P = 512;
T = 512;
firstBeat = true;
secondBeat = false;
lastBeatTime = sampleCounter;
}

2.5 秒間ビート イベントがない場合、ハートビートを検出するために使用される変数は、起動時の値に再初期化されます。 これで ISR は終了です。

Timer2 割り込みを使用することで、ビート検出アルゴリズムが「バックグラウンドで」実行され、変数値が自動的に更新されます。変数のリストと、それらが更新される頻度は次のとおりです。

変数名 リフレッシュレート 意味
Signal 2m秒 パルスセンサー信号取得
IBI 脈動ごとに ハートビートの間隔(m秒)
BPM 脈動ごとに 一分間の脈動
QS 脈動ごとにtrueになる ユーザーがクリアするP
Pulse 脈動ごとにtrueになる ISRによりクリアされる

基本的な脈動の検索コードがあります。 すべての重要な変数が自動的に更新されると、Loop()関数で楽しいものを簡単に実行できます。 処理に対応して心拍視覚装置を作成する例は、基本的な例です。

int pulsePin = 0;
int blinkPin = 13;
int fadePin = 5; 
int fadeRate = 0;
volatile int BPM;
volatile int Signal;
volatile int IBI = 600;
volatile boolean Pulse = false;
volatile boolean QS = false;
volatile int rate[10];
volatile unsigned long sampleCounter = 0;
volatile unsigned long lastBeatTime = 0;
volatile int P =512;
volatile int T = 512;
volatile int thresh = 512;
volatile int amp = 100;
volatile boolean firstBeat = true;
volatile boolean secondBeat = false; 

void setup(){
pinMode(13,OUTPUT);
pinMode(10,OUTPUT);
Serial.begin(115200);
interruptSetup(); 
// analogReference(EXTERNAL);
} 

pulsePin は、パルス センサーの紫色のワイヤが差し込まれているアナログ ピン番号です。 必要に応じて変更できます。 blinkPin はパルスで点滅します。 fadeRate 変数は、fadePin (PWM ピンである必要がありますが、3 または 11 ではありません) のすべてのビートでオプションのフェード LED 効果を提供するために使用されます。 ピン 13 の点滅よりも見栄えがします。他の変数は見慣れたものになっているはずです。 これらは、ISR およびコードの他の部分で使用されるため、volatile と宣言されています。 セットアップでは、ピンの方向が宣言され、interruptSetup ルーチンが実行されます。 セットアップの最後の行は、Arduino に電力を供給するために使用している電圧とは異なる電圧がパルス センサーに電力を供給している場合にのみ使用されます。 ほとんどの場合、コメントアウトする必要があります。

void loop(){
sendDataToProcessing('S', Signal);
if (QS == true){
sendDataToProcessing('B',BPM);
sendDataToProcessing('Q',IBI);
fadeVal = 255;
QS = false;
}
ledFadeToBeat();
delay(20);
}

これがループ機能です。 最後に遅延に注意してください。 このループは 20 ミリ秒ごとに実行されます。 Signal は自動的に更新されるため、最新の値を簡単に Processing スケッチに送信できます。また、それを使って好きなことをしたり、スピーカーのトーン出力を変調したりすることもできます。 送信する ‘S’ とその他の文字プレフィックスは、Processing が受信値をどう処理するかを知るために送信されます。 ISR が心拍を検出すると QS フラグが設定されることに注意してください。これを確認することで、心拍がいつ発生したかを知ることができます。 if ステートメント内で、IBI 値と BPM 値を Processing に送信し、fadeVal を最大輝度に設定し、次回のために QS フラグをリセットします。 遅延の前に最後に行うことは、LED をフェードさせることです。これでループは終了です。 ループで呼び出される 2 つの関数について見ていきましょう。

void sendDataToProcessing(char symbol, int data ){
  Serial.print(symbol);    
  Serial.println(data);     
}

SendDatatoprocessing関数はProcessingにデータを送ります。 キャラクターと整数変数を取得することを期待してから、連続的に送信します。 値シンボルは、どのようなデータが来ているかを処理し、Printlnが最後にラインフィードを送信するため、シリアル文字列がいつ終了するかを処理します。 (スケッチの処理へのリンクは、すぐに来ます)

void ledFadeToBeat(){
fadeRate-= 15;
fadeRate= constrain(fadeRate,0,255);
analogWrite(fadePin, fadeRate);
}

ledFadeToBeat 関数も非常に単純です。 これは、fadeRate 変数を 15 減らし (必要に応じてこれを変更できます)、fadeRate が負の数にロールオーバーしないこと、または 255 を超えないようにしてから、fadePin の LED の明るさを設定するために analogWrite を使用します。

注 1: 心臓の鼓動の瞬間がどこにあるかについて最も議論している人は、脈拍通過時間を測定しようとしているようです。 PTT は、心電図 (ECG) の点 R から四肢 (指先/つま先) までパルスが移動するのにかかる時間を測定します。 PPG は、四肢へのパルス到達のタイミングを計るために PTT 研究でよく使用されます。 睡眠学者は時々 PTT を使用します。

注2:ATMEGA328データシートの「PWMおよび非同期操作を備えた8ビットタイマー/カウンター2」私たちが使用するサンプルレートは、私たちの研究で最も言及されています。 Timer2の使用は、ライブラリやシールドとの競合が最も少ないため、それを選んだのです。 (16MHzの代わりに)8MHzで実行されるFIOまたはLilyPadを使用している場合は、TCCR2B = 0x05になるように中断セットアップを調整します。