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 байт. Обмен редкий: запрос и ответ
раз в несколько секунд.
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 Итоги
- MH-Z19B/C — NDIR-датчик CO₂ на UART 9600 бод 8N1 (
BRR= 0x1D4C); питание 5 В, уровни 3,3 В; второй выход — ШИМ. - Кадр — 9 байт:
FF, байт 1, код, пять байт, сумма = 0xFF − (байты 1…7) + 1. Чтение —FF 01 86 00 00 00 00 00 79, ppm — байты 2–3 ответа. Все 72 одиночные порчи сумма ловит, из 2556 двойных пропускает 65. - Приём — DMA1, канал 5, в кольцо 64 байта, разбор по
IDLEв главном цикле: искатьFFи код, проверять сумму, сдвигаться на байт. Ответ в модели — через 30–50 мс; нет ответа — повтор через 100 мс, три попытки. - Без суммы в опыте — 108 неверных из 20 000; с суммой — 0.
- Прогрев — 3 минуты по документации; в модели через 3 минуты ещё +75 ppm. Диапазон — 2000, 5000 или 10 000 (0x99), настройки читать обратно (0x9B, 0x7D).
- ABC — только где датчик каждый день видит уличный воздух, иначе в модели −398 ppm; 0x87 — на улице, не раньше 20 минут работы.
- ШИМ: Cppm = диапазон · (Th − 2) / (Th + Tl − 4), период 1004 мс; TIM4 в режиме ШИМ-входа, тик 0,1 мс — ошибка не больше 1 ppm, деление на 1000 мс вместо периода — до 250 ppm.
Полные исходники
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.