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

103в. Датчик пыли PMS5003 по UART: кадр 32 байта, PM1.0/PM2.5/PM10, активный и пассивный режим, сон и индекс качества воздуха

В разделе 103 MQ мерили газ нагретым слоем, в 103б TCS34725 мерил цвет. PMS5003 мерит пыль — сколько в воздухе мелких частиц. Вентилятор гонит воздух через камеру, лазер светит поперёк, фотодиод ловит вспышки рассеянного света, а микроконтроллер датчика считает частицы и пересчитывает их в микрограммы на кубометр. Наружу выходит кадр 32 байта по UART. Здесь — как устроен кадр и его сумма, как подключить датчик к F103, как принимать кадры через DMA в кольцо и не сбиваться с начала кадра, чем активный режим отличается от пассивного, зачем датчик усыплять и почему после пробуждения 30 секунд нельзя верить показаниям, что такое «CF = 1» и «атмосферные» значения и как из PM2.5 получить индекс качества воздуха.

Опоры: RM0008 (DocID13902 Rev 17): запросы DMA1 — USART1_RX на канале 5, таблица 78, с. 281; 9600 бод от 72 МГц — 468,75, ошибка 0 %, таблица 192, с. 800. DocID13587 Rev 17 (STM32F103x8/xB): PA9 — USART1_TX, PA10 — USART1_RX, оба FT, с. 31; PA2 и PA3 — не FT, с. 29. Приём через DMA в кольцо — раздел 16б, кадр с суммой по UART — 16г, здесь не повторяются. Документа на PMS5003 (Plantower) в проекте нет. Формат кадра — 42 4D, длина 28, 13 слов, сумма 30 байт — сверен с образцом кадра в драйвере pms7003 Zephyr 4.4.2 на диске. Команды 0xE1, 0xE2 и 0xE4, интервалы вывода, 30 с после пробуждения, питание и выводы SET и RESET — по документации производителя, не проверено. Все числа — прогон драйвера _verify/pm103d/pms103d.c на компьютере (gcc 12.2.0) со своей моделью PMS5003 на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3. Концентрации, шум, прогрев и связь «CF = 1» с «атмосферными» в модели — допущения: числа ниже говорят о модели, а не о датчике.

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

Оригинал Перевод
particulate matter (PM2.5, PM10) взвешенные частицы мельче 2,5 и 10 мкм
laser scattering лазерное рассеяние
mass concentration массовая концентрация, мкг/м³
particle count per 0.1 L число частиц в 0,1 л воздуха
CF = 1, standard particle «CF = 1», заводская калибровка
atmospheric environment «атмосферные» значения
active / passive mode активный режим (сам шлёт) / пассивный (по запросу)
sleep / wakeup сон / пробуждение
check code контрольная сумма
Air Quality Index (AQI) индекс качества воздуха

103в.1 Принцип и кадр 32 байта

Внутри PMS5003 — маленький вентилятор, лазерный диод и фотодиод. Воздух идёт через камеру, частица пересекает луч и рассеивает свет; чем крупнее частица, тем ярче вспышка. Датчик считает вспышки по яркости, раскладывает частицы по размерам и по их числу оценивает массу. Массу он не взвешивает — это пересчёт с допущениями о плотности и форме частиц, отсюда и разные калибровки ниже. Газ так не измерить: газы — дело MQ из 103.5.

Кадр данных — 32 байта. Формат сверен с драйвером pms7003 Zephyr 4.4.2: там в комментарии записан образец кадра, и его разбор идёт в том же порядке; у PMS5003 по документации производителя формат тот же (не проверено):

Байты Что
0, 1 42 4D — начало, «BM»
2, 3 длина: 28 = 2 × 13 + 2 — тринадцать слов и сумма
4…9 PM1.0, PM2.5, PM10 при «CF = 1», мкг/м³
10…15 PM1.0, PM2.5, PM10, «атмосферные», мкг/м³
16…27 частиц больше 0,3; 0,5; 1; 2,5; 5 и 10 мкм в 0,1 л
28, 29 слово 13: в образце Zephyr — 0x71 и 0x00 (версия и код ошибки по документации производителя)
30, 31 сумма

Все слова — старшим байтом вперёд. Сумма — 16 бит, простая сумма первых 30 байт, от 42 до слова 13 включительно. В образце Zephyr 42 4D 00 1C 00 01 00 01 00 01 00 01 00 01 00 01 01 92 00 4E 00 03 00 00 00 00 00 00 71 00 02 06: сумма 30 байт — 0x0206, драйвер разбирает его с кодом 0: все шесть концентраций — 1 мкг/м³, частиц — 402, 78, 3, 0, 0 и 0. Сам драйвер Zephyr сумму не проверяет вовсе (к чему это приводит — в 103в.3).

/* сумма кадра — 16 бит, простая сумма байтов */
uint16_t pms_sum(const uint8_t *p, uint32_t n)
{
    uint32_t s = 0;
    while (n--)
        s += *p++;
    return (uint16_t)s;
}
/* кадр данных целиком: начало, длина 28, сумма 30 байт = последнее слово */
int pms_decode(const uint8_t *f, pms_data_t *d)
{
    if (f[0] != PMS_B1 || f[1] != PMS_B2)
        return PMS_E_HEAD;
    if (w16(f, 2) != PMS_LEN_DATA)
        return PMS_E_LEN;
    if (pms_sum(f, PMS_FRAME - 2) != w16(f, PMS_FRAME - 2))
        return PMS_E_SUM;
    words(f, d);
    return PMS_OK;
}

Кадр модели при PM2.5 = 35 мкг/м³ — 42 4D 00 1C 00 17 00 23 00 2C 00 17 00 21 00 27 09 92 02 DF 00 C4 00 18 00 05 00 02 97 00 04 66: «CF = 1» — 23, 35 и 44, «атмосферные» — 23, 33 и 39, частиц — 2450, 735, 196, 24, 5 и 2; версия в модели — 0x97, выдуманная. Сумма 30 байт — 0x0466, ровно последнее слово. Сумма 28 байт без начала — 0x03D7, всех 32 байт — 0x04D0: ни одна не сходится. Один перевёрнутый бит (байт 11, бит 2) — и pms_decode возвращает −3, PMS_E_SUM.

