Компоненты · 15 мин чтения

Датчик пульса для 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. Сразу важное замечание: эти датчики не являются медицинскими приборами и не должны использоваться ни для каких решений, связанных со здоровьем.

Датчик пульса Pulse Sensor с соединительным кабелем
Модуль Pulse Sensor: на круглой плате диаметром около 16 мм собраны зелёный светодиод, датчик света и схема, усиливающая сигнал; в комплекте идёт 3-контактный кабель.

Как работает датчик пульса? Фотоплетизмография (ФПГ)

Все датчики пульса работают по методу фотоплетизмографии (ФПГ, 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: в чём разница

Датчик сердцебиения KY-039 (ИК-светодиод и фототранзистор)
KY-039: сверху ИК-светодиод, загнутый над фототранзистором, снизу фототранзистор; палец помещается между ними. Усилителя на плате нет, поэтому сигнал идёт прямо на аналоговый пин.
ХарактеристикаPulse SensorKY-039MAX30102
МетодФПГ на отражениеФПГ на просвет (палец между элементами)ФПГ на отражение
СветЗелёный светодиодИК-светодиодКрасный (660 нм) + ИК (880 нм)
Усилитель и фильтрЕсть (ОУ)НетВстроенный 18-битный АЦП, подавление внешней засветки
ВыходАналоговыйАналоговый (очень слабый)Цифровой, I2C (адрес 0x57)
Питание3–5 V3,3–5 V3,3–5 V в зависимости от модуля
Качество сигналаХорошееСлабое, чувствительно к шумуОчень хорошее
SpO2 (кислород)НетНетДа (с алгоритмом)
БиблиотекаPulseSensor PlaygroundГотовой библиотеки нетSparkFun MAX3010x

Если ты только начинаешь и хочешь просто измерить пульс, проще всего взять Pulse Sensor: сигнал усиливается и фильтруется прямо на плате, поэтому уже с первой попытки в плоттере видна чёткая волна. ОУ на обратной стороне поднимает это маленькое изменение до уровня, который Arduino может прочитать; как рассчитывается такое усиление, можно попробовать в калькуляторе усиления ОУ. KY-039 дешёвый и есть в большинстве наборов датчиков, но его выход меняется всего на несколько шагов АЦП, поэтому простой код с фиксированным порогом не даёт надёжных результатов. Если нужно измерять ещё и насыщение крови кислородом (SpO2), правильный выбор — MAX30102; о нём ниже есть отдельный раздел.

Подключение датчика пульса: Arduino Uno, ESP32 и Pico

Пин датчикаArduino UnoESP32Raspberry Pi Pico
+ (VCC, красный)5V3V33V3(OUT), пин 36
− (GND, чёрный)GNDGNDGND, пин 38
S (сигнал, фиолетовый)A0GPIO 34 (ADC1)GP26 (ADC0), пин 31
Светодиод ударов (по желанию)D13 (встроенный светодиод)GPIO 2 (встроенный светодиод)Светодиод на плате
Аналоговые входы ESP32 и Pico выдерживают не больше 3,3 V. Выход Pulse Sensor может подниматься до напряжения питания, поэтому на этих платах питай датчик от пина 3V3, а не от 5V или VBUS. У плат-клонов порядок пинов и цвета проводов могут отличаться; перед подключением всегда смотри на надписи S, + и − на плате.

Для KY-039 действует та же таблица: пин S идёт на аналоговый вход, + на питание, − на GND. Проверь, что ИК-светодиод загнут над фототранзистором и палец помещается между ними. Элементы на обратной стороне Pulse Sensor ничем не закрыты; если к ним прикасается палец или влажная от пота поверхность, появляется шум. Поэтому полезно наклеить на лицевую сторону прозрачную наклейку из комплекта, а обратную сторону залить термоклеем. Если используется внешний светодиод, рассчитай его резистор калькулятором резистора для светодиода; карту пинов своей платы можно посмотреть в утилите распиновки.

Первый тест: сырая пульсовая волна в плоттере

Прежде чем переходить к BPM, убедись, что датчик действительно видит пульс. Код ниже не использует библиотек; он читает сигнал 100 раз в секунду и отправляет его в плоттер. Загрузи код, открой в Arduino IDE Инструменты › Плоттер по последовательному соединению (Tools › Serial Plotter) и установи скорость 115200 бод. Если плоттер пока незнаком, в статье монитор порта Arduino объясняется, как устроено это окно.

C++ (Arduino IDE)
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

  1. Открой в Arduino IDE Менеджер библиотек (Library Manager), найди PulseSensor Playground и установи. Подробности об установке библиотек есть в статье как добавить библиотеку в Arduino.
  2. Подключи датчик по таблице: + к 5V, − к GND, S к A0.
  3. Загрузи код ниже и открой плоттер на скорости 115200 бод; первая линия показывает сырой сигнал, вторая — значение BPM.
  4. Положи палец на датчик, обопри руку о стол и посиди 10 секунд неподвижно; за первые несколько ударов библиотека устанавливает среднее значение.
  5. Если удобнее видеть числа построчно в мониторе порта, поменяй в коде константу PLOTTER на false; тогда при каждом ударе выводятся BPM и время между ударами (IBI).
C++ (Arduino IDE)
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 секунд после того как палец положен на датчик, сигнал устанавливается и набирается среднее; значения за это время не учитывай.
Есть простой способ проверить, правильно ли измеряет датчик: посчитай пульс на запястье или шее в течение 30 секунд, умножь на 2 и сравни с BPM на экране. В покое разница в 3–5 BPM нормальна; если разница больше, сначала исправь нажим пальца и внешний свет, а потом подстрой порог.

Пульс на OLED-дисплее

Плоттер хорош на рабочем столе, но переносному пульсометру нужен маленький экран. Код ниже рисует в верхней части 0,96-дюймового OLED-дисплея SSD1306 крупными цифрами BPM и значок сердца, который увеличивается при каждом ударе, а в нижней части — живую пульсовую волну, бегущую слева направо. Подключи OLED к пинам Uno A4 (SDA) и A5 (SCL), а питание — к 5V и GND; установка библиотеки дисплея и настройка адреса подробно описаны в статье Как использовать OLED-дисплей (SSD1306).

C++ (Arduino IDE)
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 последних интервалов между ударами.

C++ (Arduino IDE)
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; он работает с обеими версиями пакета плат.

C++ (Arduino IDE)
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, он будет запускаться сам при каждом включении платы.

MicroPython
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 — насыщение кислородом.

На некоторых дешёвых платах MAX30102 подтягивающие резисторы I2C подключены к 1,8 V; 5-вольтовый Arduino Uno не распознаёт такой уровень как HIGH, и датчик не находится. Если на плате есть паяная перемычка для выбора между 1,8 V и 3,3 V, переставь её на сторону 3,3 V. Эти модули тоже очень чувствительны к нажиму пальца и движению.

Датчик пульса — не медицинский прибор

Схемы и код из этой статьи предназначены для обучения и хобби. Модули Pulse Sensor, KY-039 и MAX30102 не проходили испытаний и не одобрены как медицинские приборы; не ставь диагноз и не принимай решений о лекарствах или лечении на основе их показаний. Если есть боль в груди, учащённое сердцебиение, головокружение, обморок или одышка, не полагайся на датчик: сразу обратись за медицинской помощью, а в экстренном случае звони на местный номер экстренных служб.

На это есть конкретные причины. Медицинские пульсометры и пульсоксиметры калибруются под определённые пределы точности и проверяются на разных оттенках кожи, при слабом кровотоке и в движении; любительские модули и наши алгоритмы таких испытаний не проходят. Кроме того, измерить пульс — не то же самое, что оценить сердечный ритм: чтобы разобраться в нарушениях ритма, нужна ЭКГ, которая записывает электрический сигнал сердца. В качестве общей информации: пульс взрослого в покое обычно составляет от 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)'» (слишком мало аргументов функции) на ESP32PulseSensorPlayground компилируется с версией 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
#11Новичок

