Самодельный точный датчик оксиметра: ИК, Arduino, LCD и руководство по компонентам
Компоненты и материалы
| ИК-излучатель, 18° | Длина волны 805–1000 нм | ×1 |
| Фотодиод LPT80A | Широкодиапазонный, должен принимать красный (600 нм) и ИК (940 нм) сигналы | ×1 |
| Красный светодиод 5 мм | Длина волны около 600 нм | ×1 |
| Резистор 10 кОм | ×1 | |
| Резистор 330 Ом | ×2 | |
| I2C LCD-дисплей 16×2 (DFRobot или аналог) | Для вывода значений | ×1 |
| Arduino UNO | ×1 | |
| Перемычки (набор) | ×1 | |
| Высокойяркостный белый светодиод | Опционально, для улучшения стабильности измерений | ×1 |
Необходимые инструменты
- Паяльник с наконечником «Gull Wing»
Приложения и онлайн-сервисы
- Arduino IDE
О проекте
В период изоляции я собрал оксиметр из имеющихся деталей. Ведь оксиметр по сути состоит из двух светодиодов и фотодиода.
Я не эксперт в медицине, и на данном этапе проект не имеет диагностической ценности, но это отличное обучающее руководство для понимания принципа работы. С небольшими доработками он может стать полноценным медицинским прибором.
Насыщение кислородом и COVID-19
Мы узнали, что одним из способов оценки дыхательной недостаточности является измерение кислорода в крови. Это делает пульсоксиметр — неинвазивный прибор, надеваемый на палец. Нормальное насыщение (SpO₂) — 95% и выше. Падение ниже 90% при кашле и температуре — повод для беспокойства. Если каждый мейкер сможет собрать оксиметр, раннее обнаружение инфекции станет доступнее, а люди смогут принимать взвешенные решения об обращении к врачу.
Сначала разбираемся с датчиком пульса
Начался проект с модуля KY-039 (ИК-светодиод + фотодиод + два резистора). Пальцем перекрывается луч, и пульсация крови меняет поглощение света. Сигнал с фотодиода считывается через вывод S.
Если нет KY-039, датчик легко собрать самостоятельно из пары светодиодов, фотодиода и трёх резисторов.
Измерение пульса по пикам сигнала
Сигнал зашумлен и слаб. Используется скользящее усреднение и детектирование восходящих фронтов (rise_threshold). Найдя пики, считаем их количество или интервалы для расчёта BPM (ударов в минуту).
Сборка оксиметра (модификация KY-039)
Кровь по-разному поглощает красный (~600 нм) и ИК (~950 нм) свет. Сравнивая показания, можно найти SpO₂. В KY-039 был только ИК-светодиод — добавил красный, отключил ИК от питания и подключил оба к разным пинам Arduino через резисторы 330 Ом.
Схему можно собрать с нуля — это просто два светодиода, фотодиод и три резистора.
Поочерёдно включаем ИК и красный светодиоды, считываем сигнал с одного фотодиода. На графике видно, что ИК-сигнал ниже красного. Важно правильно положить палец: светодиоды к ногтю, фотодиод снизу. Не двигайте пальцем и не меняйте освещение — даже тень искажает данные.
Как рассчитывается SpO₂%
SpO₂ — функция параметра R (по статье из Политехника Милано):
R = ((REDmax - REDmin) / REDmin) / ((IRmax - IRmin) / IRmin)
Нужна калибровка для связи R и SpO₂. Определяем период пульса (мс), делим на частоту дискретизации (40 мс — 20 мс на каждый светодиод), анализируем последние L отсчётов для поиска минимумов и максимумов. R пересчитывается каждый удар.
Калибровка: от R к SpO₂%
Зависимость приближается прямой: SpO₂ = K × R + M. Для нахождения K и M нужен эталонный оксиметр. Два замер: нормальное дыхание и задержка дыхания (SpO₂ падает, R растёт). Решаем систему уравнений. После калибровки прибор показывает BPM и SpO₂. Значения выводятся на LCD, если найдено ≥5 стабильных периодов (разброс ≤10%). Параметр c — число стабильных замеров.
Улучшение: устранение влияния внешнего света
Измерения сильно зависели от освещения. Добавил третий (белый) светодиод, который постоянно светит на палец, а весь сенсор закрыл непрозрачной тканью. Результаты стали стабильнее. Пришлось перекалибровать — теперь прибор быстро и точно показывает BPM и SpO₂%.
Код (oximeter-diy-ver-0.92.ino)
/* * an oximeter diy. v.0.92 (minor fixes) * by hacking a ky-039 heartbeat sensor or using an infrared led * a red led and a photodiode. * https://hackaday.io/project/170752-oximeter-do-it-yourself */
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#define maxperiod_siz 80
#define measures 10
#define samp_siz 4
#define rise_threshold 3
LiquidCrystal_I2C lcd(0x3F, 16, 2);
int T = 20;
int sensorPin = A1;
int REDLed = 3;
int IRLed = 4;
byte sym[3][8] = {
{ B00000, B01010, B11111, B11111, B01110, B00100, B00000, B00000 },
{ B00000, B00000, B00000, B11000, B00100, B01000, B10000, B11100 },
{ B00000, B00100, B01010, B00010, B00100, B00100, B00000, B00100 }
};
void setup() {
Serial.begin(9600);
Serial.flush();
pinMode(sensorPin,INPUT);
pinMode(REDLed,OUTPUT);
pinMode(IRLed,OUTPUT);
lcd.init();
lcd.backlight();
digitalWrite(REDLed,LOW);
digitalWrite(IRLed,LOW);
for(int i=0;i<8;i++) lcd.createChar(i, sym[i]);
}
void loop () {
bool finger_status = true;
float readsIR[samp_siz], sumIR,lastIR, reader, start;
float readsRED[samp_siz], sumRED,lastRED;
int period, samples;
period=0;
samples=0;
int samplesCounter = 0;
float readsIRMM[maxperiod_siz],readsREDMM[maxperiod_siz];
int ptrMM =0;
for (int i = 0; i < maxperiod_siz; i++) {
readsIRMM[i] = 0;readsREDMM[i]=0;
}
float IRmax=0; float IRmin=0; float REDmax=0; float REDmin=0;
double R=0;
float measuresR[measures];
int measuresPeriods[measures];
int m = 0;
for (int i = 0; i < measures; i++) {
measuresPeriods[i]=0; measuresR[i]=0;
}
int ptr; float beforeIR; bool rising; int rise_count; int n; long int last_beat;
for (int i = 0; i < samp_siz; i++) {
readsIR[i] = 0; readsRED[i]=0;
}
sumIR = 0; sumRED=0; ptr = 0;
while(1) {
digitalWrite(REDLed,LOW); digitalWrite(IRLed,HIGH);
n = 0; start = millis(); reader = 0.;
do { reader += analogRead (sensorPin); n++; } while (millis() < start + T);
reader /= n;
sumIR -= readsIR[ptr]; sumIR += reader; readsIR[ptr] = reader; lastIR = sumIR / samp_siz;
digitalWrite(REDLed,HIGH); digitalWrite(IRLed,LOW);
n = 0; start = millis(); reader = 0.;
do { reader += analogRead (sensorPin); n++; } while (millis() < start + T);
reader /= n;
sumRED -= readsRED[ptr]; sumRED += reader; readsRED[ptr] = reader; lastRED = sumRED / samp_siz;
readsIRMM[ptrMM]=lastIR; readsREDMM[ptrMM]=lastRED; ptrMM++; ptrMM %= maxperiod_siz; samplesCounter++;
if(samplesCounter>=samples){
samplesCounter =0; IRmax = 0; IRmin=1023; REDmax = 0; REDmin=1023;
for(int i=0;i<maxperiod_siz;i++) {
if( readsIRMM[i]> IRmax) IRmax = readsIRMM[i];
if( readsIRMM[i]>0 && readsIRMM[i]< IRmin ) IRmin = readsIRMM[i]; readsIRMM[i] =0;
if( readsREDMM[i]> REDmax) REDmax = readsREDMM[i];
if( readsREDMM[i]>0 && readsREDMM[i]< REDmin ) REDmin = readsREDMM[i]; readsREDMM[i] =0;
}
R = ( (REDmax-REDmin) / REDmin) / ( (IRmax-IRmin) / IRmin ) ;
}
if (lastRED < lastIR) {
if(finger_status==true) { finger_status = false; lcd.clear(); lcd.setCursor(0,0); lcd.print("No finger?"); }
} else {
if(finger_status==false) { lcd.clear(); finger_status = true; lcd.setCursor(10,0); lcd.print("c="); lcd.setCursor(0,0); lcd.print("bpm"); lcd.setCursor(0,1); lcd.print("SpO"); lcd.write(1); lcd.setCursor(10,1); lcd.print("R="); }
}
float avR = 0; int avBPM=0;
if (finger_status==true){
if (lastIR > beforeIR) {
rise_count++;
if (!rising && rise_count > rise_threshold) {
lcd.setCursor(3,0); lcd.write( 0 );
rising = true;
measuresR[m] = R;
measuresPeriods[m] = millis() - last_beat;
last_beat = millis();
int period = 0;
for(int i =0; i<measures; i++) period += measuresPeriods[i];
period = period / measures;
samples = period / (2*T);
int avPeriod = 0; int c = 0;
for(int i =1; i<measures; i++) {
if ( (measuresPeriods[i] < measuresPeriods[i-1] * 1.1) && (measuresPeriods[i] > measuresPeriods[i-1] / 1.1) ) {
c++; avPeriod += measuresPeriods[i]; avR += measuresR[i];
} }
m++; m %= measures;
lcd.setCursor(12,0); lcd.print(String(c)+" ");
avBPM = 60000 / ( avPeriod / c) ; avR = avR / c ;
lcd.setCursor(12,1);
if(c==0) lcd.print(" "); else lcd.print(String(avR) + " ");
if(c > 4) {
int SpO2 = -19 * R + 112;
lcd.setCursor(4,0);
if(avBPM > 40 && avBPM <220) lcd.print(String(avBPM)+" ");
lcd.setCursor(4,1);
if(SpO2 > 70 && SpO2 <150) lcd.print(" " + String(SpO2) +"% ");
} else {
if(c <3) { lcd.setCursor(3,0); lcd.write( 2 ); lcd.setCursor(4,1); lcd.write( 2 ); } }
} } else { rising = false; rise_count = 0; lcd.setCursor(3,0);lcd.print(" "); }
beforeIR = lastIR;
}
Serial.print(lastIR); Serial.print(","); Serial.print(lastRED); Serial.println();
ptr++; ptr %= samp_siz;
}
}
Схемы
Модифицированный KY-039 или схема с нуля (ИК-светодиод, красный светодиод, фотодиод, 3 резистора). Файл Fritzing: oximeter-diy_oW9ZI5zQtJ.fzz.

Производственный процесс
- Схема экономии энергии на пассивном ИК-датчике (PIR): проектирование, работа и применение
- Какая длина волны LiDAR будет доминировать в автомобильных приложениях?
- Построение DIY инфракрасного датчика движения для Raspberry Pi — пошаговое руководство
- Управление светодиодом с PIR-датчиком движения на Raspberry Pi
- Построение низкобюджетного датчика влажности почвы на Raspberry Pi для умного полива
- Тестирование датчика температуры DS18B20 на Raspberry Pi
- Создайте домашний монитор температуры на Raspberry Pi с MCP9808, InfluxDB и Grafana
- Sensor Telemetry 2.0.1 — продвинутый мониторинг температуры с Azure IoT Hub
- Датчик эмоций и ЭЭГ: низкобюджетное железо для детекции эмоций в реальном времени
- Точное инфракрасное температурное измерение для цветозависимых процессов