Простая сумма — как у DFPlayer в 16г — слабее CRC из 104б. Перебор всех 32 640 порч ровно двух бит в этом кадре: сумма не замечает 986 — каждую 33-ю. Пропускает она пары, где один и тот же бит в двух байтах сменился в разные стороны: прибавка и убыль гасят друг друга.


103в.2 Подключение к F103: 5 В, логика 3,3 В, SET и RESET

Вывод датчика STM32F103 Назначение
VCC 5 В питание вентилятора и лазера
GND GND земля
TX PA10 USART1_RX, данные от датчика
RX PA9 USART1_TX, команды датчику
SET PB0 1 или не подключён — работа, 0 — сон
RESET PB1 0 — сброс; держать в единице

По документации производителя (не проверено): питание 5 В — его требует вентилятор, а уровни TX, RX, SET и RESET — 3,3 В; SET и RESET внутри подтянуты к питанию, их можно не подключать. Разъём на датчике мелкий — обычно берут переходную плату или готовый шлейф.

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

PA10 — FT (с. 31): если попадётся партия или переходник с 5-вольтовым TX, вывод F103 это выдержит. А PA3, приём USART2, — не FT (с. 29): там 5 В на входе опасны (16.7). Обратное направление безопасно: F103 даёт 3,3 В.

static void board_init(void)
{
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_USART1EN;
    GPIOB->BSRR = 1u << 0 | 1u << 1;            /* SET = 1 (работа), RESET = 1 (не сброс) */
    GPIOB->CRL = (GPIOB->CRL & ~0xFFu) | 0x22u; /* PB0, PB1 — выходы, 2 МГц */
    GPIOA->CRH = (GPIOA->CRH & ~0xFF0u)
               | 0xBu << 4                      /* PA9 — USART1_TX, альтернативная функция */
               | 0x4u << 8;                     /* PA10 — USART1_RX, вход без подтяжки */

    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;   /* без прерываний: опрос NDTR */

    USART1->BRR = CPU_HZ / BAUD;                /* 7500 = 0x1D4C: 468 + 12/16 */
    USART1->CR3 = USART_CR3_DMAR;
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
    SysTick_Config(CPU_HZ / 1000u);
}

Вверху — подключение PMS5003 к STM32F103: PA9 — TX к RX датчика, PA10 — RX от TX датчика на USART1 9600 бод, приём через DMA1, канал 5, по кругу; PB0 — SET, PB1 — RESET; питание датчика 5 В на вентилятор, логика выводов 3,3 В; вентилятор втягивает воздух и выдувает наружу. В середине — кадр данных 32 байта: 42 4D — начало, 00 1C — длина 28, три слова PM при CF = 1, три атмосферных, шесть счётов частиц больше 0,3, 0,5, 1, 2,5, 5 и 10 мкм в 0,1 л, слово 13 — версия и ошибка, сумма 0x0466 — сумма первых 30 байт; в модели при PM2,5 = 35 мкг/м³ CF = 1 — 23, 35, 44, атмосферные — 23, 33, 39. Внизу — цикл прошивки: раз в 5 минут проснуться, 30 с ждать вентилятор, 10 запросов, уснуть; вентилятор работает 13,02 % времени; показания модели после пробуждения — 57 % от истинного в первые 5 с, 76, 86, 93, 97 % и 100 % после 30 с


103в.3 Приём через DMA в кольцо и разбор кадров

Датчик шлёт кадры сам, когда захочет, а программа может быть занята. Приём — как в 16б: DMA1, канал 5 (таблица 78, с. 281), кладёт байты в кольцо 128 байт по кругу, а главный цикл раз в 10 мс отдаёт драйверу счётчик CNDTR. Прерываний приёма нет вовсе: 128 байт — это 133,3 мс потока, запаса с избытком. Кадр в кольце разбирается на месте (16б.4):

/* разбор кольца от rd до позиции записи DMA (размер − NDTR), затем таймеры */
void pms_poll(pms_t *p, uint16_t ndtr, uint32_t now)
{
    uint16_t wr = (uint16_t)(p->size - ndtr);
    uint16_t used, len;
    uint8_t f[PMS_FRAME];
    if (wr >= p->size)                                      /* NDTR = 0 в миг перезагрузки */
        wr = 0;
    used = (uint16_t)(wr >= p->rd ? wr - p->rd : wr + p->size - p->rd);
    if (used > p->hiwater)
        p->hiwater = used;
    while (used >= 4) {
        if (at(p, 0) != PMS_B1 || at(p, 1) != PMS_B2) {
            skip(p, 1);                                     /* ищем 42 4D */
            used--;
            p->n_junk++;
            continue;
        }
        len = (uint16_t)(at(p, 2) << 8 | at(p, 3));
        if (len != PMS_LEN_DATA && len != PMS_LEN_REPLY) {
            skip(p, 1);                                     /* 42 4D внутри данных: длина не та */
            used--;
            p->n_badlen++;
            continue;
        }
        if (used < 4u + len)
            break;                                          /* кадр пришёл не весь: rd стоит на его начале */
        for (uint16_t i = 0; i < 4u + len; i++)
            f[i] = at(p, i);
        if (p->check_sum && pms_sum(f, 2u + len) != w16(f, 2u + len)) {
            skip(p, 1);                                     /* ложное начало или порча: искать со следующего байта */
            used--;
            p->n_badsum++;
            continue;
        }
        skip(p, (uint16_t)(4u + len));
        used = (uint16_t)(used - 4u - len);
        if (len == PMS_LEN_REPLY) {
            p->n_reply++;
            ev(p, PMS_EV_REPLY, 0, now);
        } else {
            on_data(p, f, now);
        }
    }
    timers(p, now);
}

Порядок проверок — от дешёвой к дорогой. Не 42 4D — сдвиг на байт. Длина не 28 и не 4 — тоже сдвиг на байт: это 42 4D внутри данных. Кадр пришёл не весь — выход, rd остаётся на его начале. Сумма не сошлась — опять сдвиг на один байт, а не на весь кадр: настоящее начало может оказаться внутри отвергнутых 32 байт.

В модели 10 минут активного режима: кадров — 272, ни одного отвергнутого; наибольшая занятость кольца — 32 байта, ровно кадр. От конца кадра в линии до разбора — 34–43 мс: кадр идёт 33,33 мс, остальное — шаг опроса 10 мс.

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

Проверка суммы Принято Отвергнуто по сумме Отвергнуто по длине Неверных принято Потеряно Наибольший PM2.5, мкг/м³
есть 18 508 1295 98 0 1494 41
нет, как в Zephyr 19 553 0 98 1177 1626 32 801

