Введение
Привет Хабр! Большинство современных метеостанций для умного дома бесполезны без интернета или локального MQTT-брокера. Пропал Wi-Fi — данные о микроклимате за последние сутки испарились вместе с перезагрузкой контроллера. Готовые логгеры стоят дорого, а их проприетарное ПО не дает доступа к сырым данным. Мне требовалось максимально дешевое и автономное решение — классический "черный ящик" для квартиры и балкона. Устройство должно пережить внезапное отключение света, не потерять ни одной точки замера и при этом отдавать накопленную историю в виде простого txt файла. Никаких облаков, подписок и зависимостей от сторонних сервисов. В этой статье я расскажу, как собрать такой регистратор на базе ESP32-C3 Super Mini, барометра bmp280, датчика температуры и влажности и обычного карт-ридера!
Выбор компонентов

В введении я уже затронул компоненты, но здесь подробно разберём почему я выбрал именно их:
Компоненты |
Почему |
Цена |
Esp32c3 super mini |
Очень компактная и довольно мощная. |
Около 250 руб. |
Совмещённая плата bmp280 + aht20 |
Компактная, у bmp280 нет возможности читать влажность, но это компенсируется aht20. |
Около 100 руб. |
Карт-ридер |
Самый простой способ подключить sd-карту. |
Около 50 руб |
Макетная плата + конекторы |
Идеально для сборки прототипа. |
Около 30 руб |
Сборка
Я расположил всё компоненты на макетной плате 3x7 см. Вот фото:

Вот схема сборки:
Модуль |
Пин на нём |
Пин на esp32 |
датчик с bmp280 и aht20 |
vcc |
3v3 |
датчик с bmp280 и aht20 |
gnd |
gnd |
датчик с bmp280 и aht20 |
scl |
gpio 8 |
датчик с bmp280 и aht20 |
sda |
gpio 9 |
карт-ридер |
3v3 |
3v3 |
карт-ридер |
gnd |
gnd |
карт-ридер |
cs |
gpio 7 |
карт-ридер |
mosi |
gpio 6 |
карт-ридер |
miso |
gpio 5 |
карт-ридер |
clk |
gpio 4 |
Распиновка платы

