이 프로젝트는 CapitanoRed가 유튜브에 올린 영상에서 영감을 받았습니다. 포토레지스터 기반 심박수 모니터를 만들어 오실로스코프에 파형을 표시하는 내용이었습니다. 영상을 보고 이 프로젝트를 재현하면서 아두이노를 추가해 심박수를 계산하고 표시하도록 했습니다. 이렇게 하면 오실로스코프로 파형을 보면서 동시에 심박수 수치도 확인할 수 있습니다.
심장이 수축해 혈액을 온몸으로 보내면 일시적인 혈압 변동이 생깁니다. 이 때문에 맥박을 느낄 수 있습니다. 피부와 살이 충분히 얇은 부위에서는 이 맥박이 투과하는 빛의 미세한 변화로 감지됩니다. 우리 눈은 빛이 몸을 투과하는 것조차 볼 수 없고 그 미세한 변동은 더더욱 볼 수 없지만, 포토레지스터는 이 정도의 감도를 가집니다.
포토레지스터는 입사광 강도에 따라 저항값이 변합니다. 조도 변화가 미세해 저항 변화도 작지만, 오피앰프 IC 몇 개로 증폭할 수 있습니다.
동작 원리
아래 다이어그램은 이 프로젝트에 사용된 회로도와 브레드보드 배선입니다.
포토레지스터는 20kΩ 저항과 전압 분배 회로를 이룹니다. 포토레지스터에 입사하는 빛이 늘어나면 분배 전압도 올라갑니다. 첫 번째 오피앰프('Amp1')는 전압 분배기의 필터 역할을 하며 고주파 노이즈를 제거합니다. 두 번째 오피앰프('Amp2')는 반전 증폭기로 설정해 필터를 통과한 신호를 최대화합니다. 세 번째 오피앰프('Amp3')는 가상 접지(virtual ground)를 설정해 신호를 2.5V 중심으로 배치합니다. 이렇게 하면 오피앰프가 0V~5V 전체 범위에서 최대 신호 스윙을 제공할 수 있습니다. 필터링과 증폭 후 신호는 아래와 같은 형태가 됩니다.
아두이노가 심박수를 측정하려면 신호가 비교기('Cmp')를 통과해야 합니다. 비교기는 출력을 하이(5V) 또는 로우(0V) 둘 중 하나로만 내는 특수 오피앰프입니다. 양단 입력이 음단 입력보다 높으면 하이 출력, 낮으면 로우 출력. 기본 구성에서 비교기는 임계값 검출기로 동작해 측정된 전압이 임계값 위인지 아래인지 알려줍니다. 하이/로우만 출력하므로 아두이노 디지털 핀과 인터페이스하기에 완벽합니다.
오피앰프 출력 파형을 자세히 보면, 전압이 내려가기 전에 이차 펄스(이중맥, dicrotic notch)가 나타납니다. 또한 노이즈가 상당히 많습니다. 이 두 가지 때문에 기본 비교기 구성으로는 맥박을 제대로 감지할 수 없습니다. 매 심박마다 단일 사각 펄스가 나와야 하는데, 노이즈 때문에 상승/하강 에지에서 임계값을 여러 번 넘나들고, 임계값 설정에 따라 이중맥 구간에서도 펄스가 발생할 수 있습니다. 결과적으로 아두이노가 실제보다 훨씬 많은 펄스를 카운트하게 됩니다.
노이즈가 많은 신호는 히스테리시스(hysteresis)로 해결할 수 있습니다. TI(텍사스 인스트루먼트)의 이 문서는 히스테리시스를 이용한 신호 컨디셔닝을 훌륭하게 다루고 있으며, 노이즈가 비교기 성능에 미치는 영향과 해결책을 보여줍니다. 5페이지와 7페이지의 회로도와 수식을 참고해 이 프로젝트용 비대칭 비교기를 설계했습니다. 핵심 아이디어는 출력에서 피드백 루프를 통해 양단 입력 전압을 변화시켜, 로우→하이 임계값과 하이→로우 임계값을 다르게 만드는 것입니다. 심박 맥락에서는 상승 에지의 한 지점에서 트리거하고, 하강 에지의 이중맥 이후 다른 지점에서 트리거되도록 설정할 수 있습니다. 이렇게 하면 아두이노는 심박마다 단일 사각 펄스를 보게 됩니다.
포토레지스터 준비
포토레지스터에 투명 열수축 튜브 등 투명 커버를 씌우는 것을 강력 권장합니다. 최소한 리드가 완전히 덮여 피부에 닿지 않게 하세요. 인체에서 발생하는 전압이 포토레지스터가 맥박 감지 시 생성하는 전압 범위와 겹치므로, 피부 접촉 시 결과가 왜곡될 수 있습니다.
심박수 모니터 튜닝
회로도의 가변저항 R7에는 싱글턴 가변저항을 사용하고, 오피앰프 게인이 포화되지 않는 선에서 최대화되도록 조정하세요. 먼저 가변저항 한 쪽을 약 375Ω로 설정하고 필터('Amp1')를 이쪽에 연결합니다. 이 정도 게인이면 WaveForms Live에서 펄스를 볼 수 있을 만큼 신호가 나옵니다. 다음 섹션('WaveForms Live에서 펄스 보기') 단계를 따라 펄스를 성공적으로 확인한 후, 필요시 게인을 조정하세요. 펄스 진폭을 키우려면 가변저항을 돌려 필터 쪽 저항을 줄이세요. 진폭이 너무 커 클리핑이 발생하면 필터 쪽 저항을 늘리세요. 다음 이미지들의 캡션을 참고해 바람직한 신호 형태를 확인하세요.
비교기 임계값 설정용 저항 R3, R4에는 정밀 조정을 위해 멀티턴 가변저항 사용을 권장합니다. 저항 R5는 10~100kΩ 범위 내 아무 저항이나 정확한 실측값을 알면 사용 가능합니다. 첨부된 스프레드시트에서 R5 실측값과 임계값 전압을 입력하면 R3, R4 설정값을 계산해줍니다. 임계값 'Vl'과 'Vh'는 오실로스코프에서 본 펄스 형태에 따라 조정해야 합니다('비교기 임계값 설정' 섹션 참조).
저항 R8, R9는 하나의 가변저항으로 대체 가능하며, 중간 핀을 오피앰프 양단 입력에 연결하면 가상 접지를 쉽게 조정할 수 있습니다. 전압계나 OpenScope로 출력 전압이 2.5V가 되도록 조정하세요.
WaveForms Live에서 펄스 보기
WaveForms Live에서 심박 파형을 보려면 몇 가지 설정을 변경해야 합니다. 기본 설정에서는 파형이 너무 길게 늘어나 해석하기 어렵거나, 업데이트 속도가 너무 느려 포토레지스터에 손가락을 대는 압력을 조절하며 맑은 펄스를 얻기 어렵습니다.
OpenScope 오실로스코프 채널 2(파란 선)를 'Amp2' 출력에 연결하고, 그라운드 선도 브레드보드 그라운드에 연결하세요. WaveForms Live에서 Time을 '1s'로, Trigger 메뉴에서 OFF 버튼을 누르세요. Osc Ch1과 Osc Ch2 메뉴 모두 Offset을 2.5V로, Samples 옆 자물쇠 아이콘을 클릭하고 나타나는 필드에 '1000'을 입력하세요. 이렇게 하면 해석하기 쉬운 타임스케일로 표시되면서 기본보다 자주 업데이트됩니다(약 4초마다). 여전히 느리면 Samples 값을 늘리세요(단, 신호 구간이 짧아짐. '2000'이면 심박 1개 정도만 캡처).
RUN 버튼을 누르고 포토레지스터에 손가락을 대고 맥을 짚으세요. 일관된 결과를 얻으려면 최적의 압력과 위치를 찾아야 합니다. 시스템이 압력 변화에 매우 민감하므로 손가락을 아주 가만히 유지하는 요령이 필요합니다. 저는 검지 첫 번째 마디에서 맥을 잡는 게 가장 잘 맞았습니다. 익숙해지는 데 시간이 좀 걸리지만 결국 최적의 방법을 찾게 됩니다. 펄스가 너무 작으면 앞 섹션('심박수 모니터 튜닝') 첫 문단대로 가변저항을 조정하세요.
비교기 임계값 설정
오실로스코프에 신호가 보이면 비교기가 트리거할 임계값을 설정해야 합니다. 대표적인 파형을 캡처해 화면에 고정하세요. 화면 하단의 CURSORS 버튼을 누르세요. Type에서 'Voltage' 선택, 두 CursorChannels 모두 'Osc 2'로 설정. 수평 점선 2개가 나타납니다. 왼쪽 삼각형을 드래그해 이동하세요. 한 줄은 펄스 피크 근처에, 다른 줄은 이중맥 아래쪽에 위치시킵니다. 화면 하단 괄호 안의 전압값 두 개를 읽으세요. 이 값들을 첨부 스프레드시트의 임계값 'Vl'과 'Vh'에 입력합니다. 작은 값이 'Vl', 큰 값이 'Vh'입니다. 이 값들과 선택한 R5 저항값을 바탕으로 스프레드시트가 계산한 R3, R4 가변저항 값을 설정하세요.
비교기 설정이 끝나면 '포토레지스터 준비' 섹션 앞의 파형과 비슷한 출력이 나와야 합니다.
아두이노 코드
아두이노 코드는 주파수 카운터와 분당 심박수(BPM) 계산 로직으로 구성됩니다. 주파수 카운터는 비교기에서 들어오는 펄스 폭을 고려해 200ms 미만 또는 800ms 초과 펄스는 거부합니다. 이렇게 하면 포토레지스터를 사용하지 않을 때 비교기 출력이 하이, 로우, 또는 급속히 토글되는 상황에서 허위 데이터 표시를 방지합니다. 코드는 지난 15초간의 심박수 이동 평균을 유지해 우발적 움직임으로 인한 노이즈로 펄스가 누락되는 경우를 보정합니다.
향후 개선 방향
이 프로젝트의 주요 단점은 포토레지스터를 잡아 펄스 진폭을 매번 일정하게 유지하기 어렵다는 점입니다. 주변광에 의존해 펄스를 감지하므로 하루 중 조도 변화에 따라 결과가 달라집니다. 흐린 날엔 비교기 임계값이 너무 넓고, 맑은 날엔 이중맥이 두드러져 허위 펄스 감지가 발생합니다. 심지어 방 안의 미세한 움직임으로 인한 조도 변화도 포토레지스터가 감지합니다. 일관성 문제는 몇 가지 방법으로 해결 가능합니다.
센서 시스템에 LED를 추가해 일정한 광원을 제공할 수 있습니다. 병원의 맥박 측정 클립이나 스마트폰 심박 센서와 유사한 방식입니다. 적색 LED를 손가락 위에, 포토레지스터를 아래에 대고 테스트해보니 결과가 좋아 보였습니다. 좋은 인클로저만 만들면 실용적일 것입니다.
소프트웨어적으로는 아두이노에 오토 레인지 알고리즘을 구현하는 방안이 있습니다. 펄스를 감지해 피크와 트러프를 찾습니다. 하지만 이것만으로는 심박수 측정이 부족합니다. 제대로 된 주파수 카운터에는 인터럽트가 필요합니다. 인터럽트 없으면 아두이노 프로세서가 핀 체크 외 다른 작업을 하다 펄스를 놓칠 수 있습니다. 인터럽트는 디지털 핀에서만 가능하므로, 디지털 가변저항을 사용해 오토 레이징을 구현하는 게 최선입니다. 아두이노가 아날로그 핀으로 펄스 범위를 감지하고, 비교기 임계값을 결정해 디지털 가변저항으로 적용한 뒤, 비교기의 디지털 신호로 주파수 카운트를 수행하는 방식입니다.
소스 코드
comparator calcs.xlsx – 비교기 비대칭 임계값 계산을 위한 엑셀 스프레드시트. R5 열을 실측 저항값으로 변경. Vl, Vh를 맑은 펄스 얻는 데 필요한 임계값으로 변경.
Arduino Heart Rate Counter – 비교기 펄스로 심박수 계산하는 아두이노 코드
comparator calcs.xlsx (아두이노)
비교기의 비대칭 임계값 설정을 위한 R3, R4 값 계산 엑셀 시트. R5 열을 실측 저항값으로 변경. Vl, Vh를 심박 펄스 정합에 필요한 임계값으로 변경.
미리보기 불가 (다운로드만 가능).
Arduino Heart Rate Counter (아두이노)
비교기가 보내는 펄스 기반으로 심박수 계산.
#define INT0 3
float frequency;
long timeCount;
int counter;
long pulseStart;
int inputPin = 3;
boolean low;
float freqAvg;
float total;
void setup() {
attachInterrupt(digitalPinToInterrupt(INT0), intrruption, CHANGE);
counter = 0;
timeCount = 0;
frequency = 0;
low = false;
Serial.begin(9600);
total = 15;
}
void loop() {
timeCount = millis();
while (millis() - timeCount < 5000) {
frequency = counter;
}
if (frequency > 3) {
total += frequency;
total -= freqAvg;
freqAvg = total / 3;
}
showHR();
counter = 0;
}
void intrruption() {
if (digitalRead(3) == 0) {
fallDetect();
low = true;
} else if (digitalRead(3) == 1) {
widthCheck();
}
}
void fallDetect() {
pulseStart = millis();
}
void widthCheck() {
long pulseEnd = millis();
if ((pulseEnd - pulseStart > 200) && (pulseEnd - pulseStart < 800) && low) {
counter++;
low = false;
}
}
void showHR() {
Serial.print("Heart rate = ");
Serial.println(freqAvg * 12);
}