Климатический датчик BME280 и ESP32

          BME280 – это комбинированный цифровой датчик абсолютного атмосферного давления (АД), температуры и относительной влажности воздуха (климатический), являющийся на сегодняшний день общепризнанным «золотым стандартом» климатический сенсоров для разработчиков DIY-электроники. Данная модель – это прямое развитие таких известных моделей как BMP085, BMP180 и BMP280.

          Этот материал из цикла статей о датчиках атмосферного давления Bosch. Чтобы полностью понять принципы работы преобразователей и войти в курс дела, рекомендую сначала ознакомиться с основами и более ранними моделями:

BMP180: пьезорезистивный метод измерения давления, собираем высотомер;

BMP280 + AHT20: как устроен и функционирует полимерный емкостной гигрометр, создаём домашнюю метеостанцию.

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

Содержание


Общее описание

          BME280 – логическое продолжение сенсоров Bosh Sensortec 2-го поколения, построенных по технологии MEMS (Micro-Electro-Mechanical Systems — микроэлектромеханическая система). По сути, эта модель представляет собой BMP280 с дополнительно интегрированным в структуру чипа емкостным гигрометром, устройство чувствительного элемента которого близко к AHT20 (об этом можно подробнее прочитать в предыдущей статье).

          Интегральная схема BME280 включает в себя 3 ключевых преобразователя:

– пьезорезистивный барометрический (подробнее про устройство можно прочитать в статье про 180-ю модель), который измеряет абсолютное атмосферное давление в диапазоне от 300 до 1100 гПа (что соответствует уровням от -500 до +9000 метров относительно моря);

– встроенный термосенсор. Это не отдельный элемент, а часть самой структуры кремниевого чипа, и представляет собой p-n переход кремниевого диода. Преобразователь предназначен, в первую очередь, для определения степени нагрева кристалла с пьезорезистивным преобразователем и с полимерным емкостным гигрометром, что крайне необходимо для тепловой компенсации показаний обоих измерительных каналов. То есть определять степень прогретости воздуха в помещении этим преобразователем будет не совсем корректно из-за высокой погрешности (по факту более ±1 ̊C);

– полимерный емкостной гигрометр, который определяет относительную влажность воздуха в диапазоне от 0 до 100 % в широком диапазоне температур от -40 °C до +85 °C, при этом обеспечивается точность ±3 % в диапазоне 20…80 % при +25 °C.

          Ближайшим аналогом для BME280 является комбинированный сенсор MS8607 от компании TE Connectivity. По большому счёту, параметры у них примерно одинаковые, а точность по давлению у аналога в 2 раза хуже, поэтому приоритет за Bosh.

          Существует также более новая модель – BME680, которая примерно повторяет параметры 280-й модели, но имеет дополнительный чувствительный элемент, который регистрирует концентрацию летучих органических соединений в воздухе (VOC – Volatile Organic Compounds). Есть также новейшая модель BME688, которая является развитием 680-й модели, и имеет дополнительную функционал анализа газов с возможностью использовать машинное обучение.

          С точки зрения прагматизма, лучше всего в качестве альтернативы рассматривать комбинацию датчиков:

          Из этого списка самым точным является связка SHT40 + BMP384. Но цена достаточно высокая для DIY-сегмента.

            Таким образом, связка BMP280 + AHT20 является наилучшим вариантом по соотношению цены и качества.

            На рисунке ниже представлена упрощённая блок-схема из официальной документации на микросхему BME280. На ней показаны основные компоненты:

– чувствительные элементы (sensing element);

– входные цепи (front-end), включающие мультиплексор для согласования аналоговых преобразователей со входом аналого-цифрового преобразователя (ADC);

– логический блок (ADC) обработки данных и калибровочных коэффициентов. Этот блок преобразует аналоговые сигналы в цифровые значения и корректирует их с помощью заводских калибровочных коэффициентов, которые хранятся в энергонезависимой памяти (NVM – Non-Volatile Memory);

– встроенный стабилизатор напряжения и источник опорного напряжения (voltage regulator & voltage reference);

– схема автоматического сброса по питанию (POR – Power-On Reset);

– генератор тактовых импульсов (OSC – Oscillator), обеспечивает функционирование АЦП, внешних интерфейсов I2C и SPI, цифрового фильтра.


Параметры и характеристики

            Для того, чтобы сформировалась более целостная картина в модельном ряде преобразователей Bosh, ниже приведена сравнительная таблица для трёх датчиков:


Режимы работы и настройка

            Микросхему можно программно настроить на функционирование в одном из трёх режимов работы:

1) Sleep mode (спящий режим). Он активируется по умолчанию сразу после подачи питания. Какие-либо преобразования не производятся, потребление минимально (примерно 0,3 мкА). Все регистры доступны для чтения и записи;

2) Forced mode (принудительное включение). В этом случае микросхема выполняет один цикл измерений (давление/температура/влажность — в зависимости от настроек), сохраняет результаты в регистры данных, после чего автоматически возвращается в состояние сна. Следующий цикл требует повторного вызова этого режима. Forced mode применяется в приложениях с батарейным питанием, где необходима экономия энергии и низкая частота опроса (например, в фитнес-браслетах и домашних метеостанциях);

3) Normal mode (нормальный режим). Чип непрерывно циклически выполняет измерения, чередуя активную фазу и фазу ожидания (длительность ожидания настраивается программно). Результаты последнего измерения всегда доступны в регистрах. Normal mode применяется встроенный IIR-фильтра с бесконечной импульсной характеристикой (IIR – Infinite Impulse Response).

            IIR-фильтр играет важную роль в качестве процесса преобразования. Его ключевой задачей является подавление сигнальных шумов и кратковременных скачков давления и температуры (для влажности не применяется), вызванных случайными помехами (хлопанье дверью или окном, сквозняки и внезапные потоки воздуха, вибрации или встряхивания микросхемы).

            Фильтр применяется в нормальном режиме (Normal Mode) и управляется программно, при этом имеет 5 вариантов настройки:

            В таблице выше поле «Количество измерений» означает, что чипу необходимо выполнить определённое число операций, чтобы набрать данные для статистики и по специальному алгоритму отфильтровать показания.

            Работу фильтра в нормальном режиме (Normal Mode) невозможно рассматривать без учёта такого параметра как «Время ожидания (Standby)». Эта пауза необходима для снижения энергопотребления и саморазогрева чипа.

            Этот параметр определяет длительность фазы ожидания между операциями сбора данных, то есть частоту выдачи данных ODR (Output Data Rate).  Длительность фазы ожидания имеет ряд фиксированных значений, которые выбираются программно:

            Значение паузы выбирается исходя из следующих соображений:

– цель применения. Для приложений, где необходима высокая скорость реакции (например, летательные аппараты или построение графиков с высокой частотой дискретизации), стоит выбирать малые временные промежутки. Для домашней метеостанции нет необходимости часто производить замеры, поэтому паузу выдерживаем подольше;

– источник питания. Для носимой электроники важна энергоэффективность, поэтому для экономии заряда аккумуляторов логично снизить частоту опроса. Если домашняя метеостанция питается от сети, то можно и увеличить скорость сбора данных.

            Время ожидания влияет на общую скорость реакции системы на изменение контролируемой физической величины. К примеру, если задан фильтр x16, то чип должен выполнить 22 операции, а между ними пауза, к примеру, 1000 мс. Получается, суммарно затрачивается 22000 миллисекунд, чтобы система адекватно показала максимально точное значение. Так будет только сразу после подачи питания. Далее, в течение продолжительной работы сенсор будет реагировать несколько быстрее: примерно 15-20 секунд.

            К примеру: что будет, если вдруг барометр резко изменит своё положение, скажем, упадёт на землю с высоты 10 метров? Падение займёт 2-3 секунды, и как только устройство окажется на земле, он будет показывать 8…9 метров. Примерно через 10 секунд показания будут 0 метров.

            Таким образом, необходимо очень тщательно выбирать параметры фильтра и длительность паузы, учитывая требования к эксплуатации и целевое назначение устройства.

            Ещё одним важным параметром является «Число выборок (Sampling)», который определяет точность одного цикла измерений для давления, температуры и влажности. Для каждой физической величины программная настройка производится персонально. Чем выше число выборок (степень точности), тем больше времени на один цикл измерений (от 5 до 30 мс) и, соответственно, больше энергопотребление.

            Ниже представлена таблица возможных вариантов параметра Oversampling (для каждого канала) и соответствующая им возможная точность на примере давления (как главный преобразователь в чипе):

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

1. Карманный высотомер

bme.setSampling(
  Adafruit_BME280::MODE_NORMAL,         // Непрерывный режим
  Adafruit_BME280::SAMPLING_X2,             // Температура — для компенсации давления
  Adafruit_BME280::SAMPLING_X8,             // Давление — ВАЖНЕЕ ВСЕГО, компромисс точности и скорости
  Adafruit_BME280::SAMPLING_NONE,       // Влажность — не нужно
  Adafruit_BME280::FILTER_X8,                    // Плавное сглаживание высоты, без сильного запаздывания
  Adafruit_BME280::STANDBY_MS_250       // Быстрая реакция (4 раза в секунду)
);

2. Домашняя метеостанция (питание от сети)

bme.setSampling(
  Adafruit_BME280::MODE_NORMAL,
  Adafruit_BME280::SAMPLING_X16,            // Температура ±0,5 °C
  Adafruit_BME280::SAMPLING_X16,            // Давление ±0,8 Па
  Adafruit_BME280::SAMPLING_X16,            // Влажность ±3 %
  Adafruit_BME280::FILTER_X16,                   // Максимальное сглаживание
  Adafruit_BME280::STANDBY_MS_1000      // Раз в 1 секунду (давление меняется медленно)
);

3. Носимая электроника (питание от аккумулятора)

bme.setSampling(
  Adafruit_BME280::MODE_NORMAL,
  Adafruit_BME280::SAMPLING_X2,               // Температура ±1,0…1,5 °C
  Adafruit_BME280::SAMPLING_X2,               // Давление ±2 Па
  Adafruit_BME280::SAMPLING_X2,               // Влажность не является приоритетом, экономия энергии
  Adafruit_BME280::FILTER_X4,                      // Умеренное сглаживание, убирает дрожание при ходьбе
  Adafruit_BME280::STANDBY_MS_1000       // Раз в 1 секунду
);

4. Летательный аппарат

bme.setSampling(
   Adafruit_BME280::MODE_NORMAL,
   Adafruit_BME280::SAMPLING_X1,        // Температура быстро
   Adafruit_BME280::SAMPLING_X4,        // Давление точно, но не максимально
   Adafruit_BME280::SAMPLING_NONE,  // Влажность выключена
   Adafruit_BME280::FILTER_X2,               // Минимальный фильтр
   Adafruit_BME280::STANDBY_MS_125  // 8 раз в секунду
);

5. Автомобильный высотомер

bme.setSampling(
  Adafruit_BME280::MODE_NORMAL,
  Adafruit_BME280::SAMPLING_X2,             // Температура для компенсации
  Adafruit_BME280::SAMPLING_X16,           // Давление максимально
  Adafruit_BME280::SAMPLING_NONE,       // Влажность выключена
  Adafruit_BME280::FILTER_X16,                  // Максимальный фильтр от вибраций
  Adafruit_BME280::STANDBY_MS_500       // 2 раза в секунду
);

6. Сверхэкономичный режим (питание от батарейки)

bme.setSampling(
  Adafruit_BME280::MODE_FORCED,         // Режим сна между измерениями
  Adafruit_BME280::SAMPLING_X1,
  Adafruit_BME280::SAMPLING_X1,
  Adafruit_BME280::SAMPLING_X1,   
  Adafruit_BME280::FILTER_OFF,
  Adafruit_BME280::STANDBY_MS_0_5       // Этот параметр ИГНОРИРУЕТСЯ в режиме FORCED
                                                                   // В режиме FORCED пользователь сам управляет паузами через delay() в loop()
);

            Очень важно отметить для режима FORCED несколько моментов:

– датчик находится в сне до вызова bme.takeForcedMeasurement();

– необходимо самостоятельно контролировать интервалы измерений через delay() в функции loop();

– параметр STANDBY_MS_ не используется, но его всё равно нужно указать (можно любой).


BME280 в составе платы (модуля) HW-611

            Чтобы экспериментировать с барометрическим сенсором, можно воспользоваться популярными платами со всей минимально необходимой обвязкой:

            1. BME280-3.3V

            GY-BME/P280 (HW-611)

            Данный модуль позволяет выполнять подключение к датчику по интерфейсу либо I2C, либо SPI. На плате размещены подтягивающие резисторы и фильтрующие конденсаторы. Отсутствует стабилизатор напряжения, поэтому подавать напряжение питания строго не более 3,3 В.

            2. BME280-5V

Читайте также:  Акселерометр и гироскоп MPU-6050: цифровой угломер на ESP32

            BME/BMP280 (GYBMEP)

            Эта плата уже включает в себя линейный стабилизатор напряжения SC662K на 3,3 Вольта, поэтому можно питать модуль от 5 Вольт. Но при этом предлагается лишь только I2C-интерфейс для подключения к микроконтроллеру.

            Исходя из питания 5 Вольт, необходимо согласовать низковольные контакты микросхемы (3,3 В) с высоковольтными выводами микроконтроллера (например, Arduino UNO с 5-вольтовой логикой). С этой целью применены N-канальные MOSFET-транзисторы.

            Ниже представлена таблица распиновки модулей:

            Важно отметить, что на таких платах может размещаться и микросхема BMP280, в зависимости от изготовителя. Поэтому, чтобы визуально различить чипы, следует обратить внимание на расположение отверстия на верхней крышке микросхемы.

            E-модель имеет квадратную форму, а отверстие расположено по середине одной из сторон.

            P-модель имеет прямоугольную форму, при этом отверстие расположено ближе к одному из углов.

            Ниже представлены принципиальные схемы модулей на 5 и на 3,3 Вольта:

– 5 Вольт

– 3,3 Вольта


Схема подключения BME280 к ESP32

            Для проведения экспериментов воспользуемся отладочной платой NodeMCU-32S (38 pin), построенной на базе модуля ESP-WROOM-32.

            Отображать показания будем на TFT-дисплей (контроллер ST7735S) с диагональю 0,96 дюйма и разрешением 80×160 пикселей.

            Ниже представлена таблица подключений платы преобразователя и дисплея к отладочной плате.

            Для наглядности ниже приведена схема подключения модуля версии «3,3 Вольта» и дисплея к отладочной плате:


Программный код (скетч)

            Проект создавался в среде программирования Arduino IDE 2. Так как компания Arduino теперь принадлежит Qualcomm, то доступ к официальному сайту может быть затруднён. Если возникают трудности с доступом и необходимо обновить среду разработки, можно воспользоваться официальным репозиторием на GitHub.

            Если нужно настроить среду для работы с ESP32, то подробная инструкция представлена в статье про диктофон на ESP32 с цифровым микрофоном INMP411.

            Для взаимодействия с TFT-дисплеем воспользуемся библиотекой tft_eSPI, как наиболее быстрой и эффективной для данной задачи. Подробная инструкция по настройке этой библиотеки приведена в статье про лазерный дальномер VL53L0X.

            Чтобы обеспечить взаимодействие с комбинированным сенсором, применим популярную и простую библиотеку Adafruit-BME280-Library. Она обеспечивает оптимальный набор функций и достаточно проста для понимания новичкам.

Комнатная домашняя метеостанция

            Выведем на дисплей базовые величины: температура (будем считать, что воздуха в комнате с точность ±1…2 °C), атмосферное давление в гектоПаскалях и миллиметрах ртутного столба, а также относительную влажность воздуха. Ниже представлен очень подробно прокомментированный листинг кода:

// Metreostation
// Подключение необходимых библиотек

#include <Wire.h>
// Подключаем библиотеку Wire для работы с шиной I2C (Inter-Integrated Circuit).

#include <Adafruit_BME280.h>
// Подключаем библиотеку Adafruit_BME280

#include <TFT_eSPI.h>
// Подключаем библиотеку TFT_eSPI для управления TFT-дисплеями

// СОЗДАНИЕ ОБЪЕКТОВ (ЭКЗЕМПЛЯРОВ КЛАССОВ)
TFT_eSPI tft = TFT_eSPI();
// Создаем объект с именем 'tft' типа TFT_eSPI.
                              
Adafruit_BME280 bme;
// Создаем объект с именем 'bme' типа Adafruit_BME280.

// ОБЪЯВЛЕНИЕ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ ДЛЯ ХРАНЕНИЯ ДАННЫХ
float pressure;
// Объявляем переменную для хранения значения атмосферного давления,
// считанного с датчика, в единицах гектоПаскаль (гПа).

float temperature;
// Объявляем переменную для хранения значения температуры в градусах Цельсия (°C).

float humidity;
// Объявляем переменную для хранения значения относительной влажности в процентах (%).

float pressure_mmHg;
// Объявляем переменную для хранения значения атмосферного давления,
// пересчитанного в миллиметры ртутного столба (мм рт. ст.).

float altitude;
// Объявляем переменную для хранения расчетной высоты над уровнем моря в метрах (м).

// Константа давления на уровне моря (стандартное значение)
#define SEALEVELPRESSURE_HPA (1013.25)
// Определяем макрос (константу на этапе препроцессора) для стандартного
// атмосферного давления на уровне моря (1013.25 гПа).
// Это значение используется для расчета приблизительной высоты.

// ФУНКЦИЯ readSensor() - ЧТЕНИЕ ДАННЫХ С ДАТЧИКА BME280
void readSensor() {
  // ЧТЕНИЕ ДАННЫХ С BME280
  temperature = bme.readTemperature();
  // Вызываем метод readTemperature() объекта bme.
  // Метод читает значение температуры с датчика, преобразует его в °C
  // и возвращает как число с плавающей точкой. Результат сохраняем в переменную temperature.

  pressure = bme.readPressure() / 100.0F;
  // Вызываем метод readPressure(). Он возвращает давление в Паскалях (Па).
  // Делим результат на 100.0, чтобы преобразовать Па в более удобные гектоПаскали (гПа).
  // Суффикс 'F' указывает, что 100.0 - это число типа float.
  // Результат сохраняем в переменную pressure.

  humidity = bme.readHumidity();
  // Вызываем метод readHumidity() для чтения относительной влажности в %.
  // Результат сохраняем в переменную humidity.
  
  // ПРОВЕРКА НА NaN (Not a Number) - специальное значение, обозначающее ошибку вычисления (например, при сбое датчика).
  if (isnan(temperature) || isnan(pressure) || isnan(humidity)) {
    // Функция isnan() проверяет, является ли аргумент NaN.
    Serial.println("Ошибка чтения датчика!");
    // Если хотя бы одна из переменных содержит NaN, выводим сообщение об ошибке в Serial.
    temperature = pressure = humidity = 0;
    // Присваиваем всем переменным значение 0, чтобы избежать дальнейших ошибок при выводе.
  }

  // Дополнительные расчеты
  pressure_mmHg = pressure * 0.750062;
  // Пересчитываем давление из гПа в мм рт. ст. по приближенной формуле.
  // Коэффициент 0.750062 - это отношение 1 гПа к 1 мм рт. ст.

  altitude = bme.readAltitude(SEALEVELPRESSURE_HPA);
  // Вызываем метод readAltitude(). Он рассчитывает приблизительную высоту
  // над уровнем моря в метрах, используя текущее давление (pressure)
  // и заданное давление на уровне моря (SEALEVELPRESSURE_HPA).
  // Формула основана на барометрической зависимости.
}

// ФУНКЦИЯ displayData() - ВЫВОД ДАННЫХ НА TFT-ДИСПЛЕЙ
void displayData() {
  // Настройка параметров текста для заголовка

  tft.setTextSize(1);
  // Устанавливаем размер шрифта. 1 - базовый размер (примерно 6x8 пикселей).

  tft.setTextColor(TFT_RED, TFT_BLACK);
  // Устанавливаем цвет текста (первый аргумент - TFT_RED) и цвет фона под текстом (второй аргумент - TFT_BLACK).

  tft.setCursor(20, 0);
  // Устанавливаем позицию курсора (левая верхняя точка следующего выводимого символа) на координаты X=20, Y=0 (в пикселях).

  tft.printf("SPRYTRON METEO BME280");
  // Выводим форматированную строку (в данном случае просто строку) на дисплей.
  
  tft.setTextSize(1);
  // Снова устанавливаем размер шрифта 1 для основного текста данных.
  
  // ВЫВОД ТЕМПЕРАТУРЫ
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  // Меняем цвет текста на желтый для названия параметра.

  tft.setCursor(0, 15);
  // Перемещаем курсор на новую строку (Y=15).

  tft.printf("Temperature:  ");
  // Выводим текст "Temperature:  " (с пробелами для форматирования).

  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  // Меняем цвет текста на белый для вывода числового значения.

  tft.printf("%.1f ", temperature);
  // Выводим значение переменной temperature с форматированием: одно число после запятой (%.1f).

  tft.print(char(247));
  // Печатаем специальный символ градуса. Код 247 в таблице символов дисплея часто соответствует этому символу.

  tft.printf("C");
  // Печатаем букву "C" для обозначения шкалы Цельсия.
  
  // ВЫВОД ДАВЛЕНИЯ (ГПА) - аналогично температуре
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.setCursor(0, 30);
  // Следующая строка (Y=30).

  tft.printf("Pressure:     ");
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.printf("%.1f hPa", pressure);
  // Выводим давление в гПа.
  
  // ВЫВОД ДАВЛЕНИЯ (ММ РТ. СТ.)
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.setCursor(0, 45);
  // Следующая строка (Y=45).

  tft.printf("Pressure:     ");
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.printf("%.1f mmHg", pressure_mmHg);
  // Выводим давление в мм рт. ст.
  
  // ВЫВОД ВЛАЖНОСТИ
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.setCursor(0, 60);
  // Следующая строка (Y=60).

  tft.printf("Humidity:     ");
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.printf("%.1f %%", humidity);
  // Символ '%' в функции printf является специальным, поэтому для его вывода используется два знака '%%'.
}

// Дополнительная функция для получения строковой информации о датчике
String getSensorInfo() {
  switch (bme.sensorID()) {
    // Вызываем метод sensorID(), который возвращает числовой идентификатор чипа датчика.
    case 0x60: return "BME280 (адрес 0x76)";
    // Если ID равен 0x60 (шестн. 60), это, вероятно, BME280 с адресом 0x76.

    case 0x58: return "BMP280 (адрес 0x76)";
    // ID 0x58 соответствует датчику BMP280 (только давление и температура) с адресом 0x76.

    case 0x56: return "BMP280 (адрес 0x77)";
    // ID 0x56 соответствует BMP280 с адресом 0x77.

    default: return "Неизвестный датчик";
    // Для любого другого ID возвращаем сообщение "Неизвестный датчик".
  }
}

// ФУНКЦИЯ printSerialData() - ВЫВОД ДАННЫХ В SERIAL MONITOR
void printSerialData() {
  Serial.println("\n--- SPRYTRON METEO BME280 ---");
  // Выводим разделительную строку. "\n" - переход на новую строку.
  Serial.printf("Температура: %.1f °C\n", temperature);
  // Выводим температуру. Форматирование аналогично tft.printf.
  Serial.printf("Давление:    %.1f гПа\n", pressure);
  Serial.printf("Давление:    %.1f мм рт. ст.\n", pressure_mmHg);
  Serial.printf("Влажность:   %.1f %%\n", humidity);
  Serial.printf("Высота:      %.1f м\n", altitude);
  Serial.println("-----------------------------");
  // Выводим нижнюю разделительную строку.
}

// ФУНКЦИЯ SETUP() - ВЫПОЛНЯЕТСЯ ОДИН РАЗ ПРИ ЗАПУСКЕ УСТРОЙСТВА
void setup() {
  // ИНИЦИАЛИЗАЦИЯ ПОСЛЕДОВАТЕЛЬНОГО ПОРТА (SERIAL)
  Serial.begin(115200);
  // Инициализируем последовательный порт (UART) со скоростью 115200 бод.
  // Это необходимо для обмена данными с компьютером через USB для отладки.
  
  // ИНИЦИАЛИЗАЦИЯ ШИНЫ I2C
  Wire.begin(21, 22);
  // Инициализируем шину I2C, указывая номера пинов микроконтроллера,
  // которые используются для сигналов SDA (данные, пин 21) и SCL (тактовый сигнал, пин 22).
  // Эти номера характерны для многих плат на ESP32.
 
  // НАСТРОЙКА TFT-ДИСПЛЕЯ
  tft.init();
  // Инициализируем дисплей. Эта команда настраивает пины и протокол связи с дисплеем.

  tft.setRotation(3);
  // Устанавливаем ориентацию дисплея. Значение 3 означает поворот на 270 градусов.
  // Это может быть необходимо для корректного отображения в зависимости от физического расположения дисплея.

  tft.fillScreen(TFT_BLACK);
  // Заливаем весь экран черным цветом, очищая его.

  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  // Устанавливаем цвет текста по умолчанию на белый, фон текста - черный.
  
  // НАСТРОЙКА ПАРАМЕТРОВ ТЕКСТА
  tft.setTextSize(1);
  // Устанавливаем базовый размер шрифта.

  tft.setTextDatum(MC_DATUM);
  // Устанавливаем точку привязки (datum) для текста как Middle-Center (по центру по вертикали и горизонтали).
  // Это влияет на позиционирование при использовании drawString (где текст центрируется относительно заданных координат).
  
  // ВЫВОД ЗАГОЛОВКА И ИНФОРМАЦИИ О ЗАГРУЗКЕ
  tft.setTextSize(2);
  // Временно увеличиваем размер шрифта для заголовка.

  tft.drawString("Meteostation", tft.width()/2, 10);
  // Рисуем строку "Meteostation".
  // Координата X: tft.width()/2 - центр экрана по горизонтали.
  // Координата Y: 10 пикселей от верха.
  // Благодаря MC_DATUM текст будет отцентрирован относительно точки (центр_экрана, 10).

  tft.drawLine(0, 30, tft.width(), 30, TFT_CYAN);
  // Рисуем горизонтальную линию бирюзового цвета (TFT_CYAN) от левого края (X=0) до правого (X=ширина_экрана) на высоте Y=30.

  delay(1000);
  // Приостанавливаем выполнение программы на 1000 миллисекунд (1 секунду).

  tft.fillScreen(TFT_BLACK);
  // Снова очищаем экран.
  tft.setTextSize(1);
  // Возвращаемся к размеру шрифта 1.
  tft.setCursor(5, 35);
  // Устанавливаем курсор вручную (этот метод переопределяет datum).
  tft.println("Initialization...");
  // Выводим сообщение "Initialization..." и переводим курсор на следующую строку.
  delay(1000);
  // Пауза 1 секунда.
  tft.fillScreen(TFT_BLACK);
  // Очистка экрана перед попыткой инициализации датчика.
  
  // ИНИЦИАЛИЗАЦИЯ ДАТЧИКА BME280
  bool sensor_ok = false;
  // Объявляем и инициализируем логическую (булеву) переменную-флаг.
  // false означает, что датчик пока не инициализирован успешно.
  
  // Попытка инициализации датчика BME280 с адресом 0x76
  if (bme.begin(0x76)) {
    // Вызываем метод begin() с адресом устройства на шине I2C, равным 0x76.
    // Метод пытается обнаружить датчик по этому адресу и вернет true в случае успеха.

    tft.setTextSize(1);
    tft.setCursor(0, 35);
    tft.println("BME280 (I2C 0x76)");
    // Сообщаем об успехе и указываем адрес.
    sensor_ok = true;
    // Устанавливаем флаг в true.
    delay(1000);
    // Пауза 1 секунда.
  } 
  // Если не сработал адрес 0x76, пробуем 0x77
  else if (bme.begin(0x77)) {
    // Вторая попытка с адресом 0x77. Некоторые датчики BME280 могут иметь этот адрес.
    tft.setTextSize(1);
    tft.setCursor(0, 35);
    tft.println("BME280 (I2C 0x77)");
    sensor_ok = true;
    delay(1000);
  }
  
  // ЕСЛИ ДАТЧИК УСПЕШНО ИНИЦИАЛИЗИРОВАН
  if (sensor_ok) {
    tft.fillScreen(TFT_BLACK);
    // Очищаем экран для вывода информации об успехе.

    tft.setTextSize(2);
    // Увеличиваем шрифт.

    tft.setCursor(0, 15);
    // Устанавливаем курсор.

    tft.println("BME280 OK!");
    // Выводим сообщение об успешной инициализации.

    tft.setCursor(0, 35);
    tft.print("ID:  0x");
    // Выводим текст "ID:  0x".

    tft.println(bme.sensorID(), HEX);
    // Выводим числовой идентификатор датчика, полученный методом sensorID().
    // Второй параметр HEX указывает, что число нужно вывести в шестнадцатеричном формате.
    
    
    // НАСТРОЙКА ПАРАМЕТРОВ BME280 ДЛЯ МАКСИМАЛЬНОЙ ТОЧНОСТИ
    bme.setSampling(Adafruit_BME280::MODE_NORMAL,
    // Устанавливаем режим работы датчика: NORMAL.
    // В этом режиме датчик непрерывно проводит измерения с заданным интервалом.
                   Adafruit_BME280::SAMPLING_X16,
                   // Устанавливаем "передискретизацию" (oversampling) для измерения температуры: коэффициент 16.
                   // Чем выше коэффициент, тем больше выборок усредняется для одного измерения
                   // что повышает точность, но увеличивает энергопотребление и время измерения.

                   Adafruit_BME280::SAMPLING_X16,
                   // Аналогично для измерения давления: коэффициент 16.

                   Adafruit_BME280::SAMPLING_X16,
                   // Аналогично для измерения влажности: коэффициент 16.

                   Adafruit_BME280::FILTER_X16,
                   // Устанавливаем коэффициент фильтрации IIR (Infinite Impulse Response) равным 16.
                   // Фильтр сглаживает кратковременные колебания давления (например, от сквозняка), повышая стабильность показаний высоты.

                   Adafruit_BME280::STANDBY_MS_1000);
                   // Устанавливаем время ожидания (standby time) между циклами измерений в режиме NORMAL равным 1000 мс (1 секунда).
                   // Датчик будет делать замер, затем "спать" 1 секунду, затем снова замер и т.д.
    
    delay(1000);
    // Пауза 1 секунда для стабилизации работы датчика после настройки.

    Serial.print("Тип датчика: ");
    // Вывод в Serial Monitor.

    Serial.println(getSensorInfo());
    // Вызов нашей вспомогательной функции и вывод ее результата.

    Serial.print("ID датчика: ");
    Serial.println(bme.sensorID(), HEX);
    // Представление в шестнадцатеричном виде.

    tft.fillScreen(TFT_BLACK);
    // Окончательная очистка экрана перед началом основного цикла.

    delay(1000);
  } else {
    // ЕСЛИ ДАТЧИК НЕ НАЙДЕН
    tft.fillScreen(TFT_BLACK);
    tft.setTextSize(2);
    tft.setCursor(0, 35);
    tft.setTextColor(TFT_RED, TFT_BLACK);
    // Меняем цвет на красный для сообщения об ошибке.

    tft.println("BME280 error!");
    // Выводим сообщение об ошибке на дисплей.
    
    // Выводим подробное сообщение об ошибке в Serial Monitor для помощи в диагностике.
    Serial.println("ОШИБКА: BME280 не найден!");
    Serial.println("Проверьте:");
    Serial.println("1. Подключение VCC, GND, SDA, SCL");
    Serial.println("2. Адрес датчика (0x76 или 0x77)");

    while (1);
    // Бесконечный пустой цикл (while(1) всегда истинно). Это останавливает выполнение программы (зацикливается на этом месте).
    // Устройство "зависает" и требует перезагрузки после устранения неисправности.
  }
}

// ФУНКЦИЯ LOOP() - ВЫПОЛНЯЕТСЯ БЕСКОНЕЧНО В ЦИКЛЕ
void loop() {
  readSensor();
  // Вызываем функцию для чтения новых данных с датчика BME280.

  displayData();
  // Вызываем функцию для отображения считанных данных на TFT-дисплее.

  printSerialData();
  // Вызываем функцию для вывода данных в Serial Monitor для отладки.
  
  delay(3000);
  // Приостанавливаем выполнение программы на 3000 миллисекунд (3 секунды).
  // Это определяет интервал между обновлениями данных на экране и в мониторе порта.
}

            После подачи питания рекомендуется несколько минут дать «прогреться» микросхеме, чтобы чувствительные элементы адаптировались к текущим климатическим условиям.

Читайте также:  Установка среды разработки Espressif IDE

            Посмотрим на результаты:

Высотомер

            Предлагаемый вариант высотомера измеряет относительную высоту относительно точки, выбранной пользователем. После включения датчик калибруется (усреднение по 20 значениям) и принимает текущую высоту за «0».

            В основном цикле измерения производятся по следующему алгоритму:

1) проверяется кнопка сброса. При нажатии текущая высота становится новым «0».

2) считываются 10 значений с датчика, усредняются для фильтрации шума.

3) вычисляется относительная высота:

Относительная высота = Текущая высота – Значение «0»

 4) результат выводится на экран и в монитор порта.

            Конкретный пример:

– установили «0» на 1 этаже;

– на 2 этаже прибор показывает +3,5 м;

– если на 2 этаже сбросить на «0», то на 3 этаже будет +3,5 м, а на 1 этаже будет -3,5 м.

            В коде используется неблокирующий таймер (на основе функции millis()) для опроса датчика каждые 200 мс, что обеспечивает быструю реакцию на кнопку.

            Ниже представлен подробный листинг кода:

// Altimeter
// // ==================== ПОДКЛЮЧЕНИЕ БИБЛИОТЕК ====================
#include <Wire.h>
// Подключаем библиотеку для работы с шиной I2C (Inter-Integrated Circuit)
#include <Adafruit_Sensor.h>
// Подключаем базовую библиотеку Adafruit для датчиков
#include <Adafruit_BME280.h>
// Подключаем библиотеку для работы с датчиком BME280
#include <TFT_eSPI.h>
// Подключаем библиотеку для работы с TFT-дисплеями

// ==================== НАСТРОЙКА ПИНОВ ПОДКЛЮЧЕНИЯ ====================
#define I2C_SDA 21
// Определяем константу I2C_SDA со значением 21 как линия данных (SDA) для шины I2C
#define I2C_SCL 22
// Определяем константу I2C_SCL со значением 22 как линия тактового сигнала (SCL) для шины I2C
#define ZERO_BTN 0
// Определяем константу ZERO_BTN со значением 0 как кнопка для сброса высоты (калибровки)

// ==================== СОЗДАНИЕ ОБЪЕКТОВ ДЛЯ ОБОРУДОВАНИЯ ====================
Adafruit_BME280 bme;
// Создаем объект (экземпляр класса) с именем 'bme' типа Adafruit_BME280
TFT_eSPI tft = TFT_eSPI();
// Создаем объект с именем 'tft' типа TFT_eSPI и сразу его инициализируем

// ==================== ПАРАМЕТРЫ ИЗМЕРЕНИЙ ====================
#define BME280_SEALEVEL_HPA (1013.25)
// Определяем константу BME280_SEALEVEL_HPA со значением 1013.25.
// Это стандартное атмосферное давление на уровне моря в гектопаскалях (гПа).
// Используется как опорное значение для расчета высоты по давлению.

// ==================== ПЕРЕМЕННЫЕ ДЛЯ ХРАНЕНИЯ СОСТОЯНИЯ ====================
float baseAltitude = 0;
// Объявляем переменную baseAltitude (базовая высота) типа float и инициализируем её нулем.
// В ней хранится высота в момент калибровки (точка, которую мы считаем "нулем").

float currentHeight = 0;
// Объявляем переменную currentHeight (текущая высота) типа float.
// Здесь будет храниться разница между текущей высотой и базовой (baseAltitude).
// Это и есть наша относительная высота над точкой калибровки.

float rawAltitude = 0;
// Объявляем переменную rawAltitude (сырая высота) типа float.  
// Здесь будет храниться "сырое" значение высоты, считанное с датчика BME280
// (расчетная высота над уровнем моря, без учета калибровки).

float temperature = 0;
// Объявляем переменную temperature (температура) типа float.
// Здесь будет храниться значение температуры в градусах Цельсия.

float pressure = 0;
// Объявляем переменную pressure (давление) типа float.
// Здесь будет храниться атмосферное давление в гектопаскалях (гПа).

float humidity = 0;
// Объявляем переменную humidity (влажность) типа float.
// Здесь будет храниться относительная влажность воздуха в процентах.

bool isZeroCalibrated = false;
// Объявляем логическую (булевую) переменную isZeroCalibrated и инициализируем значением false.
// Этот флаг будет указывать, была ли выполнена калибровка (установка "нулевой" точки).
// false - калибровка не выполнена, true - выполнена.


// Переменные для обработки кнопки
unsigned long lastButtonPress = 0;
// Объявляем переменную lastButtonPress типа unsigned long (длинное целое без знака).
// Здесь будем хранить время (в миллисекундах, возвращаемое функцией millis()) последнего нажатия кнопки.
// Нужно для реализации антидребезга (debounce).

#define DEBOUNCE_TIME 300
// Определяем константу DEBOUNCE_TIME (время антидребезга) равной 300 миллисекундам.   
// Это минимальный интервал времени между регистрацией двух нажатий кнопки.
// Помогает избежать ложных срабатываний из-за механического дребезга контактов.

// Переменные для сглаживания показаний
#define SMOOTHING_SAMPLES 10
// Определяем константу SMOOTHING_SAMPLES (количество образцов для сглаживания) равной 10.
// Определяет размер буфера для скользящего среднего. Чем больше значение, тем сильнее сглаживание,
// но тем медленнее реакция на изменения.

float altitudeBuffer[SMOOTHING_SAMPLES];
// Объявляем массив altitudeBuffer из SMOOTHING_SAMPLES (10) элементов типа float.
// В этот массив будем складывать последние 10 измерений сырой высоты для усреднения.

float tempBuffer[SMOOTHING_SAMPLES];
// Объявляем массив tempBuffer для хранения последних 10 измерений температуры.

float pressureBuffer[SMOOTHING_SAMPLES];
// Объявляем массив pressureBuffer для хранения последних 10 измерений давления.

float humidityBuffer[SMOOTHING_SAMPLES];
// Объявляем массив humidityBuffer для хранения последних 10 измерений влажности.

int bufferIndex = 0;
// Объявляем переменную bufferIndex (индекс буфера) типа int и инициализируем нулем.
// Указывает на текущую позицию в массивах-буферах, куда будет записано следующее измерение.


// ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================

/*
 * Функция: getSmoothedReadings()
 * Назначение: Получение усредненных показаний от датчика BME280
 * Принцип работы: Использует скользящее среднее для всех параметров
 */
void getSmoothedReadings() {
  // Получаем новые измерения
  altitudeBuffer[bufferIndex] = bme.readAltitude(BME280_SEALEVEL_HPA);
  // Вызываем метод readAltitude() объекта bme.
  // Он вычисляет и возвращает высоту в метрах над уровнем моря,
  // используя текущее давление и опорное давление (BME280_SEALEVEL_HPA).
  // Полученное значение сохраняем в массив altitudeBuffer на позицию bufferIndex.

  tempBuffer[bufferIndex] = bme.readTemperature();
  // Читаем температуру с датчика и сохраняем в tempBuffer.

  pressureBuffer[bufferIndex] = bme.readPressure() / 100.0F;
  // Читаем давление в Паскалях, делим на 100, чтобы получить гПа.
  // Сохраняем в pressureBuffer.

  humidityBuffer[bufferIndex] = bme.readHumidity();
  // Читаем влажность и сохраняем в humidityBuffer.
  
  // Увеличиваем индекс буфера
  bufferIndex = (bufferIndex + 1) % SMOOTHING_SAMPLES;
  // Увеличиваем bufferIndex на 1.
  // Оператор % (остаток от деления) обеспечивает циклическое перемещение индекса.
  // Когда bufferIndex достигнет значения SMOOTHING_SAMPLES (10), он сбросится в 0.
  // Таким образом, массив работает как кольцевой буфер.
  
  // Вычисляем средние значения
  float altitudeSum = 0, tempSum = 0, pressureSum = 0, humiditySum = 0;
  // Объявляем и инициализируем нулем четыре переменные для сумм.
  
  for (int i = 0; i < SMOOTHING_SAMPLES; i++) {
    altitudeSum += altitudeBuffer[i];
    // Прибавляем к altitudeSum значение из i-й ячейки массива altitudeBuffer.

    tempSum += tempBuffer[i];
    // Аналогично для температуры.

    pressureSum += pressureBuffer[i];
    // Аналогично для давления.

    humiditySum += humidityBuffer[i];
    // Аналогично для влажности.
  }
  
  // Сохраняем усредненные значения
  rawAltitude = altitudeSum / SMOOTHING_SAMPLES;
  // Вычисляем среднее арифметическое: сумма / количество элементов.
  // Результат (усредненная сырая высота) сохраняем в переменную rawAltitude.

  temperature = tempSum / SMOOTHING_SAMPLES;
  // Усредненную температуру сохраняем в temperature.

  pressure = pressureSum / SMOOTHING_SAMPLES;
  // Усредненное давление сохраняем в pressure.

  humidity = humiditySum / SMOOTHING_SAMPLES;
  // Усредненную влажность сохраняем в humidity.
}

/*
 * Функция: updateHeight()
 * Назначение: Обновление текущей относительной высоты
 */
void updateHeight() {
  // Получаем усредненные показания
  getSmoothedReadings();
  // Вызываем функцию getSmoothedReadings(). После её выполнения переменные rawAltitude, temperature,
  // pressure и humidity будут содержать усредненные значения.
  
  // Вычисляем относительную высоту
  if (isZeroCalibrated) {
    currentHeight = rawAltitude - baseAltitude;
    // Если да, то вычисляем текущую относительную высоту:
    // от сырой высоты (расчетной высоты над уровнем моря) отнимаем базовую высоту
    // (высоту над уровнем моря в точке калибровки).
    // Результат сохраняем в currentHeight.
  } else {
    currentHeight = 0;
    // Если калибровка не выполнена (isZeroCalibrated равно false)
    // Устанавливаем текущую высоту равной 0.
  }
}

/*
 * Функция: calibrateZero()
 * Назначение: Установка текущей точки как "нулевой"
 */
void calibrateZero() {
  // Делаем несколько измерений для точности
  float sum = 0;
  // Объявляем локальную переменную sum типа float для накопления суммы измерений высоты.
  for (int i = 0; i < 20; i++) {
    sum += bme.readAltitude(BME280_SEALEVEL_HPA);
    // На каждой итерации читаем высоту с датчика и прибавляем значение к sum.
    delay(50);
    // Приостанавливаем выполнение программы на 50 миллисекунд.
    // Это дает датчику время для стабилизации и обеспечивает независимость измерений.
  }
  
  // Устанавливаем нулевую точку
  baseAltitude = sum / 20.0;
  // После цикла вычисляем среднее значение высоты: сумма 20 измерений / 20.
  // Полученное значение сохраняем в переменную baseAltitude (это будет наша новая "нулевая" точка).

  currentHeight = 0;
  // Сбрасываем текущую относительную высоту в 0, так как мы только что установили новую базовую точку.

  isZeroCalibrated = true;
  // Устанавливаем флаг isZeroCalibrated в true, указывая, что калибровка выполнена.
  
  // Показываем подтверждение на дисплее
  tft.fillScreen(TFT_BLACK);
  // Очищаем весь экран, заливая его черным цветом.

  tft.setTextColor(TFT_GREEN);
  // Устанавливаем цвет текста - зеленый.

  tft.setTextSize(1);
  // Устанавливаем размер шрифта 1 (базовый, ~6x8 пикселей).

  tft.setCursor(0, 0);
  // Устанавливаем курсор в левый верхний угол экрана (координаты X=0, Y=0).

  tft.println("Zero point set!");
  // Выводим строку "Zero point set!" и переходим на следующую строку.

  tft.print("Base: ");
  // Выводим текст "Base: " без перехода на новую строку.

  tft.print(baseAltitude, 1);
  // Выводим значение baseAltitude с одним знаком после запятой.

  tft.println(" m");
  // Выводим единицу измерения " m" и переходим на новую строку.

  delay(1500);
  // Даем пользователю 1.5 секунды, чтобы прочитать сообщение на экране.
}

/*
 * Функция: checkResetButton()
 * Назначение: Проверка нажатия кнопки калибровки
 */
void checkResetButton() {
  if (digitalRead(ZERO_BTN) == LOW) {
    // Считываем состояние пина ZERO_BTN (пин 0) с помощью digitalRead().
    // Поскольку кнопка подключена с использованием INPUT_PULLUP,
    // нажатие кнопки замыкает пин на GND, и digitalRead() возвращает LOW.
    // Если кнопка нажата (состояние LOW)...
    unsigned long currentTime = millis();
    // Получаем текущее время в миллисекундах с момента запуска программы
    // с помощью функции millis(). Сохраняем в переменную currentTime.
    
    if (currentTime - lastButtonPress > DEBOUNCE_TIME) {
      // Вычисляем, сколько времени прошло с последнего зарегистрированного нажатия.
      // Если разница больше времени антидребезга (DEBOUNCE_TIME = 300 мс)...
      lastButtonPress = currentTime;
      // Обновляем переменную lastButtonPress, записывая в неё текущее время.
      // Это предотвратит повторную обработку нажатия в течение следующих 300 мс.
      calibrateZero();
      // Вызываем функцию калибровки calibrateZero().
    }
  }
}

/*
 * Функция: displayHeight()
 * Назначение: Отображение данных на TFT-дисплее
 */
void displayHeight() {
  tft.fillScreen(TFT_BLACK);
  // Очищаем экран черным цветом перед выводом новых данных.
  
  // Заголовок
  tft.setTextColor(TFT_CYAN);
  // Устанавливаем цвет текста для заголовка - бирюзовый (cyan).
  tft.setTextSize(1);
  // Устанавливаем размер шрифта 1.
  tft.setCursor(5, 5);
  // Устанавливаем курсор на координаты X=5, Y=5 (немного отступ от краев).
  tft.println("ALTIMETER BME280");
  // Выводим название устройства и переходим на новую строку.
  
  // Отображаем текущую высоту (крупными цифрами)
  tft.setTextColor(TFT_YELLOW);
  // Меняем цвет текста на желтый для вывода значения высоты.
  tft.setTextSize(3);
  // Устанавливаем большой размер шрифта (3) для лучшей читаемости.
  tft.setCursor(10, 30);
  // Устанавливаем курсор на позицию X=10, Y=30.
  tft.print(currentHeight, 2);
  // Выводим значение currentHeight с двумя знаками после запятой.
  
  // Единицы измерения
  tft.setTextColor(TFT_WHITE);
  // Меняем цвет на белый для единиц измерения.
  tft.setTextSize(2);
  // Устанавливаем размер шрифта 2 (меньше, чем само значение высоты).
  tft.print(" m");
  // Выводим пробел и букву "m" (метры) сразу после числа, без перехода на новую строку.
}

// ==================== ОСНОВНЫЕ ФУНКЦИИ ARDUINO ====================

void setup() {
  // Инициализация последовательного порта
  Serial.begin(115200);
  // Инициализируем последовательную (Serial) коммуникацию со скоростью 115200 бит в секунду.
  // Это позволит выводить отладочную информацию на монитор порта в Arduino IDE.
  
  
  pinMode(ZERO_BTN, INPUT_PULLUP);
  // Настраиваем пин ZERO_BTN (GPIO0) как вход (INPUT) с подтягивающим резистором к HIGH (PULLUP).
  // Это означает, что когда кнопка не нажата, на пине будет логическая 1 (HIGH).
  // При нажатии кнопки пин соединяется с землей (GND), и на нем устанавливается 0 (LOW).
  
  
  Wire.begin(I2C_SDA, I2C_SCL);
  // Инициализируем шину I2C, указывая номера пинов для SDA (I2C_SDA = 21) и SCL (I2C_SCL = 22).
  

  tft.init(); // Инициализация TFT-дисплея
  // Вызываем метод init() объекта tft. Эта команда инициализирует дисплей, настраивает необходимые пины и протокол связи.
  tft.setRotation(3);
  // Устанавливаем ориентацию дисплея. Значение 3 соответствует повороту на 180 градусов.
  // Это означает, что координата (0,0) будет в левом нижнем углу при обычном горизонтальном расположении дисплея.

  tft.fillScreen(TFT_BLACK);
  // Очищаем экран черным цветом.

  tft.setTextColor(TFT_WHITE);
  // Устанавливаем белый цвет текста по умолчанию.

  tft.setTextSize(1);
  // Устанавливаем размер шрифта по умолчанию.

  tft.setCursor(10, 10);
  // Устанавливаем курсор на позицию X=10, Y=10.

  tft.println("Initializing...");
  // Выводим сообщение о начале инициализации.
  
  // Инициализация датчика BME280 с максимальной точностью
  bool status = bme.begin(0x76);
  // Пытаемся инициализировать датчик BME280 по адресу 0x76 на шине I2C.
  // Метод begin() возвращает true в случае успешного обнаружения датчика, false - в случае ошибки.
  // Результат сохраняем в логическую переменную status.
  
  if (!status) {
    // Показываем ошибку на дисплее
    tft.fillScreen(TFT_BLACK);
    // Очищаем экран.

    tft.setTextColor(TFT_RED);
    // Устанавливаем красный цвет текста для сообщения об ошибке.

    tft.setTextSize(1);
    // Устанавливаем размер шрифта.

    tft.setCursor(0, 0);
    // Устанавливаем курсор в начало.

    tft.println("BME280 ERROR!");
    // Выводим сообщение об ошибке.

    tft.println("Check connection");
    // Выводим подсказку проверить соединение.
    
    while(true);
    // Входим в бесконечный пустой цикл while(true). Это полностью останавливает выполнение программы.
    // Устройство "зависает" до перезагрузки. Пользователь должен устранить проблему с подключением датчика.
  }
  
  // Настройка параметров BME280 для максимальной точности
  bme.setSampling(Adafruit_BME280::MODE_NORMAL,
  // Устанавливаем режим работы датчика: MODE_NORMAL.
  // В этом режиме датчик непрерывно проводит измерения с заданным интервалом.
                  Adafruit_BME280::SAMPLING_X2,
                  // Устанавливаем коэффициент передискретизации (oversampling) для температуры: X2.
                  // Это означает, что датчик внутренне усредняет 2 измерения для одного значения температуры.

                  Adafruit_BME280::SAMPLING_X8,
                  // Устанавливаем коэффициент передискретизации для давления: X8 (высокая точность).
                  // Давление - самый важный параметр для высотомера.

                  Adafruit_BME280::SAMPLING_NONE,
                  // Устанавливаем коэффициент для влажности: SAMPLING_NONE (измерение отключено).
                  // Поскольку для высотомера влажность не критична, мы её отключаем для экономии энергии.

                  Adafruit_BME280::FILTER_X8,
                  // Устанавливаем коэффициент цифрового фильтра IIR: X8.
                  // Фильтр сглаживает кратковременные колебания давления (например, от сквозняков).

                  Adafruit_BME280::STANDBY_MS_250);
                  // Устанавливаем время ожидания между циклами измерений в режиме NORMAL: 250 миллисекунд.
                  // Датчик будет делать замер, затем "спать" 250 мс, затем снова замер. Частота ~4 Гц.
  
  // Инициализация буферов для сглаживания
  for (int i = 0; i < SMOOTHING_SAMPLES; i++) {
    altitudeBuffer[i] = bme.readAltitude(BME280_SEALEVEL_HPA);
    // Читаем высоту и сохраняем в i-ю ячейку буфера.

    tempBuffer[i] = bme.readTemperature();
    // Читаем и сохраняем температуру.

    pressureBuffer[i] = bme.readPressure() / 100.0F;
    // Читаем давление, переводим в гПа и сохраняем.
    
    humidityBuffer[i] = bme.readHumidity();
    // Читаем и сохраняем влажность (хотя она отключена, метод может возвращать 0 или последнее значение).
    delay(20);
    // Делаем паузу 20 мс между измерениями, чтобы они были независимыми.
  }
  
  delay(2000);
  // Делаем паузу 2 секунды после инициализации. Возможно, для стабилизации системы или чтобы пользователь увидел сообщения.
  
  calibrateZero();
  // Вызываем функцию калибровки. Это установит текущее местоположение как "нулевую" точку высоты при старте устройства.
}

void loop() {
  checkResetButton();
  // Вызываем функцию checkResetButton(). Она проверит, нажата ли кнопка калибровки, и обработает нажатие.
  
  // Обновление данных каждые 200 мс
  static unsigned long lastUpdateTime = 0;
  // Объявляем статическую переменную lastUpdateTime типа unsigned long и инициализируем её нулем.
  // Ключевое слово static означает, что переменная сохраняет свое значение между вызовами функции loop().
  // Здесь будет храниться время последнего обновления данных на дисплее.
  
  if (millis() - lastUpdateTime >= 200) {
    // Проверяем условие: прошло ли 200 миллисекунд с момента последнего обновления?
    // millis() возвращает текущее время. Вычитаем lastUpdateTime, получаем прошедшее время.
    lastUpdateTime = millis();
    // Если условие истинно (прошло >=200 мс), обновляем lastUpdateTime текущим временем.
    
    updateHeight();
    // Вызываем функцию updateHeight(). Она обновит все показания датчика (сырую высоту, температуру и т.д.)
    // и вычислит текущую относительную высоту (currentHeight).
    
    displayHeight();
    // Вызываем функцию displayHeight(). Она очистит экран и отобразит текущую относительную высоту (currentHeight)
    // и другую информацию на TFT-дисплее.
  }
  // Если с последнего обновления прошло меньше 200 мс, функция просто завершится, и управление вернется в начало loop().
  // Таким образом, обновление экрана и данных происходит примерно 5 раз в секунду (1000 мс / 200 мс = 5 Гц).
}

            В качестве эксперимента попробуем определить высоту шкафа (примерно 246 см):

            На полу (0 м) выставляем относительный «НОЛЬ», нажав кнопку BOOT (GPIO_0 на плате) справа от USB-разъёма:

120 см:

245 см:

            Даже выполняя усреднение из 10 значений, в процессе работы показания постоянно «плавали» в диапазоне ±10 см относительно точки регистрации высоты. На фотографиях зафиксированы мгновенные значения, входящие в указанный диапазон. Поэтому можно сделать вывод, что прибор определяет высоту с абсолютной погрешностью как раз те самые ±10 см (настройки bme.setSampling «Карманный высотомер: точность давления, скорость реакции»).

Читайте также:  Измерение уровня заряда аккумулятора на микроконтроллере

Заключение

            В данной статье мы познакомились с комбинированным климатическим сенсором BME280 и сравнили его с ближайшими аналогами. Анализ показал, что данный датчик подтверждает свой статус универсального «золотого стандарта» среди DIY-сенсоров для измерения ключевых климатических параметров. Удачная интеграция высокоточного барометра (на уровне BMP280), термокомпенсационного датчика и ёмкостного гигрометра в один компактный корпус делает его оптимальным выбором для большинства проектов.

            Основные преимущества датчика:

1) широкий функционал. Один модуль предоставляет данные о давлении, температуре и влажности, что достаточно для построения полноценной метеостанции или высотомера.

2) высокая точность и гибкость. Программируемые параметры (режимы работы, коэффициент фильтрации, время ожидания) позволяют тонко настроить микросхему преобразователя под конкретную задачу — от энергоэффективного датчика для носимых устройств до быстрого высотомера для летательных аппаратов.

3) отличное соотношение цены и качества. Несмотря на появление более новых моделей (BME680/688) и альтернативных комбинаций (например, BMP280 + AHT20), BME280 остаётся наиболее сбалансированным решением для хобби-проектов и прототипов.

            Рассмотренный преобразователь отлично подходит для для домашней метеостанции и для относительного высотомера, способного точно фиксировать перепады высот в несколько метров. Для достижения максимальной точности в таких приложениях критически важна правильная программная настройка сенсора и использование фильтрации данных.

            Таким образом, BME280 является проверенным, хорошо документированным и доступным компонентом, который благодаря своей гибкости и точности остаётся одним из лучших вариантов для встраивания в «умные» устройства, связанные с мониторингом окружающей среды.