Embedded HandbookApps
All sections › XI. Sensors: connection, circuits, drivers
STM32F103 · level intermediate · 35 min read
Open in the interactive handbook

103в. PMS5003 dust sensor over UART: 32-byte frame, PM1.0/PM2.5/PM10, active and passive mode, sleep and the air quality index

In section 103 the MQ measured gas with a heated layer, in 103б the TCS34725 measured colour. The PMS5003 measures dust — how many fine particles are in the air. A fan drives air through a chamber, a laser shines across it, a photodiode catches flashes of scattered light, and the sensor's own microcontroller counts the particles and converts them into micrograms per cubic metre. What comes out is a 32-byte frame over UART. Here — how the frame and its checksum are built, how to connect the sensor to an F103, how to receive frames through DMA into a ring without losing the frame start, how active mode differs from passive, why the sensor is put to sleep and why its readings cannot be trusted for 30 seconds after waking, what "CF = 1" and "atmospheric" values are, and how to get the air quality index from PM2.5.

Sources: RM0008 (DocID13902 Rev 17): DMA1 requests — USART1_RX on channel 5, table 78, p. 281; 9600 baud from 72 MHz — 468.75, error 0%, table 192, p. 800. DocID13587 Rev 17 (STM32F103x8/xB): PA9 — USART1_TX, PA10 — USART1_RX, both FT, p. 31; PA2 and PA3 — not FT, p. 29. Reception through DMA into a ring is section 16б, a frame with a checksum over UART is 16г; they are not repeated here. There is no PMS5003 (Plantower) document in the project. The frame format — 42 4D, length 28, 13 words, a sum of 30 bytes — was checked against the sample frame in the pms7003 driver of Zephyr 4.4.2 on disk. Commands 0xE1, 0xE2 and 0xE4, output intervals, 30 s after waking, the supply and the SET and RESET pins are per the manufacturer's documentation, not verified. All numbers are a run of the driver _verify/pm103d/pms103d.c on a PC (gcc 12.2.0) with our own model of the PMS5003 in Python 3.11; the firmware was built with armclang 6.24 for Cortex-M3. Concentrations, noise, warm-up and the link between "CF = 1" and "atmospheric" values in the model are assumptions: the numbers below describe the model, not the sensor.

What it is called in the documentation

Original Meaning
particulate matter (PM2.5, PM10) suspended particles smaller than 2.5 and 10 µm
laser scattering light scattered by particles from a laser beam
mass concentration mass concentration, µg/m³
particle count per 0.1 L number of particles in 0.1 L of air
CF = 1, standard particle "CF = 1", the factory calibration
atmospheric environment "atmospheric" values
active / passive mode active mode (sends by itself) / passive (on request)
sleep / wakeup sleep / waking up
check code checksum
Air Quality Index (AQI) air quality index

103в.1 Principle and the 32-byte frame

Inside the PMS5003 there is a small fan, a laser diode and a photodiode. Air passes through a chamber, a particle crosses the beam and scatters light; the larger the particle, the brighter the flash. The sensor counts flashes by brightness, sorts particles by size and estimates the mass from their number. It does not weigh the mass — this is a conversion with assumptions about particle density and shape, hence the different calibrations below. Gas cannot be measured this way: gases are the business of the MQ in 103.5.

The data frame is 32 bytes. The format was checked against the Zephyr 4.4.2 pms7003 driver: its comment holds a sample frame, and it parses the frame in the same order; for the PMS5003 the format is the same per the manufacturer's documentation (not verified):