С проверкой суммы ни одного неверного кадра не принято — испорченные просто пропали. Без неё в программу ушли 1177 неверных, и PM2.5 доходил до 32 801 мкг/м³ при настоящих 5…35.

Синхронизация. Начать приём посреди кадра — обычное дело: датчик включили раньше микроконтроллера. В модели приём начинался с каждого из 32 сдвигов внутри кадра, пять кадров подряд: теряется только первый, неполный. Хуже, когда 42 4D встречается внутри данных: счёт частиц больше 0,3 мкм, равный 16 973, — это и есть 0x424D. В модели подставлен кадр, где за ним ещё и 00 1C — длина 28: 42 4D 00 1C 00 14 00 1E 00 26 00 14 00 1E 00 23 42 4D 00 1C 00 A8 00 15 00 04 00 02 97 00 03 5D. С проверкой суммы ложное начало отвергается, и снова теряется только первый кадр. Без суммы разбор цепляется за ложное начало и уже не отпускает: на 16 сдвигах из 32 все пять кадров приняты неверными, 80 неверных кадров на весь опыт.


103в.4 Активный и пассивный режим: команды 0xE1, 0xE2, 0xE4

После включения датчик в активном режиме: шлёт кадры сам. По документации производителя (не проверено) интервал зависит от концентрации: при ровной — 2,3 с, при быстрой смене — 200…800 мс. В модели 10 минут: фон 12 мкг/м³, на второй минуте задымление до 81 мкг/м³ со спадом. Промежутков между кадрами — 271: 256 по 2,3 с и 15 быстрых, 200…790 мс; из быстрых 13 — после задымления, 2 — до него, от шума.

В пассивном режиме датчик молчит, пока его не спросят. Команда — 7 байт: 42 4D, команда, два байта данных и сумма пяти байт — тот же принцип, что у кадра данных:

/* кадр команды: 42 4D, команда, данные (старший, младший), сумма пяти байт до неё */
void pms_cmd_frame(uint8_t cmd, uint16_t data, uint8_t out[PMS_CMD_LEN])
{
    uint16_t s;
    out[0] = PMS_B1;
    out[1] = PMS_B2;
    out[2] = cmd;
    out[3] = (uint8_t)(data >> 8);
    out[4] = (uint8_t)data;
    s = pms_sum(out, 5);
    out[5] = (uint8_t)(s >> 8);
    out[6] = (uint8_t)s;
}
Что Кадр
0xE2 — прочитать кадр (пассивный режим) 42 4D E2 00 00 01 71
0xE1, 0 — пассивный режим 42 4D E1 00 00 01 70
0xE1, 1 — активный режим 42 4D E1 00 01 01 71
0xE4, 0 — сон 42 4D E4 00 00 01 73
0xE4, 1 — проснуться 42 4D E4 00 01 01 74

Коды команд — по документации производителя, не проверено; суммы посчитаны драйвером и сверены с независимым расчётом на Python. На смену режима и на сон в модели датчик отвечает коротким кадром длины 4, например 42 4D 00 04 E1 00 01 74 на 0xE1, 0 (допущение модели: отвечает ли так настоящий датчик и всякая ли партия — не проверено). Поэтому разбор и пропускает длину 4. Драйвер на этот ответ не полагается.

/* пассивный режим: запрос кадра; ответа нет за PMS_REPLY_MS — повтор, всего PMS_TRIES попыток */
void pms_request(pms_t *p, uint32_t now)
{
    p->pending = 1;
    p->tries = 0;
    p->n_req++;
    send_req(p, now);
}

В модели 60 запросов раз в секунду — ответили все 60, от запроса до разобранного кадра 70–90 мс: 7,3 мс команда, задержка датчика (в модели 20…40 мс), 33,33 мс кадр и шаг опроса.

Пассивный режим удобнее, когда кадр нужен в известный момент, и экономит разбор. Но у него две ловушки. Первая — датчик во сне не отвечает: в модели запрос при SET = 0 — 3 попытки по 300 мс, через 900 мс драйвер сообщает тайм-аут. Вторая — после сбоя питания или сброса датчик снова в активном режиме и шлёт кадры без спроса. Драйвер это видит — кадр пришёл, а запроса не было — и снова шлёт 0xE1, 0. В модели после сброса первый лишний кадр пришёл через 1540 мс, драйвер вернул пассивный режим, и больше кадров без спроса не было.


103в.5 Сон, прогрев и цикл «проснулся, подождал, измерил, уснул»

Вентилятор и лазер — то, что изнашивается: при работе круглые сутки вентилятор забивается пылью и стареет, а лазер теряет яркость. Сроки службы производитель даёт в часах работы (не проверено), поэтому датчик, который мерит раз в несколько минут, разумно усыплять. Усыпить можно выводом SET (0 — сон) или командой 0xE4, 0; разбудить — SET = 1 или 0xE4, 1. Вывод проще и не зависит от того, услышал ли датчик команду.

После пробуждения вентилятор раскручивается, и по документации производителя стабильные данные — не раньше чем через 30 с (не проверено). В модели первый кадр — через 1500 мс, а показания занижены множителем 1 − 0,6 · e^(−t / 8 с) — допущение модели. Предмет — воздух с PM2.5 = 35 мкг/м³, шум выключен, доля от истинного:

После пробуждения 0–5 с 5–10 с 10–15 с 15–20 с 20–25 с 25–30 с 30–40 с 40–60 с
показание, % 57,1 75,7 85,7 93,3 97,1 97,1 100,0 100,0

Отсюда цикл прошивки: раз в 5 минут проснуться, 30 с ждать, взять 10 кадров в пассивном режиме раз в секунду, усреднить, уснуть. Пока идёт прогрев, драйвер кадры не отдаёт:

/* цикл: проснуться, warm_ms ждать вентилятор, набрать navg кадров, среднее, уснуть до wake_at + period_ms;
 * period_ms = 0 — одно среднее без сна */
