아두이노 프로젝트 6. 심장 박동 측정: 두 판 사이의 차이

아두위키 : Arduwiki
잔글편집 요약 없음
 
214번째 줄: 214번째 줄:
라이브러리를 모두 설치하셨다면 아래 코드를 아두이노에 업로드해주세요.
라이브러리를 모두 설치하셨다면 아래 코드를 아두이노에 업로드해주세요.


아두이노 IDE에 코드를 옮기신 후 화살표키(아래 사진 참고)를 누르거나 '''단축키 CTRL + U'''를 입력하시면 됩니다.
아두이노 IDE에 코드를 옮기신 후 '''화살표키'''(아래 사진 참고)를 누르거나 '''단축키 CTRL + U'''를 입력하시면 됩니다.


[[파일:아두이노업로드 버튼.png]]
[[파일:아두이노업로드 버튼.png]]

2025년 5월 17일 (토) 15:35 기준 최신판


개요

심장 박동 측정하기

손가락에 심장 박동 측정 센서를 끼워 심장 박동을 측정하고, 파형을 그립니다.

고려사항

  • 심박 펄스 센서를 활용해 심박을 측정하고, raw data를 활용해 bpm을 계산합니다.
  • 계산된 bpm과 bpm을 활용한 파형을 OLED에 표시해 심박을 확인합니다.
  • 12bit 네오픽셀을 활용해 심박 구간에 따라 다른 색깔로 표시되는 게이지를 표현합니다.

사용 하드웨어


배경과 원리

💓 심장의 박동

  • 심장은 피를 온몸으로 보내기 위해 수축과 이완을 반복합니다.
  • 이를 심장 박동(심박)이라고 하며, 분당 박동 수를 BPM (Beats Per Minute)이라고 합니다.
    • 예: 1분에 75번 뛰면 BPM = 75

🔌 심장 박동은 전기적 신호

  • 심장이 뛰면 혈관 속 피가 펄럭이듯 움직이고, 피의 양이 순간적으로 늘거나 줄어듭니다.
  • 이 변화를 심박 펄스 센서(Pulse Sensor)가 감지합니다.
💡 Pulse Sensor의 작동 원리
  • 센서에 LED와 빛의 반사를 감지하는 포토 다이오드가 있습니다.
  • 피가 많아지면 더 많은 빛이 흡수되고, 피가 적어지면 덜 흡수됩니다.
  • 이 변화가 아날로그 전압으로 출력됩니다.

📈 센서 값으로 박동을 인식하는 원리

  • 센서는 계속 전압 값을 출력하고, 전압 값의 변화 패턴을 분석해서 심장이 한 번 뛸 때를 감지합니다
  • 평소보다 갑자기 센서 값이 크게 올라가면(이 때의 기준 값을 미리 체크합니다) → 심장이 뛴 순간이라고 판단합니다.
  • 예: 0.8초마다 뛴다면 → BPM = 60 ÷ 0.8 = 75

🧠 센서 출력값 보정

  • 센서가 읽는 값은 피부 떨림, 손 움직임, 빛 세기 변화 등에 의해 노이즈(잡음)가 섞일 수 있습니다.
  • 그래서 최근 10개 값의 평균을 내서 부드럽게 처리하고, 값이 너무 갑자기 바뀌면 무시하는 기준값(maxValue)필터링을 계산 과정에 포함합니다.

🖥️ OLED 디스플레이와 💡 네오픽셀

OLED

  • 아주 작은 화면으로, 전류로 픽셀 하나하나를 켜서 그림을 그릴 수 있습니다.
  • 본 문서는 0.96inch, 128 x 64개의 픽셀이 있는 OLED를 사용합니다.

네오픽셀

  • RGB LED가 여러 개 이어진 조명 링 또는 줄입니다.
  • 원하는 색상과 밝기를 하나씩 제어 가능합니다.
  • 이 프로젝트에서는 BPM이 높을수록 더 많은 LED가 초록 - 노랑 - 주황 - 빨강 순으로 켜집니다.


제작과정

1. MDF 확인

