Embedded HandbookПриложения
Все разделы › XI. Датчики: подключение, схемы, драйверы
STM32F103, STM32F407 · уровень средний · 8 мин чтения
Открыть в интерактивном справочнике

97. Влажность: DHT22, SHT31 и AHT20

Три самых ходовых датчика влажности и два способа с ними говорить. DHT22 — свой однопроводный протокол, где бит кодируется длительностью импульса, и его приходится мерить таймером. SHT31 и AHT20 — шина I²C и ответ, защищённый CRC. Все трое заодно меряют температуру, а из температуры и влажности получается точка росы — когда запотеет корпус.

Подтяжки и провода шины I²C разобраны в разделе 95. Однопроводный протокол DHT — не 1-Wire из раздела 83.2: у него нет адресов, а биты различаются не моментом чтения, а длительностью.

Как это называется в документации

Оригинал Перевод
relative humidity (RH) относительная влажность
dew point точка росы
single-bus (proprietary) однопроводный протокол DHT — не 1-Wire
pulse width длительность импульса
checksum контрольная сумма — сумма байт
CRC-8, polynomial 0x31 CRC-8 с полиномом 0x31
single shot, repeatability однократное измерение; повторяемость
clock stretching растягивание такта: ведомый держит SCL
sign-magnitude знак и модуль — не дополнительный код
status byte, busy байт состояния; «ещё измеряю»

97.1 Три датчика

DHT22 SHT31 AHT20
интерфейс свой однопроводный I²C, адрес 0x44 или 0x45 I²C, адрес 0x38
формат влажность и температура по 16 бит, десятые доли два кода по 16 бит два кода по 20 бит
защита сумма четырёх байт CRC-8 на каждое слово CRC-8 на весь ответ
особенность бит — длительностью импульса, опрос не чаще раза в 2 с команды по 16 бит байт состояния: «ещё измеряю»

97.2 DHT22: бит — это длительность

Кадр DHT22: старт от STM32, ответ датчика и биты, различимые только длительностью единицы

STM32 прижимает линию к земле на 2 мс — «начни» (DHT11 нужно не меньше 18 мс) — и отпускает. Датчик отвечает: 80 мкс ноль, 80 мкс единица, затем 40 бит. Перед каждым битом — 50 мкс ноля, а сам бит — это длительность следующей единицы: 26–28 мкс — ноль, 70 мкс — единица. Значит, мерить надо высокие импульсы, и порог — посередине, 49 мкс.

DHT11 отличается не только стартом: в его кадре байты — целая и дробная части (влажность 2A 00 — 42 %, температура 17 05 — 23,5 °C), а не 16-битные числа в десятых долях. dht_decode ниже выдаст для него 1075,2 % — нужен свой разбор.

DHT22 на одном проводе: вывод в открытом стоке и подтяжка к питанию

Удобнее всего — прерывание на оба фронта и таймер, считающий микросекунды: на фронте запомнить время, на спаде записать длительность. TIM2 тактируется 72 МГц у F103 и 84 МГц у F407 — делитель PSC 71 и 83 даёт 1 МГц. Вывод PA0 — выход с открытым стоком: 0 прижимает линию, 1 отпускает её к подтяжке, а IDR читает настоящий уровень, так что переключать направление не нужно.

void dht_start(void)
{
    pa0_open_drain();
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    TIM2->PSC = TIM2_PSC;
    TIM2->ARR = 0xFFFFu;
    TIM2->EGR = TIM_EGR_UG;
    TIM2->CR1 = TIM_CR1_CEN;
    n_highs = 0;
    GPIOA->BSRR = GPIO_BSRR_BR0;        /* прижать линию: «начни» */
    uint16_t t0 = (uint16_t)TIM2->CNT;
    while ((uint16_t)(TIM2->CNT - t0) < 2000u) { }  /* 2 мс; DHT11 нужно не меньше 18 */
    EXTI->RTSR |= EXTI_RTSR_TR0;
    EXTI->FTSR |= EXTI_FTSR_TR0;
    EXTI->PR = EXTI_PR_PR0;
    EXTI->IMR |= EXTI_IMR_MR0;
    NVIC_EnableIRQ(EXTI0_IRQn);
    GPIOA->BSRR = GPIO_BSRR_BS0;        /* отпустить: дальше говорит датчик */
}

