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

103д. Датчик CO₂ MH-Z19B/C по UART: кадр 9 байт, контрольная сумма, чтение ppm, диапазон, автокалибровка ABC, калибровка нуля и ШИМ-выход

В 103г SCD40 мерил углекислый газ и отдавал слова по I2C. MH-Z19B и MH-Z19C — тот же газ, но дешевле и проще в обмене: UART на 9600 бод, кадр 9 байт с однобайтовой суммой, а рядом ещё и ШИМ-выход, по которому ppm можно мерить вообще без UART. Здесь — как устроены кадр и сумма; как принимать ответ через DMA по паузе в линии и находить начало кадра после помехи; сколько ждать ответа и когда повторять запрос; почему первые минуты после включения числам не верят; как задать диапазон и убедиться, что датчик его принял; чем опасна автокалибровка ABC и как калибровать ноль; как считать ppm по ШИМ таймером в режиме ШИМ-входа.

Опоры: RM0008 (DocID13902 Rev 17): запросы DMA1 — USART1_RX на канале 5, таблица 78, с. 281; 9600 бод от 72 МГц — 468,75, ошибка 0 %, таблица 192, с. 800; флаг IDLE и его снятие парой «SR, затем DR», с. 820; режим ШИМ-входа, 15.3.6, с. 384; TS = 101 и SMS = 100 в TIMx_SMCR, с. 407; CC1IF снимается чтением TIMx_CCR1, с. 410; делитель PSC, с. 418. DocID13587 Rev 17 (STM32F103x8/xB): PA9 — USART1_TX, PA10 — USART1_RX, оба FT, с. 31; PB6 — TIM4_CH1, FT, с. 32; PA0 — TIM2_CH1_ETR, не FT, с. 29. Приём через DMA в кольцо и IDLE — раздел 16б, захват таймером — 13.6, здесь не повторяются; принцип NDIR — в 103г.1. Документа Winsen на MH-Z19B/C в проекте нет. Начало FF, байт 1 = 0x01, коды 0x86, 0x79, 0x7D, 0x99 и 0x9B, байт 0xA0 для ABC, место диапазона и ppm в кадре, формула суммы и восемь готовых кадров с суммами взяты из драйвера mhz19b Zephyr 4.4.2 на диске. Команды 0x87 и 0x88, прогрев, 24 ч автокалибровки, 20 мин перед калибровкой нуля, ШИМ-выход и его формула, питание и уровни — по документации производителя, не проверено: модель их повторяет, а не проверяет. Все числа — прогон драйвера _verify/co2103f/mhz103f.c на компьютере (gcc 12.2.0) со своей моделью MH-Z19B на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3. Задержка ответа, прогрев, дрейф, старение, сглаживание, шум и сам алгоритм ABC в модели — допущения: числа ниже говорят о модели, а не о датчике.

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

Оригинал Перевод
NDIR (non-dispersive infrared) недисперсионный инфракрасный метод
gas concentration read чтение концентрации (0x86)
zero point calibration калибровка нуля (0x87)
span point calibration калибровка пролёта, второй точки (0x88)
ABC (Automatic Baseline Correction) автокалибровка базовой линии (0x79)
detection range диапазон измерения (0x99)
checksum контрольная сумма
preheat time время прогрева
PWM output ШИМ-выход
PWM input mode режим ШИМ-входа таймера

103д.1 Что это за датчик и как его подключить

Внутри MH-Z19B — инфракрасный излучатель, камера с воздухом и приёмник: CO₂ поглощает свет в своей полосе, и по ослаблению датчик считает концентрацию. Это тот же NDIR, что и в 103г.1, только без микрофона — ослабление ловит приёмник (по документации производителя, не проверено). Наружу выходит готовое число в ppm. MH-Z19C — более новая версия; по документации производителя протокол UART у неё тот же (не проверено), а драйвер Zephyr называется mhz19b и других версий не различает.

Выходов два, и они независимы: UART — по запросу, ШИМ — сам, раз в период. Ещё у датчика бывает вывод HD для калибровки нуля перемычкой и аналоговый выход (по документации производителя, не проверено); здесь они не нужны.

Вывод датчика STM32F103 Назначение
Vin 5 В питание излучателя
GND GND земля
TX PA10 USART1_RX, ответы датчика
RX PA9 USART1_TX, команды датчику
PWM PB6 TIM4_CH1, ШИМ-вход

По документации производителя (не проверено): питание — 4,5…5,5 В, уровни UART — 3,3 В, средний ток — до 60 мА, а пики — до 150 мА, когда вспыхивает излучатель. F103 даёт на TX 3,3 В — датчику это подходит. Принимающие выводы взяты FT: PA10 (с. 31) и PB6 (с. 32) выдержат 5 В, если попадётся плата-переходник с пятивольтовой логикой. Для ШИМ не взят PA0 с TIM2_CH1_ETR: он не FT (с. 29). О пиках тока — в 103д.7.

UART — 9600 бод, 8N1, USART1 на APB2 = 72 МГц: BRR = 72 000 000 / 9600 = 7500 = 0x1D4C, то есть 468 + 12/16 = 468,75, в таблице 192 RM0008 (с. 800) ровно это, ошибка 0 %. Байт идёт 1041,7 мкс, кадр 9 байт — 9,375 мс; в секунду — не больше 960 байт. Обмен редкий: запрос и ответ раз в несколько секунд.

Вверху — подключение MH-Z19B/C к STM32F103: PA9 — TX к RX датчика, PA10 — RX от TX датчика на USART1 9600 бод 8N1, приём через DMA1, канал 5, по кругу в кольцо 64 байта, разбор по паузе в линии IDLE в главном цикле; PB6 — ШИМ-выход датчика на TIM4_CH1, режим ШИМ-входа, CCR1 — период, CCR2 — единица, тик 0,1 мс; питание датчика Vin 5 В, уровни UART 3,3 В. В середине — запрос FF 01 86 00 00 00 00 00 79 и ответ FF 86 03 20 00 00 00 00 57: начало FF, код, ppm 0x0320 = 800, сумма 0xFF − 0xA9 + 1 = 0x57, от запроса до ответа в модели 30–50 мс. Внизу — период ШИМ 1004 мс: Th = 202 мс единицы, Tl = 802 мс нуля, 2 мс единицы в начале и 2 мс нуля в конце; Cppm = 5000 · (202 − 2) / (202 + 802 − 4) = 1000 ppm


103д.2 Кадр 9 байт и контрольная сумма

И команда, и ответ — ровно 9 байт. Команда: FF, байт 1 = 0x01, код, пять байт параметров, сумма. Ответ: FF, в байте 1 — код команды, шесть байт данных, сумма. Так устроен драйвер mhz19b в Zephyr 4.4.2: он шлёт FF 01 код … и в ответе на настройку сверяет байт 1 с кодом.