4개의 MDF 판이 준비되어 있습니다.

class=coders70

2. OLED 케이블

먼저 OLED에 연결된 케이블을 상판 홀에 통과시켜주세요. OLED는 아직 붙이지 않으셔도 됩니다.

class=coders70

3. 네오픽셀, OLED 결합

네오픽셀을 그림에 맞춰 부착(연결된 케이블이 위로 향하도록)하신 후 OLED를 네오픽셀 안에 부착해주세요. 부품마다 양면테이프가 붙어있습니다.

class=coders70

4. 아두이노 나노, 브레드보드 준비

회로를 구성하기에 앞서 먼저 아래 그림과 같이 아두이노 나노를 미니 브레드보드에 꽂아주세요.

손을 다치지 않게 조심하고, 나노의 평평한 면을 눌러서 꽂아주시면 됩니다.

class=coders70

5. 회로 구성

아무것도 없는 깨끗한 하트 판에 브레드보드를 부착해주세요.

class=coders70


심박 펄스 센서의 -, +, S 위치는 다음과 같습니다.

class=coders70


아래 표와 회로도에 따라 회로를 구성해주세요.

아두이노 나노 심박 펄스 센서 OLED 네오픽셀
A0 S
A4 SCK
A5 SCA
D6 DI
5V + VDD 5V
GND - GND GND

class=coders70

6. 옆 면 조립

남아있는 옆 면을 활용해 MDF판을 모두 결합해주세요.

class=coders70


조립 시에 케이블은 동그랗게 잘 말아서 정리해주세요. 동봉된 핀을 홀에 꽂아주면 완성입니다.

class=coders70


아두이노 IDE

아두이노(Arduino) IDE(통합 개발 환경)는 아두이노 마이크로컨트롤러를 프로그래밍하기 위한 소프트웨어 환경을 제공하는 툴입니다.

아두이노는 오픈 소스 하드웨어 플랫폼으로, 다양한 프로젝트에서 사용되는 간단한 컴퓨팅 장치를 제작하고 프로그래밍하는 데 사용됩니다.

C++를 기반으로 합니다.

설치

1. 아두이노 홈페이지 접속

2. Software 탭 클릭

가운데|class=coders100

3. 원하는 버전 다운로드

  • 2021년 아두이노 IDE 2.0 버전이 출시되면서 기존의 1.x 버전, 새로운 2.x 버전 다운로드가 가능합니다.
  • 2.x 버전에서 여러가지 편의기능들이 추가되었지만 기존의 1.x 버전이 익숙하신 분, 1.x 버전으로 수록된 책이나 참고 자료를 그대로 따라하고 싶으신 분들은 1.x 버전을 그대로 사용하셔도 무방합니다. ※ 본 문서는 2.x 버전으로 작성되었습니다.
  • 사용 중인 운영체제에 맞는 버전으로 설치파일을 다운로드 받은 후, 실행하여 설치해주세요.

가운데|class=coders100


사용

1. 설치된 아두이노 IDE를 실행합니다.

2. 컴퓨터(혹은 노트북)에 아두이노 보드를 연결합니다.

3. 아두이노 정품 보드가 아닌 호환 보드의 경우 별도로 드라이버를 설치하셔야 합니다. 드라이버 설치 가이드

※ 창원남산고 : 드라이버 설치 필요합니다.

4. 아두이노 IDE에서 아두이노 보드와 포트를 설정합니다.

※ 창원남산고 : Arduino Nano 가운데|class=coders100


  • 보드는 현재 사용 중인 아두이노 보드의 종류에 맞게 선택해주시면 됩니다.
  • 포트 번호를 잘 모르시는 경우에는 장치 관리자에서 확인해주시면 됩니다.

가운데|class=coders100


라이브러리 설치

이번 코드를 사용하기 위한 라이브러리를 설치합니다.

아두이노 IDE의 좌측 세 번째 아이콘(스크린샷 참고)이 라이브러리 관리자입니다.

반드시 정확한 라이브러리를 설치해주세요.