Код
Вот код схемы с комментариями:
// Пины SPI для ESP32-C3 Super Mini (GPIO) #define SCK 4 // Serial Clock - тактовый сигнал шины SPI #define MISO 5 // Master In Slave Out - линия данных от карты к контроллеру #define MOSI 6 // Master Out Slave In - линия данных от контроллера к карте #define CS 7 // Chip Select - выбор ведомого устройства (SD-карты) // Период опроса датчиков и записи в мс (1 секунда) #define PRD_WRITE 1000 #include <WiFi.h> #include <WebServer.h> const char *ssid = "ESPstation"; // Имя создаваемой точки доступа Wi-Fi WebServer server(80); // Создание веб-сервера на стандартном порту HTTP (80) #include "SPI.h" // Библиотека аппаратного SPI #include "SD.h" // Библиотека для работы с SD-картами через FS API #include "FS.h" // Абстракция файловой системы Arduino #include <GyverBME280.h> // Оптимизированная библиотека для датчика BME280 (температура, давление, влажность) GyverBME280 bme; #include <Adafruit_AHTX0.h> // Официальная библиотека Adafruit для AHT10/AHT20 (температура, влажность) Adafruit_AHTX0 aht; float dataBus[] = {0.0, 0.0, 0.0}; /* Массив для хранения усредненных показаний: dataBus[0] — Температура (°C) dataBus[1] — Давление (мм рт. ст.) dataBus[2] — Влажность (%) */ void get_data() { sensors_event_t humidity, temp; // Структуры для получения сырых данных от I2C-датчика aht.getEvent(&humidity, &temp); /* Сборка финальных значений в массиве dataBus. Важно: температура берется как среднее арифметическое между двумя датчиками, чтобы сгладить погрешности измерений. */ dataBus[0] = (temp.temperature + bme.readTemperature()) / 2.0; dataBus[1] = pressureToMmHg(bme.readPressure()); dataBus[2] = humidity.relative_humidity; // Открытие файла на SD-карте в режиме дозаписи (если файла нет — он создастся) File file = SD.open("/ESPstation_logs.txt", FILE_APPEND); if (file) { /* Запись данных БЕЗ использования класса String (экономия оперативной памяти!). Метод print() автоматически преобразует числа в текстовое представление. Использование F("...") поместит строковые литералы во флеш-память вместо RAM. */ file.print(F("time after launch:")); file.print(millis() / 60000); // Время работы в минутах file.print(F(", temp: ")); file.print(dataBus[0], 1); // 1 знак после запятой file.print(F(", MmHg: ")); file.print(dataBus[1], 1); file.print(F(", humd: ")); file.print(dataBus[2], 1); file.println(); // Добавляем перенос строки (\n) file.close(); // Обязательно закрываем файл, иначе данные могут остаться в буфере и не записаться физически } else { Serial.println(F("Failed to open file for writing")); } } void handleRoot() { // Формирование HTML-страницы "на лету" String html = "<!DOCTYPE html>"; html += "<html><head>"; // Автоматическая перезагрузка страницы каждые 3 секунды для обновления данных без JS html += "<meta http-equiv='refresh' content='3'>"; html += "<meta charset='UTF-8'>"; // Инлайн-стили для оформления интерфейса метеостанции html += "<style>"; html += "body { font-family: Montserrat, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #f0f4f8; }"; html += "h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }"; html += "h2 { color: #34495e; margin-top: 30px; }"; html += ".data-box { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin: 15px 0; }"; html += "</style>"; html += "</head><body>"; html += "<h1>ESPstation: локальная метеостанция</h1>"; // Блок вывода комнатной температуры и влажности html += "<div class='data-box'>"; html += "<h2>Комната</h2>"; html += "<p>Температура: " + String(dataBus[0], 1) + " °C</p>"; html += "<p>Влажность: " + String(dataBus[2], 1) + " %</p>"; html += "</div>"; // Блок вывода атмосферного давления html += "<div class='data-box'>"; html += "<h2>Давление</h2>"; html += "<p>Давление: " + String(dataBus[1], 1) + " мм рт. ст.</p>"; html += "</div>"; html += "</body></html>"; server.send(200, "text/html", html); // Отправка сформированного кода браузеру } void setup() { Serial.begin(115200); // Скорость монитора порта delay(2000); // Задержка для корректного отображения сообщений при подаче питания Serial.println("\n=== Start ESPstation ==="); // Инициализация SPI с явным указанием пинов (актуально для кастомных плат или переназначения GPIO) SPI.begin(SCK, MISO, MOSI, CS); // Небольшая задержка для стабилизации питания карты после подачи напряжения delay(500); // Инициализация SD карты. // Указываем пин CS и максимальную частоту SPI (4 МГц для стабильности на макетке/длинных проводах) if (!SD.begin(CS, SPI, 4000000)) { Serial.println(F("Card Mount Failed! Check wiring and power.")); return; // Выходим из setup, дальнейшая работа невозможна без накопителя } uint8_t cardType = SD.cardType(); if (cardType == CARD_NONE) { Serial.println(F("No SD card attached")); return; } // Диагностика типа и объема установленной карты Serial.print(F("SD Card Type: ")); if (cardType == CARD_MMC) Serial.println(F("MMC")); else if (cardType == CARD_SD) Serial.println(F("SDSC")); else if (cardType == CARD_SDHC) Serial.println(F("SDHC")); else Serial.println(F("UNKNOWN")); uint64_t cardSize = SD.cardSize() / (1024 * 1024); Serial.printf(F("SD Card Size: %lluMB\n"), cardSize); // Явная инициализация I2C ПЕРЕД использованием библиотек сенсоров Wire.begin(); delay(100); // Даем время шине стабилизироваться // Инициализация BME280 по адресу 0x77 (адрес зависит от подтяжки пина SDO на плате) if (!bme.begin(0x77)) { Serial.println(F("BME280 not found!")); } // Инициализация AHT (автоматически ищет адрес на шине) if (!aht.begin()) { Serial.println(F("AHT not found!")); } // Поднятие собственной точки доступа (SoftAP). Станция подключается напрямую к модулю. WiFi.softAP(ssid); Serial.print(F("http://")); Serial.println(WiFi.softAPIP()); // Выводим IP-адрес точки доступа в монитор // Привязка корневого пути ("/") к функции генерации HTML server.on("/", handleRoot); server.begin(); // Запуск веб-сервера Serial.println(F("\nInitialization and test complete!")); } void loop() { static uint32_t tmr = 0; // Таймер периодических задач (статик сохраняет значение между итерациями) // Неблокирующий таймер: проверка раз в секунду (PRD_WRITE) if (millis() - tmr >= PRD_WRITE) { tmr = millis(); // Сброс опорной метки времени get_data(); // Чтение сенсоров и запись на SD } // Обработка входящих запросов от браузера (обязательно вызывать в цикле) server.handleClient(); }
Тесты
Итак, подключаем плату к питанию, ждем пару секунд и вот:


Всё исправно работает!
Итоги
Итак, я сделал оффлайн погодный логгер.
Идеи по улучшению:
Сделать приложение для чтения, вывода и прогнозирования данных
Добавить радиатор на плату esp
Сделать корпус
Пишите ваши идеи по доработке в комментариях, спасибо за внимание!
Комментарии (45)

ArduDragon Автор
17.07.2026 17:50Можно поставить конденсаторы на линию питания и заряжать их непосредственно перед записью

Coder007
17.07.2026 17:50Это как? Заряжать конденсаторы непосредственно перед записью, когда они уже на линии питания находятся? Ничего не смущает? Они там просто будут находиться всегда заряженные на 100%.

LinkToOS
17.07.2026 17:50Они там просто будут находиться всегда заряженные на 100%.
Значит одной проблемой меньше.

ArduDragon Автор
17.07.2026 17:50Мы соединяем + питания с + конденсаторов а - пропускаем через ключ. И чуть заранее заряжаем и включаем в цепь, а после отключаем

Coder007
17.07.2026 17:50А зачем такие непонятные действия делать? И потом коммутировать перед началом записи? Типа дополнительная емкость, ну тогда нужно перед ними, на линии питания диод ставить, что бы разряд шёл только с схему, а не на другие конденсаторы и стабилизаторы (если они там будут).
Я что-то похожее делал для бистабильно го реле, что бы отключать его, если пропало питание. Они же без тока не переключаться обратно, поэтому дополнительное микрореле ставил и коммутировать его так, что бы заряженные конденсаторы отрабатывали на отключающие катушки. Вам можно сделать аналогично. Коммутация конденсатора в момент отключения питания. Реле подключено на самом входе без конденсатор в, потом диод, что бы конденсаторы не удерживали катушку реле, за диодом конденсатор базовый и следом подключаемый.

LinkToOS
17.07.2026 17:50В устройствах с передатчиком на 433мгц, которые питаются от литиевой таблетки 3 вольта, ставят керамику на 100 мкф, без всякой коммутации. Устройства рассчитаны на год и более работы от одной батарейки. Утечка через керамику никого не смущает. А здесь емкость батареи в разы больше.