Команда Кадр Ответ
0x86 — прочитать CO₂ FF 01 86 00 00 00 00 00 79 ppm — байты 2–3
0x79 — ABC включить FF 01 79 A0 00 00 00 00 E6 байт 1 = 0x79
0x79 — ABC выключить FF 01 79 00 00 00 00 00 86 байт 1 = 0x79
0x7D — прочитать ABC FF 01 7D 00 00 00 00 00 82 ABC — байт 7
0x99 — диапазон 2000 FF 01 99 00 00 00 07 D0 8F байт 1 = 0x99
0x99 — диапазон 5000 FF 01 99 00 00 00 13 88 CB байт 1 = 0x99
0x99 — диапазон 10 000 FF 01 99 00 00 00 27 10 2F байт 1 = 0x99
0x9B — прочитать диапазон FF 01 9B 00 00 00 00 00 64 диапазон — байты 4–5
0x87 — калибровка нуля FF 01 87 00 00 00 00 00 78 по документации — нет
0x88 — пролёт 2000 ppm FF 01 88 07 D0 00 00 00 A0 по документации — нет

Первые восемь кадров в Zephyr записаны готовыми, с суммами; драйвер собирает их сам, и все восемь совпали байт в байт. Коды 0x87 и 0x88 и место пролёта в байтах 3–4 — по документации производителя, не проверено: в Zephyr их нет.

Сумма — 0xFF минус сумма байтов 1…7 плюс 1, то есть просто минус сумма по модулю 256. Байт 0 (FF) в неё не входит. Если сложить байты 1…8 вместе с суммой, выйдет ноль по модулю 256:

/* контрольная сумма: 0xFF − (байты 1…7) + 1, то есть минус сумма по модулю 256; байт 0 (FF) не входит */
uint8_t mhz_sum(const uint8_t *f)
{
    uint8_t s = 0;
    for (uint32_t i = 1; i < MHZ_FRAME - 1; i++)
        s = (uint8_t)(s + f[i]);
    return (uint8_t)(0xFFu - s + 1u);
}
/* кадр команды: FF 01 cmd, байты 3–4 = w34, байт 5 = 0, байты 6–7 = w67, сумма */
void mhz_cmd_frame(uint8_t cmd, uint16_t w34, uint16_t w67, uint8_t out[MHZ_FRAME])
{
    out[0] = MHZ_HEAD;
    out[1] = MHZ_ADDR;
    out[2] = cmd;
    out[3] = (uint8_t)(w34 >> 8);
    out[4] = (uint8_t)w34;
    out[5] = 0;
    out[6] = (uint8_t)(w67 >> 8);
    out[7] = (uint8_t)w67;
    out[8] = mhz_sum(out);
}

Ответ модели при 800 ppm — FF 86 03 20 00 00 00 00 57: ppm в байтах 2–3, 0x0320 = 800. Байты 4–7 в модели — нули: что в них кладёт настоящий датчик, Zephyr не разбирает, и здесь это не проверено. Сумма байтов 1…7 — 0x86 + 0x03 + 0x20 = 169 = 0xA9, а 0xFF − 0xA9 + 1 = 0x57 — ровно последний байт. Две частые ошибки: сложить и байт 0 — выйдет 0x58; забыть «+ 1» — 0x56. Ни то, ни другое не сойдётся ни с одним кадром. Один перевёрнутый бит (байт 3, бит 4) — и mhz_check возвращает −2, MHZ_E_SUM.

Сумма однобайтовая, и она слабая. Перебор всех 72 одиночных порч в этом кадре: пропущено 0. Перебор всех 2556 порч ровно двух бит (с проверкой, что начало FF 86 цело): пропущено 65 — каждая 39-я. Пропускает она пары, где один и тот же бит в двух байтах сменился в разные стороны, и пары старших битов: 0x80 + 0x80 = 0x100 — ноль по модулю 256; таких пар 21 из 65. Поэтому ещё одна защита — проверять, что ответ пришёл на свой запрос (103д.3) и что число правдоподобно.


103д.3 Приём через DMA по IDLE: начало кадра, тайм-аут и повтор

Приём — как в 16б: DMA1, канал 5 (таблица 78, с. 281), кладёт байты в кольцо 64 байта по кругу. Половина и конец кольца здесь не нужны: ответ — 9 байт, а за ним пауза. Флаг IDLE встаёт, когда линия молчит целый символ после последнего байта (с. 820), и снимается парой «чтение SR, затем DR». Обработчик только ставит флаг, а разбор идёт в главном цикле: тогда драйвер живёт в одном потоке, и общий mhz_t не надо защищать от прерывания.

void USART1_IRQHandler(void)
{
    uint32_t sr = USART1->SR;
    /* IDLE, ORE, NE, FE снимает пара «чтение SR, затем DR»; при RXNE = 1 байт ещё ждёт DMA — не трогаем (16б.3) */
    if ((sr & (USART_SR_IDLE | USART_SR_ORE | USART_SR_NE | USART_SR_FE)) && !(sr & USART_SR_RXNE))
        (void)USART1->DR;
    if (sr & USART_SR_IDLE)
        idle_flag = 1;                           /* разбор — в главном цикле: драйвер живёт в одном потоке */
}

Разбор идёт прямо в кольце (16б.4), от rd до позиции записи DMA. Начало кадра — FF и за ним код, на который бывает ответ. Не начало — сдвиг на байт. Кадр пришёл не весь — выход, rd стоит на его начале. Сумма не сошлась — снова сдвиг на один байт, а не на девять: настоящее начало может оказаться внутри отвергнутого:

/* разбор кольца от rd до позиции записи DMA (размер − NDTR): зовут по IDLE, затем таймеры */
void mhz_on_rx(mhz_t *m, uint16_t ndtr, uint32_t now)
{
    uint16_t wr = (uint16_t)(m->size - ndtr);
    uint16_t used;
    uint8_t f[MHZ_FRAME];
    if (wr >= m->size)                                      /* NDTR = 0 в миг перезагрузки */
        wr = 0;
    m->wr = wr;
    used = (uint16_t)(wr >= m->rd ? wr - m->rd : wr + m->size - m->rd);
    if (used > m->hiwater)
        m->hiwater = used;
    while (used >= (m->search ? 2u : MHZ_FRAME)) {
        if (m->search && (at(m, 0) != MHZ_HEAD || !known(at(m, 1)))) {
            skip(m, 1);                                     /* ищем FF и код ответа */
            used--;
            m->n_junk++;
            continue;
        }
        if (used < MHZ_FRAME)
            break;                                          /* кадр пришёл не весь: rd стоит на его начале */
        for (uint16_t i = 0; i < MHZ_FRAME; i++)
            f[i] = at(m, i);
        if ((!m->search && f[0] != MHZ_HEAD) || (m->check_sum && mhz_sum(f) != f[8])) {
            uint16_t n = m->search ? 1u : MHZ_FRAME;        /* ложное начало или порча: искать со следующего байта */
            skip(m, n);
            used = (uint16_t)(used - n);
            m->n_badsum++;
            continue;
        }
        skip(m, MHZ_FRAME);
        used = (uint16_t)(used - MHZ_FRAME);
        on_frame(m, f, now);
    }
    mhz_poll(m, now);
}

Флаги check_sum и search нужны только для опытов ниже: в прошивке оба — единицы. С search = 0 драйвер ведёт себя как простые драйверы: перед запросом сбрасывает кольцо и берёт девять байт подряд. Так же, девять байт подряд без поиска начала, читает ответ и Zephyr.