void pms_cycle(pms_t *p, uint32_t period_ms, uint32_t warm_ms, uint16_t navg, uint32_t now)
{
    p->period_ms = period_ms;
    p->warm_ms = warm_ms;
    p->navg = navg < 1u ? 1u : navg > PMS_NAVG ? PMS_NAVG : navg;
    pms_wake(p, now);
}
static void timers(pms_t *p, uint32_t now)
{
    if (p->pending && due(now, p->req_at + PMS_REPLY_MS)) {
        if (++p->tries < PMS_TRIES) {
            p->n_retry++;
            send_req(p, now);
        } else {
            p->pending = 0;
            p->n_tmo++;
            ev(p, PMS_EV_TIMEOUT, 0, now);
        }
    }
    if (p->state == PMS_SLEEP && p->period_ms && due(now, p->next_at)) {
        pms_wake(p, now);
    } else if (p->state == PMS_WARM && due(now, p->wake_at + p->warm_ms)) {
        p->state = p->navg ? PMS_MEASURE : PMS_RUN;
        p->nhave = 0;
        for (uint32_t i = 0; i < 12; i++)
            p->acc[i] = 0;
        p->next_at = now;
    }
    if (p->state == PMS_MEASURE && p->passive && !p->pending && due(now, p->next_at)) {
        pms_request(p, now);
        p->next_at = now + PMS_GAP_MS;
    }
}

В модели два часа с этим циклом: циклов — 24, вентилятор работает 13,02 % времени — около 1141 ч в год вместо 8760. Среднее до события на кухне — 97,5 % от истинного. Тот же цикл без ожидания: вентилятор работает 3,03 % времени, зато среднее — 68,8 % от истинного: измерено на ещё не раскрученном вентиляторе.


103в.6 Усреднение, «CF = 1» против «атмосферных» и индекс AQI

Усреднение. Шум одного кадра в модели — 5 % или 1,5 мкг/м³, что больше (допущение модели). Среднее «атмосферного» PM2.5 при 35 мкг/м³ «CF = 1», по 100 опытам на каждое число кадров:

Кадров в среднем 1 4 10 30
разброс среднего, мкг/м³ 1,083 0,575 0,418 0,207
время, с 1 4 10 30

Вчетверо больше кадров — почти вдвое меньше разброс. Десять кадров — разумная середина для цикла выше. Среднее за цикл драйвер считает в десятых долях без округления вверх — так велит EPA для AQI: сумма 356 за три кадра — 118,67, в индекс идёт 118,6.

«CF = 1» и «атмосферные». В кадре по три концентрации дважды. По документации производителя «CF = 1» — для заводских условий, а «атмосферные» — для обычного воздуха (не проверено). Как одни пересчитаны в другие, производитель не раскрывает. Частая картина у таких датчиков — до нескольких десятков мкг/м³ они равны, выше «атмосферные» меньше. В модели так и сделано: до 30 мкг/м³ равны, выше наклон 2/3 — допущение модели:

CF = 1, PM2.5 5 10 20 30 50 100 200 500
атмосферные, PM2.5 5 10 20 30 43 77 143 343

Для сравнения с индексом берут «атмосферные». Но ни те, ни другие не равны тому, что намерил бы эталонный прибор с фильтром и весами: датчик считает частицы светом и оценивает массу. Для серьёзных выводов датчик сравнивают с эталоном на месте и вводят свою поправку.

Влажность. Капли тумана и разбухшие от воды частицы для лазера — такие же частицы. Во влажном воздухе, в тумане и рядом с паром PMS5003 завышает показания, иногда в разы; в модели это не воспроизводится. Датчик влажности рядом хотя бы помечает такие показания.

AQI. Индекс качества воздуха США (EPA) по PM2.5 считается кусочно-линейно по таблице. Таблица ниже — редакция EPA 2024 года, для среднего за 24 часа; документа EPA в проекте нет, границы — по памяти о публикации, не сверены:

PM2.5, мкг/м³ AQI Категория
0,0–9,0 0–50 хорошо
9,1–35,4 51–100 умеренно
35,5–55,4 101–150 вредно для чувствительных групп
55,5–125,4 151–200 вредно
125,5–225,4 201–300 очень вредно
225,5–325,4 301–500 опасно

I = I_lo + (I_hi − I_lo) · (C − C_lo) / (C_hi − C_lo), к ближайшему целому. В драйвере — целыми, в десятых мкг/м³, без деления с плавающей точкой:

/* I = Ilo + (Ihi − Ilo) · (C − Clo) / (Chi − Clo), к ближайшему целому; выше таблицы — 500 */
uint16_t aqi_pm25(uint32_t c10, uint8_t *cat)
{
    for (uint32_t k = 0; k < AQI_N; k++) {
        const aqi_bp_t *b = &aqi_pm25_tab[k];
        if (c10 <= b->c_hi) {
            uint32_t num = (uint32_t)(b->i_hi - b->i_lo) * (c10 - b->c_lo) * 2u + (b->c_hi - b->c_lo);
            if (cat)
                *cat = (uint8_t)k;
            return (uint16_t)(b->i_lo + num / (2u * (b->c_hi - b->c_lo)));
        }
    }
    if (cat)
        *cat = AQI_N - 1u;
    return 500u;
}
PM2.5, мкг/м³ 5,0 9,1 12,0 20,0 35,5 55,5 100,0 150,0 300,0 400,0
AQI 28 51 56 71 101 151 182 225 449 500

Все 19 значений совпали с независимым расчётом на Python. Выше 325,4 функция возвращает 500 — «за пределами шкалы». Строго индекс считают по среднему за сутки; среднее за 10 кадров каждые 5 минут — оценка «сейчас», и сравнивать её с официальным индексом надо осторожно.

Кухня в модели: фон 8 мкг/м³, на 30-й минуте всплеск до 148 со спадом за четверть часа. Цикл раз в 5 минут, каждое второе среднее; AQI — по среднему за цикл, как в прошивке:

Минута PM2.5, мкг/м³ AQI Истинное «атмосферное» в модели
0,7 7,5 42 8,0
10,7 7,7 43 8,0
20,7 8,3 46 8,0
30,7 104,5 185 105,1
40,7 60,5 155 61,4
50,7 38,4 108 39,0
60,7 25,6 82 26,2
70,7 17,6 67 17,4
80,7 12,6 58 12,8
90,7 10,4 53 10,5
100,7 8,9 49 9,3
110,7 9,2 51 8,7

103в.7 Грабли

Пять вольт на выводы. Питание датчика — 5 В, а логика — 3,3 В (по документации производителя). F103 даёт 3,3 В, это датчику подходит. Опасно другое — 5-вольтовый TX переходника или чужой платы на вход без FT: у F103 PA10 выдержит (с. 31), PA3 — нет (с. 29). А датчик, чей RX кормят от 5-вольтовой платы, — через делитель.