Coder007
17.07.2026 17:50Вместо sd карты можно установить FRAM CY15B102J, на 2 МБит (512КБайт), этого вполне достаточно что бы хранить там метку времени на 32 бита и 3 числа типа float в количестве 16384 записи. Даже если метеостанция будет каждые 10 минут снимать показания (а это слишком много), то этого объема в 16 384 записи вам хватит на 113 дней непрерывной автономной работы без перезаписи!
Но я все таки надеюсь, что и опрос будет реже и вы все таки не будете просто хранить данные на метеостанция за весь период, а будете передавать их на хост и очищать память. А запись будет только в том случае, если нет связи с хостом, иначе непонятно, зачем вообще их хранить на метеостанции. Если для прикола только.
А теперь преимуществ FRAM-памяти (CY15B102J) перед SD-картой при записи малых объемов данных (метка времени и три числа float):
В 100 раз выше скорость работы. Запись одной пачки данных во FRAM занимает всего 0,17–0,4 миллисекунды, тогда как на SD-карту текстовая строчка пишется от 10 до 40 миллисекунд из-за долгой инициализации и работы файловой системы FAT32.
В 500 раз ниже потребление тока. В момент физической записи чип FRAM потребляет около 150 микроампер, в то время как SD-карта при перезаписи флеш-массива требует от 50 до 100 миллиампер.
Практически бесконечный ресурс. FRAM выдерживает до 100 триллионов циклов перезаписи. SD-карта при постоянной циклической дозаписи мелких строчек выйдет из строя в сотни раз быстрее из-за ограниченного ресурса ячеек флеш-памяти. А дешёвая карта очень быстро, как это бывало на Raspberry Pi.
Отсутствие лишних операций с памятью. Во FRAM можно записать ровно 16–20 байт данных. На SD-карту невозможно записать блок меньше одного сектора (512 байт), поэтому системе приходится каждый раз считывать, модифицировать и перезаписывать сотни лишних байт.
Высокая надежность при внезапном отключении питания. Во FRAM данные сохраняются физически в момент отправки последнего бита. Если питание пропадет при записи на SD-карту, файловая система FAT32 может полностью разрушиться, что приведет к потере всего текстового файла.
Экономия памяти микроконтроллера. Для работы с FRAM используется простой и легкий код передачи данных по шине I2C. Для SD-карты требуется подключать тяжеловесные библиотеки для работы с файловой системой, которые занимают много оперативной и флеш-памяти контроллера.
Просто были подобные проекты и там этих граблей было так много, и что мы только не использовали. А вот вариант с FRAM стал спасением.

ArduDragon Автор
17.07.2026 17:50По поводу опроса, 1 секунда была для отладки, такая высокая чистота не нужна. Веб страница нужна что бы в ходе эксперимента смотреть за показателями в камере, а по завершении - снять чёрный ящик и на пк обработать данные. Спасибо за совет! И это частично решит риск с отключением питания в момент записи.

tklim
17.07.2026 17:50все из пушки по воробьям.
Этот ФРАМ стоит дороже чем все здесь вместе взятое.
По факту тут никакой СД-карты не требуется.
У ESP32C3 4MByte паматя внутри. В ардуине в списке стандартных есть разметка 1МБ приложение, 3МБ файловая система.из данных на нужно 32 бита на время, 16 на температуру, 8 влажности и 16-24 на давление (хотя нет смысла во второй цифре после запятой). Это все нам дает 10 байт на измерение.
Если хранить с интервалом в 1минуту - то 3 мегабайта выльются в ~200 дней. Если реже, легко посчитать самому.Ну и никто не мешает сделать выгрузку прямо с веб-страницы, чтоб картотчки не совать туда-сюда.
А если заменить на ESP32-S2/S3 - то там вообще можно поднять USB Mass Storage Device, и обращаться к флешке напрямую, через USB

LinkToOS
17.07.2026 17:50А что насчет MRAM?
Справедливо, что вопрос о необходимом объеме памяти требует уточнения - сколько требуется записей, возможно ли сжатие.Ну и никто не мешает сделать выгрузку прямо с веб-страницы, чтоб карточки не совать туда-сюда.
Все равно остается вопрос о работе при потере связи и питания.

Coder007
17.07.2026 17:50Эта версия, да, стоит дорого, но есть дешевле, объёмом немного меньше.
FM24V10-G на 1 мегабит, или MB85RC256V на 256 килобит. Эти стоят менее 100 рублей, но они вечные, ни одна SD карта столько не отработает и запись на них простая, я выше писал преимущества.

