Help us understand the problem. What is going on with this article?

Raspberry Piと心拍センサ(MAX30102)で脈を見てみよう

More than 1 year has passed since last update.

(
2019.05.02 追記: github側で「redとirの読み出しがごっちゃだよ」と指摘があったので修正しました.
影響範囲はmax30102.pyread_fifo()部分と,「グラフ表示してみる」全般です.
)

健康,気になりませんか?

いまどきはApple WatchやFitbitで心拍数(BPM)をモニタできますよね.当然アプリでも数字は確認できますが,生の波形を見たくないですか?私は見たいです.

ということで見てみましょう.この記事では,「i2c対応の心拍センサを使い,(1)Raspberry Pi上のPythonから制御を行い,(2)心拍波形を読み取って保存し,(3)グラフを作る」を目標にします.

使うハードウェア

  • Raspberry Pi (ここでは,Raspberry Pi Zero Wを使いました)
    USB電源やキーボード・モニタ・ネットワーク等はよしなに
  • 心拍センサMAXREFDES117#
    販売ページ: https://www.switch-science.com/catalog/3208/
  • ジャンパ線
    心拍センサとRaspberry Piの接続に使います(はんだ付けが必要です.今回はピンヘッダを生やしてブレッドボードに刺しました)

IMG_4065.jpg
↑こんな感じで動かしてました


まずは公式の情報を確認

