Датчик пульса для Arduino: Pulse Sensor и KY-039, измерение BPM
Датчик пульса для Arduino: подключение Pulse Sensor и KY-039, BPM с библиотекой PulseSensorPlayground, пульсовая волна в плоттере, OLED, код для ESP32 и Pico.
Датчик пульса для Arduino, его ещё называют датчиком сердцебиения, — это маленький модуль, который с помощью света следит за кровотоком в кончике пальца или мочке уха и измеряет, сколько раз в минуту бьётся сердце, то есть BPM (beats per minute, ударов в минуту). В любительской электронике чаще всего встречаются две модели: круглый Pulse Sensor и KY-039 с ИК-светодиодом. Оба подключаются тремя проводами и читаются с одного аналогового пина, но сильно различаются качеством сигнала и тем, насколько трудно написать для них надёжный код.
В этом руководстве ты пошагово разберёшься, на каком физическом принципе работает датчик пульса, как увидеть настоящую пульсовую волну в плоттере по последовательному соединению (Serial Plotter), как вычислить BPM с библиотекой PulseSensorPlayground, как обработать слабый сигнал KY-039 алгоритмом без библиотек и как вывести результат на OLED-дисплей со значком сердца. Здесь же код для ESP32 и Raspberry Pi Pico, факторы, которые портят измерение, и отличие от MAX30102. Сразу важное замечание: эти датчики не являются медицинскими приборами и не должны использоваться ни для каких решений, связанных со здоровьем.

Как работает датчик пульса? Фотоплетизмография (ФПГ)
Все датчики пульса работают по методу фотоплетизмографии (ФПГ, PPG). При каждом сокращении сердце выталкивает в артерии волну крови; когда эта волна доходит до мелких сосудов в кончике пальца, объём крови в ткани ненадолго растёт. Гемоглобин крови поглощает свет, поэтому чем больше крови, тем меньше света отражается от ткани или проходит сквозь неё. Модуль освещает ткань светодиодом, а фотодатчик измеряет вернувшийся свет; эта небольшая регулярная пульсация количества света и есть пульс.
Сложность в том, что почти весь измеряемый свет постоянен. Кожа, мышцы, кости и кровь в венах всё время поглощают одинаковое количество света; это называется постоянной (DC) составляющей сигнала. Переменная (AC) составляющая, которая меняется с каждым ударом сердца, обычно составляет всего около 1–2% от всего света. Получается, что настоящая задача датчика — выделить и усилить маленькую волну, которая едет поверх большого постоянного уровня. Волна ФПГ сначала быстро поднимается до пика (систолический пик), а затем медленно спадает; небольшая выемка посередине спада и маленький горбик сразу за ней (дикротическая выемка) появляются из-за закрытия аортального клапана. Хороший алгоритм считает каждый удар сердца один раз и не принимает эту выемку за второй удар.
Важно и то, как свет попадает в ткань. Pulse Sensor работает на отражение: светодиод и датчик света находятся на одной стороне и измеряют свет, рассеянный кожей обратно. Его зелёный свет сильно поглощается гемоглобином и даёт чёткий сигнал от сосудов прямо под кожей; именно поэтому на обратной стороне умных часов стоят зелёные светодиоды. KY-039 работает на просвет: ИК-светодиод освещает палец с одной стороны, а фототранзистор с другой стороны измеряет прошедший сквозь него свет. ИК-свет проникает в ткань глубже, но у KY-039 нет схемы усиления сигнала, поэтому волна на выходе получается очень маленькой.
| Характеристика | Значение для Pulse Sensor |
|---|---|
| Метод измерения | ФПГ на отражение |
| Источник света | Зелёный светодиод |
| Датчик света | APDS-9008, пик чувствительности на 565 нм |
| Питание | 3–5 V |
| Ток | Около 4 mA (при 5 V) |
| Выход | Аналоговый; в покое половина напряжения питания (~2,5 V при 5 V, ~512 на Uno) |
| Диаметр платы | Около 16 мм (0,625 дюйма) |
| Провода | У оригинала: красный +, чёрный GND, фиолетовый сигнал; у клонов цвета могут отличаться |
Pulse Sensor, KY-039 и MAX30102: в чём разница