CyberexTech
17.07.2026 17:50Очень давно что-то подобное делал, только для записи лога качества электрической сети. В качестве резервного источника питания, на случай пропадания основного, использовал Li-ion аккумулятор. Лог тоже записывался на SD-карту. Даже фото осталось.

Логгер 
ArduDragon Автор
17.07.2026 17:50Круто! Читал вашу статью про датчик влажности. Использую такой же в своём проекте, как вы справляетесь с окислением дорожек на датчике?

Coder007
17.07.2026 17:50Так там же емкостный датчик влажности применяется, он не имеет контакта открытой меди с грунтом, соответственно окисления нет. Его ещё отдельно лаком заливают, что бы герметичность была лучше.

CyberexTech
17.07.2026 17:50вы справляетесь с окислением дорожек на датчике?
Там ёмкостной датчик, дорожки защищены маской.

Coder007
17.07.2026 17:50А здесь на входе 220В просто делитель стоит? Я просто не вижу ни диодов ни чего-то ещё. И развязки я так понимаю нет, везде есть высокий потенциал?

Coder007
17.07.2026 17:50Очень странно, я же спросил автора, а не критиковал, а мне за вопрос кинули - 1, для меня это действительно интересно, потому что хочу сделать просто как на плате у автора, но мне ещё важна безопасность при эксплуатации.

CyberexTech
17.07.2026 17:50В этой версии нет развязки, поэтому нужно применять хорошо изолированный источник питания. Стоит делитель. Ещё была версия с трансформаторной развязкой.

sergyk2
17.07.2026 17:50
вот как-то так выглядит лог здорового человека

