MAX30102 펄스 옥시미터로 정밀 심박수 & SpO₂ 모니터링 - 아두이노 우노
부품 및 소모품
| 부품 |
수량 |
비고 |
| 아두이노 우노 |
1 |
|
| 아다프루트 OLED 128x32 |
1 |
I2C 인터페이스 |
| 부저 |
1 |
심박 동기 비프음용 |
| 맥심 MAX30102 고감도 펄스 옥시미터/심박 센서 |
1 |
웨어러블 헬스용, WAVGAT 모듈 등 변형 상관없음 (IC가 MAX30102면 됨) |
프로젝트 소개
소개
안녕하세요. 이 튜토리얼에서는 MAX30102 펄스 옥시미터/심박수 모니터 모듈을 아두이노 우노와 인터페이스하고, OLED 디스플레이와 부저를 활용해 BPM(분당 심박수) 측정 프로젝트를 만듭니다.
- BPM: 분당 심박수. 안정 시 일반인 65-75, 운동선수는 더 낮을 수 있음
- SpO₂: 산소 포화도. 일반인 95% 이상
구성품
사용 부품들
테스트
사용한 코드는 스파크펀(SparkFun) MAX3010x 라이브러리 예제들로 명확합니다. OLED와 부저용으로 만든 코드는 라이브러리의 "HeartRate" 예제를 수정해 손가락을 센서에 대라는 안내를 추가했습니다.
"손가락을 대세요" 안내 화면
손가락을 대고 가만히 있으면 부저 '삐-' 소리가 심박과 동기화되거나 OLED 애니메이션이 심박과 맞춰지며, 그때 정확한 BPM을 읽을 수 있습니다.
참고: 코드에서 평균 BPM을 출력합니다. 4회 BPM 평균이라 더 정확합니다. 약간 시간 주세요.
평균 BPM 표시 화면
OLED용 비트맵 만들기
심박 감지 시 작은 하트 → 큰 하트 비트맵 전환으로 박동 애니메이션 구현. 부저 소리와 동기화.
코드에 포함된 두 하트 비트맵 (32x32, 24x21 픽셀)
비트맵 생성 과정:
- 검은 그림/흰 배경 이미지 준비 (OLED 해상도 128x32, 비트맵은 이보다 작게)
- LCD Assistant 다운로드 및 실행
- 이미지 불러오기 → 설정 조정 (수평/수직 스캔 등) → C 배열로 내보내기
- 생성된 배열을 코드에 복사
LCD Assistant 설정 예시
생성된 16진수 배열
코드에서 호출 예시:
display.drawBitmap(5, 5, logo2_bmp, 24, 21, WHITE);
// display.drawBitmap(시작 x, 시작 y, 비트맵 이름, 너비, 높이, 색상);
손가락 감지 시 logo2(작은 하트), 심박 감지 시 logo3(큰 하트) 호출.
소스 코드
/* This code works with MAX30102 + 128x32 OLED i2c + Buzzer and Arduino UNO
* It displays the Average BPM on the screen, with an animation and a buzzer sound
* everytime a heart pulse is detected
* It's a modified version of the HeartRate library example
* Refer to www.surtrtech.com for more details or SurtrTech YouTube channel */
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"
MAX30105 particleSensor;
const byte RATE_SIZE = 4;
byte rates[RATE_SIZE];
byte rateSpot = 0;
long lastBeat = 0;
float beatsPerMinute;
int beatAvg;
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// 작은 하트 비트맵 (24x21)
static const unsigned char PROGMEM logo2_bmp[] = {
0x03, 0xC0, 0xF0, 0x06, 0x71, 0x8C, 0x0C, 0x1B, 0x06, 0x18, 0x0E, 0x02,
0x10, 0x0C, 0x03, 0x10, 0x04, 0x01, 0x10, 0x04, 0x01, 0x10, 0x40, 0x01,
0x10, 0x40, 0x01, 0x10, 0xC0, 0x03, 0x08, 0x88, 0x02, 0x08, 0xB8, 0x04,
0xFF, 0x37, 0x08, 0x01, 0x30, 0x18, 0x01, 0x90, 0x30, 0x00, 0xC0, 0x60,
0x00, 0x60, 0xC0, 0x00, 0x31, 0x80, 0x00, 0x1B, 0x00, 0x00, 0x0E, 0x00,
0x00, 0x04, 0x00
};
// 큰 하트 비트맵 (32x32)
static const unsigned char PROGMEM logo3_bmp[] = {
0x01, 0xF0, 0x0F, 0x80, 0x06, 0x1C, 0x38, 0x60, 0x18, 0x06, 0x60, 0x18,
0x10, 0x01, 0x80, 0x08, 0x20, 0x01, 0x80, 0x04, 0x40, 0x00, 0x00, 0x02,
0x40, 0x00, 0x00, 0x02, 0xC0, 0x00, 0x08, 0x03, 0x80, 0x00, 0x08, 0x01,
0x80, 0x00, 0x18, 0x01, 0x80, 0x00, 0x1C, 0x01, 0x80, 0x00, 0x14, 0x00,
0x80, 0x00, 0x14, 0x00, 0x80, 0x00, 0x14, 0x00, 0x40, 0x10, 0x12, 0x00,
0x40, 0x10, 0x12, 0x00, 0x7E, 0x1F, 0x23, 0xFE, 0x03, 0x31, 0xA0, 0x04,
0x01, 0xA0, 0xA0, 0x0C, 0x00, 0xA0, 0xA0, 0x08, 0x00, 0x60, 0xE0, 0x10,
0x00, 0x20, 0x60, 0x20, 0x06, 0x00, 0x40, 0x60, 0x03, 0x00, 0x40, 0xC0,
0x01, 0x80, 0x01, 0x80, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x60, 0x06, 0x00,
0x00, 0x30, 0x0C, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0x06, 0x60, 0x00,
0x00, 0x03, 0xC0, 0x00, 0x00, 0x01, 0x80, 0x00
};
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.display();
delay(3000);
particleSensor.begin(Wire, I2C_SPEED_FAST);
particleSensor.setup();
particleSensor.setPulseAmplitudeRed(0x0A); // 빨간 LED 낮게 설정 (센서 동작 표시)
}
void loop() {
long irValue = particleSensor.getIR();
if (irValue > 7000) { // 손가락 감지
display.clearDisplay();
display.drawBitmap(5, 5, logo2_bmp, 24, 21, WHITE);
display.setTextSize(2);
display.setTextColor(WHITE);
display.setCursor(50, 0);
display.println("BPM");
display.setCursor(50, 18);
display.println(beatAvg);
display.display();
if (checkForBeat(irValue) == true) { // 심박 감지
display.clearDisplay();
display.drawBitmap(0, 0, logo3_bmp, 32, 32, WHITE);
display.setTextSize(2);
display.setTextColor(WHITE);
display.setCursor(50, 0);
display.println("BPM");
display.setCursor(50, 18);
display.println(beatAvg);
display.display();
tone(3, 1000); // 부저 100ms 비프 (짧게 줄여도 됨)
delay(100);
noTone(3);
long delta = millis() - lastBeat;
lastBeat = millis();
beatsPerMinute = 60 / (delta / 1000.0);
if (beatsPerMinute < 255 && beatsPerMinute > 20) {
rates[rateSpot++] = (byte)beatsPerMinute;
rateSpot %= RATE_SIZE;
beatAvg = 0;
for (byte x = 0; x < RATE_SIZE; x++) beatAvg += rates[x];
beatAvg /= RATE_SIZE;
}
}
}
if (irValue < 7000) { // 손가락 미감지
beatAvg = 0;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(30, 5);
display.println("Please Place ");
display.setCursor(30, 15);
display.println("your finger ");
display.display();
noTone(3);
}
}
필수 라이브러리
회로도
두 모듈 모두 I2C 인터페이스. 2핀 부저(-)는 GND, (+)는 저항 거쳐 D3 핀에 연결.