Уровни у разных партий. Встречаются платы-переходники и партии, где уровни не те, что ждёшь. Перед подключением — вольтметр на TX датчика в покое.

Вентилятор вверх ногами или вход закрыт. Датчик мерит то, что прогнал вентилятор. Если впуск и выпуск закрыты корпусом, прижаты к стенке или смотрят друг на друга, датчик гоняет один и тот же воздух. По документации производителя у датчика есть разрешённые положения и нужен зазор у впуска (не проверено); корпус проектируют под поток.

Сразу после пробуждения. Первые секунды — заниженные числа: 57,1 % от истинного в первые 5 с в модели. Ждать 30 с.

Пассивный режим молчит. Запрос во сне, перепутаны TX и RX, датчик перезагрузился и опять активен. Драйвер ограничивает ожидание (три попытки по 300 мс) и сам возвращает пассивный режим, если кадры пошли без спроса.

Сумма по 30 байтам. Сумма — от 42 до слова 13, 30 байт. По 28 байтам без начала (0x03D7) или по всем 32 (0x04D0) не сойдётся ни один кадр.

Без суммы. Драйвер, не проверяющий сумму (так сделан pms7003 в Zephyr 4.4.2), примет и битый кадр, и ложное начало: 1177 неверных из 20 002 в опыте 103в.3 и бесконечный сбой синхронизации на ложном 42 4D 00 1C.


103в.8 Итоги

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

pms103d.h — кадр, команды, сроки, состояния и функции драйвера PMS5003.

/* pms103d.h — раздел 103в: драйвер датчика пыли PMS5003 (Plantower) по UART без регистров микроконтроллера.
 * Приём — кольцо, в которое пишет DMA (раздел 16б): драйвер получает только счётчик NDTR и разбирает кадры от
 * rd до позиции записи. Передача, вывод SET и время — функции порта (pms_port_*): в прошивке fw103d.c их делают
 * USART1, PB0 и SysTick STM32F103, в стенде host103d.c — модель model103d.py.
 * Кадр данных 42 4D 00 1C, 13 слов старшим байтом вперёд, сумма — 16 бит, сумма 30 байт до неё. Формат совпадает
 * с образцом кадра в драйвере pms7003 Zephyr 4.4.2; команды 0xE1/0xE2/0xE4, интервалы вывода, 30 с после
 * пробуждения и выводы SET/RESET — по документации производителя, с датчиком не сверено. */
#ifndef PMS103D_H
#define PMS103D_H
#include <stdint.h>

#define PMS_B1          0x42u           /* 'B' */
#define PMS_B2          0x4Du           /* 'M' */
#define PMS_LEN_DATA    28u             /* 13 слов + сумма */
#define PMS_LEN_REPLY   4u              /* ответ на команду (так в модели) */
#define PMS_FRAME       32u             /* 4 + 28 */
#define PMS_CMD_LEN     7u              /* 42 4D CMD DATAH DATAL LRCH LRCL */
#define PMS_CMD_READ    0xE2u           /* пассивный режим: прочитать кадр */
#define PMS_CMD_MODE    0xE1u           /* 0 — пассивный, 1 — активный */
#define PMS_CMD_SLEEP   0xE4u           /* 0 — сон, 1 — проснуться */

#define PMS_WARM_MS     30000u          /* после пробуждения: данные стабильны не раньше (по документации) */
#define PMS_REPLY_MS    300u            /* пассивный режим: ждать кадр после 0xE2 */
#define PMS_TRIES       3u              /* попыток одного запроса */
#define PMS_GAP_MS      1000u           /* пассивный режим: запрос раз в секунду */
#define PMS_REMODE_MS   1000u           /* кадр без запроса — снова 0xE1 0, не чаще */
#define PMS_NAVG        30u             /* наибольшее число кадров в среднем */

enum { PMS_OK = 0, PMS_E_HEAD = -1, PMS_E_LEN = -2, PMS_E_SUM = -3 };
enum { PMS_RUN = 0, PMS_SLEEP = 1, PMS_WARM = 2, PMS_MEASURE = 3 };
enum { PMS_EV_DATA = 0, PMS_EV_AVG = 1, PMS_EV_REPLY = 2, PMS_EV_TIMEOUT = 3, PMS_EV_REMODE = 4 };

typedef struct {
    uint16_t cf[3];                     /* PM1.0, PM2.5, PM10 при CF = 1, мкг/м³ */
    uint16_t at[3];                     /* то же, «атмосферные», мкг/м³ */
    uint16_t cnt[6];                    /* частиц > 0,3 / 0,5 / 1 / 2,5 / 5 / 10 мкм в 0,1 л */
    uint8_t ver, err;                   /* слово 13: у образца Zephyr 0x71 и 0x00 */
} pms_data_t;

typedef struct pms pms_t;
typedef void (*pms_cb)(pms_t *p, uint8_t ev, const pms_data_t *d, uint32_t now);

struct pms {
    const volatile uint8_t *ring;       /* кольцо DMA */
    uint16_t size, rd, hiwater;
    uint8_t check_sum;                  /* 1; 0 — как драйвер pms7003 в Zephyr: сумму не смотреть (для опыта) */
    uint8_t passive, set_pin;           /* пассивный режим; спать выводом SET (1) или командой 0xE4 (0) */
    uint8_t state, pending, tries;
    uint32_t req_at, wake_at, next_at, remode_at, period_ms, warm_ms;
    uint16_t navg, nhave;
    uint32_t acc[12];                   /* суммы шести концентраций и шести счётов за цикл */
    pms_data_t last, avg;
    pms_cb cb;
    uint32_t n_good, n_badsum, n_badlen, n_junk, n_reply, n_req, n_retry, n_tmo, n_unsol, n_warm, n_cycles;
};

/* ── порт: в прошивке — USART1, PB0 и SysTick; в стенде — строки к модели ── */
void pms_port_tx(const uint8_t *p, uint16_t n);
void pms_port_set(uint8_t level);       /* вывод SET: 1 — работа, 0 — сон */