Запрос — один за раз: пока ждём ответ, второй вызов mhz_read вернёт −3, MHZ_E_BUSY, и ответы не перепутаются. Нет ответа за 100 мс — повтор той же команды, всего три попытки:

/* ответа нет за MHZ_REPLY_MS — повтор той же команды, всего MHZ_TRIES попыток, потом событие «тайм-аут» */
void mhz_poll(mhz_t *m, uint32_t now)
{
    if (m->pending && due(now, m->req_at + MHZ_REPLY_MS)) {
        if (++m->tries < MHZ_TRIES) {
            m->n_retry++;
            m->req_at = now;
            if (!m->search)
                m->rd = m->wr;
            tx_cmd(m);
        } else {
            m->pending = 0;
            m->n_tmo++;
            ev(m, MHZ_EV_TIMEOUT, m->cmd, now);
        }
    }
}

В модели датчик отвечает через 10…30 мс после команды (допущение модели). 120 запросов раз в 5 с — ответили все 120, от запроса до события — 30–50 мс: 9,4 мс команда, задержка, 9,4 мс ответ и 1 мс до IDLE. Повторов — 0, наибольшая занятость кольца — 9 байт, ровно кадр. Датчик молчит (обрыв, перепутаны TX и RX) — три попытки, через 300 мс событие «тайм-аут». Zephyr ждёт ответа 1 с, в комментарии к этому числу написано, что оно взято произвольно.

Помехи. Опыт на 20 000 запросах: в каждом байте ответа с вероятностью 0,2 % перевёрнут бит, с вероятностью 0,05 % байт потерян и с вероятностью 0,05 % за ним вставлен лишний (так в модели; у настоящей линии ошибки реже):

Разбор Принято Неверных принято Наибольшее неверное, ppm Повторов Тайм-аутов
поиск FF и кода, сумма 19 999 0 — 519 1
без суммы 19 997 108 64 259 178 0
девять подряд, сумма 19 999 0 — 519 1

С суммой неверных нет: испорченный ответ отвергнут, и через 100 мс его спросили снова. Один запрос из 20 000 не получил ответа и за три попытки. Без суммы в программу ушли 108 неверных чисел, до 64 259 ppm — больше любого диапазона. В Zephyr ответ на чтение сумма проверяет, а ответ на настройку — нет: там сверяется только байт 1.

Синхронизация. В этом обмене пауза между ответами выручает: что не разобрано, то сброшено перед следующим запросом. Хуже, когда мусор приходит перед самим ответом — помеха на линии, датчик только что включился. В модели перед ответом шли последние k байт чужого кадра FF 86 01 FF 86 00 00 00 F4, k = 0…8. В нём ppm = 511, а 0x86 в байте 4 даёт внутри второе FF 86 (допущение модели: байты 4–7 в модели любые):

Разбор Ответ получен Неверных принято Повторов Наибольшая задержка, мс
поиск FF и кода, сумма 9 из 9 0 0 40
без суммы 6 из 9 3 0 40
девять подряд, сумма 9 из 9 0 8 146

С поиском и суммой мусор пропущен, ответ разобран сразу. Без суммы при k = 6, 7 и 8 драйвер зацепился за ложное FF 86 и принял 0 ppm, а настоящий ответ потерял. «Девять подряд» ответ не теряет, но платит повтором: первые девять байт отвергнуты, хвост ждёт до тайм-аута.


103д.4 Прогрев, диапазон и проверка настроек

Прогрев. По документации производителя после включения датчику нужно 3 минуты (не проверено). Все это время он отвечает, и число похоже на правду. В модели показание после включения завышено на 250 · e^(−t / 150 с) ppm (допущение модели). Воздух — 600 ppm, запрос раз в 5 с:

После включения 30 с 1 мин 2 мин 3 мин 5 мин 10 мин 20 мин
ошибка, ppm +205 +168 +112 +75 +34 +5 0

Драйвер отдаёт число и в прогреве, но ставит valid = 0: из 240 чтений первые 35 (до 175 с) помечены, первое верное — на 180 с. Модель показывает и другое: 3 минуты по документации — минимум, а не гарантия. Калибровать — только после долгой работы (103д.5).

Диапазон. Команда 0x99 задаёт верх шкалы: в Zephyr — 2000, 5000 или 10 000 ppm, байты 6–7. Драйвер другие числа не шлёт и возвращает −5, MHZ_E_ARG. Что делает датчик выше диапазона, без документа сказать нельзя; в модели число упирается в верх (допущение модели). Воздух — 3000 ppm:

Датчик Задан диапазон Прочитан обратно (0x9B) По UART, ppm Th ШИМ, мс
обычный 2000 2000 2000 1002
обычный 5000 5000 3000 602
обычный 10 000 10 000 3000 302
«подделка» 2000 5000 3000 602
«подделка» 10 000 5000 3000 602

От диапазона зависит и ШИМ: при 3000 ppm единица длится 602 мс на диапазоне 5000 и 302 мс на 10 000 (103д.6).

Проверка настроек. Датчик подтверждает 0x79 и 0x99 кадром с тем же кодом, но подтверждение не значит, что настройка встала. В модели «подделка» отвечает так же, а ничего не меняет (допущение модели: чем именно отличаются поддельные датчики, без образцов не проверено). Поэтому прошивка после каждой настройки читает её обратно — 0x7D и 0x9B — и при расхождении ставит флаг suspect:

static void on_ev(mhz_t *m, uint8_t ev, uint16_t v, uint32_t now)
{
    if (ev == MHZ_EV_PPM) {
        co2_ppm = v;
        co2_valid = m->valid;
        reads++;
        return;
    }
    if (ev == MHZ_EV_TIMEOUT) {
        timeouts++;
        return;                                  /* настройку повторит следующий шаг цикла */
    }
    if (ev == MHZ_EV_ABC) {
        abc_back = v;
        suspect |= v != 0;                       /* выключали — а датчик говорит «включена» */
    } else if (ev == MHZ_EV_RANGE) {
        range_back = v;
        suspect |= v != RANGE;
    }
    if (step < 4)
        step++;
    next_step(now);
}

В модели обычный датчик после «ABC выключить» читает обратно 0, а «подделка» — 1; диапазон 2000 у «подделки» читается как 5000.


103д.5 Автокалибровка ABC, калибровка нуля 0x87 и пролёта 0x88

NDIR-датчик со временем уходит. Что с этим делать, в общих чертах уже было в 103г.6; здесь — как это устроено у MH-Z19 и что выходит в модели. В модели новый датчик врёт на +80 ppm и стареет на +5 ppm в сутки (допущение модели).

ABC. По документации производителя (не проверено) автокалибровка включена с завода и раз в 24 ч от включения принимает наименьшее показание за сутки за 400 ppm. Команда — 0x79: байт 3 = 0xA0 включает, 0x00 выключает. В модели ABC делает ровно это (допущение модели: настоящий алгоритм не раскрыт). Ошибка в полдень, неделя:

Помещение ABC День 1 День 2 Дни 3–7
квартира, утром час проветривания (420 ppm) вкл +83 −14 −14
комната без проветривания, не ниже 800 ppm вкл +82 −398 −393
то же выкл +82 +87 +92 … +112