Bytes What
0, 1 42 4D — start, "BM"
2, 3 length: 28 = 2 × 13 + 2 — thirteen words and the sum
4…9 PM1.0, PM2.5, PM10 at "CF = 1", µg/m³
10…15 PM1.0, PM2.5, PM10, "atmospheric", µg/m³
16…27 particles larger than 0.3; 0.5; 1; 2.5; 5 and 10 µm in 0.1 L
28, 29 word 13: in the Zephyr sample — 0x71 and 0x00 (version and error code per the manufacturer's documentation)
30, 31 sum

All words go high byte first. The sum is 16 bits, a plain sum of the first 30 bytes, from 42 up to and including word 13. In the Zephyr sample 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: the sum of 30 bytes is 0x0206, and the driver parses it with code 0: all six concentrations are 1 µg/m³, particles — 402, 78, 3, 0, 0 and 0. The Zephyr driver itself does not check the sum at all (what that leads to — in 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;
}

The model's frame at PM2.5 = 35 µg/m³ is 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 and 44, "atmospheric" — 23, 33 and 39, particles — 2450, 735, 196, 24, 5 and 2; the version in the model is 0x97, made up. The sum of 30 bytes is 0x0466, exactly the last word. The sum of 28 bytes without the start is 0x03D7, of all 32 bytes 0x04D0: neither matches. One flipped bit (byte 11, bit 2) — and pms_decode returns −3, PMS_E_SUM.

A plain sum — like the DFPlayer's in 16г — is weaker than the CRC from 104б. Trying all 32 640 corruptions of exactly two bits in this frame: the sum misses 986 — one in every 33. It misses pairs where the same bit changed in two bytes in opposite directions: the gain and the loss cancel each other.


103в.2 Connecting to the F103: 5 V, 3.3 V logic, SET and RESET

Sensor pin STM32F103 Purpose
VCC 5 V supply of the fan and the laser
GND GND ground
TX PA10 USART1_RX, data from the sensor
RX PA9 USART1_TX, commands to the sensor
SET PB0 1 or unconnected — running, 0 — sleep
RESET PB1 0 — reset; keep it high

Per the manufacturer's documentation (not verified): the 5 V supply is what the fan needs, while TX, RX, SET and RESET levels are 3.3 V; SET and RESET are pulled up inside, they may be left unconnected. The connector on the sensor is tiny — people usually take an adapter board or a ready-made cable.

UART — 9600 baud, 8N1. USART1 sits on APB2 = 72 MHz, the divider is 72 000 000 / 9600 = 7500 = 0x1D4C, that is 468 + 12/16 = 468.75: table 192 of RM0008 (p. 800) has exactly this, error 0%. A byte takes 1041.7 µs, a data frame 33.33 ms, a command 7.29 ms; per second — no more than 960 bytes.

PA10 is FT (p. 31): if you run into a batch or an adapter with a 5-volt TX, the F103 pin will withstand it. But PA3, the USART2 receive pin, is not FT (p. 29): 5 V on that input is dangerous (16.7). The other direction is safe: the F103 outputs 3.3 V.

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);
}

Top — connecting the PMS5003 to the STM32F103: PA9 — TX to the sensor's RX, PA10 — RX from the sensor's TX on USART1 at 9600 baud, reception through DMA1, channel 5, circular; PB0 — SET, PB1 — RESET; the sensor is powered from 5 V for the fan, the pin logic is 3.3 V; the fan draws air in and blows it out. Middle — the 32-byte data frame: 42 4D — start, 00 1C — length 28, three PM words at CF = 1, three atmospheric, six particle counts larger than 0.3, 0.5, 1, 2.5, 5 and 10 µm in 0.1 L, word 13 — version and error, sum 0x0466 — the sum of the first 30 bytes; in the model at PM2.5 = 35 µg/m³ CF = 1 — 23, 35, 44, atmospheric — 23, 33, 39. Bottom — the firmware cycle: every 5 minutes wake up, wait 30 s for the fan, 10 requests, go to sleep; the fan runs 13.02% of the time; the model's readings after waking — 57% of the true value in the first 5 s, 76, 86, 93, 97% and 100% after 30 s


103в.3 Reception through DMA into a ring and frame parsing

The sensor sends frames on its own whenever it likes, and the program may be busy. Reception is as in 16б: DMA1, channel 5 (table 78, p. 281), puts bytes into a 128-byte ring, circularly, and the main loop hands the driver the CNDTR counter every 10 ms. There are no receive interrupts at all: 128 bytes is 133.3 ms of the stream, ample margin. The frame is parsed in place in the ring (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);
}

The checks go from cheap to expensive. Not 42 4D — shift by a byte. Length neither 28 nor 4 — also shift by a byte: this is a 42 4D inside data. The frame has not fully arrived — return, rd stays at its start. The sum does not match — again shift by one byte, not by the whole frame: the real start may lie inside the rejected 32 bytes.

In the model, 10 minutes of active mode: frames — 272, not one rejected; the largest ring occupancy is 32 bytes, exactly a frame. From the end of a frame on the line to its parsing — 34–43 ms: the frame takes 33.33 ms, the rest is the 10 ms polling step.

Damaged frames. An experiment on a stream of 20 002 frames back to back, without gaps: in each byte, with probability 0.2%, a bit is flipped and with probability 0.05% the byte is lost (so in the model; a real line has fewer errors):

Checksum check Accepted Rejected by sum Rejected by length Wrong accepted Lost Largest PM2.5, µg/m³
yes 18 508 1295 98 0 1494 41
no, as in Zephyr 19 553 0 98 1177 1626 32 801

With the checksum check not one wrong frame was accepted — the damaged ones simply disappeared. Without it 1177 wrong frames reached the program, and PM2.5 went up to 32 801 µg/m³ with the true value at 5…35.