void EXTI0_IRQHandler(void)
{
    uint16_t now = (uint16_t)TIM2->CNT;
    if (GPIOA->IDR & 1u)
        rise_at = now;                  /* фронт — начало единицы */
    else if (n_highs < DHT_HIGHS)
        dht_high_us[n_highs++] = (uint16_t)(now - rise_at);    /* спад — её длительность */
    EXTI->PR = EXTI_PR_PR0;
}

Спадов будет 42: первая единица — это отпущенная линия до ответа датчика, вторая — ответ 80 мкс, и только с третьей идут 40 бит.


97.3 Разбор 40 бит и сумма

int dht_decode(const uint16_t hi_us[40], int16_t *t_dc, uint16_t *rh_dm)
{
    uint8_t b[5] = { 0 };
    for (unsigned i = 0; i < 40; i++)
        b[i / 8] = (uint8_t)((b[i / 8] << 1) | (hi_us[i] > DHT_BIT1_US));
    if ((uint8_t)(b[0] + b[1] + b[2] + b[3]) != b[4])
        return 0;                       /* сумма не сошлась */
    *rh_dm = (uint16_t)(b[0] << 8 | b[1]);
    int16_t t = (int16_t)((b[2] & 0x7F) << 8 | b[3]);
    *t_dc = (b[2] & 0x80) ? (int16_t)-t : t;    /* старший бит — знак, а не дополнительный код */
    return 1;
}

Пять байт кадра 02 92 01 0D A2: влажность 0x0292 = 658 — 65,8 %, температура 0x010D = 269 — 26,9 °C, сумма 0x02 + 0x92 + 0x01 + 0x0D = 0xA2. Программа на компьютере собирала кадры из импульсов с разбросом и отдавала их этой функции: кадр разобран верно даже с импульсами на краях допуска — 36…38 и 58…60 мкс, а кадр с испорченной суммой отвергнут.

Знак — отдельным битом. Отрицательная температура у DHT22 — это старший бит плюс модуль: код 0x8065 — это −10,1 °C (сумма такого кадра — 0x79). Прочитанный как int16_t, тот же код дал бы −32 667, то есть −3266,7 °C. У DS18B20 (раздел 96) — наоборот, дополнительный код: одна и та же ошибка «не тот формат» у двух датчиков даёт разную чушь.


97.4 SHT31 и AHT20: CRC с полиномом 0x31

Оба датчика прикладывают к данным CRC-8 — с полиномом 0x31 и начальным значением 0xFF, старшим битом вперёд:

uint8_t crc8_31(const uint8_t *p, size_t n)
{
    uint8_t crc = 0xFF;
    while (n--) {
        crc ^= *p++;
        for (int i = 0; i < 8; i++)
            crc = (uint8_t)((crc & 0x80) ? (crc << 1) ^ 0x31 : crc << 1);
    }
    return crc;
}

Это тот же многочлен x⁸ + x⁵ + x⁴ + 1, что у 1-Wire (раздел 83.2): 0x8C там — это 0x31, записанное задом наперёд. Но там биты идут младшим вперёд и счёт начинается с нуля, поэтому результат другой, и чужая функция CRC здесь не подойдёт. Контрольное значение из документации: для байт 0xBE 0xEF — 0x92; функция его даёт.

Адреса и команды:

#define SHT31_ADDR     0x44u            /* 0x45, если вывод ADDR — к питанию */
#define SHT31_MEASURE  0x2400u          /* однократно, высокая повторяемость, без растягивания такта; до 15 мс */
#define AHT20_ADDR     0x38u
#define AHT20_TRIGGER  { 0xAC, 0x33, 0x00 }     /* начать измерение; готово через 80 мс */