В квартире ABC убрала сдвиг: −14 — это 420, принятые за 400, и старение. В комнате без проветривания она приняла 800 за 400 и стала занижать почти на 400 ppm — хуже, чем без неё. Без ABC сдвиг остаётся и растёт на 5 ppm в сутки. Отсюда правило: ABC — только там, где датчик каждый день видит уличный воздух. В теплице, в погребе, в спальне с закрытым окном её выключают и калибруют ноль руками.

Ноль, 0x87. По документации производителя команда принимает текущее показание за 400 ppm, а перед ней датчик должен не меньше 20 минут пробыть в воздухе около 400 ppm (не проверено). В модели калибровка берёт текущее сглаженное показание, а показание догоняет воздух с постоянной 50 с (допущения модели). Датчик час работал в комнате при 900 ppm, ABC выключена:

Случай До, ppm Ответ драйвера Ошибка через 30 мин, ppm Через 4 ч, ppm
на улице, через 20 мин +80 0 0 +1
на улице, через 2 мин +118 0 −38 −37
в комнате при 900 ppm +80 0 −500 −499

Через 2 минуты на улице датчик ещё не догнал воздух, и −38 ppm остались навсегда. В комнате калибровка поверила, что 900 — это 400, и испортила датчик на 500 ppm. Второе правило драйвер проверить может — не раньше 20 минут после включения:

/* калибровка нуля: датчик сейчас на свежем воздухе (около 400 ppm); без force — не раньше 20 мин после включения */
int mhz_zero(mhz_t *m, uint8_t force, uint32_t now)
{
    if (!force && !due(now, m->on_at + MHZ_ZERO_MIN_MS))
        return MHZ_E_EARLY;
    return send(m, MHZ_CMD_ZERO, 0, 0, 0, now);
}

Датчик включили прямо на улице и через минуту позвали 0x87: драйвер вернул −4, MHZ_E_EARLY, и через час ошибка — прежние +80 ppm. С force = 1 калибровка прошла посреди прогрева, и через час ошибка — −167 ppm. А вот был ли датчик на улице, драйвер не знает: это забота того, кто нажимает кнопку.

Пролёт, 0x88. Ноль сдвигает шкалу, пролёт меняет её наклон: в датчик подают газ известной концентрации и шлют её в байтах 3–4 (по документации производителя, не проверено; нужен баллон с поверочной смесью). В модели наклон ошибочен на 6 % (допущение модели). После калибровки нуля: 2000 ppm читаются как 2096, 1000 — как 1036; после 0x88 с 2000 — 2000, 1000 и 400 ppm читаются верно. Пролёт ниже 400 ppm драйвер не шлёт: −5, MHZ_E_ARG. И только после нуля — иначе пролёт закрепит чужой сдвиг.


103д.6 ШИМ-выход: ppm таймером в режиме ШИМ-входа

По документации производителя (не проверено) ШИМ-выход повторяется с периодом 1004 мс ±5 %: в начале 2 мс единицы, в конце 2 мс нуля, а середина 1000 мс делится пропорционально концентрации. Отсюда формула:

Cppm = диапазон · (Th − 2 мс) / (Th + Tl − 4 мс),

где Th — длительность единицы, Tl — нуля. Диапазон — тот, что задан командой 0x99. В модели при 1000 ppm на диапазоне 5000: Th = 202 мс, Tl = 802 мс, и формула даёт 5000 · 200 / 1000 = 1000.

Мерить Th и Tl удобно таймером в режиме ШИМ-входа (RM0008, 15.3.6, с. 384): оба канала захвата смотрят на один вход TI1, первый — по фронту, второй — по спаду, а фронт ещё и сбрасывает счётчик. Тогда CCR1 — период, CCR2 — единица. TIM4 сидит на APB1, но считает от 72 МГц (13.3); PSC = 7199 даёт 10 кГц (с. 418) — тик 0,1 мс, а период 1004 мс — 10 040 тиков, в 16 бит помещается и с запасом +5 %:

    /* ШИМ-вход (RM0008, 15.3.6): IC1 и IC2 — оба с TI1, IC1 по фронту, IC2 по спаду, сброс счётчика по TI1FP1 */
    TIM4->PSC = TIM_PSC;
    TIM4->ARR = 0xFFFFu;
    TIM4->EGR = TIM_EGR_UG;                     /* PSC вступает в силу сразу (13.4) */
    TIM4->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_1;        /* CC1S = 01 (TI1), CC2S = 10 (TI1) */
    TIM4->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC2P;   /* CC1P = 0 — фронт, CC2P = 1 — спад */
    TIM4->SMCR = TIM_SMCR_TS_2 | TIM_SMCR_TS_0 | TIM_SMCR_SMS_2;  /* TS = 101 (TI1FP1), SMS = 100 (сброс) */
    TIM4->DIER = TIM_DIER_CC1IE;
    TIM4->CR1 = TIM_CR1_CEN;

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

void TIM4_IRQHandler(void)
{
    if (TIM4->SR & TIM_SR_CC1IF) {               /* нарастающий фронт: CCR1 — период, CCR2 — единица до него */
        uint16_t per = TIM4->CCR1;               /* чтение CCR1 снимает CC1IF */
        uint16_t hi = TIM4->CCR2;
        if (pwm_skip) {
            pwm_skip = 0;                        /* первый фронт после включения: счётчик ещё не сбрасывался */
        } else {
            pwm_per = per;
            pwm_hi = hi;
            pwm_new = 1;
        }
    }
}

Расчёт — целыми, в тиках, к ближайшему целому. Период вне 1004 мс ±5 % — не ШИМ датчика (обрыв, помеха, не тот вывод): −6, MHZ_E_PWM. Единица короче 2 мс — 0, длиннее периода без 2 мс — весь диапазон:

/* ШИМ-выход: Cppm = диапазон · (Th − 2 мс) / (Th + Tl − 4 мс); th и period — в тиках таймера, к ближайшему целому */
int32_t mhz_pwm_ppm(uint32_t th, uint32_t period, uint16_t range, uint32_t ticks_per_ms)
{
    uint32_t edge = MHZ_PWM_EDGE_MS * ticks_per_ms;
    uint32_t nom = MHZ_PWM_CYCLE_MS * ticks_per_ms;
    uint32_t span;
    if (period < nom - nom * MHZ_PWM_TOL_PCT / 100u || period > nom + nom * MHZ_PWM_TOL_PCT / 100u || th > period)
        return MHZ_E_PWM;                                   /* не ШИМ датчика: обрыв, помеха, не тот вывод */
    if (th <= edge)
        return 0;
    if (th >= period - edge)
        return range;
    span = period - 2u * edge;
    return (int32_t)(((uint32_t)range * (th - edge) * 2u + span) / (2u * span));
}

Зачем делить на измеренный период, а не на 1000 мс? Часы датчика плывут в пределах ±5 %, а с ними растягивается и Th. В модели — семь концентраций от 0 до 5000 ppm на диапазоне 5000, по 50 случайных фаз тика, всего 350 захватов на каждый ход часов:

Ход часов датчика ×0,95 ×1,00 ×1,05
период, мс 953,8 1004,0 1054,2
ошибка по формуле, ppm, не больше 1 0 1
ошибка, если делить на 1000 мс, ppm, не больше 250 0 250