Измерение освещённости с LDR

Хочешь увидеть яркость своей комнаты в виде числа? LDR — маленькая деталь, реагирующая на свет, — измеряет освещённость вокруг, а Arduino каждые полсекунды выводит значение на экран компьютера. Закрой

4.6(81)
139
Измерение освещённости модулем LDR на ESP32
#113Новичок

Измерение освещённости модулем LDR на ESP32

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

4.6(36)
55
Работа с зуммером
#13Новичок

Работа с зуммером

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

4.7(89)
161
Температура и влажность с DHT11
#55Средний

Температура и влажность с DHT11

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

4.7(47)
73
Цифровой вольтметр с LCD-дисплеем
#56Средний
Premium

Цифровой вольтметр с LCD-дисплеем

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

4.5(27)
36
ESP32 HTTPClient + API погоды (с OLED-дисплеем SSD1306)
#134Продвинутый
Premium

ESP32 HTTPClient + API погоды (с OLED-дисплеем SSD1306)

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

4.3(10)
19
Глубокий сон ESP32: экономия батареи (OLED + счётчик в RTC RAM)
#135Продвинутый
Premium

Глубокий сон ESP32: экономия батареи (OLED + счётчик в RTC RAM)

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

4.3(10)
16
Плавное затухание светодиода с PWM на Pico (эффект дыхания)
#160Новичок

Плавное затухание светодиода с PWM на Pico (эффект дыхания)

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

0.0(0)
4