/* ── драйвер ──────────────────────────────────────────────────────────── */
uint16_t pms_sum(const uint8_t *p, uint32_t n);
void pms_cmd_frame(uint8_t cmd, uint16_t data, uint8_t out[PMS_CMD_LEN]);
int pms_decode(const uint8_t *f, pms_data_t *d);
void pms_init(pms_t *p, const volatile uint8_t *ring, uint16_t size, pms_cb cb);
void pms_poll(pms_t *p, uint16_t ndtr, uint32_t now);
void pms_set_mode(pms_t *p, uint8_t passive, uint32_t now);
void pms_request(pms_t *p, uint32_t now);
void pms_sleep(pms_t *p, uint32_t now);
void pms_wake(pms_t *p, uint32_t now);
void pms_cycle(pms_t *p, uint32_t period_ms, uint32_t warm_ms, uint16_t navg, uint32_t now);
uint32_t pms_next_ms(const pms_t *p, uint32_t now);

#endif

pms103d.c — драйвер: сумма, кадр команды, разбор кольца, режимы, сон, цикл и среднее.

/* pms103d.c — раздел 103в: драйвер PMS5003. Разбор кадров прямо из кольца DMA, команды с суммой, пассивный
 * режим с повтором запроса, цикл «проснулся, прогрелся, измерил, уснул» и среднее за цикл. Регистров
 * микроконтроллера здесь нет: тот же файл собирается в прошивку и в стенд на компьютере. */
#include "pms103d.h"

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

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

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

static uint16_t w16(const uint8_t *f, uint32_t i)           /* слово кадра: старший байт первым */
{
    return (uint16_t)(f[i] << 8 | f[i + 1]);
}

/* сумма кадра — 16 бит, простая сумма байтов */
uint16_t pms_sum(const uint8_t *p, uint32_t n)
{
    uint32_t s = 0;
    while (n--)
        s += *p++;
    return (uint16_t)s;
}

/* кадр команды: 42 4D, команда, данные (старший, младший), сумма пяти байт до неё */
void pms_cmd_frame(uint8_t cmd, uint16_t data, uint8_t out[PMS_CMD_LEN])
{
    uint16_t s;
    out[0] = PMS_B1;
    out[1] = PMS_B2;
    out[2] = cmd;
    out[3] = (uint8_t)(data >> 8);
    out[4] = (uint8_t)data;
    s = pms_sum(out, 5);
    out[5] = (uint8_t)(s >> 8);
    out[6] = (uint8_t)s;
}

static void words(const uint8_t *f, pms_data_t *d)
{
    for (uint32_t i = 0; i < 3; i++) {
        d->cf[i] = w16(f, 4 + 2 * i);                       /* слова 1–3: CF = 1 */
        d->at[i] = w16(f, 10 + 2 * i);                      /* слова 4–6: атмосферные */
    }
    for (uint32_t i = 0; i < 6; i++)
        d->cnt[i] = w16(f, 16 + 2 * i);                     /* слова 7–12: счёт частиц в 0,1 л */
    d->ver = f[28];
    d->err = f[29];
}

/* кадр данных целиком: начало, длина 28, сумма 30 байт = последнее слово */
int pms_decode(const uint8_t *f, pms_data_t *d)
{
    if (f[0] != PMS_B1 || f[1] != PMS_B2)
        return PMS_E_HEAD;
    if (w16(f, 2) != PMS_LEN_DATA)
        return PMS_E_LEN;
    if (pms_sum(f, PMS_FRAME - 2) != w16(f, PMS_FRAME - 2))
        return PMS_E_SUM;
    words(f, d);
    return PMS_OK;
}

void pms_init(pms_t *p, const volatile uint8_t *ring, uint16_t size, pms_cb cb)
{
    *p = (pms_t){0};
    p->ring = ring;
    p->size = size;
    p->cb = cb;
    p->check_sum = 1;
    p->set_pin = 1;
    p->state = PMS_RUN;
}

static void ev(pms_t *p, uint8_t e, const pms_data_t *d, uint32_t now)
{
    if (p->cb)
        p->cb(p, e, d, now);
}

static void tx_cmd(uint8_t cmd, uint16_t data)
{
    uint8_t f[PMS_CMD_LEN];
    pms_cmd_frame(cmd, data, f);
    pms_port_tx(f, PMS_CMD_LEN);
}

static void send_req(pms_t *p, uint32_t now)
{
    tx_cmd(PMS_CMD_READ, 0);
    p->req_at = now;
}

/* пассивный режим: запрос кадра; ответа нет за PMS_REPLY_MS — повтор, всего PMS_TRIES попыток */
void pms_request(pms_t *p, uint32_t now)
{
    p->pending = 1;
    p->tries = 0;
    p->n_req++;
    send_req(p, now);
}

void pms_set_mode(pms_t *p, uint8_t passive, uint32_t now)
{
    p->passive = passive;
    p->pending = 0;
    p->remode_at = now + PMS_REMODE_MS;                     /* кадры, уже бывшие в пути, — не повод повторять */
    tx_cmd(PMS_CMD_MODE, passive ? 0u : 1u);
}

void pms_sleep(pms_t *p, uint32_t now)
{
    (void)now;
    if (p->set_pin)
        pms_port_set(0);
    else
        tx_cmd(PMS_CMD_SLEEP, 0);
    p->state = PMS_SLEEP;
    p->pending = 0;
}

void pms_wake(pms_t *p, uint32_t now)
{
    if (p->set_pin)
        pms_port_set(1);
    else
        tx_cmd(PMS_CMD_SLEEP, 1);
    p->state = PMS_WARM;
    p->wake_at = now;
}

/* цикл: проснуться, warm_ms ждать вентилятор, набрать navg кадров, среднее, уснуть до wake_at + period_ms;
 * period_ms = 0 — одно среднее без сна */
void pms_cycle(pms_t *p, uint32_t period_ms, uint32_t warm_ms, uint16_t navg, uint32_t now)
{
    p->period_ms = period_ms;
    p->warm_ms = warm_ms;
    p->navg = navg < 1u ? 1u : navg > PMS_NAVG ? PMS_NAVG : navg;
    pms_wake(p, now);
}

static void finish(pms_t *p, uint32_t now)
{
    uint32_t n = p->nhave;
    for (uint32_t i = 0; i < 3; i++) {
        p->avg.cf[i] = (uint16_t)((p->acc[i] + n / 2u) / n);
        p->avg.at[i] = (uint16_t)((p->acc[3 + i] + n / 2u) / n);
    }
    for (uint32_t i = 0; i < 6; i++)
        p->avg.cnt[i] = (uint16_t)((p->acc[6 + i] + n / 2u) / n);
    p->avg.ver = p->last.ver;
    p->avg.err = p->last.err;
    p->n_cycles++;
    ev(p, PMS_EV_AVG, &p->avg, now);
    if (p->period_ms) {
        pms_sleep(p, now);
        p->next_at = p->wake_at + p->period_ms;
    } else {
        p->state = PMS_RUN;                                 /* период 0: одно среднее, без сна */
    }
}