По формуле ошибка — не больше 1 ppm: только округление тика 0,1 мс. С делением на 1000 мс — до 250 ppm у верха шкалы, ровно те 5 %.

ШИМ медленнее UART — одно число в 1004 мс, — зато не нужен ни запрос, ни разбор кадров. Но диапазон в формуле должен быть тем же, что в датчике: датчик на 5000 при 3000 ppm даёт Th = 602 мс, и формула с диапазоном 2000 выдаёт 1200 ppm вместо 3000.


103д.7 Грабли

ABC в непроветриваемой комнате. Автокалибровка принимает суточный минимум за 400 ppm. Где воздух ни разу не бывает уличным, она «чинит» исправный датчик: в модели −398 ppm на второй день. Выключить ABC (0x79 с 0x00) и прочитать обратно (0x7D).

0x87 не на улице или слишком рано. Калибровка нуля верит, что вокруг 400 ppm. В комнате при 900 ppm в модели — −500 ppm навсегда; через 2 минуты на улице — −38 ppm; через минуту после включения — −167 ppm. Не меньше 20 минут работы и на свежем воздухе.

Сумма не по тем байтам. Сумма — по байтам 1…7, без FF, и с «+ 1». С байтом 0 выходит 0x58, без «+ 1» — 0x56 вместо 0x57: ни один кадр не сойдётся.

Без суммы. 108 неверных чисел из 20 000 в опыте 103д.3, до 64 259 ppm, и ложное начало FF 86 в данных.

ШИМ: не тот диапазон или период 1000 мс. Диапазон в формуле — как в датчике: иначе 1200 вместо 3000 ppm. Делить — на измеренный период: иначе до 250 ppm при ходе часов датчика на 5 %.

Настройка «принята», но не встала. Подтверждение 0x79 и 0x99 ничего не гарантирует. Встречаются поддельные и перемаркированные датчики; чем они отличаются, без образцов не проверено. Читать настройки обратно (0x7D, 0x9B) и сравнивать: в модели «подделка» на «диапазон 2000» отвечает «5000».

Первые минуты. Прогрев — 3 минуты по документации, но в модели и через 3 минуты +75 ppm. Число в прогреве — с флагом valid = 0.

Пики тока. По документации производителя средний ток — до 60 мА, а в миг вспышки излучателя — до 150 мА (не проверено, в модели этого нет). Питание от вывода 5 В отладочной платы через тонкий длинный провод проседает на каждой вспышке. Конденсатор у самого датчика и отдельный провод питания; без них жди сбросов датчика и помех на UART.

Пять вольт на выводы. Уровни UART датчика — 3,3 В (по документации производителя), но переходники бывают пятивольтовые. Принимать на FT: PA10 и PB6 (с. 31, 32) выдержат, PA0 (с. 29) — нет.


103д.8 Итоги

Полные исходники

mhz103f.h — кадр, коды команд, сроки, коды ошибок и функции драйвера MH-Z19B/C.

/* mhz103f.h — раздел 103д: драйвер датчика CO2 MH-Z19B/C (Winsen) по UART без регистров микроконтроллера.
 * Приём — кольцо, в которое пишет DMA (раздел 16б); разбор зовут по паузе в линии (IDLE) с текущим NDTR.
 * Передача и время — функции порта (mhz_port_tx): в прошивке fw103f.c это USART1 и SysTick STM32F103, в стенде
 * host103f.c — модель model103f.py.
 * Кадр — 9 байт: FF, байт 1, команда (в ответе — байт 1), пять байт, контрольная сумма 0xFF − (байты 1…7) + 1.
 * Начало FF, байт 1 = 0x01 в команде, коды 0x86/0x79/0x7D/0x99/0x9B, 0xA0 для ABC, диапазон в байтах 6–7, ppm в
 * ответе — байты 2–3, диапазон — 4–5, ABC — 7, и сумма — как в драйвере mhz19b Zephyr 4.4.2. Команды 0x87 и 0x88,
 * прогрев 3 мин, 20 мин перед калибровкой нуля и ШИМ-выход — по документации производителя, с датчиком не сверено. */
#ifndef MHZ103F_H
#define MHZ103F_H
#include <stdint.h>

#define MHZ_FRAME         9u
#define MHZ_HEAD          0xFFu
#define MHZ_ADDR          0x01u         /* байт 1 команды (в Zephyr — MHZ19B_RESERVED) */
#define MHZ_CMD_READ      0x86u         /* прочитать CO2: ответ — ppm в байтах 2–3 */
#define MHZ_CMD_ZERO      0x87u         /* калибровка нуля: сейчас вокруг 400 ppm (по документации) */
#define MHZ_CMD_SPAN      0x88u         /* калибровка пролёта: ppm в байтах 3–4 (по документации) */
#define MHZ_CMD_ABC       0x79u         /* автокалибровка: байт 3 = 0xA0 — вкл, 0x00 — выкл */
#define MHZ_CMD_ABC_GET   0x7Du         /* ответ — байт 7 */
#define MHZ_CMD_RANGE     0x99u         /* диапазон: байты 6–7, 2000/5000/10000 */
#define MHZ_CMD_RANGE_GET 0x9Bu         /* ответ — байты 4–5 */
#define MHZ_ABC_ON        0xA0u

#define MHZ_REPLY_MS      100u          /* ждать ответ: 9,4 мс команда + задержка датчика + 9,4 мс ответ */
#define MHZ_TRIES         3u            /* попыток одного запроса */
#define MHZ_WARM_MS       180000u       /* прогрев после включения: 3 мин (по документации) */
#define MHZ_ZERO_MIN_MS   1200000u      /* калибровка нуля — не раньше 20 мин работы (по документации) */
#define MHZ_PWM_EDGE_MS   2u            /* ШИМ: 2 мс единицы в начале и 2 мс нуля в конце периода */
#define MHZ_PWM_CYCLE_MS  1004u         /* период ШИМ, ±5 % (по документации) */
#define MHZ_PWM_TOL_PCT   5u

enum { MHZ_OK = 0, MHZ_E_HEAD = -1, MHZ_E_SUM = -2, MHZ_E_BUSY = -3, MHZ_E_EARLY = -4, MHZ_E_ARG = -5, MHZ_E_PWM = -6 };
enum { MHZ_EV_PPM = 0, MHZ_EV_RANGE = 1, MHZ_EV_ABC = 2, MHZ_EV_ACK = 3, MHZ_EV_TIMEOUT = 4 };

typedef struct mhz mhz_t;
typedef void (*mhz_cb)(mhz_t *m, uint8_t ev, uint16_t value, uint32_t now);

struct mhz {
    const volatile uint8_t *ring;       /* кольцо DMA */
    uint16_t size, rd, wr, hiwater;     /* wr — позиция записи DMA при последнем разборе */
    uint8_t check_sum;                  /* 1; 0 — сумму не смотреть (для опыта) */
    uint8_t search;                     /* 1 — искать FF и код, сдвиг на байт; 0 — девять байт подряд (для опыта) */
    uint8_t pending, tries, cmd, valid, abc;
    uint16_t w34, w67, ppm, range;
    uint32_t req_at, on_at;
    mhz_cb cb;
    uint32_t n_good, n_badsum, n_junk, n_req, n_retry, n_tmo, n_stray;
};

