코로나19 대응 오픈소스 맥박 산소 측정기 DIY: 아두이노 나노 + MAX30102
부품 및 소모품
| 부품 |
수량 |
비고 |
| 아두이노 나노 R3 |
1 |
|
| 맥심 MAX30102 맥박/심박 센서 |
1 |
I2C 인터페이스, 적외선/적색 LED 내장 |
| DFRobot Gravity I2C OLED 128x64 디스플레이 |
1 |
심박수(HR) 및 산소포화도(SpO2) 표시 |
필요 도구
앱 및 온라인 서비스
- 아두이노 IDE
- Autodesk Fusion 360 (케이스 설계)
프로젝트 소개
코로나19의 도전
COVID-19는 호흡기계를 주로 공격합니다. 경증은 발열, 오한 정도지만 폐렴으로 진행하면 호흡곤란이 옵니다. 병원이 포화 상태일 때 환자 스스로 산소포화도를 모니터링해 입원 시기를 판단할 수 있도록 저비용 오픈소스 맥박 산소 측정기를 개발했습니다.
⚠️ 중요 면책 조항
이 장치는 의료용 진단 기기가 아닙니다! 참고용으로만 사용하세요. 정확한 진단은 전문 의료 기기와 의료진 상담이 필요합니다.
전자 부품 상세
MAX30102 센서
반사형 맥박 산소 측정 원리: 적색(660nm)과 적외선(880nm) LED를 손가락에 조사하고, 혈액 산소포화도에 따른 흡수율 차이로 SpO2 계산. 심박수도 동시에 측정.

128x64 OLED 디스플레이
I2C 인터페이스, 선명한 텍스트 표시. HR과 SpO2 수치 실시간 출력.

아두이노 나노
소형 폼팩터, I2C 핀(A4=SDA, A5=SCL) 기본 제공. 5V/3.3V 모두 호환.

제작 단계별 가이드
1. 3D 프린팅 및 후가공
- 프로젝트 첨부 파일에서 STL 다운로드 후 슬라이서로 출력 (PLA, 인필 70-80%, 미디엄 서포트)
- 서포트 제거 후 사포로 다듬어 조립 시 간섭 없도록 함

2. 전자 회로 납땜
- 퍼프보드(44x30mm)에 아두이노 나노 장착
- MAX30102: VIN, GND, SDA(A4), SCL(A5) 배선 → 하우징 하단으로 배선 라우팅
- OLED: 나노에 커넥터 장착 후 상단 디스플레이까지 배선
- 완성된 전자부품 어셈블리를 하우징에 삽입, 3mm 나사로 고정

3. 기계적 조립
- OLED를 상단 커버에 부착 후 본체와 나사 결합
- 힌지 구조로 뚜껑 개폐 가능 (손가락 삽입용)

4. 스케치 업로드
- 필수 라이브러리:
Wire.h, MAX30105.h, spo2_algorithm.h, SSD1306Ascii.h, SSD1306AsciiWire.h
- 보드: 아두이노 나노, 프로세서: ATmega328P (구형 부트로더 시도 필요할 수 있음)
- 업로드 후 시리얼 모니터(115200 baud)로 디버깅 가능
5. 사용법
- 손가락 끝을 센서 위에 올리고 뚜껑을 살며시 닫아 밀착시킴
- 전원 연결 (USB 또는 배터리)
- 초기 보정(약 4초) 후 심박수(HR)와 산소포화도(SpO2) 표시됨
- 측정 중 손가락 움직임 최소화

소스 코드
/* 하드웨어 연결:
* -5V = 5V (3.3V도 가능) -GND = GND
* -SDA = A4 (또는 SDA) -SCL = A5 (또는 SCL)
* -INT = 미연결
* MAX30105는 5V 또는 3.3V I2C 로직 모두 지원. 5V 권장.
*/
#include
#include "MAX30105.h"
#include "spo2_algorithm.h"
#include "SSD1306Ascii.h"
#include "SSD1306AsciiWire.h"
MAX30105 particleSensor;
SSD1306AsciiWire oled;
#define MAX_BRIGHTNESS 255
#if defined(__AVR_ATmega328P__) || defined(__AVR_ATmega168__)
// 우노/나노는 SRAM 부족으로 32비트 대신 16비트 버퍼 사용
uint16_t irBuffer[50];
uint16_t redBuffer[50];
#else
uint32_t irBuffer[50];
uint32_t redBuffer[50];
#endif
int32_t spo2; int8_t validSPO2;
int32_t heartRate; int8_t validHeartRate;
void setup() {
Serial.begin(115200);
oled.begin(&Adafruit128x64, 0x3C);
oled.setFont(Arial14);
if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
Serial.println(F("MAX30105 not found. Check wiring/power."));
while (1);
}
// 설정: ledBrightness=55, sampleAverage=4, ledMode=2, sampleRate=200, pulseWidth=411, adcRange=4096
particleSensor.setup(55, 4, 2, 200, 411, 4096);
}
void loop() {
// 최초 50샘플 수집으로 신호 범위 결정
for (byte i = 0; i < 50; i++) {
while (!particleSensor.available()) particleSensor.check();
redBuffer[i] = particleSensor.getRed();
irBuffer[i] = particleSensor.getIR();
particleSensor.nextSample();
Serial.print("red="); Serial.print(redBuffer[i]);
Serial.print(", ir="); Serial.println(irBuffer[i]);
}
// 최초 HR/SpO2 계산
maxim_heart_rate_and_oxygen_saturation(irBuffer, 50, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
// 지속적 측정 루프 (1초마다 갱신)
while (1) {
// 오래된 25샘플 버림, 최신 25샘플을 앞쪽으로 시프트
for (byte i = 25; i < 50; i++) {
redBuffer[i - 25] = redBuffer[i];
irBuffer[i - 25] = irBuffer[i];
}
// 새 25샘플 수집
for (byte i = 25; i < 50; i++) {
while (!particleSensor.available()) particleSensor.check();
redBuffer[i] = particleSensor.getRed();
irBuffer[i] = particleSensor.getIR();
particleSensor.nextSample();
// 시리얼 디버그 출력
Serial.print("red="); Serial.print(redBuffer[i]);
Serial.print(", ir="); Serial.print(irBuffer[i]);
Serial.print(", HR="); Serial.print(heartRate);
Serial.print(", HRvalid="); Serial.print(validHeartRate);
Serial.print(", SPO2="); Serial.print(spo2);
Serial.print(", SPO2Valid="); Serial.println(validSPO2);
}
// 이동 평균으로 HR/SpO2 재계산
maxim_heart_rate_and_oxygen_saturation(irBuffer, 50, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
printToScreen();
}
}
void printToScreen() {
oled.clear();
oled.setCursor(0, 0);
if (validSPO2 && validHeartRate) {
oled.print(F("HR: ")); oled.println(heartRate, DEC);
oled.print(F("SPO2: ")); oled.println(spo2, DEC);
} else {
oled.print(F("Not valid"));
}
}
회로도

커스텀 파트 및 인클로저
3D 프린터용 STL 파일은 프로젝트 페이지 첨부 파일에서 다운로드 가능. PLA 권장, 서포트 포함 출력.