| Характеристика | Pulse Sensor | KY-039 | MAX30102 |
|---|---|---|---|
| Метод | ФПГ на отражение | ФПГ на просвет (палец между элементами) | ФПГ на отражение |
| Свет | Зелёный светодиод | ИК-светодиод | Красный (660 нм) + ИК (880 нм) |
| Усилитель и фильтр | Есть (ОУ) | Нет | Встроенный 18-битный АЦП, подавление внешней засветки |
| Выход | Аналоговый | Аналоговый (очень слабый) | Цифровой, I2C (адрес 0x57) |
| Питание | 3–5 V | 3,3–5 V | 3,3–5 V в зависимости от модуля |
| Качество сигнала | Хорошее | Слабое, чувствительно к шуму | Очень хорошее |
| SpO2 (кислород) | Нет | Нет | Да (с алгоритмом) |
| Библиотека | PulseSensor Playground | Готовой библиотеки нет | SparkFun MAX3010x |
Если ты только начинаешь и хочешь просто измерить пульс, проще всего взять Pulse Sensor: сигнал усиливается и фильтруется прямо на плате, поэтому уже с первой попытки в плоттере видна чёткая волна. ОУ на обратной стороне поднимает это маленькое изменение до уровня, который Arduino может прочитать; как рассчитывается такое усиление, можно попробовать в калькуляторе усиления ОУ. KY-039 дешёвый и есть в большинстве наборов датчиков, но его выход меняется всего на несколько шагов АЦП, поэтому простой код с фиксированным порогом не даёт надёжных результатов. Если нужно измерять ещё и насыщение крови кислородом (SpO2), правильный выбор — MAX30102; о нём ниже есть отдельный раздел.
Подключение датчика пульса: Arduino Uno, ESP32 и Pico
| Пин датчика | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| + (VCC, красный) | 5V | 3V3 | 3V3(OUT), пин 36 |
| − (GND, чёрный) | GND | GND | GND, пин 38 |
| S (сигнал, фиолетовый) | A0 | GPIO 34 (ADC1) | GP26 (ADC0), пин 31 |
| Светодиод ударов (по желанию) | D13 (встроенный светодиод) | GPIO 2 (встроенный светодиод) | Светодиод на плате |
Для KY-039 действует та же таблица: пин S идёт на аналоговый вход, + на питание, − на GND. Проверь, что ИК-светодиод загнут над фототранзистором и палец помещается между ними. Элементы на обратной стороне Pulse Sensor ничем не закрыты; если к ним прикасается палец или влажная от пота поверхность, появляется шум. Поэтому полезно наклеить на лицевую сторону прозрачную наклейку из комплекта, а обратную сторону залить термоклеем. Если используется внешний светодиод, рассчитай его резистор калькулятором резистора для светодиода; карту пинов своей платы можно посмотреть в утилите распиновки.
Первый тест: сырая пульсовая волна в плоттере
Прежде чем переходить к BPM, убедись, что датчик действительно видит пульс. Код ниже не использует библиотек; он читает сигнал 100 раз в секунду и отправляет его в плоттер. Загрузи код, открой в Arduino IDE Инструменты › Плоттер по последовательному соединению (Tools › Serial Plotter) и установи скорость 115200 бод. Если плоттер пока незнаком, в статье монитор порта Arduino объясняется, как устроено это окно.
1// Nabız sensörü ham sinyal testi (Seri Çizici) / Pulse sensor raw signal test (Serial Plotter)
2const int PULSE_PIN = A0; // sensörün S (mor) kablosu / sensor S (purple) wire
3const unsigned long PERIOD_MS = 10; // 10 ms = saniyede 100 örnek / 10 ms = 100 samples per second
4
5unsigned long lastSample = 0;
6
7void setup() {
8 Serial.begin(115200); // Seri Çizici'de de 115200 seç / pick 115200 in the Serial Plotter too
9 lastSample = millis();
10}
11
12void loop() {
13 if (millis() - lastSample >= PERIOD_MS) {
14 lastSample += PERIOD_MS; // sabit aralıkla örnekle / sample at a fixed interval
15 int raw = analogRead(PULSE_PIN); // 0-1023 arası değer / value between 0 and 1023
16 Serial.print("Sinyal:");
17 Serial.print(raw);
18 Serial.println(",Orta:512"); // boştaki orta seviye çizgisi / idle midpoint line
19 }
20}Без пальца линия Pulse Sensor ровно держится около 512 с небольшим дрожанием; это половина напряжения питания, и она показывает, что датчик подключён правильно. Если слегка положить кончик пальца на датчик, через несколько секунд появляется волна, которая с каждым ударом сердца быстро поднимается и медленно опускается; её амплитуда обычно от 100 до 300 шагов. Если волны не видно, не переходи к коду BPM: сначала исправь подключение, питание и нажим пальца. Если пики превращаются в ровную линию, прилипшую к 0 или 1023, сигнал ограничивается (клиппинг); обычно палец двигается или датчик прижат слишком сильно. Если интересно, откуда берётся диапазон 0–1023 у аналогового чтения, загляни в статью АЦП и аналого-цифровое преобразование.
Измерение BPM с PulseSensorPlayground
- Открой в Arduino IDE Менеджер библиотек (Library Manager), найди PulseSensor Playground и установи. Подробности об установке библиотек есть в статье как добавить библиотеку в Arduino.
- Подключи датчик по таблице: + к 5V, − к GND, S к A0.
- Загрузи код ниже и открой плоттер на скорости 115200 бод; первая линия показывает сырой сигнал, вторая — значение BPM.
- Положи палец на датчик, обопри руку о стол и посиди 10 секунд неподвижно; за первые несколько ударов библиотека устанавливает среднее значение.
- Если удобнее видеть числа построчно в мониторе порта, поменяй в коде константу PLOTTER на false; тогда при каждом ударе выводятся BPM и время между ударами (IBI).
1// PulseSensor Playground ile BPM ölçme / Measuring BPM with PulseSensor Playground
2#include <PulseSensorPlayground.h>
3
4const int PULSE_PIN = A0; // sensörün S (mor) kablosu / sensor S (purple) wire
5const int LED_PIN = LED_BUILTIN; // her atışta yanan LED (D13) / LED lit on each beat (D13)
6const int THRESHOLD = 550; // başlangıç eşiği, boşta ~512 / starting threshold, idle ~512
7const bool PLOTTER = true; // true: Seri Çizici, false: Seri Monitör / true: Serial Plotter, false: Serial Monitor
8
9PulseSensorPlayground pulseSensor; // kütüphane nesnesi / library object
10unsigned long lastPrint = 0;
11
12void setup() {
13 Serial.begin(115200);
14 pulseSensor.analogInput(PULSE_PIN);
15 pulseSensor.blinkOnPulse(LED_PIN);
16 pulseSensor.setThreshold(THRESHOLD);
17 if (!pulseSensor.begin()) { // 2 ms'lik zamanlayıcıyı başlatır / starts the 2 ms timer
18 Serial.println(F("PulseSensor baslatilamadi / could not start"));
19 while (true) {}
20 }
21}
22
23void loop() {
24 // Atış anı: BPM ve atışlar arası süre / moment of a beat: BPM and inter-beat interval
25 if (pulseSensor.sawStartOfBeat() && !PLOTTER) {
26 Serial.print(F("BPM: "));
27 Serial.print(pulseSensor.getBeatsPerMinute());
28 Serial.print(F(" IBI: "));
29 Serial.print(pulseSensor.getInterBeatIntervalMs());
30 Serial.println(F(" ms"));
31 }
32
33 // 20 ms'de bir çizim satırı / one plotter line every 20 ms
34 if (PLOTTER && millis() - lastPrint >= 20) {
35 lastPrint = millis();
36 Serial.print(F("Sinyal:"));
37 Serial.print(pulseSensor.getLatestSample());
38 Serial.print(F(",BPM:"));
39 Serial.println(pulseSensor.getBeatsPerMinute());
40 }
41}| Функция | Что делает |
|---|---|
| analogInput(пин) | Выбирает аналоговый пин, к которому подключён датчик (по умолчанию A0) |
| blinkOnPulse(пин) | Зажигает светодиод на этом пине, пока сигнал выше порога, то есть во время каждого удара |
| fadeOnPulse(пин) | Зажигает светодиод на ШИМ-пине при каждом ударе и плавно гасит его |
| setThreshold(значение) | Начальный порог; если удар не найден, порог возвращается к этому значению |
| begin() | Запускает выборку; при успехе возвращает true |
| sawStartOfBeat() | Возвращает true, если с прошлого вызова начался новый удар |
| getBeatsPerMinute() | BPM, вычисленный по среднему из 10 последних интервалов между ударами |
| getInterBeatIntervalMs() | Время между двумя последними ударами (IBI), в миллисекундах |
| getLatestSample() | Последнее сырое значение (0–1023) |
| isInsideBeat() | Возвращает true, пока сигнал выше порога |
Библиотека читает датчик в фоне по прерыванию таймера каждые 2 мс (500 раз в секунду), поэтому выборка остаётся равномерной, даже если в loop() выполняются долгие задачи. При каждом ударе она запоминает пик и впадину волны и ставит порог точно посередине между ними; то есть значение, переданное в setThreshold(), — лишь отправная точка. Подъёмы, которые приходят в пределах 250 мс после предыдущего удара или раньше, чем пройдут три пятых последнего интервала, она игнорирует, так что дикротическая выемка не считается вторым ударом. BPM вычисляется по среднему из 10 последних интервалов; если удар не находится в течение 2,5 секунды, всё сбрасывается и getBeatsPerMinute() возвращает 0.
Начальный порог выбирай, глядя на сырой сигнал: хорошая отправная точка — на 30–50 шагов выше самого высокого значения, до которого доходит линия без пальца (на большинстве датчиков от 530 до 600). Если порог слишком низкий, шум считается ударами даже без пальца; если слишком высокий, слабые удары пропускаются. Ещё одно предупреждение: на Arduino Uno библиотека использует для выборки Timer1, поэтому analogWrite() не работает на пинах 9 и 10. Если добавить в проект библиотеку Servo, библиотека переходит на Timer2; тогда вместо этого страдают ШИМ на пинах 3 и 11 и функция tone().
Для точного измерения: нажим пальца, внешний свет и движение
Большинство причин, которые портят измерение пульса, связаны не с кодом, а с тем, как держать датчик. Попробуй пункты ниже по одному при открытом плоттере; увидеть, как каждый из них меняет волну, — самый быстрый способ понять датчик.
- Нажим пальца: прижимай легко, но ровно. При слишком лёгком касании каждое подрагивание пальца создаёт шум больше самой волны; при слишком сильном нажиме сжимаются капилляры, кровоток падает, и волна сплющивается или вовсе пропадает. Лучший результат получается, когда датчик закреплён на пальце ремешком на липучке.
- Внешний свет: солнечный свет и лампы проникают в датчик сбоку. При сети 50 Hz многие лампы мерцают 100 раз в секунду (при сети 60 Hz — 120 раз), и это мерцание попадает в сигнал. Полностью закрой пальцем область вокруг датчика или оберни его сверху тёмной лентой.
- Движение: качающийся кабель, разговор, движение пальцем или ходьба дают сигналы намного больше пульса. Во время измерения обопри руку о стол и не шевелись.
- Холодные руки: в замёрзшем пальце сосуды сужаются, и сигнал слабеет. Согрей руки или закрепи датчик на мочке уха; мочка к тому же меньше страдает от движения.
- Пот и жир: влажная кожа рассеивает свет иначе и загрязняет поверхность датчика. Используй прозрачную защитную наклейку на лицевой стороне и время от времени протирай датчик сухой тканью.
- Время установления: в первые 5–10 секунд после того как палец положен на датчик, сигнал устанавливается и набирается среднее; значения за это время не учитывай.
Пульс на OLED-дисплее
Плоттер хорош на рабочем столе, но переносному пульсометру нужен маленький экран. Код ниже рисует в верхней части 0,96-дюймового OLED-дисплея SSD1306 крупными цифрами BPM и значок сердца, который увеличивается при каждом ударе, а в нижней части — живую пульсовую волну, бегущую слева направо. Подключи OLED к пинам Uno A4 (SDA) и A5 (SCL), а питание — к 5V и GND; установка библиотеки дисплея и настройка адреса подробно описаны в статье Как использовать OLED-дисплей (SSD1306).
1// Pulse Sensor + 0,96" OLED: BPM, kalp simgesi ve nabız dalgası / BPM, heart icon and pulse wave
2#include <Wire.h>
3#include <Adafruit_GFX.h>
4#include <Adafruit_SSD1306.h>
5#include <PulseSensorPlayground.h>
6
7Adafruit_SSD1306 display(128, 64, &Wire, -1); // 128x64, reset pini yok / no reset pin
8PulseSensorPlayground pulseSensor;
9
10const int PULSE_PIN = A0; // sensörün S pini / sensor S pin
11const int THRESHOLD = 550; // başlangıç eşiği / starting threshold
12const int GRAPH_TOP = 24; // dalga alanının üst kenarı / top edge of the wave area
13const int GRAPH_H = 40; // dalga alanının yüksekliği / height of the wave area
14
15int x = 0; // tarama sütunu (0-127) / sweep column (0-127)
16int lastY = GRAPH_TOP + GRAPH_H / 2; // önceki noktanın y'si / y of the previous point
17int lo = 1023, hi = 0; // bu taramadaki en küçük/büyük değer / min/max in this sweep
18int scaleLo = 400, scaleHi = 624; // çizimde kullanılan aralık / range used for drawing
19unsigned long lastDraw = 0, beatTime = 0;
20
21// İki daire ve bir üçgenle kalp çiz / draw a heart from two circles and a triangle
22void drawHeart(int cx, int cy, bool big) {
23 int r = big ? 6 : 4; // atış anında büyük kalp / bigger heart on a beat
24 display.fillCircle(cx - r, cy - r / 2, r, SSD1306_WHITE);
25 display.fillCircle(cx + r, cy - r / 2, r, SSD1306_WHITE);
26 display.fillTriangle(cx - 2 * r, cy, cx + 2 * r, cy, cx, cy + 2 * r, SSD1306_WHITE);
27}
28
29void setup() {
30 Serial.begin(115200);
31 if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // I2C adresi 0x3C / I2C address 0x3C
32 Serial.println(F("SSD1306 allocation failed"));
33 while (true) {}
34 }
35 display.clearDisplay();
36 pulseSensor.analogInput(PULSE_PIN);
37 pulseSensor.setThreshold(THRESHOLD);
38 pulseSensor.begin(); // örnekleme kesmeyle sürer / sampling runs in an interrupt
39}
40
41void loop() {
42 if (pulseSensor.sawStartOfBeat()) beatTime = millis(); // atış anını sakla / store the beat time
43
44 if (millis() - lastDraw < 40) return; // saniyede 25 kare / 25 frames per second
45 lastDraw = millis();
46
47 // Alt bölüm: soldan sağa taranan nabız dalgası / bottom: pulse wave swept left to right
48 int s = pulseSensor.getLatestSample();
49 if (s < lo) lo = s;
50 if (s > hi) hi = s;
51 int y = map(constrain(s, scaleLo, scaleHi), scaleLo, scaleHi, GRAPH_TOP + GRAPH_H - 1, GRAPH_TOP);
52 display.drawFastVLine(x, GRAPH_TOP, GRAPH_H, SSD1306_BLACK); // eski sütunu sil / erase the old column
53 display.drawFastVLine((x + 1) % 128, GRAPH_TOP, GRAPH_H, SSD1306_BLACK); // önündeki boşluk / gap ahead
54 if (x > 0) display.drawLine(x - 1, lastY, x, y, SSD1306_WHITE);
55 lastY = y;
56 if (++x >= 128) { // tarama bitti: ölçeği yenile / sweep done: refresh the scale
57 x = 0;
58 if (hi - lo > 20) { scaleLo = lo; scaleHi = hi; }
59 lo = 1023;
60 hi = 0;
61 }
62
63 // Üst bölüm: kalp ve BPM / top: heart and BPM
64 display.fillRect(0, 0, 128, GRAPH_TOP, SSD1306_BLACK);
65 drawHeart(14, 10, millis() - beatTime < 150);
66 int bpm = pulseSensor.getBeatsPerMinute();
67 display.setTextColor(SSD1306_WHITE);
68 display.setTextSize(3);
69 display.setCursor(40, 0);
70 if (bpm > 0) display.print(bpm);
71 else display.print(F("--")); // parmak yok / no finger
72 display.setTextSize(1);
73 display.setCursor(104, 14);
74 display.print(F("BPM"));
75 display.display(); // tamponu ekrana gönder / push the buffer to the screen
76}Код обновляет экран 25 раз в секунду и в каждом кадре добавляет к волне одну точку, так что по ширине экрана помещается примерно 5 секунд записи пульса. В конце каждой развёртки волна заново масштабируется по наименьшему и наибольшему значению этой развёртки, поэтому на экран помещается и слабый, и сильный сигнал. Отправка одного кадра на дисплей по I2C занимает около 25–30 мс, но библиотека делает выборку в прерывании, поэтому за это время ни один удар не теряется. На Uno буфер дисплея занимает 1 КБ ОЗУ; код рисует волну прямо на экране, не сохраняя её в массив, поэтому памяти хватает, хотя свободными остаются лишь около 450 байт.
Измерение пульса с KY-039
В KY-039 фототранзистор включён последовательно с резистором, и выходное напряжение зависит от падающего света: больше света — ниже напряжение, меньше света — выше. Никакого усилителя или фильтра между ними нет. Когда палец на месте, удар сердца обычно сдвигает это напряжение всего на 2–10 шагов АЦП, то есть примерно на 10–50 mV; даже небольшое изменение внешнего освещения больше этого. Поэтому код, написанный для Pulse Sensor, и фиксированный порог вроде 550 на KY-039 не работают: сначала сигнал нужно очистить, а затем использовать порог, который подстраивается под размер самой волны.
Код ниже делает это в четыре шага. Сначала он непрерывно читает аналоговый пин в течение 20 мс (около 175 чтений) и усредняет результаты; это снижает случайный шум, а поскольку 20 мс — ровно один период сети 50 Hz, почти полностью убирает мерцание ламп. Затем он сглаживает сигнал лёгким фильтром нижних частот и убирает постоянный уровень очень медленным скользящим средним, так что остаётся только пульсовая волна. На третьем шаге вычисляются две огибающие, которые следят за пиками и впадинами волны, и порог ставится посередине между ними. Наконец, удар засчитывается, когда сигнал пересекает порог снизу вверх, а BPM находится по среднему из 8 последних интервалов между ударами.
1// KY-039 ile nabız ölçme (kütüphanesiz) / Heart rate with the KY-039 (no library)
2const int SENSOR_PIN = A0; // KY-039 S pini / KY-039 S pin
3const int LED_PIN = LED_BUILTIN; // atışta yanan LED / LED lit on each beat
4const bool PLOTTER = true; // true: Seri Çizici, false: Seri Monitör / true: Serial Plotter, false: Serial Monitor
5const unsigned long WINDOW_US = 20000; // 20 ms = 50 Hz şebekenin bir periyodu / one period of 50 Hz mains
6const float MIN_AMP = 1.2; // tepe-tepe gürültü sınırı (ADC birimi) / peak-to-peak noise limit (ADC units)
7const int AVG_BEATS = 8; // BPM için ortalanan aralık sayısı / intervals averaged for BPM
8
9float smooth, baseline; // süzülmüş sinyal ve DC seviye / filtered signal and DC level
10float posEnv = 0, negEnv = 0; // tepe ve çukur zarfları / peak and trough envelopes
11bool armed = true; // yeni atışa hazır mı / ready for a new beat
12unsigned long lastBeat = 0; // son atışın zamanı / time of the last beat
13unsigned int ibi[AVG_BEATS]; // atışlar arası süreler (ms) / inter-beat intervals (ms)
14int ibiCount = 0, ibiIndex = 0, bpm = 0;
15
16// 20 ms boyunca okuyup ortalama al / read for 20 ms and average
17float readWindow() {
18 unsigned long start = micros();
19 long sum = 0;
20 int n = 0;
21 while (micros() - start < WINDOW_US) {
22 sum += analogRead(SENSOR_PIN);
23 n++;
24 }
25 return (float)sum / n;
26}
27
28float averageIbi() {
29 if (ibiCount == 0) return 500.0; // geçmiş yoksa 120 BPM varsay (ilk bekleme 350 ms) / assume 120 BPM without history (first wait 350 ms)
30 unsigned long sum = 0;
31 for (int i = 0; i < ibiCount; i++) sum += ibi[i];
32 return (float)sum / ibiCount;
33}
34
35void addInterval(unsigned int ms) {
36 ibi[ibiIndex] = ms; // halka tampon / ring buffer
37 ibiIndex = (ibiIndex + 1) % AVG_BEATS;
38 if (ibiCount < AVG_BEATS) ibiCount++;
39 bpm = (int)(60000.0 / averageIbi() + 0.5);
40}
41
42void setup() {
43 Serial.begin(115200);
44 pinMode(LED_PIN, OUTPUT);
45 smooth = baseline = readWindow(); // başlangıç seviyesi / starting level
46}
47
48void loop() {
49 float x = readWindow(); // saniyede 50 örnek / 50 samples per second
50 unsigned long now = millis();
51
52 smooth += (x - smooth) * 0.5; // hafif alçak geçiren süzgeç / light low-pass filter
53 baseline += (smooth - baseline) * 0.05; // yavaş DC takibi (~0,4 s) / slow DC tracking (~0.4 s)
54 float ac = smooth - baseline; // yalnız nabız dalgası / pulse wave only
55
56 // Zarflar: uçları hemen yakala, yavaşça bırak / envelopes: catch extremes fast, release slowly
57 posEnv = (ac > posEnv) ? ac : posEnv * 0.99;
58 negEnv = (ac < negEnv) ? ac : negEnv * 0.99;
59 float amp = posEnv - negEnv; // tepe-tepe genlik / peak-to-peak amplitude
60 float mid = (posEnv + negEnv) / 2; // eşik: dalganın ortası / threshold: middle of the wave
61
62 unsigned long sinceBeat = now - lastBeat;
63 float refractory = constrain(averageIbi() * 0.7, 250.0, 600.0); // dikrotik çentiği atla / skip the dicrotic notch
64
65 if (armed && amp > MIN_AMP && ac > mid && sinceBeat > refractory) {
66 armed = false; // atış bulundu / beat found
67 digitalWrite(LED_PIN, HIGH);
68 if (sinceBeat < 2000) addInterval(sinceBeat); // 30 BPM'den yavaşı alma / ignore gaps slower than 30 BPM
69 lastBeat = now;
70 if (!PLOTTER && bpm > 0) {
71 Serial.print(F("BPM: "));
72 Serial.println(bpm);
73 }
74 }
75 if (!armed && ac < mid - amp * 0.25) { // dalga yeterince indi: yeniden hazır / wave fell enough: re-arm
76 armed = true;
77 digitalWrite(LED_PIN, LOW);
78 }
79 if (sinceBeat > 2500 && ibiCount > 0) { // 2,5 s atış yok: sıfırla / no beat for 2.5 s: reset
80 ibiCount = 0;
81 bpm = 0;
82 }
83
84 if (PLOTTER) {
85 Serial.print(F("Nabiz:"));
86 Serial.print(ac);
87 Serial.print(F(",Esik:"));
88 Serial.println(mid);
89 }
90}| Константа / значение | Что делает |
|---|---|
| WINDOW_US = 20000 (окно усреднения) | Окно усреднения; убирает мерцание от сети 50 Hz (в странах с сетью 60 Hz поставь 16667) |
| 0.5 (smooth, сглаживание) | Лёгкий фильтр нижних частот; сглаживает резкие скачки |
| 0.05 (baseline, базовая линия) | Следует за постоянным уровнем примерно за 0,4 секунды; убирает медленные изменения нажима пальца |
| 0.99 (огибающие) | Скорость спада огибающих пиков и впадин (около 2 секунд) |
| MIN_AMP = 1.2 (минимальная амплитуда) | Волна с размахом ниже этого значения считается шумом |
| refractory (рефрактерный период) | 70% от последнего среднего интервала (250–600 мс; 350 мс, пока истории ещё нет, чтобы не пропустить частый пульс); не даёт дикротической выемке засчитаться вторым ударом |
| mid - amp * 0.25 (уровень перевзвода) | Уровень повторного взведения; новый удар не ищется, пока волна не опустится достаточно низко |
Если загрузить код и открыть плоттер, видны две линии: Nabiz (пульс) показывает очищенную волну, а Esik (порог) — адаптивный порог, проходящий через её середину. Посмотри, насколько колеблется линия Nabiz без пальца, и поставь MIN_AMP чуть выше этого размаха; если сигнал очень слабый, уменьши значение. Больше о сглаживании сигналов — в статье фильтрация данных датчиков. Тот же код работает и с Pulse Sensor; в этом случае достаточно поднять MIN_AMP примерно до 20.
Датчик пульса с ESP32
На ESP32 питай датчик от пина 3V3, а сигнал подключи к GPIO 34. Этот пин относится к группе ADC1; пины ADC2 нельзя читать, пока включён Wi-Fi, поэтому если данные пульса будут уходить в интернет, нужно обязательно выбрать один из пинов ADC1 (GPIO 32–39). АЦП ESP32 12-битный (0–4095), и при питании 3,3 V Pulse Sensor в покое выдаёт значение около 2000.
Свежие версии PulseSensorPlayground поддерживают и ESP32, но библиотека использует функции таймеров из версии 3.x пакета плат ESP32 и не компилируется с версией 2.x. В своих примерах она снижает разрешение АЦП до 10 бит через analogReadResolution(10), поэтому порог задаётся в той же шкале 0–1023, что и на Uno (в примере для ESP32 это 685). Чтобы не зависеть от различий между версиями, ниже приведён скетч без библиотек, использующий тот же алгоритм, что и в разделе про KY-039; он работает с обеими версиями пакета плат.
1// ESP32 + Pulse Sensor: kütüphanesiz BPM / BPM without a library
2const int SENSOR_PIN = 34; // ADC1 pini, Wi-Fi açıkken de çalışır / ADC1 pin, works with Wi-Fi on
3const int LED_PIN = 2; // çoğu DevKit'te dahili LED / built-in LED on most DevKits
4const unsigned long PERIOD_MS = 10; // saniyede 100 örnek / 100 samples per second
5const float MIN_AMP = 80; // 12 bitte tepe-tepe gürültü sınırı / peak-to-peak noise limit in 12-bit units
6const int AVG_BEATS = 8; // BPM için ortalanan aralık sayısı / intervals averaged for BPM
7
8float smooth, baseline, posEnv = 0, negEnv = 0;
9bool armed = true;
10unsigned long lastSample = 0, lastBeat = 0;
11unsigned long ibi[AVG_BEATS];
12int ibiCount = 0, ibiIndex = 0, bpm = 0;
13
14// 8 okumanın ortalaması: ESP32 ADC'si gürültülüdür / average of 8 reads: the ESP32 ADC is noisy
15float readSensor() {
16 long sum = 0;
17 for (int i = 0; i < 8; i++) sum += analogRead(SENSOR_PIN);
18 return sum / 8.0;
19}
20
21float averageIbi() {
22 if (ibiCount == 0) return 500.0; // geçmiş yoksa 120 BPM varsay (ilk bekleme 350 ms) / assume 120 BPM without history (first wait 350 ms)
23 unsigned long sum = 0;
24 for (int i = 0; i < ibiCount; i++) sum += ibi[i];
25 return (float)sum / ibiCount;
26}
27
28void setup() {
29 Serial.begin(115200);
30 pinMode(LED_PIN, OUTPUT);
31 analogReadResolution(12); // 0-4095 arası değer / values from 0 to 4095
32 smooth = baseline = readSensor();
33 lastSample = millis();
34}
35
36void loop() {
37 if (millis() - lastSample < PERIOD_MS) return;
38 lastSample += PERIOD_MS; // sabit aralıkla örnekle / sample at a fixed interval
39 unsigned long now = millis();
40
41 float x = readSensor();
42 smooth += (x - smooth) * 0.3; // alçak geçiren süzgeç (~6 Hz) / low-pass filter (~6 Hz)
43 baseline += (smooth - baseline) * 0.02; // DC takibi (~0,5 s) / DC tracking (~0.5 s)
44 float ac = smooth - baseline;
45
46 posEnv = (ac > posEnv) ? ac : posEnv * 0.995; // tepe zarfı / peak envelope
47 negEnv = (ac < negEnv) ? ac : negEnv * 0.995; // çukur zarfı / trough envelope
48 float amp = posEnv - negEnv;
49 float mid = (posEnv + negEnv) / 2;
50
51 unsigned long sinceBeat = now - lastBeat;
52 float refractory = constrain(averageIbi() * 0.7, 250.0, 600.0);
53
54 if (armed && amp > MIN_AMP && ac > mid && sinceBeat > refractory) {
55 armed = false;
56 digitalWrite(LED_PIN, HIGH);
57 if (sinceBeat < 2000) { // 30-240 BPM aralığı / 30-240 BPM range
58 ibi[ibiIndex] = sinceBeat;
59 ibiIndex = (ibiIndex + 1) % AVG_BEATS;
60 if (ibiCount < AVG_BEATS) ibiCount++;
61 bpm = (int)(60000.0 / averageIbi() + 0.5);
62 Serial.printf("BPM: %d IBI: %lu ms\n", bpm, sinceBeat);
63 }
64 lastBeat = now;
65 }
66 if (!armed && ac < mid - amp * 0.25) {
67 armed = true;
68 digitalWrite(LED_PIN, LOW);
69 }
70 if (sinceBeat > 2500 && ibiCount > 0) { // parmak çekildi / finger removed
71 ibiCount = 0;
72 bpm = 0;
73 Serial.println("Parmak yok / no finger");
74 }
75}В этом коде сигнал читается 100 раз в секунду, а поскольку АЦП ESP32 шумный, каждый отсчёт — среднее из 8 чтений. Значения порога и огибающих подобраны под 12-битную шкалу, поэтому MIN_AMP равен 80 (около 65 mV). Настоящая сила ESP32 — это Wi-Fi: можно передавать BPM в реальном времени на веб-страницу через WebSocket, отправлять сообщение в Telegram, когда пульс выходит за предел, или записывать измерения в Google Таблицы.
Измерение пульса на Raspberry Pi Pico с MicroPython
На Raspberry Pi Pico подключи пин + датчика к 3V3(OUT) (пин 36), − к GND (пин 38), а S к GP26 (ADC0, пин 31). В MicroPython read_u16() возвращает значение от 0 до 65535; код сдвигает его вправо на 4 бита и приводит к той же шкале 0–4095, что и на ESP32, поэтому значения порогов на обеих платах одинаковые. Если сохранить код на Pico в Thonny под именем main.py, он будет запускаться сам при каждом включении платы.
1# Raspberry Pi Pico + Pulse Sensor: MicroPython ile BPM / BPM with MicroPython
2from machine import ADC, Pin
3import time
4
5sensor = ADC(26) # GP26 = ADC0, sensörün S pini / sensor S pin
6led = Pin("LED", Pin.OUT) # kartın üzerindeki LED / on-board LED
7
8PERIOD_MS = 10 # saniyede 100 örnek / 100 samples per second
9MIN_AMP = 80 # 12 bitte tepe-tepe gürültü sınırı / peak-to-peak noise limit (12-bit)
10AVG_BEATS = 8 # BPM için ortalanan aralık / intervals averaged for BPM
11
12def read12():
13 # 4 okumanın ortalaması, 0-4095 / average of 4 reads, 0-4095
14 s = 0
15 for _ in range(4):
16 s += sensor.read_u16() >> 4
17 return s / 4
18
19smooth = baseline = read12()
20pos_env = neg_env = 0.0
21armed = True
22ibis = []
23bpm = 0
24last_beat = time.ticks_ms()
25next_t = time.ticks_ms()
26
27while True:
28 while time.ticks_diff(time.ticks_ms(), next_t) < 0:
29 pass # bir sonraki örnek zamanını bekle / wait for the next sample time
30 next_t = time.ticks_add(next_t, PERIOD_MS)
31 now = time.ticks_ms()
32
33 x = read12()
34 smooth += (x - smooth) * 0.3 # alçak geçiren süzgeç / low-pass filter
35 baseline += (smooth - baseline) * 0.02 # DC takibi / DC tracking
36 ac = smooth - baseline
37
38 pos_env = ac if ac > pos_env else pos_env * 0.995
39 neg_env = ac if ac < neg_env else neg_env * 0.995
40 amp = pos_env - neg_env
41 mid = (pos_env + neg_env) / 2
42
43 since = time.ticks_diff(now, last_beat)
44 avg_ibi = sum(ibis) / len(ibis) if ibis else 500 # geçmiş yoksa 120 BPM varsay / assume 120 BPM without history
45 refractory = min(max(avg_ibi * 0.7, 250), 600) # dikrotik çentiği atla / skip the dicrotic notch
46
47 if armed and amp > MIN_AMP and ac > mid and since > refractory:
48 armed = False
49 led.on()
50 if since < 2000: # 30 BPM'den yavaşı alma / ignore gaps slower than 30 BPM
51 ibis.append(since)
52 if len(ibis) > AVG_BEATS:
53 ibis.pop(0)
54 bpm = round(60000 / (sum(ibis) / len(ibis)))
55 print("BPM:", bpm, "IBI:", since)
56 last_beat = now
57 if not armed and ac < mid - amp * 0.25:
58 armed = True
59 led.off()
60 if since > 2500 and ibis: # parmak çekildi / finger removed
61 ibis = []
62 bpm = 0
63 print("Parmak yok / no finger")Код работает по той же логике, что и версия для ESP32: берёт отсчёт каждые 10 мс, убирает постоянный уровень, находит удар по адаптивному порогу и вычисляет BPM по среднему из 8 последних интервалов. При каждом ударе загорается светодиод на плате, а BPM и интервал между ударами выводятся в окно Shell (оболочка) Thonny. Если синтаксис Python пока незнаком, хорошее начало — статья основы MicroPython.
Отличие от MAX30102: измерение кислорода в крови (SpO2)
MAX30102 — однокристальный датчик, который Analog Devices (раньше Maxim Integrated) разработала для измерения пульса и насыщения крови кислородом. Внутри у него два светодиода, красный (660 нм) и ИК (880 нм), фотодиод, схема подавления внешней засветки и 18-битный АЦП. Он сам выполняет измерение, накапливает отсчёты в памяти на 32 значения (FIFO) и отправляет их числами по шине I2C (адрес 0x57). Шум, который собирают аналоговые провода, и ограничение 10-битного АЦП Arduino здесь уже не мешают, поэтому сигнал у него намного чище, чем у Pulse Sensor.
Секрет измерения SpO2 в использовании света двух разных цветов. Гемоглобин, несущий кислород, сильнее поглощает ИК-свет и слабее красный; гемоглобин, отдавший кислород, ведёт себя наоборот. Датчик измеряет переменную и постоянную составляющие пульсовой волны для обоих источников света и вычисляет отношение R = (AC_красный / DC_красный) / (AC_ИК / DC_ИК); SpO2 находится по этому отношению с помощью эмпирической калибровочной кривой. Pulse Sensor и KY-039 используют свет одного цвета, поэтому вычислить это отношение не могут, а значит, кислород они не измеряют. Самая популярная библиотека Arduino для MAX30102 — SparkFun MAX3010x; её пример Example5_HeartRate вычисляет пульс, а Example8_SPO2 — насыщение кислородом.
Датчик пульса — не медицинский прибор
На это есть конкретные причины. Медицинские пульсометры и пульсоксиметры калибруются под определённые пределы точности и проверяются на разных оттенках кожи, при слабом кровотоке и в движении; любительские модули и наши алгоритмы таких испытаний не проходят. Кроме того, измерить пульс — не то же самое, что оценить сердечный ритм: чтобы разобраться в нарушениях ритма, нужна ЭКГ, которая записывает электрический сигнал сердца. В качестве общей информации: пульс взрослого в покое обычно составляет от 60 до 100 ударов в минуту; у людей, которые регулярно занимаются спортом, он может опускаться до 40–60. Если твой результат выходит за этот диапазон, сначала проверь датчик и код; если беспокойство остаётся, поговори с врачом.
Идеи проектов с датчиком пульса
- Свет в ритме сердца: при каждом ударе зажигай светодиодное кольцо WS2812B красным и плавно гаси; с ростом BPM смещай цвет от синего к красному.
- Пищащий пульсометр: каждый раз, когда sawStartOfBeat() возвращает true, издавай короткий писк зуммером; забавный проект, который подражает звуку больничных мониторов.
- Переносной пульсометр: помести OLED-вариант в корпус вместе с аккумулятором и схемой зарядки; выключай экран по окончании измерения, чтобы аккумулятор работал дольше.
- Тест восстановления после тренировки: измерь пульс сразу после тренировки и вычисли, на сколько ударов он снижается за первую минуту; записывай значение каждый день и следи за прогрессом.
- Регистратор пульса: записывай значения BPM с отметкой времени на SD-карту или передавай их в Excel и строй график.
- Игра с дыхательным упражнением: показывай на экране, как меняется пульс при медленном глубоком дыхании; победа — когда пульс опускается ниже целевого значения.
Частые ошибки и как их исправить
| Симптом | Вероятная причина | Решение |
|---|---|---|
| BPM всегда 0 или «--» | Удары не обнаруживаются: слишком высокий порог, нет пальца или пин S подключён не к тому входу | Проверь волну скетчем сырого сигнала; перенеси порог в середину волны |
| Очень высокий BPM, например 150–220 | Шум или дикротическая выемка считаются вторым ударом | Подними порог, зафиксируй палец, закрой датчик от внешнего света |
| BPM показывается даже без пальца | Порог ниже шума в покое; мерцание ламп попадает в сигнал | Поставь порог на 30–50 шагов выше самого высокого значения в покое, закрой датчик сверху |
| Сигнал стоит ровно на 0 или 1023 | Перепутаны + и −, оборван провод, пин S не на своём месте или датчик неисправен | Сверь подключение с таблицей; на ESP32 и Pico используй 3V3 |
| Волна есть, но постоянно разваливается | Палец двигается или меняется нажим | Закрепи датчик ремешком на липучке, обопри руку о стол и не шевелись |
| На KY-039 волны нет совсем | Светодиод не загнут над фототранзистором, или сильный внешний свет насыщает фототранзистор | Загни светодиод так, чтобы он был над пальцем, используй датчик в тени, уменьши MIN_AMP |
| «too few arguments to function 'hw_timer_t* timerBegin(uint8_t, uint16_t, bool)'» (слишком мало аргументов функции) на ESP32 | PulseSensorPlayground компилируется с версией 2.x пакета плат ESP32 | Обнови пакет плат до 3.x или используй код для ESP32 без библиотек из этой статьи |
| analogWrite() не работает на пине 9 или 10 | Библиотека использует Timer1 на Uno | Для ШИМ используй пины 3, 5, 6 или 11 |
Датчик пульса — один из тех редких проектов, которые с помощью одного аналогового пина сразу учат аналоговому чтению, фильтрации сигнала, прерываниям по таймеру и построению графиков в реальном времени, а результат виден на собственном теле. Если нужен проект для начинающих, который тоже измеряет свет, хороший следующий шаг — датчик света LDR. В приложении BlueGrays есть проекты с деталями из этой статьи, например чтение освещённости с LDR, зуммер, проекты на ESP32 с OLED-дисплеем и эффект «дыхания» на ШИМ с Pico, с пошаговыми схемами и кодом.
Частые вопросы
Насколько точно измеряет датчик пульса для Arduino?
Если датчик хорошо закреплён, а человек сидит неподвижно, Pulse Sensor обычно измеряет пульс в покое с точностью до нескольких BPM. Движение, внешний свет, холодные руки и нажим пальца быстро увеличивают ошибку. Сигнал KY-039 намного слабее, поэтому его показания сильнее скачут; ни один из этих датчиков не подходит для медицинских измерений.
Чем отличаются Pulse Sensor и KY-039?
Pulse Sensor использует зелёный светодиод и метод отражения, усиливает и фильтрует сигнал с помощью ОУ на плате, а в покое держит выход на половине напряжения питания. KY-039 измеряет ИК-свет, проходящий сквозь палец, усилителя на плате у него нет, и его выход меняется всего на несколько шагов АЦП. Поэтому Pulse Sensor легко работает с готовой библиотекой, а KY-039 требует фильтрации и адаптивного порога.
Может ли датчик пульса измерять кислород в крови (SpO2)?
Pulse Sensor и KY-039 не могут, потому что вычисление SpO2 основано на сравнении света двух разных длин волн, красного и инфракрасного. Для этого нужен датчик с двумя светодиодами, например MAX30102. Но и показание MAX30102 — это некалиброванная оценка, которая не заменяет медицинский оксиметр.
Почему датчик пульса показывает очень высокие или скачущие значения?
Самая частая причина — шум или дикротическая выемка на пульсовой волне считаются вторым ударом; в этом случае BPM получается примерно вдвое больше реального. Подними порог, зафиксируй палец ремешком на липучке и закрой датчик от света. Взгляд на сырую волну в плоттере сразу показывает источник проблемы.
Работает ли Pulse Sensor с ESP32 и Raspberry Pi Pico?
Да, но на обеих платах датчик нужно питать от пина 3V3, потому что их аналоговые входы не выдерживают больше 3,3 V. На ESP32 используй пин ADC1, например GPIO 34, чтобы не было конфликта с Wi-Fi; на Pico — GP26, GP27 или GP28. PulseSensorPlayground на ESP32 требует версию 3.x пакета плат, а в MicroPython алгоритм пишется самостоятельно, как показано в этой статье.
Какой пульс считается нормальным?
У взрослых в покое пульс обычно составляет от 60 до 100 ударов в минуту; у тех, кто регулярно занимается спортом, он может быть 40–60, а у детей выше. Пульс естественно растёт при нагрузке, стрессе, от кофеина и при температуре. Это общая информация; свои показания и любые симптомы обсуди с врачом.
Что собрать после прочтения
Все проекты
Измерение освещённости с LDR
Хочешь увидеть яркость своей комнаты в виде числа? LDR — маленькая деталь, реагирующая на свет, — измеряет освещённость вокруг, а Arduino каждые полсекунды выводит значение на экран компьютера. Закрой