/* ── порт: в прошивке — USART1; в стенде — строки к модели ── */
void mhz_port_tx(const uint8_t *p, uint16_t n);

/* ── драйвер ──────────────────────────────────────────────────────────── */
uint8_t mhz_sum(const uint8_t *f);
void mhz_cmd_frame(uint8_t cmd, uint16_t w34, uint16_t w67, uint8_t out[MHZ_FRAME]);
int mhz_check(const uint8_t *f);
void mhz_init(mhz_t *m, const volatile uint8_t *ring, uint16_t size, mhz_cb cb, uint32_t now);
void mhz_on_rx(mhz_t *m, uint16_t ndtr, uint32_t now);
void mhz_poll(mhz_t *m, uint32_t now);
int mhz_read(mhz_t *m, uint32_t now);
int mhz_set_abc(mhz_t *m, uint8_t on, uint32_t now);
int mhz_get_abc(mhz_t *m, uint32_t now);
int mhz_set_range(mhz_t *m, uint16_t range, uint32_t now);
int mhz_get_range(mhz_t *m, uint32_t now);
int mhz_zero(mhz_t *m, uint8_t force, uint32_t now);
int mhz_span(mhz_t *m, uint16_t ppm, uint32_t now);
int32_t mhz_pwm_ppm(uint32_t th, uint32_t period, uint16_t range, uint32_t ticks_per_ms);
uint32_t mhz_next_ms(const mhz_t *m, uint32_t now);

#endif

mhz103f.c — драйвер: сумма, кадр команды, разбор кольца по IDLE, запрос с повтором, настройки, калибровка и ppm по ШИМ.

/* mhz103f.c — раздел 103д: драйвер MH-Z19B/C. Кадр 9 байт с суммой, разбор прямо из кольца DMA по IDLE с поиском
 * начала, запрос с тайм-аутом и повтором, ABC, диапазон, калибровка нуля и пролёта, ppm по ШИМ-выходу. Регистров
 * микроконтроллера здесь нет: тот же файл собирается в прошивку и в стенд на компьютере. */
#include "mhz103f.h"

static int due(uint32_t now, uint32_t t)
{
    return (int32_t)(now - t) >= 0;
}

static uint8_t at(const mhz_t *m, uint16_t k)             /* k-й байт от rd; без деления на размер кольца */
{
    uint32_t i = (uint32_t)m->rd + k;
    if (i >= m->size)
        i -= m->size;
    return m->ring[i];
}

static void skip(mhz_t *m, uint16_t n)
{
    uint32_t i = (uint32_t)m->rd + n;
    if (i >= m->size)
        i -= m->size;
    m->rd = (uint16_t)i;
}

static int known(uint8_t c)                                 /* коды, на которые бывает ответ */
{
    return c == MHZ_CMD_READ || c == MHZ_CMD_ABC || c == MHZ_CMD_ABC_GET || c == MHZ_CMD_RANGE
        || c == MHZ_CMD_RANGE_GET || c == MHZ_CMD_ZERO || c == MHZ_CMD_SPAN;
}

/* контрольная сумма: 0xFF − (байты 1…7) + 1, то есть минус сумма по модулю 256; байт 0 (FF) не входит */
uint8_t mhz_sum(const uint8_t *f)
{
    uint8_t s = 0;
    for (uint32_t i = 1; i < MHZ_FRAME - 1; i++)
        s = (uint8_t)(s + f[i]);
    return (uint8_t)(0xFFu - s + 1u);
}

/* кадр команды: FF 01 cmd, байты 3–4 = w34, байт 5 = 0, байты 6–7 = w67, сумма */
void mhz_cmd_frame(uint8_t cmd, uint16_t w34, uint16_t w67, uint8_t out[MHZ_FRAME])
{
    out[0] = MHZ_HEAD;
    out[1] = MHZ_ADDR;
    out[2] = cmd;
    out[3] = (uint8_t)(w34 >> 8);
    out[4] = (uint8_t)w34;
    out[5] = 0;
    out[6] = (uint8_t)(w67 >> 8);
    out[7] = (uint8_t)w67;
    out[8] = mhz_sum(out);
}

int mhz_check(const uint8_t *f)
{
    if (f[0] != MHZ_HEAD)
        return MHZ_E_HEAD;
    if (mhz_sum(f) != f[8])
        return MHZ_E_SUM;
    return MHZ_OK;
}

void mhz_init(mhz_t *m, const volatile uint8_t *ring, uint16_t size, mhz_cb cb, uint32_t now)
{
    *m = (mhz_t){0};
    m->ring = ring;
    m->size = size;
    m->cb = cb;
    m->check_sum = 1;
    m->search = 1;
    m->range = 5000u;
    m->on_at = now;                                         /* от включения питания датчика */
}

static void ev(mhz_t *m, uint8_t e, uint16_t v, uint32_t now)
{
    if (m->cb)
        m->cb(m, e, v, now);
}

static void tx_cmd(const mhz_t *m)
{
    uint8_t f[MHZ_FRAME];
    mhz_cmd_frame(m->cmd, m->w34, m->w67, f);
    mhz_port_tx(f, MHZ_FRAME);
}

static int send(mhz_t *m, uint8_t cmd, uint16_t w34, uint16_t w67, uint8_t reply, uint32_t now)
{
    if (m->pending)
        return MHZ_E_BUSY;                                  /* один запрос за раз: ответы не перепутаются */
    m->cmd = cmd;
    m->w34 = w34;
    m->w67 = w67;
    if (reply) {
        m->pending = 1;
        m->tries = 0;
        m->n_req++;
        m->req_at = now;
    }
    if (!m->search)
        m->rd = m->wr;                                      /* «девять подряд»: сбросить всё, что было до запроса */
    tx_cmd(m);
    return MHZ_OK;
}

static void on_frame(mhz_t *m, const uint8_t *f, uint32_t now)
{
    uint8_t c = f[1];                                       /* в ответе байт 1 — код команды */
    m->n_good++;
    if (m->pending && c == m->cmd)
        m->pending = 0;
    else
        m->n_stray++;                                       /* ответ без запроса: опоздал после повтора */
    if (c == MHZ_CMD_READ) {
        m->ppm = (uint16_t)(f[2] << 8 | f[3]);
        m->valid = (uint8_t)due(now, m->on_at + MHZ_WARM_MS);   /* в прогреве число есть, но ему не верят */
        ev(m, MHZ_EV_PPM, m->ppm, now);
    } else if (c == MHZ_CMD_RANGE_GET) {
        m->range = (uint16_t)(f[4] << 8 | f[5]);
        ev(m, MHZ_EV_RANGE, m->range, now);
    } else if (c == MHZ_CMD_ABC_GET) {
        m->abc = f[7];
        ev(m, MHZ_EV_ABC, f[7], now);
    } else {
        ev(m, MHZ_EV_ACK, c, now);
    }
}