static void on_data(pms_t *p, const uint8_t *f, uint32_t now)
{
    pms_data_t d;
    words(f, &d);
    p->last = d;
    p->n_good++;
    if (p->passive) {
        if (p->pending) {
            p->pending = 0;
        } else {
            p->n_unsol++;                                   /* кадр без запроса: датчик снова в активном режиме */
            if (due(now, p->remode_at)) {
                ev(p, PMS_EV_REMODE, &d, now);
                pms_set_mode(p, 1, now);
            }
        }
    }
    if (p->state == PMS_WARM || p->state == PMS_SLEEP) {
        p->n_warm++;                                        /* вентилятор не раскрутился: не верить */
        return;
    }
    ev(p, PMS_EV_DATA, &d, now);
    if (p->state != PMS_MEASURE)
        return;
    for (uint32_t i = 0; i < 3; i++) {
        p->acc[i] += d.cf[i];
        p->acc[3 + i] += d.at[i];
    }
    for (uint32_t i = 0; i < 6; i++)
        p->acc[6 + i] += d.cnt[i];
    if (++p->nhave >= p->navg)
        finish(p, now);
}

static void timers(pms_t *p, uint32_t now)
{
    if (p->pending && due(now, p->req_at + PMS_REPLY_MS)) {
        if (++p->tries < PMS_TRIES) {
            p->n_retry++;
            send_req(p, now);
        } else {
            p->pending = 0;
            p->n_tmo++;
            ev(p, PMS_EV_TIMEOUT, 0, now);
        }
    }
    if (p->state == PMS_SLEEP && p->period_ms && due(now, p->next_at)) {
        pms_wake(p, now);
    } else if (p->state == PMS_WARM && due(now, p->wake_at + p->warm_ms)) {
        p->state = p->navg ? PMS_MEASURE : PMS_RUN;
        p->nhave = 0;
        for (uint32_t i = 0; i < 12; i++)
            p->acc[i] = 0;
        p->next_at = now;
    }
    if (p->state == PMS_MEASURE && p->passive && !p->pending && due(now, p->next_at)) {
        pms_request(p, now);
        p->next_at = now + PMS_GAP_MS;
    }
}

/* разбор кольца от rd до позиции записи DMA (размер − NDTR), затем таймеры */
void pms_poll(pms_t *p, uint16_t ndtr, uint32_t now)
{
    uint16_t wr = (uint16_t)(p->size - ndtr);
    uint16_t used, len;
    uint8_t f[PMS_FRAME];
    if (wr >= p->size)                                      /* NDTR = 0 в миг перезагрузки */
        wr = 0;
    used = (uint16_t)(wr >= p->rd ? wr - p->rd : wr + p->size - p->rd);
    if (used > p->hiwater)
        p->hiwater = used;
    while (used >= 4) {
        if (at(p, 0) != PMS_B1 || at(p, 1) != PMS_B2) {
            skip(p, 1);                                     /* ищем 42 4D */
            used--;
            p->n_junk++;
            continue;
        }
        len = (uint16_t)(at(p, 2) << 8 | at(p, 3));
        if (len != PMS_LEN_DATA && len != PMS_LEN_REPLY) {
            skip(p, 1);                                     /* 42 4D внутри данных: длина не та */
            used--;
            p->n_badlen++;
            continue;
        }
        if (used < 4u + len)
            break;                                          /* кадр пришёл не весь: rd стоит на его начале */
        for (uint16_t i = 0; i < 4u + len; i++)
            f[i] = at(p, i);
        if (p->check_sum && pms_sum(f, 2u + len) != w16(f, 2u + len)) {
            skip(p, 1);                                     /* ложное начало или порча: искать со следующего байта */
            used--;
            p->n_badsum++;
            continue;
        }
        skip(p, (uint16_t)(4u + len));
        used = (uint16_t)(used - 4u - len);
        if (len == PMS_LEN_REPLY) {
            p->n_reply++;
            ev(p, PMS_EV_REPLY, 0, now);
        } else {
            on_data(p, f, now);
        }
    }
    timers(p, now);
}

/* ближайший срок, к которому драйверу нужен вызов pms_poll (без учёта приходящих байтов) */
uint32_t pms_next_ms(const pms_t *p, uint32_t now)
{
    uint32_t best = now + 0x7FFFFFFFu;
    uint32_t t[3];
    uint32_t n = 0;
    if (p->pending)
        t[n++] = p->req_at + PMS_REPLY_MS;
    if (p->state == PMS_SLEEP && p->period_ms)
        t[n++] = p->next_at;
    else if (p->state == PMS_WARM)
        t[n++] = p->wake_at + p->warm_ms;
    else if (p->state == PMS_MEASURE && p->passive && !p->pending)
        t[n++] = p->next_at;
    for (uint32_t i = 0; i < n; i++)
        if ((int32_t)(t[i] - best) < 0)
            best = t[i];
    return best;
}

aqi103d.h — таблица AQI и функции.

/* aqi103d.h — раздел 103в: индекс качества воздуха AQI по PM2.5. Таблица — EPA (США), редакция 2024 года, для
 * среднего за 24 часа; в проекте её документа нет, числа таблицы — по памяти о публикации EPA, не сверены.
 * Концентрация — в десятых мкг/м³, отброшенных до 0,1 (так велит EPA); индекс — кусочно-линейно, к целому. */
#ifndef AQI103D_H
#define AQI103D_H
#include <stdint.h>

#define AQI_N 6u

typedef struct {
    uint16_t c_lo, c_hi;                /* концентрация, десятые мкг/м³ */
    uint16_t i_lo, i_hi;                /* индекс */
} aqi_bp_t;

extern const aqi_bp_t aqi_pm25_tab[AQI_N];

uint32_t aqi_c10(uint32_t sum, uint32_t n);          /* среднее n значений в мкг/м³ → десятые, с отбрасыванием */
uint16_t aqi_pm25(uint32_t c10, uint8_t *cat);       /* индекс 0…500; cat — номер строки таблицы 0…5 */

#endif

aqi103d.c — AQI по PM2.5 целыми.