Install 버튼 클릭 후 뜨는 창에서는 Install All 버튼을 클릭하시면 됩니다.


1. SSD1306

class=coders50


2. Neopixel

class=coders50


아두이노 코드

라이브러리를 모두 설치하셨다면 아래 코드를 아두이노에 업로드해주세요.

아두이노 IDE에 코드를 옮기신 후 화살표키(아래 사진 참고)를 누르거나 단축키 CTRL + U를 입력하시면 됩니다.

파일:아두이노업로드 버튼.png

// 파형 그래프 범위, 딜레이 및 BPM 유효 범위 설정
#define BPM_MIN 40
#define BPM_MAX 180

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>

// OLED 디스플레이 초기 설정
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// 심박센서 아날로그 입력 핀
const int pulsePin = A0;

// 네오픽셀 설정 (핀 번호, LED 개수)
#define NEOPIXEL_PIN 6
#define NUM_LEDS 12
Adafruit_NeoPixel pixels(NUM_LEDS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

// OLED 상에서 실시간 파형 그래프 그리기용 변수
int graphX = 0;            // 현재 x좌표
int lastY = SCREEN_HEIGHT - 1;  // 이전 y좌표
int prevBpm = 0;           // 이전에 측정된 BPM

// 심박센서 데이터 평균용 큐 최대 크기
#define MAX_QUEUE_SIZE 100

// 원형 큐 구조체 정의: 아날로그 입력값 평균 산출
struct CircularQueue {
  int data[MAX_QUEUE_SIZE];
  int front = 0;
  int rear = 0;
  int count = 0;

  bool isFull() {
    return count == MAX_QUEUE_SIZE;
  }

  bool isEmpty() {
    return count == 0;
  }

  bool enqueue(int value) {
    if (isFull()) return false;
    data[rear] = value;
    rear = (rear + 1) % MAX_QUEUE_SIZE;
    count++;
    return true;
  }

  int dequeue() {
    if (isEmpty()) return -1; // or any sentinel value
    int value = data[front];
    front = (front + 1) % MAX_QUEUE_SIZE;
    count--;
    return value;
  }

  int peek() {
    if (isEmpty()) return -1;
    return data[front];
  }

  int size() {
    return count;
  }
};

long avg = 0; // 최근 10개의 심박 raw 데이터를 평균한 값

int getBPM() {
  static long sum = 0;                      // 평균 계산용 합계
  static CircularQueue q;                   // 최근 raw 데이터 저장용 큐
  static unsigned long lastBeatTime;        // 마지막 심박 탐지 시각
  static unsigned long pretime;             // maxValue 갱신 주기 체크용 시간

  static long maxValue = 500;               // 기준 맥박 강도 (dynamic threshold)
  static long tmpMaxValue = 0;              // 5초간 측정된 최고 평균값 (기준 업데이트용)
  long bpm = -1;

  int rawValue = analogRead(pulsePin);       // 심박센서 아날로그 읽기
  
  sum += rawValue;                           // 합계 누적
  q.enqueue(rawValue);                       // 큐에 추가
  if(q.size() >= 10){
    sum -= q.dequeue();                      // 10개 유지: 가장 오래된 값 제거
    avg = sum / 10;                          // 평균 계산
  }  
  
  unsigned long currentTime = millis();

  // 평균값이 max 기준값의 90% 이상이면 심박으로 간주
  if(avg >= maxValue * 0.9){
    unsigned long interval = currentTime - lastBeatTime;

    // 500~2000ms 범위면 유효한 심박 (30~120 BPM 범위)
    if (interval > 500 && interval < 2000) {
      bpm = 60000 / interval;      
      lastBeatTime = currentTime;
    }

    // 2.5초 이상 지나면 맥박 놓친 것으로 간주하고 시간만 리셋
    if(interval > 2500){
       lastBeatTime = currentTime;
    }
  }

  // 5초 주기로 maxValue 갱신
  if(currentTime - pretime > 5000){
    pretime = currentTime;
    maxValue = tmpMaxValue;    // 최근 5초간의 최대 평균값을 기준으로 설정
    tmpMaxValue = 0;           // 임시값 초기화
  }

  // 평균이 기존보다 크면 임시 최대값 갱신
  if(tmpMaxValue < avg){
    tmpMaxValue = avg;
  }

  return bpm;
}

// BPM에 따라 네오픽셀 색상 및 점등 개수 조정
void updateNeopixelGauge(int bpm) {
  int ledsToLight = 0;
  if (bpm < 60) ledsToLight = 3;
  else if (bpm < 90) ledsToLight = 6;
  else if (bpm < 120) ledsToLight = 9;
  else ledsToLight = 12;

  for (int i = 0; i < NUM_LEDS; i++) {
    if (i < ledsToLight) {
      uint32_t color;
      if (i < 3) color = pixels.Color(0, 40, 0);         // 초록
      else if (i < 6) color = pixels.Color(30, 30, 0);   // 노랑
      else if (i < 9) color = pixels.Color(40, 15, 0);   // 주황
      else color = pixels.Color(40, 0, 0);               // 빨강
      pixels.setPixelColor(i, color);
    } else {
      pixels.setPixelColor(i, 0);
    }
  }
  pixels.show();
}

void setup() {

  Serial.begin(9600);

  // OLED 디스플레이 초기화
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.display();

  // 네오픽셀 초기화
  pixels.begin();
  pixels.clear();
  pixels.setBrightness(30);  // 밝기 낮춤
  pixels.show();
}

void loop() {
  unsigned long currentTime = millis();
  Serial.println(avg);

  // 파형 y좌표 맵핑 (센서 출력값을 OLED 화면 범위에 맞춤)
  int y = map(avg, 450, 470, 63, 20);
  y = constrain(y, 20, 63);

  // 실시간 그래프 그리기
  display.drawLine(graphX, lastY, graphX + 1, y, SSD1306_WHITE);
  lastY = y;
  graphX++;

  // 화면 오른쪽 끝에 도달하면 그래프 영역 클리어
  if (graphX >= SCREEN_WIDTH) {
    graphX = 0;
    lastY = 63;
    display.fillRect(0, 20, SCREEN_WIDTH, 44, SSD1306_BLACK);
  }

  int ans = getBPM();
  if(ans != -1){
    int bpm = ans;

    // BPM 값이 유효 범위 내에 있고 이전 BPM과의 차이가 크지 않은 경우만 표시
    bool isValid = (bpm >= BPM_MIN && bpm <= BPM_MAX);
 
    if (isValid) {
      prevBpm = bpm;

      // 상단 텍스트 영역 클리어 및 하트 + BPM 출력
      display.fillRect(0, 0, SCREEN_WIDTH, 20, SSD1306_BLACK);
      display.setTextSize(2);
      display.setCursor(0, 0);
      display.write(3);  // ♥ 출력
      display.print(" BPM: ");
      display.print(bpm);
      Serial.print("-------------------bpm : ");
      Serial.println(bpm);

      // 네오픽셀 업데이트
      updateNeopixelGauge(bpm);
    }
  }

  // 디스플레이 출력 적용
  display.display();
  delay(10);
}

심박 펄스 센서 장착

검지손가락의 손바닥 면을 심박 펄스 센서에 덮고 벨크로를 활용해 고정해주세요.

지긋이 눌러주는 느낌으로 조여주시면 됩니다.

혈류량에 따라 값이 다를 수 있기 때문에 검지 외에 중지로 측정하셔도 괜찮습니다.


class=coders50


결과

OLED의 플리커링 현상으로 인해 사진 상으로 잘 담기지 않아 그림으로 대체합니다.


class=coders50

심박수 측정

심박 펄스 센서를 통해 심박수를 측정하여 OLED에 출력합니다.

아래 파형을 통해 심박을 함께 확인할 수 있습니다.

BPM에 따른 네오픽셀 점등

심박수에 따라 네오픽셀이 점등되어 게이지의 역할을 합니다.

심박 구간에 따라 각기 다른 색의 LED가 켜집니다.