/* разбор кольца от rd до позиции записи DMA (размер − NDTR): зовут по IDLE, затем таймеры */
void mhz_on_rx(mhz_t *m, uint16_t ndtr, uint32_t now)
{
    uint16_t wr = (uint16_t)(m->size - ndtr);
    uint16_t used;
    uint8_t f[MHZ_FRAME];
    if (wr >= m->size)                                      /* NDTR = 0 в миг перезагрузки */
        wr = 0;
    m->wr = wr;
    used = (uint16_t)(wr >= m->rd ? wr - m->rd : wr + m->size - m->rd);
    if (used > m->hiwater)
        m->hiwater = used;
    while (used >= (m->search ? 2u : MHZ_FRAME)) {
        if (m->search && (at(m, 0) != MHZ_HEAD || !known(at(m, 1)))) {
            skip(m, 1);                                     /* ищем FF и код ответа */
            used--;
            m->n_junk++;
            continue;
        }
        if (used < MHZ_FRAME)
            break;                                          /* кадр пришёл не весь: rd стоит на его начале */
        for (uint16_t i = 0; i < MHZ_FRAME; i++)
            f[i] = at(m, i);
        if ((!m->search && f[0] != MHZ_HEAD) || (m->check_sum && mhz_sum(f) != f[8])) {
            uint16_t n = m->search ? 1u : MHZ_FRAME;        /* ложное начало или порча: искать со следующего байта */
            skip(m, n);
            used = (uint16_t)(used - n);
            m->n_badsum++;
            continue;
        }
        skip(m, MHZ_FRAME);
        used = (uint16_t)(used - MHZ_FRAME);
        on_frame(m, f, now);
    }
    mhz_poll(m, now);
}

/* ответа нет за MHZ_REPLY_MS — повтор той же команды, всего MHZ_TRIES попыток, потом событие «тайм-аут» */
void mhz_poll(mhz_t *m, uint32_t now)
{
    if (m->pending && due(now, m->req_at + MHZ_REPLY_MS)) {
        if (++m->tries < MHZ_TRIES) {
            m->n_retry++;
            m->req_at = now;
            if (!m->search)
                m->rd = m->wr;
            tx_cmd(m);
        } else {
            m->pending = 0;
            m->n_tmo++;
            ev(m, MHZ_EV_TIMEOUT, m->cmd, now);
        }
    }
}

int mhz_read(mhz_t *m, uint32_t now)
{
    return send(m, MHZ_CMD_READ, 0, 0, 1, now);
}

int mhz_set_abc(mhz_t *m, uint8_t on, uint32_t now)
{
    return send(m, MHZ_CMD_ABC, on ? (uint16_t)(MHZ_ABC_ON << 8) : 0u, 0, 1, now);
}

int mhz_get_abc(mhz_t *m, uint32_t now)
{
    return send(m, MHZ_CMD_ABC_GET, 0, 0, 1, now);
}

int mhz_set_range(mhz_t *m, uint16_t range, uint32_t now)
{
    if (range != 2000u && range != 5000u && range != 10000u)
        return MHZ_E_ARG;                                   /* как в Zephyr: только три диапазона */
    return send(m, MHZ_CMD_RANGE, 0, range, 1, now);
}

int mhz_get_range(mhz_t *m, uint32_t now)
{
    return send(m, MHZ_CMD_RANGE_GET, 0, 0, 1, now);
}

/* калибровка нуля: датчик сейчас на свежем воздухе (около 400 ppm); без force — не раньше 20 мин после включения */
int mhz_zero(mhz_t *m, uint8_t force, uint32_t now)
{
    if (!force && !due(now, m->on_at + MHZ_ZERO_MIN_MS))
        return MHZ_E_EARLY;
    return send(m, MHZ_CMD_ZERO, 0, 0, 0, now);
}

/* калибровка пролёта: в датчике газ известной концентрации ppm; только после калибровки нуля */
int mhz_span(mhz_t *m, uint16_t ppm, uint32_t now)
{
    if (ppm <= 400u || ppm > m->range)
        return MHZ_E_ARG;                                   /* пролёт — выше свежего воздуха и в пределах диапазона */
    return send(m, MHZ_CMD_SPAN, ppm, 0, 0, now);
}

/* ШИМ-выход: Cppm = диапазон · (Th − 2 мс) / (Th + Tl − 4 мс); th и period — в тиках таймера, к ближайшему целому */
int32_t mhz_pwm_ppm(uint32_t th, uint32_t period, uint16_t range, uint32_t ticks_per_ms)
{
    uint32_t edge = MHZ_PWM_EDGE_MS * ticks_per_ms;
    uint32_t nom = MHZ_PWM_CYCLE_MS * ticks_per_ms;
    uint32_t span;
    if (period < nom - nom * MHZ_PWM_TOL_PCT / 100u || period > nom + nom * MHZ_PWM_TOL_PCT / 100u || th > period)
        return MHZ_E_PWM;                                   /* не ШИМ датчика: обрыв, помеха, не тот вывод */
    if (th <= edge)
        return 0;
    if (th >= period - edge)
        return range;
    span = period - 2u * edge;
    return (int32_t)(((uint32_t)range * (th - edge) * 2u + span) / (2u * span));
}

/* ближайший срок, к которому драйверу нужен вызов mhz_poll (без учёта приходящих байтов) */
uint32_t mhz_next_ms(const mhz_t *m, uint32_t now)
{
    return m->pending ? m->req_at + MHZ_REPLY_MS : now + 0x7FFFFFFFu;
}

fw103f.c — прошивка F103: USART1 с DMA1 и IDLE, TIM4 в режиме ШИМ-входа на PB6, настройка с проверкой и чтение раз в 5 с.

/* fw103f.c — раздел 103д: датчик CO2 MH-Z19B/C на STM32F103 (BluePill), 72 МГц.
 * USART1, 9600 бод: PA9 — TX к RX датчика, PA10 — RX от TX датчика (PA10 — FT). Приём — DMA1, канал 5, по кругу в
 * кольцо 64 байта (раздел 16б); прерывание IDLE только ставит флаг, разбор — в главном цикле, там же тайм-ауты.
 * PB6 (FT) — ШИМ-выход датчика на TIM4_CH1 в режиме ШИМ-входа: CCR1 — период, CCR2 — единица, тик 0,1 мс.
 * При старте: ABC выключить, диапазон 5000, обе настройки прочитать обратно. Затем запрос CO2 раз в 5 с.
 * Результат — в переменных для отладчика. Тактирование до 72 МГц — как в разделе 6; USART1 на APB2 = 72 МГц,
 * TIM4 на APB1 = 36 МГц считает от 72 МГц (13.3). */
#include "stm32f10x.h"
#include "mhz103f.h"

#define CPU_HZ      72000000u
#define BAUD        9600u
#define RX_SIZE     64u                 /* ответ — 9 байт; IDLE после каждого, кольцо с запасом */
#define POLL_MS     10u
#define READ_MS     5000u
#define RANGE       5000u
#define TIM_PSC     7199u               /* 72 МГц / 7200 = 10 кГц: тик 0,1 мс, период 1004 мс = 10 040 тиков */
#define TICKS_MS    10u

_Static_assert(CPU_HZ / BAUD == 7500u && CPU_HZ % BAUD == 0u, "USART1: BRR = 7500 = 0x1D4C — 9600 бод");
_Static_assert(CPU_HZ / (TIM_PSC + 1u) == TICKS_MS * 1000u, "TIM4: 10 кГц");
_Static_assert(1054u * TICKS_MS < 65536u, "период ШИМ +5 % помещается в 16 бит");

