1. 測定の基本原理
1.1 PPG(光電容積脈波)法によるSpO2測定
MAX30102センサは660nmの赤色LEDと880nmの近赤外LED、並びに高感度フォトディテクタを集積したモジュールです。血液中のヘモグロビンは酸素結合状態により異なる波長の光を選択的に吸収します。
- 酸素化ヘモグロビン(HbO₂):近赤外光を強く吸収
- 脱酸素ヘモグロビン(Hb):赤色光を強く吸収
SpO2値は以下の関係式により算出されます:
\[SpO_2 = 110 - 25 \times R\]
ここでR値(吸光度比)はPPG信号の交流成分と直流成分の比率で決定します:
\[R = \frac{AC_{red}/DC_{red}}{AC_{ir}/DC_{ir}} \]
AC成分は心拍に同期した脈動成分、DC成分は組織や静脈血による一定の吸収成分を表します。
1.2 ハードウェア接続構成
| MAX30102端子 | STM32F103C8T6端子 | 接続要件 |
|---|---|---|
| VIN | 3.3V | 電源供給(ノイズ対策のためデカップリング必須) |
| GND | GND | グランド共通 |
| SDA | PB7 | I2Cデータ線(4.7kΩプルアップ) |
| SCL | PB6 | I2Cクロック線(4.7kΩプルアップ) |
| INT | PA0 | 割り込み信号(オプション、Lowアクティブ) |
| RD | 未接続 | 使用しない |
2. ファームウェア実装
2.1 ドライバヘッダファイル(spo2_sensor.h)
#ifndef SPO2_SENSOR_H
#define SPO2_SENSOR_H
#include "stm32f10x.h"
#include "i2c_driver.h"
// MAX30102レジスタマップ
#define REG_INTERRUPT_STATUS 0x00
#define REG_FIFO_DATA_PTR 0x07
#define REG_FIFO_CONFIG 0x08
#define REG_MODE_CONFIG 0x09
#define REG_SPO2_CONFIG 0x0A
#define REG_LED_PULSE_AMPLITUDE 0x0C
#define REG_DEVICE_ID 0xFF
// システム定数
#define QUEUE_LENGTH 128
#define SAMPLING_FREQUENCY 100
#define WINDOW_SIZE 5
// グローバル変数
extern volatile uint32_t infrared_data[QUEUE_LENGTH];
extern volatile uint32_t red_led_data[QUEUE_LENGTH];
extern volatile uint8_t measurement_ready;
void spo2_initialize(void);
void spo2_capture_sample(void);
float spo2_calculate_value(void);
#endif
2.2 センサ初期化処理(spo2_sensor.c)
#include "spo2_sensor.h"
#include "system_delay.h"
#include "serial_console.h"
volatile uint32_t infrared_data[QUEUE_LENGTH];
volatile uint32_t red_led_data[QUEUE_LENGTH];
volatile uint8_t sample_index = 0;
void spo2_initialize(void)
{
uint8_t chip_id;
// デバイス確認
chip_id = i2c_read_register(REG_DEVICE_ID);
console_print("Device ID: 0x%02X\n", chip_id);
// ソフトウェアリセット
i2c_write_register(REG_MODE_CONFIG, 0x40);
delay_ms(50);
// FIFO設定(ロールオーバー有効)
i2c_write_register(REG_FIFO_CONFIG, 0x0F);
// SpO2モード設定
i2c_write_register(REG_MODE_CONFIG, 0x03);
// サンプリング設定:100Hz, 411μsパルス, 18ビット分解能
i2c_write_register(REG_SPO2_CONFIG, 0x27);
// LED駆動電流設定(適切な光強度に調整)
// 赤LED: 7.0mA, 近赤外LED: 7.0mA
i2c_write_register(REG_LED_PULSE_AMPLITUDE, 0x33);
// 割り込みクリア
i2c_read_register(REG_INTERRUPT_STATUS);
console_print("Sensor initialization complete\n");
}
void spo2_capture_sample(void)
{
uint8_t raw_bytes[6];
i2c_read_multiple(REG_FIFO_DATA_PTR, raw_bytes, 6);
red_led_data[sample_index] = ((uint32_t)raw_bytes[0] << 16) |
((uint32_t)raw_bytes[1] << 8) |
raw_bytes[2];
red_led_data[sample_index] &= 0x3FFFF;
infrared_data[sample_index] = ((uint32_t)raw_bytes[3] << 16) |
((uint32_t)raw_bytes[4] << 8) |
raw_bytes[5];
infrared_data[sample_index] &= 0x3FFFF;
sample_index++;
if(sample_index >= QUEUE_LENGTH) {
sample_index = 0;
measurement_ready = 1;
}
}
2.3 SpO2演算アルゴリズム(spo2_calculator.c)
#include "spo2_sensor.h"
#include "arm_math.h"
static void apply_noise_reduction(volatile uint32_t *source, float *dest, uint16_t len)
{
for(uint16_t i = WINDOW_SIZE; i < len; i++) {
float accumulator = 0.0f;
for(uint8_t j = 0; j < WINDOW_SIZE; j++) {
accumulator += (float)source[i - j];
}
dest[i] = accumulator / WINDOW_SIZE;
}
}
static float calculate_baseline(float *signal, uint16_t length)
{
float total = 0.0f;
for(uint16_t i = 0; i < length; i++) {
total += signal[i];
}
return total / length;
}
static float compute_modulation_amplitude(float *signal, uint16_t length)
{
float baseline = calculate_baseline(signal, length);
float variance = 0.0f;
for(uint16_t i = 0; i < length; i++) {
variance += powf(signal[i] - baseline, 2);
}
return sqrtf(variance / length) * 1.414f;
}
float spo2_calculate_value(void)
{
float red_processed[QUEUE_LENGTH];
float ir_processed[QUEUE_LENGTH];
if(!measurement_ready) return -1.0f;
apply_noise_reduction(red_led_data, red_processed, QUEUE_LENGTH);
apply_noise_reduction(infrared_data, ir_processed, QUEUE_LENGTH);
float red_baseline = calculate_baseline(red_processed, QUEUE_LENGTH);
float ir_baseline = calculate_baseline(ir_processed, QUEUE_LENGTH);
float red_modulation = compute_modulation_amplitude(red_processed, QUEUE_LENGTH);
float ir_modulation = compute_modulation_amplitude(ir_processed, QUEUE_LENGTH);
if(red_baseline < 1000.0f || ir_baseline < 1000.0f) return 0.0f;
float ratio = (red_modulation / red_baseline) / (ir_modulation / ir_baseline);
float saturation = 110.0f - 25.0f * ratio;
if(saturation > 100.0f) saturation = 100.0f;
if(saturation < 70.0f) saturation = 0.0f;
return saturation;
}
2.4 アプリケーションメイン(main.c)
#include "stm32f10x.h"
#include "system_clock.h"
#include "serial_console.h"
#include "i2c_driver.h"
#include "spo2_sensor.h"
int main(void)
{
float current_spo2;
clock_setup();
console_init(115200);
i2c_master_init();
console_print("Pulse Oximeter System Boot\n");
spo2_initialize();
while(1) {
spo2_capture_sample();
if(measurement_ready) {
current_spo2 = spo2_calculate_value();
console_print("SpO2: %.1f%%\n", current_spo2);
measurement_ready = 0;
}
delay_ms(5);
}
}
3. 実装上の注意点と最適化
3.1 トラブルシューティング
| 症状 | 原因分析 | 対処方法 |
|---|---|---|
| 常時0%表示 | LED電流不足または指の配置不良 | LED電流値を増加(0x7Fが最大)し、指を正しく配置 |
| 値が不安定 | 外部光ノイズまたは接触不良 | 遮光カバーを装着し、指を適度に押し付ける |
| SpO2が90%未満 | 環境光干渉 | センサを完全に覆い、蛍光灯下での使用を避ける |
| FIFOオーバーフロー | サンプリング周期の不一致 | I2Cクロックを400kHzに設定、FIFO割り込みを有効化 |
3.2 キャリブレーション手法
医療機器並みの精度を得るには以下の手順でキャリブレーションを実施します:
- 基準測定:医療用パルスオキシメータとの同時測定で基準値を取得
- 係数調整:測定値が基準より高い場合は25.0f係数を増加、低い場合は減少させる
- 個体差補正: 異なる被験者で85-99%の範囲で複数点キャリブレーションを実施
3.3 ハードウェア設計のベストプラクティス
安定した測定を得るための設計指針:
- 遮光対策:黒色シリコン材質の指カバーが必須。環境光はPPG信号を完全に覆い隠す
- 電源設計:VIN近傍に100nFと10μFのコンデンサを並列配置。電源リップルは直接ノイズとなる
- 指の配置:爪を上向きにし、腹側をセンサレンズに軽く密着。過度な圧迫は血流を阻害する