Synchronisation. Starting reception in the middle of a frame is normal: the sensor was switched on before the microcontroller. In the model reception started at each of 32 offsets inside a frame, five frames in a row: only the first, incomplete one is lost. Worse is when 42 4D occurs inside data: a count of particles larger than 0.3 µm equal to 16 973 is exactly 0x424D. The model substituted a frame where it is followed by 00 1C as well — length 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. With the checksum check the false start is rejected, and again only the first frame is lost. Without the sum the parser latches onto the false start and never lets go: at 16 offsets out of 32 all five frames were accepted wrong, 80 wrong frames over the whole experiment.


103в.4 Active and passive mode: commands 0xE1, 0xE2, 0xE4

After power-up the sensor is in active mode: it sends frames by itself. Per the manufacturer's documentation (not verified) the interval depends on the concentration: 2.3 s when steady, 200…800 ms when changing fast. In the model, 10 minutes: background 12 µg/m³, in the second minute smoke up to 81 µg/m³ with a decay. Gaps between frames — 271: 256 of 2.3 s and 15 fast ones, 200…790 ms; of the fast ones 13 — after the smoke, 2 — before it, from noise.

In passive mode the sensor is silent until asked. A command is 7 bytes: 42 4D, the command, two data bytes and the sum of five bytes — the same principle as the data frame:

/* кадр команды: 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;
}
What Frame
0xE2 — read a frame (passive mode) 42 4D E2 00 00 01 71
0xE1, 0 — passive mode 42 4D E1 00 00 01 70
0xE1, 1 — active mode 42 4D E1 00 01 01 71
0xE4, 0 — sleep 42 4D E4 00 00 01 73
0xE4, 1 — wake up 42 4D E4 00 01 01 74

The command codes are per the manufacturer's documentation, not verified; the sums were computed by the driver and checked against an independent Python calculation. To a mode change and to sleep the model sensor replies with a short frame of length 4, for example 42 4D 00 04 E1 00 01 74 to 0xE1, 0 (a model assumption: whether the real sensor replies like this, and every batch — not verified). That is why the parser also accepts length 4. The driver does not rely on this reply.

/* пассивный режим: запрос кадра; ответа нет за 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);
}

In the model, 60 requests once a second — all 60 were answered, from the request to the parsed frame 70–90 ms: 7.3 ms of command, the sensor's delay (20…40 ms in the model), 33.33 ms of frame and the polling step.

Passive mode is handier when a frame is needed at a known moment, and it saves parsing. But it has two traps. The first — the sensor does not answer while asleep: in the model a request with SET = 0 — 3 attempts of 300 ms, after 900 ms the driver reports a timeout. The second — after a power glitch or a reset the sensor is in active mode again and sends frames unprompted. The driver sees this — a frame came, but there was no request — and sends 0xE1, 0 again. In the model, after a reset the first stray frame arrived after 1540 ms, the driver restored passive mode, and there were no more unprompted frames.


103в.5 Sleep, warm-up and the cycle "wake, wait, measure, sleep"

The fan and the laser are what wears out: running around the clock, the fan clogs with dust and ages, and the laser loses brightness. The manufacturer gives lifetimes in hours of operation (not verified), so a sensor that measures once every few minutes is sensibly put to sleep. It can be put to sleep with the SET pin (0 — sleep) or with the command 0xE4, 0; woken with SET = 1 or 0xE4, 1. The pin is simpler and does not depend on whether the sensor heard the command.

After waking the fan spins up, and per the manufacturer's documentation stable data come no earlier than 30 s later (not verified). In the model the first frame comes after 1500 ms, and readings are lowered by the factor 1 − 0.6 · e^(−t / 8 s) — a model assumption. The object is air with PM2.5 = 35 µg/m³, noise off, share of the true value:

After waking 0–5 s 5–10 s 10–15 s 15–20 s 20–25 s 25–30 s 30–40 s 40–60 s
reading, % 57.1 75.7 85.7 93.3 97.1 97.1 100.0 100.0

Hence the firmware cycle: every 5 minutes wake up, wait 30 s, take 10 frames in passive mode once a second, average, go to sleep. While warm-up is on, the driver does not hand out frames:

/* цикл: проснуться, 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;
    }
}

In the model, two hours with this cycle: cycles — 24, the fan runs 13.02% of the time — about 1141 h a year instead of 8760. The average before the kitchen event is 97.5% of the true value. The same cycle without the wait: the fan runs 3.03% of the time, but the average is 68.8% of the true value: measured with a fan not yet spun up.


103в.6 Averaging, "CF = 1" versus "atmospheric" and the AQI

Averaging. The noise of one frame in the model is 5% or 1.5 µg/m³, whichever is larger (a model assumption). The average of "atmospheric" PM2.5 at 35 µg/m³ "CF = 1", over 100 trials for each number of frames:

Frames in the average 1 4 10 30
spread of the average, µg/m³ 1.083 0.575 0.418 0.207
time, s 1 4 10 30

Four times more frames — almost half the spread. Ten frames is a sensible middle for the cycle above. The driver computes the cycle average in tenths without rounding up — as EPA prescribes for the AQI: a sum of 356 over three frames is 118.67, and 118.6 goes into the index.

"CF = 1" and "atmospheric". The frame carries three concentrations twice. Per the manufacturer's documentation "CF = 1" is for factory conditions and "atmospheric" for ordinary air (not verified). How one is converted into the other, the manufacturer does not disclose. A common picture with such sensors is that up to a few tens of µg/m³ they are equal, above that "atmospheric" is smaller. The model does exactly this: equal up to 30 µg/m³, above that a slope of 2/3 — a model assumption:

CF = 1, PM2.5 5 10 20 30 50 100 200 500
atmospheric, PM2.5 5 10 20 30 43 77 143 343

For comparison with the index "atmospheric" values are used. But neither equals what a reference instrument with a filter and scales would measure: the sensor counts particles with light and estimates the mass. For serious conclusions the sensor is compared with a reference on site and given its own correction.

Humidity. Fog droplets and particles swollen with water are particles too for the laser. In humid air, in fog and near steam the PMS5003 overreads, sometimes several times over; the model does not reproduce this. A humidity sensor nearby at least flags such readings.

AQI. The US air quality index (EPA) for PM2.5 is computed piecewise-linearly from a table. The table below is the EPA 2024 revision, for a 24-hour average; there is no EPA document in the project, the breakpoints are from memory of the publication, not checked:

PM2.5, µg/m³ AQI Category
0.0–9.0 0–50 good
9.1–35.4 51–100 moderate
35.5–55.4 101–150 unhealthy for sensitive groups
55.5–125.4 151–200 unhealthy
125.5–225.4 201–300 very unhealthy
225.5–325.4 301–500 hazardous

I = I_lo + (I_hi − I_lo) · (C − C_lo) / (C_hi − C_lo), to the nearest integer. In the driver — in integers, in tenths of µg/m³, without floating-point division:

/* 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, µg/m³ 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

All 19 values matched an independent Python calculation. Above 325.4 the function returns 500 — "beyond the scale". Strictly, the index is computed from a daily average; an average of 10 frames every 5 minutes is a "now" estimate, and comparing it with the official index needs care.

A kitchen in the model: background 8 µg/m³, at minute 30 a burst up to 148 with a decay over a quarter of an hour. The cycle is every 5 minutes, every second average shown; the AQI is from the cycle average, as in the firmware:

Minute PM2.5, µg/m³ AQI True "atmospheric" in the model
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 Pitfalls

Five volts on the pins. The sensor's supply is 5 V and its logic 3.3 V (per the manufacturer's documentation). The F103 outputs 3.3 V, which suits the sensor. The danger is elsewhere — a 5-volt TX of an adapter or another board on an input without FT: on the F103 PA10 withstands it (p. 31), PA3 does not (p. 29). And a sensor whose RX is driven from a 5-volt board needs a divider.

Levels differ between batches. There are adapter boards and batches whose levels are not what you expect. Before connecting — a voltmeter on the sensor's TX at idle.

Fan upside down or inlet blocked. The sensor measures what the fan drove through it. If the inlet and outlet are blocked by the enclosure, pressed to a wall or face each other, the sensor recirculates the same air. Per the manufacturer's documentation the sensor has allowed orientations and needs clearance at the inlet (not verified); the enclosure is designed for the flow.

Right after waking. The first seconds give low numbers: 57.1% of the true value in the first 5 s in the model. Wait 30 s.

Passive mode stays silent. A request during sleep, TX and RX swapped, the sensor rebooted and is active again. The driver bounds the wait (three attempts of 300 ms) and restores passive mode by itself if frames arrive unprompted.

Sum over 30 bytes. The sum runs from 42 to word 13, 30 bytes. Over 28 bytes without the start (0x03D7) or over all 32 (0x04D0) not one frame will match.

No checksum. A driver that does not check the sum (that is how pms7003 in Zephyr 4.4.2 is written) accepts both a damaged frame and a false start: 1177 wrong out of 20 002 in the experiment of 103в.3 and an endless synchronisation failure on a false 42 4D 00 1C.


103в.8 Summary

Full sources

pms103d.h — frame, commands, timings, states and functions of the PMS5003 driver.

/* 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 — driver: checksum, command frame, ring parsing, modes, sleep, cycle and average.

/* 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 table and functions.

/* 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 from PM2.5 in integers.

/* 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 firmware: USART1, circular DMA1 channel 5, SET on PB0, RESET on PB1, a cycle every 5 minutes.

/* 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 */
    }
}

Next: section 104 "Thermocouple: MAX31855 and MAX6675" — the thermocouple, the cold junction and ready-made converters on SPI.

Open in the interactive handbook