/* aqi103d.c — раздел 103в: AQI по PM2.5, целыми, без деления с плавающей точкой (у F103 нет FPU). */
#include "aqi103d.h"

/* EPA, PM2.5 за 24 ч, редакция 2024 года (по памяти, не сверено): хорошо, умеренно, вредно для чувствительных,
 * вредно, очень вредно, опасно */
const aqi_bp_t aqi_pm25_tab[AQI_N] = {
    {0, 90, 0, 50},       {91, 354, 51, 100},     {355, 554, 101, 150},
    {555, 1254, 151, 200}, {1255, 2254, 201, 300}, {2255, 3254, 301, 500},
};

uint32_t aqi_c10(uint32_t sum, uint32_t n)
{
    return n ? sum * 10u / n : 0u;
}

/* I = Ilo + (Ihi − Ilo) · (C − Clo) / (Chi − Clo), к ближайшему целому; выше таблицы — 500 */
uint16_t aqi_pm25(uint32_t c10, uint8_t *cat)
{
    for (uint32_t k = 0; k < AQI_N; k++) {
        const aqi_bp_t *b = &aqi_pm25_tab[k];
        if (c10 <= b->c_hi) {
            uint32_t num = (uint32_t)(b->i_hi - b->i_lo) * (c10 - b->c_lo) * 2u + (b->c_hi - b->c_lo);
            if (cat)
                *cat = (uint8_t)k;
            return (uint16_t)(b->i_lo + num / (2u * (b->c_hi - b->c_lo)));
        }
    }
    if (cat)
        *cat = AQI_N - 1u;
    return 500u;
}

fw103d.c — прошивка F103: USART1, DMA1 канал 5 по кругу, SET на PB0, RESET на PB1, цикл раз в 5 минут.

/* fw103d.c — раздел 103в: датчик пыли PMS5003 на STM32F103 (BluePill), 72 МГц.
 * USART1, 9600 бод: PA9 — TX к RX датчика, PA10 — RX от TX датчика (PA10 — FT). Приём — DMA1, канал 5, по кругу в
 * кольцо 128 байт (раздел 16б); прерываний приёма нет: главный цикл раз в 10 мс отдаёт драйверу NDTR.
 * PB0 — SET датчика (1 — работа, 0 — сон), PB1 — RESET (держим в единице). Цикл: раз в 5 минут проснуться,
 * 30 с ждать вентилятор, 10 запросов в пассивном режиме раз в секунду, среднее, AQI по PM2.5, уснуть.
 * Результат — в переменных для отладчика. Тактирование до 72 МГц — как в разделе 6; USART1 на APB2 = 72 МГц. */
#include "stm32f10x.h"
#include "pms103d.h"
#include "aqi103d.h"

#define CPU_HZ      72000000u
#define BAUD        9600u
#define RX_SIZE     128u                /* 133 мс потока на 9600 бод: опрос раз в 10 мс с большим запасом */
#define POLL_MS     10u
#define PERIOD_MS   300000u
#define NAVG        10u

_Static_assert(CPU_HZ / BAUD == 7500u && CPU_HZ % BAUD == 0u, "USART1: BRR = 7500 = 0x1D4C — 9600 бод");

static pms_t D;
static uint8_t rx_ring[RX_SIZE];
static volatile uint32_t ms;
volatile uint32_t pm25_c10, frames, timeouts;  /* PM2.5 в десятых мкг/м³ — среднее за цикл */
volatile uint16_t aqi, pm_cf[3], pm_at[3];
volatile uint8_t aqi_cat;

void SysTick_Handler(void)
{
    ms++;
}

/* ── порт драйвера ────────────────────────────────────────────────────── */
void pms_port_tx(const uint8_t *p, uint16_t n)
{
    for (uint16_t i = 0; i < n; i++) {           /* 7 байт — 7,3 мс; кольцо приёма за это время не переполнится */
        while (!(USART1->SR & USART_SR_TXE)) {
        }
        USART1->DR = p[i];
    }
}

void pms_port_set(uint8_t level)
{
    GPIOB->BSRR = level ? 1u << 0 : 1u << 16;   /* PB0: SET */
}

static void on_ev(pms_t *p, uint8_t ev, const pms_data_t *d, uint32_t now)
{
    (void)now;
    if (ev == PMS_EV_DATA) {
        frames++;
    } else if (ev == PMS_EV_TIMEOUT) {
        timeouts++;
    } else if (ev == PMS_EV_AVG) {
        uint8_t cat;
        for (uint32_t i = 0; i < 3; i++) {
            pm_cf[i] = d->cf[i];
            pm_at[i] = d->at[i];
        }
        pm25_c10 = aqi_c10(p->acc[4], p->nhave);  /* сумма атмосферных PM2.5 за цикл → десятые, с отбрасыванием */
        aqi = aqi_pm25(pm25_c10, &cat);
        aqi_cat = cat;
    }
}

static void board_init(void)
{
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_USART1EN;
    GPIOB->BSRR = 1u << 0 | 1u << 1;            /* SET = 1 (работа), RESET = 1 (не сброс) */
    GPIOB->CRL = (GPIOB->CRL & ~0xFFu) | 0x22u; /* PB0, PB1 — выходы, 2 МГц */
    GPIOA->CRH = (GPIOA->CRH & ~0xFF0u)
               | 0xBu << 4                      /* PA9 — USART1_TX, альтернативная функция */
               | 0x4u << 8;                     /* PA10 — USART1_RX, вход без подтяжки */

    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;   /* без прерываний: опрос NDTR */

    USART1->BRR = CPU_HZ / BAUD;                /* 7500 = 0x1D4C: 468 + 12/16 */
    USART1->CR3 = USART_CR3_DMAR;
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
    SysTick_Config(CPU_HZ / 1000u);
}

int main(void)
{
    uint32_t last = 0;
    board_init();
    pms_init(&D, rx_ring, RX_SIZE, on_ev);
    pms_set_mode(&D, 1, ms);                    /* после включения датчик в активном режиме */
    pms_cycle(&D, PERIOD_MS, PMS_WARM_MS, NAVG, ms);
    for (;;) {
        uint32_t now = ms;
        if (now - last >= POLL_MS) {
            last = now;
            pms_poll(&D, (uint16_t)DMA1_Channel5->CNDTR, now);
        }
        __WFI();                                /* до следующего тика SysTick */
    }
}

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

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