static mhz_t D;
static uint8_t rx_ring[RX_SIZE];
static volatile uint32_t ms;
static volatile uint8_t idle_flag, pwm_new, pwm_skip = 1;
static volatile uint16_t pwm_per, pwm_hi;
static uint8_t step;                    /* 0 — ABC выкл, 1 — читать ABC, 2 — диапазон, 3 — читать диапазон, 4 — работа */
volatile uint16_t co2_ppm, abc_back, range_back;
volatile int32_t pwm_ppm;
volatile uint8_t co2_valid, suspect;    /* suspect: настройка не встала — датчик не тот или поддельный */
volatile uint32_t reads, timeouts;

void SysTick_Handler(void)
{
    ms++;
}

/* ── порт драйвера ────────────────────────────────────────────────────── */
void mhz_port_tx(const uint8_t *p, uint16_t n)
{
    for (uint16_t i = 0; i < n; i++) {           /* 9 байт — 9,4 мс; приём идёт через DMA и не мешает */
        while (!(USART1->SR & USART_SR_TXE)) {
        }
        USART1->DR = p[i];
    }
}

void USART1_IRQHandler(void)
{
    uint32_t sr = USART1->SR;
    /* IDLE, ORE, NE, FE снимает пара «чтение SR, затем DR»; при RXNE = 1 байт ещё ждёт DMA — не трогаем (16б.3) */
    if ((sr & (USART_SR_IDLE | USART_SR_ORE | USART_SR_NE | USART_SR_FE)) && !(sr & USART_SR_RXNE))
        (void)USART1->DR;
    if (sr & USART_SR_IDLE)
        idle_flag = 1;                           /* разбор — в главном цикле: драйвер живёт в одном потоке */
}

void TIM4_IRQHandler(void)
{
    if (TIM4->SR & TIM_SR_CC1IF) {               /* нарастающий фронт: CCR1 — период, CCR2 — единица до него */
        uint16_t per = TIM4->CCR1;               /* чтение CCR1 снимает CC1IF */
        uint16_t hi = TIM4->CCR2;
        if (pwm_skip) {
            pwm_skip = 0;                        /* первый фронт после включения: счётчик ещё не сбрасывался */
        } else {
            pwm_per = per;
            pwm_hi = hi;
            pwm_new = 1;
        }
    }
}

static void next_step(uint32_t now)
{
    switch (step) {
    case 0: (void)mhz_set_abc(&D, 0, now); break;
    case 1: (void)mhz_get_abc(&D, now); break;
    case 2: (void)mhz_set_range(&D, RANGE, now); break;
    case 3: (void)mhz_get_range(&D, now); break;
    default: break;
    }
}

static void on_ev(mhz_t *m, uint8_t ev, uint16_t v, uint32_t now)
{
    if (ev == MHZ_EV_PPM) {
        co2_ppm = v;
        co2_valid = m->valid;
        reads++;
        return;
    }
    if (ev == MHZ_EV_TIMEOUT) {
        timeouts++;
        return;                                  /* настройку повторит следующий шаг цикла */
    }
    if (ev == MHZ_EV_ABC) {
        abc_back = v;
        suspect |= v != 0;                       /* выключали — а датчик говорит «включена» */
    } else if (ev == MHZ_EV_RANGE) {
        range_back = v;
        suspect |= v != RANGE;
    }
    if (step < 4)
        step++;
    next_step(now);
}

static void board_init(void)
{
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_USART1EN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;
    GPIOA->CRH = (GPIOA->CRH & ~0xFF0u)
               | 0xBu << 4                      /* PA9 — USART1_TX, альтернативная функция */
               | 0x4u << 8;                     /* PA10 — USART1_RX, вход без подтяжки */
    GPIOB->CRL = (GPIOB->CRL & ~(0xFu << 24)) | 0x4u << 24;   /* PB6 — вход без подтяжки: TIM4_CH1 */

    DMA1_Channel5->CCR = 0;                     /* канал выключен: только так пишутся CNDTR и адреса */
    DMA1_Channel5->CPAR = (uint32_t)&USART1->DR;
    DMA1_Channel5->CMAR = (uint32_t)rx_ring;
    DMA1_Channel5->CNDTR = RX_SIZE;
    DMA1->IFCR = DMA_IFCR_CGIF5;
    DMA1_Channel5->CCR = DMA_CCR5_MINC | DMA_CCR5_CIRC | DMA_CCR5_EN;

    USART1->BRR = CPU_HZ / BAUD;                /* 7500 = 0x1D4C: 468 + 12/16 */
    USART1->CR3 = USART_CR3_DMAR | USART_CR3_EIE;
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_IDLEIE;

    /* ШИМ-вход (RM0008, 15.3.6): IC1 и IC2 — оба с TI1, IC1 по фронту, IC2 по спаду, сброс счётчика по TI1FP1 */
    TIM4->PSC = TIM_PSC;
    TIM4->ARR = 0xFFFFu;
    TIM4->EGR = TIM_EGR_UG;                     /* PSC вступает в силу сразу (13.4) */
    TIM4->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_1;        /* CC1S = 01 (TI1), CC2S = 10 (TI1) */
    TIM4->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E | TIM_CCER_CC2P;   /* CC1P = 0 — фронт, CC2P = 1 — спад */
    TIM4->SMCR = TIM_SMCR_TS_2 | TIM_SMCR_TS_0 | TIM_SMCR_SMS_2;  /* TS = 101 (TI1FP1), SMS = 100 (сброс) */
    TIM4->DIER = TIM_DIER_CC1IE;
    TIM4->CR1 = TIM_CR1_CEN;

    NVIC_SetPriority(USART1_IRQn, 5);
    NVIC_SetPriority(TIM4_IRQn, 6);
    NVIC_EnableIRQ(USART1_IRQn);
    NVIC_EnableIRQ(TIM4_IRQn);
    SysTick_Config(CPU_HZ / 1000u);
}

int main(void)
{
    uint32_t last = 0, last_read = 0;
    board_init();
    mhz_init(&D, rx_ring, RX_SIZE, on_ev, ms);  /* питание датчика включилось вместе с платой */
    next_step(ms);
    for (;;) {
        uint32_t now = ms;
        if (idle_flag) {
            idle_flag = 0;
            mhz_on_rx(&D, (uint16_t)DMA1_Channel5->CNDTR, now);
        } else if (now - last >= POLL_MS) {
            last = now;
            mhz_poll(&D, now);
            if (step >= 4 && !D.pending && now - last_read >= READ_MS) {
                last_read = now;
                (void)mhz_read(&D, now);
            } else if (step < 4 && !D.pending) {
                next_step(now);                  /* шаг настройки не получил ответа — ещё раз */
            }
        }
        if (pwm_new) {
            pwm_new = 0;
            pwm_ppm = mhz_pwm_ppm(pwm_hi, pwm_per, RANGE, TICKS_MS);
        }
        __WFI();                                /* до тика SysTick, IDLE или фронта ШИМ */
    }
}

Дальше: раздел 104 «Термопара: MAX31855 и MAX6675» — термопара, холодный спай и готовые преобразователи на SPI.

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