Чего только не реализовано на ESP32?

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

Однако ванильные Arduino‑скетчи, коими завалены все туториалы, меня не интересуют. Хочется «настоящего» программирования, сложностей, бессонных ночей, разборов документации и тому подобное...
Потому я выбрал путь изучения ESP32 с помощью C и ESP‑IDF.

С чего же начать? — конечно же метеостанция!

Это небольшой проект, которые покажет насколько ты вообще DIYщик, научит основам работы с ESP и её функциям.
Для своей метеостации я выбрал только основные датчики: давления, освещенности, температуры, влажности. Вот о последнем сегодня и пойдёт речь.

В качестве подопытного был приобретён популярный DHT22. Приобретался он сразу на плате с обвязкой (подтягивающий резистор, конденсатор и удобные пины для подключения).

Первый этап - подключение

Здесь всё просто: плюс к плюсу, минус к минусу, дату на любой цифровой GPIO. Главное чтобы GPIO умел работать как на ввод, так и на вывод. Питание можно брать как 3.3 В, так и 5 В.

Подключение DHT22 к ESP32
Подключение DHT22 к ESP32

Программирование

Опустим вопросы настройки IDE (в моём случае VS Code) и ESP-IDF. Перейдём сразу к сути и создадим новый пустой проект с благозвучным именем weather-station

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

idf.py create-component -C components dht22

У нас появится папка компонентов со следующей структурой:

components/dht22/
├── include/
│   └── dht22.h
├── CMakeLists.txt
└── dht22.c

Для начала определим заголовочный файл dht22.h
От датчика нам нужна только одна функция, которая получит на вход номер GPIO и «вернёт» нам значения влажности и температуры.

#pragma once
#include <driver/gpio.h>
#include <esp_log.h>

esp_err_t dht22_read(gpio_num_t pin, float *temperature, float *humidity);

Можно было сделать возвращаемое значение void, но с ESP удобно возвращать стандартный тип ошибки esp_err_t. Это помогает потом понять что пошло не так, или наоборот — что всё работает отлично.

Для использования GPIO необходимо в CMakeList.txt подключить компонент esp_driver_gpio:

idf_component_register(SRCS "dht22.c"
                    INCLUDE_DIRS "include"
                    REQUIRES esp_driver_gpio)

А вот дальше начинается веселье... Нужно разобраться как работает датчик и реализовать это в коде.
Открываем документацию на датчик и видим следующее:

Принцип работы обмена данными по 1 проводу с DHT22
Принцип работы обмена данными по 1 проводу с DHT22

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

Диаграмма работы и таблица таймингов
Диаграмма работы и таблица таймингов

Чтож, приступаем к реализации!
Для работы с микросекундами самым простым вариантом будет использование стандартной функции задержки ets_delay_us(uint32_t us), которая входит в библиотеку rom/ets_sys.h.

А дальше всё по порядку:

  1. Инициализация датчика (строки 17-30)

  2. Подсчёт импульсов/бит (строки 32-47)

  3. Проверка чексуммы (строки 50-53)

  4. Перевод в человеческие значения (строки 55-67)

Получившийся код dht22.c приведён ниже:

#include "dht22.h"
#include <driver/gpio.h>
#include <rom/ets_sys.h>
#include <esp_log.h>

static int dht_wait_level(gpio_num_t pin, int level, uint32_t timeout_us)
{
    while (gpio_get_level(pin) == level) {
        if (!timeout_us--) return -1;
        ets_delay_us(1);
    }
    return 0;
}

esp_err_t dht22_read(gpio_num_t pin, float *temperature, float *humidity)
{
    uint8_t data[5] = {0};

    // Start signal
    gpio_set_direction(pin, GPIO_MODE_INPUT_OUTPUT_OD);
    gpio_set_level(pin, 0);
    ets_delay_us(2000);              // 2 ms low (0.8-20 ms)
    gpio_set_level(pin, 1);
    ets_delay_us(40);                // 40 us high (20-200 us)

    // Response
    if (dht_wait_level(pin, 0, 80) < 0) return ESP_ERR_TIMEOUT;
    if (dht_wait_level(pin, 1, 80) < 0) return ESP_ERR_TIMEOUT;

    // Read 40 bits
    for (int i = 0; i < 40; i++) {
        // wait for low to high edge
        if (dht_wait_level(pin, 0, 60) < 0) return ESP_ERR_TIMEOUT;

        // measure high pulse
        uint32_t t = 0;
        while (gpio_get_level(pin)) {
			if (++t > 100) return ESP_ERR_TIMEOUT;
            ets_delay_us(1);
        }

        int byte = i / 8;
        data[byte] <<= 1;
        if (t > 40)            // >40 us means bit = 1
            data[byte] |= 1;
    }
	
    // checksum
    uint8_t sum = data[0] + data[1] + data[2] + data[3];
    if ((sum & 0xFF) != data[4]) {
        return ESP_ERR_INVALID_CRC;
    }
	
    uint16_t raw_h = (data[0] << 8) | data[1];
    uint16_t raw_t = (data[2] << 8) | data[3];

    *humidity = raw_h / 10.0f;

    if (raw_t & 0x8000) {
        raw_t &= 0x7FFF;
        *temperature = -((float)raw_t / 10.0f);
    } else {
        *temperature = raw_t / 10.0f;
    }

    return ESP_OK;
}