Чтение и запись по шине — как в разделе 17: команда, пауза на измерение, чтение 6 байт у SHT31 и 7 байт у AHT20. Перед первым измерением AHT20 через 40 мс после включения читают байт состояния: если бит 3 («откалиброван») не поднят, посылают 0xBE 0x08 0x00 и ждут 10 мс.

SHT31 и AHT20 на одной шине I²C: вывод ADDR задаёт адрес SHT31 — 0x44 или 0x45, у AHT20 всегда 0x38


97.5 Перевод кодов

У SHT31 два 16-битных кода, каждый со своей CRC. Температура — −45 + 175 · S / 65535 °C, влажность — 100 · S / 65535 %:

int sht31_convert(const uint8_t d[6], int16_t *t_c100, uint16_t *rh_c100)
{
    if (crc8_31(d, 2) != d[2] || crc8_31(d + 3, 2) != d[5])
        return 0;
    uint32_t st = (uint32_t)d[0] << 8 | d[1], srh = (uint32_t)d[3] << 8 | d[4];
    *t_c100 = (int16_t)((int32_t)((17500u * st + 32767u) / 65535u) - 4500);
    *rh_c100 = (uint16_t)((10000u * srh + 32767u) / 65535u);
    return 1;
}

Код температуры 0x6666 даёт 25,00 °C, код влажности 0x8000 — 50,00 %; их CRC — 0x93 и 0xA2. Один сбитый бит — и функция отвергает ответ.

У AHT20 коды по 20 бит, упакованные в пять байт после байта состояния, и одна CRC на все шесть. Влажность — S / 2²⁰ · 100 %, температура — S / 2²⁰ · 200 − 50 °C:

int aht20_convert(const uint8_t d[7], int16_t *t_c100, uint16_t *rh_c100)
{
    if (d[0] & 0x80)
        return 0;                       /* ещё измеряет */
    if (crc8_31(d, 6) != d[6])
        return 0;
    uint32_t srh = (uint32_t)d[1] << 12 | (uint32_t)d[2] << 4 | d[3] >> 4;
    uint32_t st = ((uint32_t)d[3] & 0x0F) << 16 | (uint32_t)d[4] << 8 | d[5];
    *rh_c100 = (uint16_t)(((uint64_t)srh * 10000u + (1u << 19)) >> 20);
    *t_c100 = (int16_t)((int32_t)(((uint64_t)st * 20000u + (1u << 19)) >> 20) - 5000);
    return 1;
}

Код влажности 0x80000 — 50,00 %, код температуры 0x60000 — 25,00 °C, CRC ответа — 0x4E. Старший бит байта состояния — «ещё измеряю»: такой ответ отвергается, его надо перечитать позже.


97.6 Точка росы

Относительная влажность сама по себе мало говорит конструктору. Важнее точка росы — температура, при которой этот воздух начнёт отдавать воду: если корпус или плата холоднее, на них выпадет конденсат. Формула Магнуса над водой:

double dew_point(double t_c, double rh)
{
    const double b = 17.62, c = 243.12;
    double g = log(rh / 100.0) + b * t_c / (c + t_c);
    return c * g / (b - g);
}

При 25 °C и 60 % точка росы — 16,69 °C, при 20 °C и 50 % — 9,26 °C. У F407 блок плавающей точки — одинарной точности, и double там всё равно считается программно; для раза в секунду это неважно, а при частом расчёте лучше float и logf.


97.7 Грабли

DHT22 молчит. Стартовый импульс короче нужного (DHT11 нужно не меньше 18 мс) или датчик опрашивают чаще раза в 2 с.

На морозе DHT22 показывает тысячи градусов. Знак и модуль прочитаны как дополнительный код. Старший бит — знак.

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

CRC SHT31 не сходится никогда. Взята функция CRC от 1-Wire: многочлен тот же, но порядок бит и начальное значение другие.

AHT20 отдаёт одно и то же. Прочли раньше конца измерения: старший бит байта состояния — «ещё измеряю».

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


97.8 Итоги

Дальше: раздел 98 «Давление и высота: BMP280 и BME280» — калибровочные коэффициенты в самом датчике и формулы компенсации.

Открыть в интерактивном справочнике