Coder007
17.07.2026 17:50А где лог? Я вижу только график и он без пояснений, а лог - это обычно текст, с отметкой времени и данными. Возможно с пояснения и, заметками, отметками. Формат любой по желанию. И лог не предназначен для вывода в виде графика, в виде данных и так далее, лог нужен для того, что бы восстановить пропущенные данные в базах, увидеть и понять какие либо ошибки, а график, который представлен здесь нужен для понимания работы процесса, датчика и так далее, для поиска зависимостей, и всего того, что душа пожелает.
xSVPx
Что будет когда при записи пропадет питание ?
ArduDragon Автор
Надо проверить, есть ли у вас идеи по решению проблемы?
Sdima1357
Элементарно. Пишите на неформатированную флешку (sdcard) блоками по 512 и котролируете питание.Тут fat лишний.
ptr128
Сложно и читать потом неудобно. Проще сразу аллоцировать и заполнить нулями большой файл для данных и небольшой для WAL, не забывая, что пишем секторами (блоками), а не байтами.
Sdima1357
чтобы создать файл, нужна файловая система,со своими проблемами. А я предлагаю обойтись без нее. Сначала забиваете все блоки флешки 0xff , а потом одна запись - один блок, если писать раз в 10 минут - флешки хватит лет на 200.
512*6*24*365*200 = 5.3 GB. Последнюю запись можно найти за log2(6*24*365*200) - 23 чтения
ptr128
Но и с тем преимуществом, что для её чтения не потребуется специализированной программы.
Во-первых, фиксировать показания может потребоваться даже раз в секунду.
Во-вторых, Вы забыли, что физический блок записи может быть до 16 KБ. Размер физического блока в 512 можно встретить только на старых флешках, размером до гигабайта.
Хотя на ESP32-C3 я в похожем сценарии обхожусь без флешки, так как всегда успеваю забирать данные быстрее, чем заканчивается внутренняя флеш-память MK, которой мне хватает на несколько месяцев.
Sdima1357
"физический блок записи может быть до 16 KБ"
Там стирание большими блоками, а писать можно по 512. Контроллеры поддерживают. На внутренней флеш вообще можно побайтно писать.
"esp32-c3"
Ну а я пользуюсь stm-ками. Там батарейку можно вообще лет на 10-20 растянуть
ptr128
Стирание на современных флешка вообще блоками до 16 МБ. А вот запись, обычно, от 4 до 16К. SSDs do not expose their actual NAND flash memory page size, which typically ranges from 4 KiB to 16 KiB
Я и STM32, и STM8 тоже пользуюсь. Но когда нужен WiFi, то не думаю, что ST67W - лучший выбор.
Не получится. Радиомодуль и датчики жрут больше. На STM8L151C8 в паре с nRF24L01+ у меня получилось только чуть больше года на CR2032 при мониторинге только температуры и влажности в погребе.
Sdima1357
nrf - это где-то 10-20 mA. (https://cdn.sparkfun.com/datasheets/Wireless/Nordic/nRF24L01_Product_Specification_v2_0.pdf)
cr2032 - continues drain 0.2 mA
https://energy.panasonic.com/dam/master/pdf/en/datasheet/lithium/CR2032_Datasheet_EN.pdf
Вы просто убиваете эту батарейку.
https://w.electrodragon.com/w/File:CR2032-discharge.png
Ставьте подходящую и не забывайте выключать питание сенсоров,
ptr128
Вы явно или не прочитали, что я процитировал, или не знаете английский:
"SSDs do not expose their actual NAND flash memory page size, which typically ranges from 4 KiB to 16 KiB, instead their reported physical sector size is the same as their logical sector size"
перевод:
"SSD не раскрывают информацию о фактическом размере страницы NAND-флеш-памяти (который обычно составляет от 4 до 16 КиБ), вместо этого сообщаемый ими размер физического сектора совпадает с размером логического сектора."
Average Supply current @ -6dBm output power, ShockBurstTM 0.12 mA
Не надо давать советы, которых у Вас не спрашивали.
Sdima1357
Average - это не peak. SSD это не SD.
"Не надо давать советы, которых у Вас не спрашивали." -
Ну так это у Вас не работает от батарейки :)
ptr128
А continuous drain - это peak? А наличие ёмкости на питании - это неправильно? )))
У меня как раз работает и менять батарейку приходится реже, чем раз в год. Это у Вас завелась фантастическая батарейка со сроком службы в 20 лет )))
Sdima1357
Bobbin cells have higher capacities and can have self-discharge rates under 1% per year enabling up to 40-year battery lives.
cc
https://www.batterypowertips.com/why-self-discharge-is-important-in-batteries/
10 Years+
https://uniross.com/product/uniross-industrial-uer14250-1-2aa-lithium-thionyl-chloride-battery/
Coder007
Посмотрите комментарий внизу, решение очень простое.
А ещё, самая большая проблема в том, что ваше устройство никуда данные не передаёт, ни на сервер, ни в систему умного дома, просто хранит их у себя и все. При этом вы ежесекундно считываете данные, а отображает только последние из буфера. Зачем? Почему? Какова цель? Ну хотя тогда сравнивайте текущие с предыдущим и записывайте только те, которые изменились и для корректной метки времени обязательно нужны часы типа DS3231. Потому что после перезагрузки, у вас метка времени будет кривой и она будет повторяться в вашем файле. Логгирование всегда привязано к реальному времени или к порядковому номеру, если время не важно. У вас ни того ни другого, просто таймер millis / 60000. После перезагрузки МК он обнуляется.
В чем смысл тогда логгирования. Это же просто сбор данных без возможности дальнейшего использования. А потом вы что, возьмёте SD карту и с ней пойдёте к ПК переносить инфу? А куда? В Excel? Так сейчас не 2000 год, а 2026, и у вас микроконтроллер с Wi-Fi, Bluetooth, а это такие возможности!
А SD карта, в таком режиме сдохнет через 1-3 месяца, китайская за 1,5 - 2 недели.
Это тоже проходили и убивали карты.
LinkToOS
Питание каждый по своему вкусу организует. Здесь только основная платформа предложена.
xSVPx
Питание тут не очень причем. Как эту штуку вообще безопасно выключать?
LinkToOS
Логично. “Безопасное извлечение” не помешало бы.