Теперь остаётся вызывать эту функцию из основного кода.
Для этого в main.c

  • определяем порт (я подключил к 23)

  • добавляем бесконечный цикл с вызовом функции

  • добавляем задержку между вызовами, чтобы дёргать датчик только раз в несколько секунд

  • выводим информацию — я сделал в консоль с помощью функции ESP_LOGI(tag, format,...)

#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_log.h>
#include "dht22.h"

#define DHT22_GPIO 23

void app_main(void)
{
    float temperature, humidity = 0;
    while (true){
        if (dht22_read(DHT22_GPIO, &temperature, &humidity) == ESP_OK)
            ESP_LOGI("main", "Temperature = %0.1f, Humidity = %0.1f", temperature, humidity);
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

Билдим, прошиваем и смотрим на вывод:

I (2275) main: Temperature = 22.6, Humidity = 28.3
I (4275) main: Temperature = 22.6, Humidity = 28.4
I (6275) main: Temperature = 22.6, Humidity = 28.3

Всё заработало!

Датчик исправно работает и показывает температур и влажность в помещении. Сейчас зима, поэтому влажность низкая.

Первый шаг в IOT сделан! Дальше будет только интереснее!

Комментарии (9)


  1. fiego
    28.01.2026 08:06

    Умение читать документацию -- отличный навык! Есть, правда, пара субъективных замечаний по коду.

    1. Вместо постоянного переключения направления GPIO имеет смысл один раз сконфигурировать пин как GPIO_MODE_INPUT_OUTPUT_OD . Переключение отнимает время, а тайминги тут критичны.

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

    ```

    typedef struct {
        gpio_num_t pin;
        float humidity;
        float temperature;
    } dht22_t;
    
    dht22_t * dht22_init(gpio_num_t pin);
    esp_err_t dht22_read(dht22_t *dht);
    

    Суть в том, что будет чётко определённая инициализация, вызов чтения будет сохранять измеренные значения в структуру, из которой потом можно уже читать что надо. Если не нравится идея использования динамической памяти, хотя на ESP32 это не так критично, как на более мелких MCU, то вот другой вариант интерфейса инициализации:

    esp_err_t dht22_init(dht22_t *dht, gpio_num_t pin);


    1. PeterBozhko Автор
      28.01.2026 08:06

      Всё чётко и по делу. Согласен с обоими советами.
      Я только начинаю разбираться что и как устроено как в С, так и в ESP, поэтому годные советы очень помогают.
      Спасибо

      P.S. Почитал про GPIO_MODE_INPUT_OUTPUT_OD, он тут идеально подходит. Поэтому поменял в коде. Ещё раз спасибо за подсказку)


      1. Gariks
        28.01.2026 08:06

        Я бы ещё сверху навернул подсказок компилятору для оптимизации, на функцию init attribute cold, а в самом алгоритме на ветку проверки ошибки unlikely, на wait level inline


      1. DanilinS
        28.01.2026 08:06

        Зачем вам тут режим 2-х направленного вывода? Просто возьмите и объедините 2 вывода. Один на выход, второй на вход. Вы выводы решили экономить?


  1. morozold
    28.01.2026 08:06

    И чем же это отличается от "скетчей"? Мы поток тормозим на все время обмена с датчиком. Если железка будет делать что то ещё, этот код начнет либо глючить, либо тормозить другие задачи. Ведь наверняка на этой esp есть и таймера с захватом и dma и прерывания.


  1. FGV
    28.01.2026 08:06

    1. Выше уже написали - как только есп32 чем нибудь нагрузить весь обмен посыпятся, т.к. уплывут тайминги. Выход - смотреть в сторону переферии, у нас есть rmt, как пример: https://github.com/mongoose-os-libs/onewire-rmt/blob/master/src/onewire_rmt.c

    2. Для отрицательных температур - код будет возвращать чушь. Лучше уж сразу raw_t объявить знаковым интом16.


  1. Xius
    28.01.2026 08:06

    DHT22 уже сильно устарел в 26-м году - погрешность ±0,5 градуса. Сегодня даже спутники измеряют температуру Земли с точностью в 0,3 градуса.


    1. RezonanS1
      28.01.2026 08:06

      И зачем такая точность для дома? Изменилась температура на улице на 0.3 градуса, непременно надо надеть трусы с начесом...?) Для домашнего применения, коматные/уличные градусники точности и разрешающей способности в 1 градус достаточно более чем. Если датчик еще заведенив какую либо систему регулирования 0.5 градуса достаточно.


  1. xaqbyca
    28.01.2026 08:06

    А где Вы смотрите результаты измерений?