Измерение освещённости модулем LDR на ESP32
Наблюдение в мониторе порта в реальном времени за сырым значением ADC модуля LDR и относительным процентом освещённости, вычисленным по опорным значениям темноты и света. Этот процент — не измерение в люксах. Учит логике калибровки модуля. Идеально выполнить его перед проектом 10, потому что он нужен для определения порога.

Работа с зуммером
Интересно, как дверной звонок или сигнализация издают звук? Arduino заставляет маленький зуммер пищать через короткие промежутки: пик, пауза, пик, пауза. Ты видишь, как электричество превращается в зв

Температура и влажность с DHT11
Насколько тепло в твоей комнате и влажный ли воздух или сухой? Датчик DHT11 измеряет температуру и влажность, а Arduino выводит значения на двухстрочный LCD-экран и обновляет их каждые пару секунд. Ес

Цифровой вольтметр с LCD-дисплеем
Узнай, какое напряжение даёт батарейка или блок питания, с помощью измерителя, собранного своими руками. Делитель из двух резисторов понижает высокое напряжение до уровня, который может прочитать плат

ESP32 HTTPClient + API погоды (с OLED-дисплеем SSD1306)
В этом проекте ESP32 через HTTPClient.h делает вызов REST API к сервису погоды (wttr.in). В отличие от прошлой версии с I2C LCD, здесь используется OLED SSD1306 (128×64), который позволяет крупные шрифты, векторные значки и богатую картинку на одном экране. Семь векторных значков погоды (солнечно, переменная облачность, облачно, дождь, снег, гроза, туман) рисуются во время работы (растровые файлы не нужны). Температура выводится крупными цифрами.

Глубокий сон ESP32: экономия батареи (OLED + счётчик в RTC RAM)
Режим глубокого сна (Deep Sleep) у ESP32 снижает потребление до ~10 µA, выключая процессор и большую часть периферии, — это важно для IoT-устройств на батарейках. В этом проекте используются три источника пробуждения: таймер (15 секунд), внешний пин (EXT0 — кнопка GPIO 33) и касание (T0/GPIO 4). Переменные с RTC_DATA_ATTR сохраняются во время глубокого сна — счётчики загрузок, пробуждений по таймеру и по кнопке ведутся непрерывно.

Плавное затухание светодиода с PWM на Pico (эффект дыхания)
Светодиод больше не просто мигает — он дышит! С помощью PWM (широтно-импульсной модуляции) Raspberry Pi Pico яркость светодиода медленно растёт и медленно падает, как огонёк спящего MacBook. Изучи класс machine.PWM, duty_u16() (16-битная яркость 0–65535), частоту PWM (freq(1000) — 1000 циклов вкл/выкл в секунду, мерцания не видно), циклы range() с шагом (вверх и вниз) и почти аналоговый выход с цифрового пина.
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