Switch Scienceの通販ページでは,資料としてサンプルコードへのリンクが貼られています.
(ここ: https://www.maximintegrated.com/en/design/reference-design-center/system-board/6300.html/tb_tab2 )
mbedおよびArduinoのサンプルコードが載っていますが,Raspberry Pi向けはありません.
i2c対応だからADCが無いRaspberry Piでもデータを取れるじゃんと思ったのですが……

ないなら書こうということでArduinoのコードを見て,それをRaspberry Piに移植してみます.

Arduino向けコードを見る

↑のサンプルコードのうちArduino Quick Start Guideを見ると,Arduino向けコードとして https://github.com/MaximIntegratedRefDesTeam/RD117_ARDUINO/ がリンクされています.
サンプルコードを動かすと,シリアルモニタに以下の情報が流れます.

  • 心拍センサのLED(赤色,IR)読み取り値
  • 心拍数とSpO2およびその値が正しいかどうか(valid)

サンプルコードのRD117_ARDUINO.inoを見ると,Arduinoでお馴染みsetup()loop()がありますね.
setup()ではシリアルモニタの設定と心拍センサの状態リセット・初期設定が,loop()ではセンサからの値の読み取りと心拍数/SpO2の計算が,それぞれ行われます.

心拍センサ関連の関数はmax30102.cppに実体が入っており,以下があります.

  • bool maxim_max30102_init(): デバイスの初期化
  • bool maxim_max30102_read_fifo() (引数違いで2種): データの読み出し
  • bool maxim_max30102_write_reg(): レジスタに書き込み
  • bool maxim_max30102_read_reg(): レジスタから読み出し
  • bool maxim_max30102_reset(): デバイスのリセット

これらはi2c通信で情報をやり取りしており,SoftI2CMaster.hでソフト的に実装されているようです.

一方,心拍数の計算とSpO2の計算はalgorithm.cppにありますが,生データを得るのが目的のこの記事ではカバーしません.

いざRaspberry Piで動かす

Raspberry Piのi2c機能の有効化

このあたりの話はRaspberryPiではじめてのI2C通信〜温度計測編〜を参考にしました.
この記事ではRaspbian stretchで動作確認しています.

i2c機能を有効化します.以下のコマンドを実行します.

$ sudo raspi-config

設定画面が呼び出されるので,5 Interfacing Optionsを選択します.

raspi-config.png

P5 I2Cを選びます.

interfacing-options.png

Would you like the ARM I2C interface to be enabled?と聞かれるのでYesと答えると有効化されます(自分の場合は再起動の必要はありませんでした).

i2c.png

i2c関連ツールの導入

関連のツールとPython用ライブラリを入れます.

$ sudo apt-get update
$ sudo apt-get install i2c-tools python-smbus

今回はGPIOピンも利用するため,以下のコマンドを実行して必要なライブラリを入れておきます.

$ sudo pip install rpi.gpio

心拍センサの接続と最低限の動作確認

GPIOピンに対し以下の図のようにセンサを接続しました.

pizero-sensor-connection.png

(i2cは電源/GND/データ線2本の4線で通信するようですが,この心拍センサでは割り込みピンが出ています.
このピンの状態でデータ取得の可否が通知されます.今回はこのピンを7番に接続しました.)

接続が終わったら,ターミナルからセンサが見えているか確認してみましょう.
$ sudo i2cdetect -y 1すると次のような表示が得られるはずです.今回は0x57がデバイスのアドレスとして設定されています.

$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- 57 -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

Pythonからi2c通信を行って赤色/IR LEDの生データを取得する

書いたコードをこちらに置いておきます: https://github.com/vrano714/max30102-tutorial-raspberrypi

Arduinoコードの移植1: 最低限のデータ読み書き

i2cでのデータ読み/書きはsmbusで可能なので,max30102.cppから移植するのはinit()read_fifo()およびreset()になります.
とはいってもレジスタに値を送りつけるだけなので,このままベタに移植していきます.
各値の意味はデータシートを参照( https://www.mouser.jp/datasheet/2/256/maximintegratedproducts_MAX30102%20DS-1179649.pdf )

max30102.py
import smbus

# レジスタアドレス省略

class MAX30102():
    # setup()相当の部分
    def __init__(self, channel=1, address=0x57, gpio_pin=7):
        # Raspberry Piでi2cを行う各種設定
        self.address = address
        self.channel = channel
        # i2cのセットアップ
        self.bus = smbus.SMBus(self.channel)

        # (一部省略)

        self.reset()

        sleep(1) # wait 1 sec

        # 割り込みレジスタを読み捨て(この心拍センサは割り込みレジスタを読むと割り込み状態をクリアする)
        reg_data = self.bus.read_i2c_block_data(self.address, REG_INTR_STATUS_1, 1)

        self.setup()

    def reset(self):
        self.bus.write_i2c_block_data(self.address, REG_MODE_CONFIG, [0x40])

    def setup(self, led_mode=0x03):
        # INTR setting
        self.bus.write_i2c_block_data(self.address, REG_INTR_ENABLE_1, [0xc0])
        self.bus.write_i2c_block_data(self.address, REG_INTR_ENABLE_2, [0x00])

        # FIFO_WR_PTR[4:0]
        self.bus.write_i2c_block_data(self.address, REG_FIFO_WR_PTR, [0x00])
        # OVF_COUNTER[4:0]
        self.bus.write_i2c_block_data(self.address, REG_OVF_COUNTER, [0x00])
        # FIFO_RD_PTR[4:0]
        self.bus.write_i2c_block_data(self.address, REG_FIFO_RD_PTR, [0x00])

        # sample avg = 4, fifo rollover = false, fifo almost full = 17
        self.bus.write_i2c_block_data(self.address, REG_FIFO_CONFIG, [0x4f])

        # 0x02 for read-only, 0x03 for SpO2 mode, 0x07 multimode LED
        self.bus.write_i2c_block_data(self.address, REG_MODE_CONFIG, [led_mode])
        # SPO2_ADC range = 4096nA, SPO2 sample rate = 100Hz, LED pulse-width = 411uS
        self.bus.write_i2c_block_data(self.address, REG_SPO2_CONFIG, [0x27])

        # choose value for ~7mA for LED1
        self.bus.write_i2c_block_data(self.address, REG_LED1_PA, [0x24])
        # choose value for ~7mA for LED2
        self.bus.write_i2c_block_data(self.address, REG_LED2_PA, [0x24])
        # choose value fro ~25mA for Pilot LED
        self.bus.write_i2c_block_data(self.address, REG_PILOT_PA, [0x7f])

    # 赤色/IRのLEDのデータを読むのはこれ
    def read_fifo(self):
        red_led = None
        ir_led = None

        # read 1 byte from registers (values are discarded)
        reg_INTR1 = self.bus.read_i2c_block_data(self.address, REG_INTR_STATUS_1, 1)
        reg_INTR2 = self.bus.read_i2c_block_data(self.address, REG_INTR_STATUS_2, 1)

        # read 6-byte data from the device
        d = self.bus.read_i2c_block_data(self.address, REG_FIFO_DATA, 6)

        # mask MSB [23:18]
        red_led = (d[0]<<16 | d[1] << 8 | d[2]) & 0x03FFFF
        ir_led = (d[3]<<16 | d[4] << 8 | d[5]) & 0x03FFFF

        return red_led, ir_led

先程確認した0x57をデフォルトのアドレスとして__init()__に入れておきました.また,channelはRaspberry Piのi2cバス番号らしい?です(詳しい方〜〜).

Arduinoコードの移植2: 割り込みを見つつデータを継続的に取得できるようにする

上でも書きましたが,今回使っている心拍センサでは割り込みでデータの取得可否を通知します.
これはサンプルコードでもsetup()でピンを指定し,loop()内でピンの状態を監視してから値の取得を行うよう実装されているので,今回のコードでもその通りにします.
read_sequential()で指定した量のデータを取得できるようにしました.

RD117_ARDUINO.ino
// setup()内
pinMode(10, INPUT);  //pin D10 connects to the interrupt output pin of the MAX30102

// loop()内の各所
while(digitalRead(10)==1);  //wait until the interrupt pin asserts
max30102.py
# GPIOを触るためにライブラリを読み込み
import RPi.GPIO as GPIO

class MAX30102():
    # setup()相当の部分
    def __init__(self, channel=1, address=0x57, gpio_pin=7):
        # (一部省略)
        self.interrupt = gpio_pin

        # GPIO.BOARD: ボード上ピン番号(=物理的な番号)で指定
        GPIO.setmode(GPIO.BOARD)
        # デフォルトでは7番ピンが入力ピンとして設定される
        GPIO.setup(self.interrupt, GPIO.IN)

    # 途中省略

    # 指定した量のデータを読み込む
    def read_sequential(self, amount=100):
        red_buf = []
        ir_buf = []
        for i in range(amount):
            while(GPIO.input(self.interrupt) == 1):
                # 割り込みピンがLOW(0)になるまで待つ: 待っている間は何もしない
                pass
            red, ir = self.read_fifo()
            red_buf.append(red)
            ir_buf.append(ir)

        return red_buf, ir_buf

最終的に出来上がったクラスは↑のソースコードに入っています.

生データを読んでダンプする

実際に値を読んでみます.hrdump.pyを書いてみました.やっていることはシンプルで,read_sequential()で1000サンプル取得して,ファイルに保存するだけです.
指が乗っていなくても値が出るので,実行前からセンサに指を乗せておくほうがよいかもしれません
(データシートには距離で割り込みをかけるような記載もありましたが,今回は実装していません).

hrdump.py
import max30102

m = max30102.MAX30102()

red, ir = m.read_sequential(1000)
with open("./red.log", "w") as f:
    for r in red:
        f.write("{0}\n".format(r))
with open("./ir.log", "w") as f:
    for r in ir:
        f.write("{0}\n".format(r))

m.shutdown()

グラフ表示してみる

グラフの生成はred.logir.logをscp等で手元(mac)に持ってきて行いました.Raspberry Pi上でやる場合はmatplotlib & numpyを導入して行うことになります(CLI上で行う場合はmatplotlibをGUI無しで動作させるための書き換えが必要です).

makegraph.py
import matplotlib.pyplot as plt
import numpy as np

red = []
with open("./red.log", "r") as f:
    for r in f:
        red.append(int(r))

ir = []
with open("./ir.log", "r") as f:
    for r in f:
        ir.append(int(r))

# 横軸用
x = np.arange(len(red))

fig = plt.figure()
ax = fig.add_subplot(111)

# 赤色LED
ax.plot(x, red, c="red", label="RED LED")
# IR LED
ax.plot(x, ir, c="orange", label="IR LED")

# 表示範囲を調整
ax.set_ylim(100000, 150000)

# 凡例表示
ax.legend(loc="best")

# 画像を表示
plt.show()

そうしてできたグラフがこちらです.

Figure_0_fix.png

なんだかわからないので,IR LEDの値に下駄を履かせた上でax.set_ylim()を調整してみます.

# IR LED
-ax.plot(x, ir, c="orange", label="IR LED")
+ax.plot(x, np.array(ir)-20000, c="orange", label="IR LED (shifted)")

# 表示範囲を調整
-ax.set_ylim(100000, 150000)
+ax.set_ylim(122000, 126000)

Figure_1_fix.png

それっぽい波形が見えてきました.比較的きれいそうな400-600サンプル目に絞ってみます.

# 表示範囲を調整
 ax.set_ylim(122000, 126000)
+ax.set_xlim(400, 600)

Figure_2_fix.png

それっぽくなりました.赤色LEDのほうはゴチャついていますが,IR LEDはきれいに値が変動しています.
現在(=サンプルコード)は秒間25サンプル取得なので,落ちるスパイクの感覚的に60BPM程度の心拍数だと思われます.

おわりに

この記事ではi2c接続の心拍センサをRaspberry Pi + Pythonで動かして,値を保存し,グラフにプロットするまでを行いました.
心拍数の計算等もできるようになったら投稿したいですね.

補足

実はRaspberry Pi向けのコードを作っている人がいますので参考までに紹介しておきます.このコードでは一通りの機能が利用できるよう設計されています.
こちら: https://github.com/oscaratnc/MAX30102-in-raspberry-pi-

vrn
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした