Датчик атмосферного давления BMP180: высотомер на ESP32

          BMP180 — это цифровой датчик атмосферного давления (АД) и температуры окружающей среды, разработанный компанией Bosch Sensortec. Данный сенсор позволяет рассчитывать высоту над уровнем моря и отслеживать тенденции изменения погоды. В рамках данной статьи мы подробно рассмотрим устройство и принцип работы преобразователя, а также соберём, кроме классической метеостанции, ещё один полезный прибор — цифровой высотомер (альтиметр) на базе ESP32 с индикацией на OLED-дисплее.


Содержание


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

          BMP180 представляет собой микросхему цифрового барометра и термометра, чувствительные элементы которой построены по технологии MEMS (Micro-Electro-Mechanical Systems, микроэлектромеханическая система). Датчик является преемником модели BMP085.

          Чип включает в себя два ключевых сенсора:

          — пьезорезистивный барометрический преобразователь, который измеряет абсолютное АД в диапазоне от 300 до 1100 гПа (гектоПаскалей, где 1 гПа = 100 Па = 100 Н/м² ≈ 0,75 мм рт. ст.). Этот диапазон покрывает приблизительно от -500 до +9000 метров относительно моря;

          — встроенный преобразователь температуры (термистор, NTC-терморезистор), предназначенный, в первую очередь, для определения температуры окружающей среды (необходимо для температурной компенсации показаний барометра).

          Для более требовательных или современных приложений рек рассмотреть следующие аналоги:

          — BMP280: прямой наследник BMP180 с лучшей точностью (±1 гПа против ±2 гПа), меньшим энергопотреблением и размерами;

          — BME280: тот же BMP280, но с добавлением преобразователя влажности, создавая комплексный сенсор для простой метеостанции;

          — MS5611: профессиональный барометрический детектор с аналогичной высокой точностью и интерфейсами SPI/I2C, часто применяемый в авиамоделировании;

          — LPS22HH: современный MEMS-барометр от STMicroelectronics с улучшенной стабильностью и перегрузочной способностью.

          Для реализации высотомера (альтиметра) получаемых данных с BMP180, как правило, достаточно. Однако для достижения максимальной точности на практике рекомендуется:

          1) использовать усреднение нескольких измерений;

          2) регулярно обновлять значение опорного АД, которое будет соответствовать условному значению «0 метров», относительно которого производится измерение высоты или глубины;

          3) учитывать температурную компенсацию;

          4) применять алгоритмы сглаживания для устранения шумов.

          Таким образом, BMP180 представляет собой проверенное, надежное и доступное решение для определения АД и высоты. Его простота подключения по интерфейсу I2C, обширная поддержка в виде библиотек для Arduino и других платформ, а также доступная цена обеспечили ему популярность в DIY-проектах.


Устройство и принцип работы

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

          В основе сенсора лежит кремниевая подложка, а на ней — кремниевая мембрана толщиной 10 микрон (в 10 раз тоньше человеческого волоса). Мембрана является частью единого кремниевого кристалла, которую вытравили с обратной стороны до нужной толщины, создав вакуумную полость под ней. При изменении воздушного воздействия на 1 гПа мембрана прогибается на 0,1-0,2 микрона. Это микроскопическое движение — основа всех измерений.

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

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

          Кстати, может возникнуть вопрос, а чем же пьезорезисторы отличаются от тензорезисторов. Они оба изменяют сопротивление от приложенного физического воздействия. Разница в том, что тензорезисторы меняют сопротивление из-за геометрической деформации (растянулся — стал длиннее и тоньше). А пьезорезисторы меняют сопротивление, в первую очередь, из-за изменения внутренних свойств материала, вызванного деформацией. Геометрические изменения у них вторичны. Это характерно для чувствительных элементов из полупроводниковых материалов. В этом принципиальное различие.

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

          Изменение сопротивления пьезорезисторов в составе моста Уитстона преобразуется в изменение напряжения. Это аналоговое напряжение оцифровывается встроенным высокоточным аналого-цифрового преобразователя (АЦП). Также производится оцифровка напряжения со встроенного температурного сенсора. Полученные «сырые» цифровые значение через встроенный контроллер управления отправляются на внешнее устройство (например, ESP32) по I2C-интерфейсу. Внешний микроконтроллер, используя уникальные калибровочные коэффициенты (они записаны в EEPROM-памяти каждого чипа при производстве), рассчитывает итоговые точные значения давления и температуры. На рисунке ниже представлен фрагмент из официальной документации к чипу, на котором изображена блок-схема чипа и подключение к внешнему микроконтроллеру:

          В чистом виде данные из микросхемы поступают на внешнее устройство в виде числовых отсчётов АЦП, без компенсации: «сырой» код термистора (Uncompensated Temperature — UT), «сырой» код барометра (Uncompensated Pressure — UP). Также передаются индивидуальные для чипа калибровочные коэффициента из EEPROM.

          По специальным формулам, представленным в технической документации к чипу BMP180, вычисляется текущее абсолютное АД. Всю эту работу выполняет библиотека (например, Adafruit_BMP085), поэтому не будем на этом останавливаться. Для быстрого ознакомления приведу алгоритм расчёта из даташита к чипу:

          Абсолютное АД — это физическая величина, численно равная силе, с которой столб атмосферы давит на единицу площади в точке регистрации. Большинство барометрических сенсоров измеряют именно эту физическую величину относительно абсолютного вакуума. То есть абсолютное АД не приведено к уровню моря. Соответственно, сенсор фиксирует АД в том месте, где он расположен. К примеру, Москва расположена на отметке примерно 156 метров над морем, поэтому для этого города нормальное значение приблизительно 745 мм рт. ст. На отечественных сайтах прогноза погоды как раз такой тип АД представляют.

          На основе зарегистрированной величины АД и барометрической формулы осуществляется расчёт высоты над морем, поскольку воздействие воздуха равномерно уменьшается при подъёме вверх.

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

          Барическая ступень — это высота (в метрах), на которую нужно подняться или опуститься, чтобы АД изменилось на 1 гектопаскаль (гПа). Её величина непостоянна (изменяется по экспоненте) и зависит от двух ключевых факторов:

          1) температуры воздуха: в тёплом воздухе ступень больше (если подниматься выше в таких условиях, то давление падает медленнее), в холодном — меньше;

          2) исходной отметки: чем выше над уровнем мирового океана, тем больше барическая ступень, так как воздух становится более разреженным.

          Фактически это выглядит так:

          — на уровне моря (0…1 км) АД падает примерно на 1 гПа каждые 10,5 метров;

          — выше (1…2 км) для такого же падения на 1 гПа нужно подняться уже на 11,9 метров;

          — выше (2…3 км) падения на 1 гПа соответствует перепаду в 13,5 метров.

          В нашем случае для измерения высоты по АД в стандартных для человека условиях (температура 0…20 °C, отметка 0…300 м над морем) будем исходить из того, что изменение давления на 1 мм рт. ст. соответствует изменению высоты приблизительно на 11 метров.

          Теперь рассмотрим самое интересное: как абсолютное АД, зафиксированное микросхемой, пересчитать в высоту. Как правило, расчёт выполняют относительно давления на уровне мирового океана (1013,25 гПа). Немного забегая вперёд, отмечу, что в нашем альтиметре мы будем сами определять опорное давление, относительного которого будем производить последующий расчёт.

          Для расчёта отметки относительно уровня моря применяется либо упрощённая (стандартная) барометрическая формула, либо более сложная формула с учётом температуры.

          1) Упрощённая (стандартная) барометрическая формула:

где h (м) — отметка над уровнем моря;

P (Па) — абсолютное АД (которое получено от сенсора);

P0 = 101325 Па — стандартное АД на уровне мирового океана;

5,255 — безразмерный коэффициент, выведенный из физических констант.

          2) Барометрическая формула повышенной точности:

где T0 (℃) — температура воздуха вокруг микросхемы;

273,15 — для перевода значений градусов (из Цельсия в Кельвина);

R = 8,31446 (Дж/(моль‧К)) — универсальная газовая постоянная;

g = 9,80665 (м/с²) — ускорение свободного падения;

M = 0,0238644 (кг/моль) — молярная масса сухого воздуха.


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

          Преобразователь имеет 4 режима работы, которые выбираются программно. Каждый режим регламентирует параметры процесса измерения: разрешение и скорость. Всё это достигается за счёт техники многократной регистрации одного и того же сигнала с последующим усреднением (OverSampling Setting — OSS).

Читайте также:  ESP32-C3 и TFT-дисплей: вывод времени по Wi-Fi

          Благодаря многократной регистрации и последующему вычислению среднего арифметического, удаётся нивелировать влияние шумов, что увеличивает точность. Однако с увеличением числа выборок растёт энергопотребление и время на одно преобразование. Ниже представлена таблица с параметрами режимов работы:


Заводские калибровочные коэффициенты

          Каждый сенсор в процессе производства на заводе проходит индивидуальную калибровку при трёх известных значениях давления и температуры. Персонально в EEPROM чипа записываются 11 уникальных коэффициентов, которые занимают 176 бит (22 байта).

          Калибровочные коэффициенты содержат следующие данные:

          — AC1-AC6: полиномиальные коэффициенты для температурной компенсации;

          — B1-B2: коэффициенты для коррекции нелинейности воздушного воздействия на мембрану;

          — MB, MC, MD: поправочные коэффициенты для различных режимов работы.

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

          При желании можно просмотреть коэффициент на конкретно Вашем экземпляре сенсора, подключив его к микроконтроллеру, прошитый следующим кодом:

#include <Wire.h>

#define BMP180_ADDRESS 0x77 // адрес BMP180

int16_t readReg16(uint8_t reg) {
  Wire.beginTransmission(BMP180_ADDRESS);
  Wire.write(reg);
  Wire.endTransmission();
  
  Wire.requestFrom(BMP180_ADDRESS, 2);
  while (Wire.available() < 2);
  
  uint8_t msb = Wire.read();
  uint8_t lsb = Wire.read();
  
  return (msb << 8) | lsb;
}

void printCoefficientName(int index) {
  const char* names[] = {"AC1", "AC2", "AC3", "AC4", "AC5", "AC6", 
                         "B1", "B2", "MB", "MC", "MD"};
  Serial.print(names[index]);
}

void setup() {
  Serial.begin(115200);
  Wire.begin();
  
  Serial.println("Коэффициенты BMP180 из EEPROM:");
  Serial.println("-------------------------------");
  
  // Читаем и выводим все 11 коэффициентов
  for (int i = 0; i < 11; i++) {
    uint8_t reg = 0xAA + (i * 2);
    int16_t value = readReg16(reg);
    
    Serial.print("Коэффициент ");
    printCoefficientName(i);
    Serial.print(" (0x");
    Serial.print(reg, HEX);
    Serial.print("): ");
    Serial.println(value);
  }
}

void loop() {
 
}

          Загрузив код и перезагрузив контроллер, откройте монитор порта, и Вы увидите все 11 коэффициентов:


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

          Чтобы экспериментировать с барометрическим сенсором, мы воспользуемся популярной платой HW-596 (иногда подобная плата маркируется как GY-68) со всей минимально необходимой обвязкой:

          На плате размещены все необходимые элементы: подтягивающие резисторы, фильтрующие конденсаторы, а также линейный стабилизатор напряжения на 3,3 В / 300 мА.

          Ниже представлена таблица распиновки и принципиальная схема платы модуля HW-596:


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

          Для построения альтиметра воспользуемся отладочной платой NodeMCU-32S (38 pin), которая построена на базе модуля ESP-WROOM-32. Для отображения информации применим OLED-дисплей на базе контроллера SSD1306. Интерфейс подключения для обоих модулей — I2C. Ниже представлена таблица подключений сенсора и дисплея к отладочной плате.

          Схема подключения и фотография собранного макета:


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

          Разработка прошивки выполнялась в среде программирования Arduino IDE. Чтобы программировать ESP32, для среды необходимы дополнительные настройки. Подробная инструкция приведена в статье про микрофон INMP441.

          Для работы с OLED-дисплеем воспользуемся популярной библиотекой Adafruit_SSD1306 (дополнительно можно подключить Adafruit_GFX для отображения графики).

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

Вывод на дисплей базовых величин

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

// Подключаем необходимые библиотеки для работы с оборудованием
#include <Wire.h>              // Библиотека для работы с шиной I2C (межмикросхемной связью)
#include <Adafruit_Sensor.h>   // Общая библиотека для датчиков Adafruit (базовые функции)
#include <Adafruit_BMP085.h>   // Специализированная библиотека для датчика давления BMP180/BMP085
#include <Adafruit_GFX.h>      // Графическая библиотека для создания изображений на дисплее
#include <Adafruit_SSD1306.h>  // Библиотека для управления OLED-дисплеем SSD1306

// Определяем номера пинов микроконтроллера ESP32 для подключения шины I2C
#define I2C_SDA 21  // Пин для данных (Serial Data) шины I2C
#define I2C_SCL 22  // Пин для тактового сигнала (Serial Clock) шины I2C

// Определяем физические параметры OLED-дисплея для корректного отображения
#define SCREEN_WIDTH 128   // Ширина дисплея в пикселях (128 точек по горизонтали)
#define SCREEN_HEIGHT 64   // Высота дисплея в пикселях (64 точки по вертикали)
#define OLED_RESET -1      // Пин сброса дисплея (-1 означает, что отдельный пин не используется)

// Создаем программные объекты для взаимодействия с аппаратными компонентами
Adafruit_BMP085 bmp;  // Объект для работы с датчиком BMP180
// Объект для работы с OLED-дисплеем с указанием параметров:
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Функция setup() выполняется однократно при запуске микроконтроллера
void setup() {
  // Инициализируем последовательный порт для отладки и вывода данных
  Serial.begin(115200);  // Скорость передачи данных 115200 бит/секунду
  Serial.println("Тест BMP180 + ESP32");  // Вывод приветственного сообщения в монитор порта
  
  // Инициализируем шину I2C на указанных пинах
  Wire.begin(I2C_SDA, I2C_SCL);  // Активация I2C с назначением пинов SDA и SCL
  
  // Инициализируем OLED-дисплей
  // SSD1306_SWITCHCAPVCC - режим питания от внутреннего преобразователя
  // 0x3C - I2C-адрес дисплея (стандартный адрес для большинства OLED-дисплеев)
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    // Если инициализация не удалась, выводим сообщение об ошибке
    Serial.println(F("Ошибка инициализации дисплея"));
    for(;;);  // Бесконечный цикл (останавливаем выполнение программы)
  }
  
  // Инициализируем датчик BMP180
  if (!bmp.begin()) {
    // Если датчик не найден, выводим сообщение об ошибке
    Serial.println("Датчик BMP180 не найден!");
    while (1);  // Бесконечный цикл (останавливаем выполнение программы)
  }
  
  // Настраиваем параметры отображения на OLED-дисплее
  display.clearDisplay();       // Очищаем весь буфер дисплея (заполняем черным цветом)
  display.setTextColor(SSD1306_WHITE);  // Устанавливаем белый цвет текста (на черном фоне)
  display.setTextSize(1);       // Устанавливаем размер шрифта 1 (стандартный, 6x8 пикселей)
  
  // Отображаем приветственное сообщение на дисплее
  display.setCursor(0, 0);      // Устанавливаем курсор в начальную позицию (верхний левый угол)
  display.println("Sprytron.ru");      // Выводим текст и переходим на следующую строку
  display.println("Initialized!");     // Выводим второй текст
  display.display();            // Отправляем содержимое буфера на физический дисплей
  delay(2000);                  // Пауза 2 секунды для чтения приветственного сообщения
  
  display.clearDisplay();       // Очищаем дисплей после отображения приветствия
}

// Функция loop() выполняется циклически после завершения setup()
void loop() {
  // БЛОК 1: ЧТЕНИЕ ДАННЫХ С ДАТЧИКА
  
  // Считываем температуру с датчика
  // readTemperature() возвращает значение в градусах Цельсия
  float temperature = bmp.readTemperature();

  // Считываем атмосферное давление
  // readPressure() возвращает давление в паскалях (Па)
  float pressure_Pa = bmp.readPressure();
  
  // Конвертируем исходное давление в разные единицы измерения
  // readPressure() возвращает давление в паскалях (Па)
  // Деление на 100.0F преобразует паскали в гектопаскали (гПа)
  // Суффикс F указывает, что 100.0 - это число с плавающей точкой типа float
  float pressure_hPa = pressure_Pa / 100.0F;
  
  // Конвертируем давление из паскалей в миллиметры ртутного столба
  // Коэффициент 0.00750062 - это точное соотношение: 1 Па = 0.00750062 мм рт. ст.
  float pressure_mmHg = pressure_Pa * 0.00750062;
  
  // Считываем расчетную высоту над уровнем моря
  // readAltitude() использует текущее давление и сравнивает его с давлением на уровне моря
  // Возвращает высоту в метрах (основано на барометрической формуле)
  float altitude = bmp.readAltitude();
  
  //---------------------------------------------------------------------------------------

  // БЛОК 2: ВЫВОД ДАННЫХ В ПОСЛЕДОВАТЕЛЬНЫЙ ПОРТ (SERIAL MONITOR)
  
  // Выводим температуру с поясняющим текстом
  Serial.print("Температура: ");
  Serial.print(temperature);    // Вывод значения температуры
  Serial.println(" *C");        // Вывод единиц измерения и переход на новую строку
  
  // Выводим давление в гектопаскалях
  Serial.print("Давление: ");
  Serial.print(pressure_hPa);   // Вывод значения давления в гПа
  Serial.println(" гПа");       // Вывод единиц измерения
  
  // Выводим давление в миллиметрах ртутного столба
  Serial.print("Давление: ");
  Serial.print(pressure_mmHg);  // Вывод значения давления в мм рт. ст.
  Serial.println(" мм рт. ст."); // Вывод единиц измерения

  // Выводим высоту над уровнем моря
  Serial.print("Высота над уровнем моря: ");
  Serial.print(altitude);  // Вывод значения давления в метрах
  Serial.println(" м"); // Вывод единиц измерения

  // Разделительная линия
  Serial.println(" ");
  Serial.println("---------------------------------------------");
  Serial.println(" ");
  
  //---------------------------------------------------------------------------------------

  // БЛОК 3: ОТОБРАЖЕНИЕ ДАННЫХ НА OLED-ДИСПЛЕЕ
  
  display.clearDisplay();       // Очищаем предыдущее изображение с дисплея
  
  // Отображаем заголовок в верхней строке
  display.setCursor(0, 0);      // Позиция: x=0, y=0 (верхний левый угол)
  display.setTextSize(1);       // Стандартный размер шрифта
  display.println("Sprytron-ESP32-BMP180");  // Название проекта/устройства
  
  // Отображаем температуру
  display.setCursor(0, 16);     // Позиция: x=0, y=16 пикселей от верха
  display.print("Temp     : "); // Метка с выравниванием (пробелы для красоты)
  display.print(temperature, 1);// Вывод температуры с 1 знаком после запятой
  display.print(" ");         // Пробелы для разделения
  display.write(247);           // Символ градуса (код 247 в таблице символов)
  display.println("C");         // Буква C для Цельсия и переход на новую строку
  
  // Отображаем давление в гектопаскалях
  display.setCursor(0, 28);       // Позиция: x=0, y=28 пикселей от верха
  display.print("Press_hPa: ");   // Метка с указанием единиц измерения
  display.print(pressure_hPa, 0); // Вывод давления без десятичных знаков
  display.println(" hPa");      // Единицы измерения (гектоПаскали)
  
  // Отображаем давление в миллиметрах ртутного столба
  display.setCursor(0, 40);         // Позиция: x=0, y=40 пикселей от верха
  display.print("Press_mm : ");     // Метка для давления в мм рт. ст.
  display.print(pressure_mmHg, 0);  // Вывод давления без десятичных знаков
  display.println(" mmHg");         // Единицы измерения

  // Отображаем высоту над уровнем моря в метрах
  display.setCursor(0, 52);     // Позиция: x=0, y=52 пикселей от верха
  display.print("Altitude : "); // Метка для высоты над уровнем моря в метрах
  display.print(altitude, 0);   // Вывод высоты без десятичных знаков
  display.println(" m");        // Единицы измерения
  
  
  // Отправляем сформированное изображение на физический дисплей
  display.display();
  
  // Задержка перед следующим циклом измерений
  delay(1000);  // Пауза 1000 мс (1 секунда) для удобства чтения и экономии энергии
}

          При загрузке кода на плату, возможно, потребуется нажать кнопку «Boot» справа от USB-разъёма, чтобы запустить процесс перепрошивки.

Читайте также:  VL53L0X и ESP32: собираем лазерный дальномер

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

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

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

          Зафиксированное датчиком АД: 770 мм рт. ст. Сверимся с данными на известном сайте прогноза погоды в момент проведения эксперимента:

          Как видим, показания совпадают благодаря активации в коде режима повышенной точности (Ultra High Resolution).

          Также обратим внимание, что отметка над уровнем моря, в моём случае, явно не соответствует действительности (зафиксированные -112 м против фактических приблизительно +90 м). Это вполне ожидаемо от сенсора, поскольку всё дело в его базовой заводской калибровке. Используемый в коде метод bmp.readAltitude() применяет стандартную атмосферную модель — давление на уровне моря (QNH). То есть библиотека по умолчанию использует значение: 1013,25 гПа (760 мм рт. ст.).

          QNH (Q Nautical Height) — авиационный код для обозначения АД, приведённого к уровню моря.

          В этом и есть проблема: реальное давление на поверхности моря в конкретном регионе сегодня может быть вовсе не 1013,25 гПа! Причём актуальное показание очень сильно зависит от текущих погодных условий. Поэтому нужно уточнять актуальное значение на специальных ресурсах (например, Гидрометеоцентр).

Альтиметр на базе BMP180

          Теперь перейдём непосредственно к теме данной статьи — прибор для определения относительной высоты. Ниже представлен максимально подробно прокомментированный код:

// ==================== ПОДКЛЮЧЕНИЕ БИБЛИОТЕК ====================
/*
 * Библиотеки предоставляют готовые функции для работы с оборудованием
 * Wire.h - для связи по протоколу I2C (используется для общения с датчиком и дисплеем)
 * Adafruit_BMP085.h - специальные команды для работы с датчиком давления BMP180
 * Adafruit_GFX.h - графические функции для рисования на дисплее
 * Adafruit_SSD1306.h - управление конкретной моделью OLED-дисплея SSD1306
 */
#include <Wire.h>
#include <Adafruit_BMP085.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

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

// ==================== ПАРАМЕТРЫ OLED-ДИСПЛЕЯ ====================
/*
 * Конфигурация дисплея: размер экрана и управляющие параметры
 * Эти значения зависят от конкретной модели дисплея
 */
#define SCREEN_WIDTH 128     // Физическая ширина дисплея: 128 пикселей
#define SCREEN_HEIGHT 64     // Физическая высота дисплея: 64 пикселя
#define OLED_RESET -1        // Если у дисплея нет отдельного пина сброса, ставим -1

// ==================== СОЗДАНИЕ ОБЪЕКТОВ ДЛЯ ОБОРУДОВАНИЯ ====================
/*
 * Создаем программные объекты, которые будут представлять физические устройства
 */

// Объект для работы с датчиком BMP180
Adafruit_BMP085 bmp;  

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); 
// Объект для работы с дисплеем. Параметры конструктора:
// 1. Ширина дисплея
// 2. Высота дисплея  
// 3. Указатель на объект шины I2C (&Wire)
// 4. Пин сброса дисплея

// ==================== ПЕРЕМЕННЫЕ ДЛЯ ХРАНЕНИЯ СОСТОЯНИЯ ====================
/*
 * Эти переменные хранят текущее состояние программы и данные измерений
 * Они изменяются в процессе работы программы
 */

// Основные переменные для работы с высотой
float baseAltitude = 0;      // "Нулевая точка" - высота, от которой ведется отсчет
                             // При калибровке сохраняем текущую высоту как 0
float currentHeight = 0;     // Текущая относительная высота
                             // Рассчитывается как: текущая_высота - baseAltitude
float rawAltitude = 0;       // "Сырое" значение высоты от датчика (без обработки)
                             // Это абсолютная высота над уровнем моря

bool isZeroCalibrated = false; // Флаг, показывающий, выполнена ли калибровка
                               // false - калибровка не выполнена, показываем 0
                               // true - калибровка выполнена, показываем реальную высоту

// Переменные для обработки кнопки
unsigned long lastButtonPress = 0; // Время последнего нажатия кнопки в миллисекундах
                                   // Нужно для борьбы с "дребезгом" контактов
#define DEBOUNCE_TIME 300    // Время в миллисекундах, в течение которого игнорируем
                             // повторные нажатия кнопки (антидребезг)

// Переменные для сглаживания показаний (фильтрации)
#define SMOOTHING_SAMPLES 10 // Количество измерений для усреднения
                             // Чем больше, тем плавнее показания, но медленнее реакция
float altitudeBuffer[SMOOTHING_SAMPLES]; // Массив-буфер для хранения последних измерений
                                         // Размер: 10 элементов типа float (4 байта каждый)
int bufferIndex = 0;         // Текущая позиция в буфере для записи нового измерения

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

/*
 * Функция: getSmoothedAltitude()
 * Назначение: Получение усредненного значения высоты от датчика
 * Принцип работы: Использует "скользящее среднее" для фильтрации шумов
 * Возвращает: Усредненное значение высоты в метрах
 */
float getSmoothedAltitude() {
  // Шаг 1: Получаем новое измерение от датчика и сохраняем в буфер
  altitudeBuffer[bufferIndex] = bmp.readAltitude();
  // bmp.readAltitude() читает давление и преобразует его в высоту в метрах
  
  // Шаг 2: Перемещаем индекс записи по кругу (циклический буфер)
  // % SMOOTHING_SAMPLES означает "остаток от деления"
  // Когда bufferIndex достигает 10, он сбрасывается в 0
  bufferIndex = (bufferIndex + 1) % SMOOTHING_SAMPLES;
  
  // Шаг 3: Вычисляем среднее арифметическое всех значений в буфере
  float sum = 0;  // Переменная для накопления суммы
  // Проходим по всем элементам буфера
  for (int i = 0; i < SMOOTHING_SAMPLES; i++) {
    sum += altitudeBuffer[i];  // Добавляем каждый элемент к сумме
  }
  
  // Шаг 4: Возвращаем среднее значение
  return sum / SMOOTHING_SAMPLES;  // Сумма / количество элементов
}

/*
 * Функция: updateHeight()
 * Назначение: Обновление текущей относительной высоты
 * Принцип работы: Получает сырые данные, вычитает нулевую точку
 * Не возвращает значения, но изменяет глобальные переменные
 */
void updateHeight() {
  // Получаем усредненную высоту от датчика
  rawAltitude = getSmoothedAltitude();  // Вызываем функцию сглаживания
  
  // Проверяем, выполнена ли калибровка
  if (isZeroCalibrated) {
    // Если калибровка выполнена, вычисляем относительную высоту
    currentHeight = rawAltitude - baseAltitude;
    // Пример: если rawAltitude = 150.5 м, baseAltitude = 100.0 м
    // то currentHeight = 150.5 - 100.0 = 50.5 м
  } else {
    // Если калибровка не выполнена, показываем ноль
    currentHeight = 0;
  }
}

/*
 * Функция: calibrateZero()
 * Назначение: Установка текущей точки как "нулевой" (калибровка)
 * Принцип работы: Делает несколько измерений, усредняет их, сохраняет как базу
 * Вызывается: При старте программы и при нажатии кнопки
 */
void calibrateZero() {
  // Сообщаем о начале калибровки в монитор порта
  Serial.println("Начинаю калибровку нулевой точки...");
  
  // Шаг 1: Делаем 20 измерений с паузами для повышения точности
  float sum = 0;  // Переменная для накопления суммы измерений
  for (int i = 0; i < 20; i++) {
    sum += bmp.readAltitude();  // Считываем высоту и добавляем к сумме
    delay(30);  // Пауза 30 мс между измерениями для стабилизации
  }
  
  // Шаг 2: Вычисляем среднее значение (это и будет наша нулевая точка)
  baseAltitude = sum / 20.0;  // Делим сумму на количество измерений
  
  // Шаг 3: Сбрасываем текущую высоту в 0 (мы сейчас в нулевой точке)
  currentHeight = 0;
  
  // Шаг 4: Устанавливаем флаг, что калибровка выполнена
  isZeroCalibrated = true;
  
  // Шаг 5: Показываем подтверждение на дисплее
  display.clearDisplay();  // Очищаем экран
  display.setTextSize(1);  // Устанавливаем размер шрифта 1 (маленький)
  display.setCursor(0, 0); // Устанавливаем курсор в начало экрана (x=0, y=0)
  display.println("Zero point set!");  // Выводим текст
  display.print("Base: ");  // Выводим текст без перехода на новую строку
  display.print(baseAltitude, 1);  // Выводим значение baseAltitude с 1 знаком после запятой
  display.println(" m");  // Выводим " m" и переходим на новую строку
  display.display();  // Отправляем данные на физический дисплей
  delay(1000);  // Пауза 1 секунда, чтобы пользователь прочитал сообщение
  
  // Шаг 6: Выводим информацию в монитор порта для отладки
  Serial.println("Калибровка завершена!");
  Serial.print("Нулевая точка: ");
  Serial.print(baseAltitude, 2);  // Выводим с 2 знаками после запятой
  Serial.println(" м над уровнем моря");
  Serial.println();  // Пустая строка для читаемости
}

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

/*
 * Функция: displayHeight()
 * Назначение: Отображение текущей высоты на OLED-дисплее
 * Принцип работы: Формирует изображение в памяти и отправляет на дисплей
 * Вызывается: В основном цикле программы (каждые 200 мс)
 */
void displayHeight() {
  // Очищаем экран (заполняем черным цветом)
  display.clearDisplay();
  
  // Шаг 1: Отображаем заголовок (мелким шрифтом вверху)
  display.setTextSize(1);  // Размер шрифта 1 (маленький)
  display.setCursor(0, 0); // Позиция: верхний левый угол
  display.println("    ALTIMETER");  // Выводим название прибора
  
  // Шаг 2: Отображаем текущую высоту (крупными цифрами в центре)
  display.setTextSize(3);  // Размер шрифта 3 (крупный)
  display.setCursor(0, 30); // Позиция: x=0, y=30 пикселей от верха
  display.print(currentHeight, 1);  // Выводим высоту с 1 знаком после запятой
  
  // Шаг 3: Отображаем единицы измерения (средним шрифтом)
  display.setTextSize(2);  // Размер шрифта 2 (средний)
  display.println(" m");  // Выводим " m" (метры) и переходим на новую строку
  
  // Шаг 4: Обновляем физический дисплей (отправляем данные)
  display.display();
}

/*
 * Функция: printToSerial()
 * Назначение: Вывод подробной информации в монитор порта для отладки
 * Принцип работы: Раз в секунду отправляет данные по последовательному порту
 * Вызывается: В основном цикле программы, но срабатывает раз в секунду
 */
void printToSerial() {
  // Статическая переменная - сохраняет значение между вызовами функции
  static unsigned long lastPrintTime = 0;
  
  // Проверяем, прошла ли 1 секунда (1000 мс) с последнего вывода
  if (millis() - lastPrintTime >= 1000) {
    // Обновляем время последнего вывода
    lastPrintTime = millis();
    
    // Выводим разделитель для лучшей читаемости
    Serial.println("=================================");
    Serial.println(" ");  // Пустая строка
    
    // Выводим текущую относительную высоту
    Serial.print("Текущая высота: ");
    Serial.print(currentHeight, 1);  // 1 знак после запятой
    Serial.println(" м");
    
    // Выводим абсолютную высоту над уровнем моря
    Serial.print("Высота над уровнем моря: ");
    Serial.print(rawAltitude, 1);  // 1 знак после запятой
    Serial.println(" м");
    
    // Выводим значение нулевой точки
    Serial.print("Нулевая точка: ");
    Serial.print(baseAltitude, 1);  // 1 знак после запятой
    Serial.println(" м");
    
    // Закрываем блок вывода разделителем
    Serial.println("=================================");
    Serial.println(" ");  // Пустая строка
  }
}

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

/*
 * Функция: setup()
 * Назначение: Инициализация системы (выполняется один раз при включении)
 * Принцип работы: Настраивает оборудование и готовит систему к работе
 */
void setup() {
  // Инициализируем последовательный порт для отладки
  // 115200 - скорость передачи данных в битах в секунду
  Serial.begin(115200);
  Serial.println("Инициализация высотомера...");
  
  // Настраиваем пин кнопки
  // INPUT_PULLUP - режим входа с внутренней подтяжкой к питанию
  // Это значит, что когда кнопка не нажата, на пине будет напряжение (HIGH)
  // При нажатии кнопка замыкает пин на землю (LOW)
  pinMode(ZERO_BTN, INPUT_PULLUP);
  
  // Инициализируем шину I2C с указанием пользовательских пинов
  // По умолчанию ESP32 использует пины 21 и 22 для I2C
  Wire.begin(I2C_SDA, I2C_SCL);
  
  // Инициализируем OLED-дисплей
  // SSD1306_SWITCHCAPVCC - режим питания дисплея
  // 0x3C - I2C-адрес дисплея (стандартный адрес для большинства OLED)
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("Ошибка дисплея!");
    // Если дисплей не инициализировался, останавливаем программу
    while(true);  // Бесконечный цикл
  }
  
  // Инициализация датчика BMP180 и настройка режима работы (точности)
  // 0 = BMP085_ULTRALOWPOWER = 0 Ultra Low Power
  // 1 = BMP085_STANDART = Standart
  // 2 = BMP085_HIGHRES = High Resolution
  // 3 = BMP085_ULTRAHIGHRES = Ultra High Resolution
  if (!bmp.begin(BMP085_ULTRAHIGHRES)) {
    Serial.println("Ошибка датчика BMP180!");
    
    // Показываем ошибку на дисплее
    display.clearDisplay();
    display.setTextSize(1);
    display.setCursor(0, 0);
    display.println("SENSOR ERROR!");  // Сообщение об ошибке
    display.display();
    
    // Останавливаем программу
    while(true);  // Бесконечный цикл
  }
  

  // Показываем приветственное сообщение на дисплее
  display.clearDisplay();        // Очищаем экран
  display.setTextColor(SSD1306_WHITE);  // Устанавливаем белый цвет текста
  display.setTextSize(1);        // Устанавливаем размер шрифта
  display.setCursor(0, 0);       // Устанавливаем курсор
  display.println("ALTIMETER");  // Название прибора
  display.println("Press button");  // Инструкция
  display.println("to calibrate");  // Продолжение инструкции
  display.display();             // Отправляем на дисплей
  
  // Пауза 2 секунды, чтобы пользователь успел прочитать сообщение
  delay(2000);
  
  // Автоматически выполняем калибровку при старте
  // Это устанавливает точку включения как "нулевую"
  calibrateZero();
  
  // Сообщаем, что система готова к работе
  Serial.println("Высотомер готов к работе!");
  Serial.println("Нажмите кнопку GPIO_0 для калибровки");
}

/*
 * Функция: loop()
 * Назначение: Основной рабочий цикл (выполняется постоянно после setup())
 * Принцип работы: Бесконечный цикл, который постоянно обновляет данные
 * Частота обновления: Каждые 200 миллисекунд (5 раз в секунду)
 */
void loop() {
  // Проверяем, не нажата ли кнопка калибровки
  // Эта функция вызывается постоянно, но сама обрабатывает антидребезг
  checkResetButton();
  
  // Статическая переменная для контроля времени обновления данных
  // static означает, что переменная сохраняет значение между вызовами loop()
  static unsigned long lastUpdateTime = 0;
  
  // Проверяем, прошло ли 200 мс с последнего обновления
  // millis() возвращает количество миллисекунд с момента старта программы
  if (millis() - lastUpdateTime >= 200) {
    // Обновляем время последнего обновления
    lastUpdateTime = millis();
    
    // Обновляем значение высоты (читаем с датчика и вычисляем относительную)
    updateHeight();
    
    // Отображаем высоту на OLED-дисплее
    displayHeight();
    
    // Выводим подробные данные в монитор порта (раз в секунду)
    printToSerial();
  }
  
  // После выполнения всех действий цикл начинается заново
  // Нет явного delay(), чтобы система могла быстро реагировать на кнопку
}

          Тестировать прибор будем по следующему сценарию. Исходим из того, что высота одного этажа здания, в котором будет производиться эксперимент, примерно 4 метра. Соответственно, на втором этаже прибор покажет около 4 метров, на третьем — 8 метров, на четвёртом — 12 метров.

Читайте также:  Магнитометр HMC5883L (QMC5883L): компас на ESP32

          Макетную плату прибора положим в коробку и запитаем от аккумулятора:

          Стоя на полу 1-го этажа:

          2-й этаж:

          3-й:

          4-й:

          Как видим, показания альтиметра вполне адекватные и соответствуют ожидаемым. Сложно оценить фактическую погрешность измерений, поскольку нет эталонного прибора, на показания которого можно опираться. Однако можно предположить, что точность приблизительно ±0,5 метра.

          Попробуем выполнить более требовательный к точности тест: измерим размер шкафа (2,46 м):

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

          120 сантиметров:

          245 сантиметров:

          Даже выполняя усреднение из 10 значений, в процессе работы показания постоянно «плавали» в диапазоне ±0,2 метра. По фотографиям видно, что абсолютная погрешность составляет приблизительно те самые ±0,2 м.


Алгоритм работы альтиметра

          Представленный код реализует относительный высотомер, который измеряет не абсолютную высоту над уровнем мирового океана, а изменение этой величины относительно точки калибровки. Это как «ноль» на штангенциркуле — устанавливается ноль там, где удобно пользователю, и проводятся измерения от этой точки.

          При подаче питания на прибор производится инициализация (функция setup()) микроконтроллера и подключенной к нему периферии (OLED-дисплей и барометрический сенсор).

           Далее выполняется первичная автокалибровка: программа делает 20 измерений и устанавливает текущую отметку как «НОЛЬ». Прибор готов к работе.

          В основном бесконечном цикле loop() последовательно выполняется несколько операций:

          — опрос кнопки обновления «НОЛЯ» (checkResetButton). Если было зафиксировано, что кнопка была нажата, то производится обновление «НОЛЯ» (calibrateZero);

          — чтение данных с датчика и вычисление высоты относительно ноля (updateHeight);

          — отображение результатов на OLED-дисплее (displayHeight);

          — вывод данных в монитор порта (printToSerial).

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

          В каждом цикле производится обновление значения высоты (updateHeight). Для этого выполняется выборка из 10 значений при помощи встроенного в библиотеку метода bmp.readAltitude(). Этот метод читает давление от сенсора и преобразует в высоту в метрах через барометрическую формулу. Последующее вычисление среднего арифметического убирает случайные скачки и шумы.

          Далее выполняется расчёт искомой относительной высоты для нашего альтиметра. Из полученного от преобразователя значения вычитается величина нулевого уровня, полученной при калибровке.

          Пример сценария работы прибора:

          1) включаем прибор на первом этаже — нажали кнопку обновления «НОЛЯ», показывает «0 м»;

          2) пошли на второй этаж — прибор показывает «+3,5 м»;

          3) вернулись на первый — показывает «0 м»

          4) вновь поднялись на второй этажи и нажали «НОЛЬ» — теперь здесь «0 м»;

          5) пошли на третий этаж — показывает «+3,5 м»;

          6) спустились на первый этаж — показывает «-3,5 м» (со знаком минус);

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


Заключение

          В этой статье мы познакомились с микросхемой BMP180 — цифровой датчик атмосферного давления и температуры. Несмотря на появление более современных аналогов (BMP280, BME280), данный чип остается актуальным благодаря своей доступности, простоте и надежности, а также тому, что его параметров достаточно для широкого круга задач. Его работа, основанная на пьезорезистивном эффекте и дополненная индивидуальной заводской калибровкой и температурной компенсацией, обеспечивает необходимую точность для любительских и полупрофессиональных применений.

          Использование готовых модулей (HW-596 или GY-68) и библиотек для популярных аппаратных платформ делают работу с микросхемой технически несложной задачей.

          Хоть BMP180 и является устаревшей моделью, он способен определять перепад высот с погрешностью в пределах ±0,2 метра в режиме максимальной точности, дополнительно используя метод многократной регистрации с последующим усреднением и организовывая программную фильтрацию. Таким образом, BMP180 по прежнему остаётся актуальным модулем для образовательных целей и DIY-проектов благодаря оптимальному балансу цены, качества и точности.