(
2019.05.02 追記: github側で「redとirの読み出しがごっちゃだよ」と指摘があったので修正しました.
影響範囲はmax30102.py
のread_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の接続に使います(はんだ付けが必要です.今回はピンヘッダを生やしてブレッドボードに刺しました)
まずは公式の情報を確認
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
を選択します.
P5 I2C
を選びます.
Would you like the ARM I2C interface to be enabled?
と聞かれるのでYes
と答えると有効化されます(自分の場合は再起動の必要はありませんでした).
i2c関連ツールの導入
関連のツールとPython用ライブラリを入れます.
$ sudo apt-get update
$ sudo apt-get install i2c-tools python-smbus
今回はGPIOピンも利用するため,以下のコマンドを実行して必要なライブラリを入れておきます.
$ sudo pip install rpi.gpio
心拍センサの接続と最低限の動作確認
GPIOピンに対し以下の図のようにセンサを接続しました.
(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 )
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()
で指定した量のデータを取得できるようにしました.
// 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
# 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サンプル取得して,ファイルに保存するだけです.
指が乗っていなくても値が出るので,実行前からセンサに指を乗せておくほうがよいかもしれません
(データシートには距離で割り込みをかけるような記載もありましたが,今回は実装していません).
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.log
とir.log
をscp等で手元(mac)に持ってきて行いました.Raspberry Pi上でやる場合はmatplotlib & numpyを導入して行うことになります(CLI上で行う場合はmatplotlibをGUI無しで動作させるための書き換えが必要です).
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()
そうしてできたグラフがこちらです.
なんだかわからないので,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)
それっぽい波形が見えてきました.比較的きれいそうな400-600サンプル目に絞ってみます.
# 表示範囲を調整
ax.set_ylim(122000, 126000)
+ax.set_xlim(400, 600)
それっぽくなりました.赤色LEDのほうはゴチャついていますが,IR LEDはきれいに値が変動しています.
現在(=サンプルコード)は秒間25サンプル取得なので,落ちるスパイクの感覚的に60BPM程度の心拍数だと思われます.
おわりに
この記事ではi2c接続の心拍センサをRaspberry Pi + Pythonで動かして,値を保存し,グラフにプロットするまでを行いました.
心拍数の計算等もできるようになったら投稿したいですね.
補足
実はRaspberry Pi向けのコードを作っている人がいますので参考までに紹介しておきます.このコードでは一通りの機能が利用できるよう設計されています.
こちら: https://github.com/oscaratnc/MAX30102-in-raspberry-pi-