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

103г. SCD40/SCD41 CO₂ sensor over I2C: commands and CRC-8, periodic and single-shot measurement, ppm, °C and %RH, pressure and calibration

In section 103 the MQ measured gas with a heated layer, in 103в the PMS5003 counted dust particles with a laser. The SCD40 and SCD41 measure carbon dioxide with light: how much infrared radiation the air in a small chamber has absorbed. The answer is a ready number in parts per million (ppm), with temperature and humidity next to it. Here — how the I2C exchange works: 16-bit commands, words with CRC-8 and pauses for execution; how to check the CRC on the example 0xBEEF → 0x92; how to measure every 5 s, every 30 s or once at a time (SCD41); how to convert raw words into °C and %RH; why the sensor needs pressure and altitude; how automatic and forced calibration work and why the settings must not be saved to EEPROM on every start-up.

Sources: RM0008 (DocID13902 Rev 17): master reception of more than two bytes, method 2, pp. 762–763; I2C_CCR, pp. 783–784; I2C_TRISE, p. 784. DocID13587 Rev 17 (STM32F103x8/xB): PB6 — I2C1_SCL, PB7 — I2C1_SDA, both FT, p. 32. The I2C bus and its setup are section 17 and are not repeated here. There is no Sensirion SCD40/SCD41 document in the project. The command codes, their execution times, the CRC-8 (polynomial 0x31, initial value 0xFF), the mask of the data_ready word, the °C and %RH formulas, the forced calibration reply, the pressure and altitude limits, the 2000 EEPROM writes, the automatic calibration periods and the 5 s and 30 s measurement periods are taken from the scd4x driver of Zephyr 4.4.2 on disk, the example 0xBEEF → 0x92 from its CRC test. The address 0x62 and the measuring principle are per the manufacturer's documentation, not verified. All numbers are a run of the driver _verify/co2_103e/scd103e.c on a PC (gcc 12.2.0) with our own model of the SCD41 as an I2C slave in Python 3.11; the firmware was built with armclang 6.24 for Cortex-M3. How the sensor reacts to errors, the warm-up, drift, noise and the calibration algorithms themselves are assumptions in the model: the numbers below describe the model, not the sensor.

What it is called in the documentation

Original Meaning
NDIR (non-dispersive infrared) non-dispersive infrared method
photoacoustic the absorbed light is heard as sound
ppm (parts per million) millionth part, 1 ppm = 0.0001%
periodic / low power periodic measurement periodic (5 s) / low-power (30 s) measurement
single shot one measurement on request
data ready status "a new measurement is available"
command execution time time the command takes to execute
ambient pressure / sensor altitude pressure of the surrounding air / installation altitude
automatic self-calibration (ASC) automatic calibration
forced recalibration (FRC) forced calibration
persist settings save the settings to EEPROM

103г.1 Principle: photoacoustic NDIR and what comes out

CO₂ molecules absorb infrared light in a narrow band. Per the manufacturer's documentation (not verified), inside the SCD4x there are an IR source, a chamber with air and a microphone: the source shines in pulses, CO₂ absorbs part of the light and warms up slightly, the gas in the chamber expands and contracts in step with the pulses — this is sound. The more CO₂, the louder. Hence "photoacoustic": the absorption is heard rather than seen by a photodetector.

The microphone hears the number of CO₂ molecules in the chamber, not their share. With the same air at altitude there are fewer molecules in the chamber, and the reading drops — so the sensor has to know the pressure (103г.5). Next to it in the package is a temperature and humidity sensor: its words arrive together with CO₂.

SCD40 SCD41
periodic measurement every 5 s every 5 s
low-power per the manufacturer's documentation, not verified every 30 s
single shot (0x219D, 5 s) no yes
sleep and wake-up (0x36E0, 0x36F6) — for the single-shot mode
automatic calibration periods (44 and 156 h) not available in Zephyr changed by commands

This is how the Zephyr 4.4.2 driver separates them: the "single shot" mode and the automatic calibration periods exist there only for the SCD41. Outdoors CO₂ is about 400 ppm, in a closed room with people it is several times higher; how accurate the sensor itself is cannot be said without the document.


103г.2 Connecting to the F103 and the exchange: command, word, CRC, pause

Module pin STM32F103 Purpose
SCL PB6 clock I2C1_SCL
SDA PB7 data I2C1_SDA
VDD 3.3 V supply
GND GND ground

The SCL and SDA pull-ups are usually on the module. PB6 and PB7 are FT (p. 32): if the module is pulled up to 5 V, the F103 pin will survive it. The exchange is rare — nine bytes every 5 s, so the bus runs at 100 kHz, standard mode. I2C1 sits on APB1 = 36 MHz: in standard mode Thigh = Tlow = CCR · TPCLK1 (p. 784), CCR = 36 MHz / (2 × 100 kHz) = 180 — 5 µs per half period; TRISE = 1000 ns × 36 MHz + 1 = 37 (same page: in standard mode the edge is up to 1000 ns):

static void i2c1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u;  /* PB6, PB7 — альтернативная функция, открытый сток */
    I2C1->CR1 = 0;                                          /* настраивать при PE = 0 */
    I2C1->CR2 = APB1_MHZ;
    I2C1->CCR = 180u;                                       /* Sm: Thigh = Tlow = 180 × 27,8 нс = 5 мкс — 100 кГц */
    I2C1->TRISE = APB1_MHZ * 1000u / 1000u + 1u;            /* 1000 нс × 36 МГц = 36 → 36 + 1 = 37 */
    I2C1->CR1 = I2C_CR1_PE;
}

The SCD4x has no registers. It has 16-bit commands, high byte first. A command comes in three kinds: just two bytes; two bytes plus a data word with its CRC; two bytes, a pause and reading the reply in a separate transaction. While the sensor executes a command it does not answer — neither a write nor a read; how long to wait is written in the table of the Zephyr driver:

Command Code Wait, ms Reply
start_periodic_measurement 0x21B1 0 —
start_low_power_periodic_measurement 0x21AC 0 —
stop_periodic_measurement 0x3F86 500 —
read_measurement 0xEC05 1 3 words: CO₂, T, RH
get_data_ready_status 0xE4B8 1 1 word
set / get_sensor_altitude 0x2427 / 0x2322 1 — / 1 word
set / get_ambient_pressure 0xE000 1 — / 1 word
perform_forced_recalibration 0x362F 400 1 word
set / get_automatic_self_calibration 0x2416 / 0x2313 1 — / 1 word
persist_settings 0x3615 800 —
reinit 0x3646 30 —
measure_single_shot 0x219D 5000 —
power_down / wake_up 0x36E0 / 0x36F6 1 / 30 —

A data word goes as three bytes: high, low and the CRC-8 of these two. A write with a word is five bytes:

/* команда со словом: два байта команды, слово старшим байтом вперёд и CRC-8 этого слова */
int scd_cmd_word(scd_t *d, uint16_t cmd, uint16_t w, uint32_t ms)
{
    uint8_t tx[5] = {(uint8_t)(cmd >> 8), (uint8_t)cmd, (uint8_t)(w >> 8), (uint8_t)w, 0};
    int rc;
    tx[4] = scd_crc(&tx[2], 2);
    rc = xfer_rc(d, scd_port_write(d->addr, tx, 5));
    if (rc == SCD_OK)
        pause_ms(d, ms);
    return rc;
}

The reply is read in a separate transaction — without a repeated start, as with register sensors in 17.2, but after a pause. The CRC is checked for every word:

/* чтение n слов: по три байта — старший, младший, CRC; слово с неверной CRC — ошибка всего чтения */
static int read_words(scd_t *d, uint16_t *w, uint32_t n)
{
    uint8_t rx[9];
    int rc;
    if (n == 0u || n > 3u)
        return SCD_E_ARG;
    rc = xfer_rc(d, scd_port_read(d->addr, rx, 3u * n));
    if (rc != SCD_OK)
        return rc;
    for (uint32_t i = 0; i < n; i++) {
        if (d->check_crc && scd_crc(&rx[3u * i], 2) != rx[3u * i + 2u]) {
            d->crc_errors++;
            return SCD_E_CRC;
        }
        w[i] = (uint16_t)(rx[3u * i] << 8 | rx[3u * i + 1u]);
    }
    return SCD_OK;
}

Receiving 3 and 9 bytes in the firmware is method 2 from RM0008 (pp. 762–763), as in 103б. Reading a measurement in the model at 800 ppm, 23.4 °C and 41.2%: write EC 05, pause 1 ms, then 03 20 2A 64 0F 79 69 78 56 — three words 0x0320, 0x640F, 0x6978, each followed by its CRC. At 100 kHz writing the command takes 290 µs, reading nine bytes 920 µs, the whole read together with the pause 2210 µs.

Top — the SCD41 connected to an STM32F103: PB6 — SCL, PB7 — SDA on I2C1 at 100 kHz, pull-ups on the module; inside the sensor an IR source, a chamber and a microphone, next to them a temperature and humidity sensor; photoacoustic NDIR, a measurement every 5 s or in low-power mode every 30 s. Middle — reading a measurement: the address with write, the command EC 05, a pause of 1 ms, the address with read and nine bytes 03 20 2A, 64 0F 79, 69 78 56 — the CO₂, T and RH words, each with its own CRC-8; 0x0320 — 800 ppm, T = −45 + 175 · 0x640F / 65535 = 23.40 °C, RH = 100 · 0x6978 / 65535 = 41.20%; CRC-8 with polynomial 0x31 and initial value 0xFF, 0xBEEF → 0x92. Bottom — the order of commands: start_periodic 0x21B1, data_ready 0xE4B8, read_measurement 0xEC05, stop_periodic 0x3F86 with a 500 ms pause, then FRC calibration 0x362F 400 ms, altitude or ASC, persist_settings 0x3615 800 ms — only on a change; single shot 0x219D 5 s — SCD41 only


103г.3 Sensirion CRC-8: polynomial 0x31, initial value 0xFF

The CRC is computed over the two bytes of a word, most significant bit first, with initial value 0xFF and no final XOR. The polynomial 0x31 is x⁸ + x⁵ + x⁴ + 1:

/* CRC-8 Sensirion: полином x^8 + x^5 + x^4 + 1 (0x31), начало 0xFF, старший бит первым, без итогового XOR */
uint8_t scd_crc(const uint8_t *p, uint32_t n)
{
    uint8_t crc = 0xFFu;
    while (n--) {
        crc ^= *p++;
        for (uint32_t b = 0; b < 8u; b++)
            crc = (crc & 0x80u) ? (uint8_t)((crc << 1) ^ 0x31u) : (uint8_t)(crc << 1);
    }
    return crc;
}

The test example is the word 0xBEEF: CRC = 0x92. Exactly this is expected by the CRC test in Zephyr 4.4.2 (crc8 with polynomial 0x31 and initial value 0xFF), and the same is given by scd_crc on the stand and an independent Python computation. A few more words:

Word 0x0000 0x0001 0x0190 0x0320 0x8000 0xFFFF
CRC 0x81 0xB0 0x4C 0x2A 0xA2 0xAC

The CRC of zero is not zero — that is what the initial value 0xFF gives: a broken line that reads as all zeros or all ones will not pass the CRC. The word 0x0320 is the very 800 ppm from the frame above.

Which corruptions the CRC catches: an enumeration of all sets of corrupted bits in the three bytes "word + CRC" for 0xBEEF:

Bits corrupted 1 2 3 4
sets 24 276 2024 10 626
CRC missed 0 0 0 80

Any one, two and three errors in a word the CRC always catches; it misses only some of the quadruple ones. This is stronger than the simple sum of the PMS5003 from 103в, which missed every 33rd double corruption. The same class of protection is the PEC of the MLX90614 in 104б, but there the polynomial is 0x07 and the initial value 0.

Corruption experiment. In the model every bit read from the sensor is flipped with a probability of 0.2% (so in the model; a real bus errs far less often). 6000 attempts to read a measurement:

CRC check Correct accepted Wrong accepted Rejected by CRC Other errors Largest CO₂ error, ppm
on 3983 0 2017 0 0
off 4920 532 0 548 32 768

With the check not a single wrong measurement was accepted — the corrupted one simply disappeared, and the firmware waits for the next one 5 s later. Without the check 532 wrong ones went into the program, and one flipped high bit gave an error of 32 768 ppm. Much is rejected because the data_ready replies were corrupted too: the driver asks it up to six times per measurement.


103г.4 Measurement: start, data_ready, conversion into ppm, °C and %RH

Start-up. After power-up the sensor needs 30 ms (a constant of the Zephyr driver), and the firmware waits them out. Then scd_init: stop the measurement — it may be running since the previous start, with the microcontroller merely reset; no answer — then it is asleep, wake it up; then reinit — reread the settings from EEPROM. In the model on a fresh sensor this is two commands and 530.58 ms: 500 ms stop, 30 ms reinit. After a microcontroller reset while the sensor kept measuring — the same. A sleeping sensor does not acknowledge the stop, and the path is different: 4 exchanges, 2 of them NACK, 91.16 ms.

Wake-up. The sensor executes the wake_up command but does not acknowledge it — this is also written in the Zephyr driver, which sends it twice. The first NACK is expected, the second command must pass:

/* пробуждение: первая команда будит, но датчик её не подтверждает — NACK ожидаем; вторая должна пройти */
int scd_wake(scd_t *d)
{
    uint8_t tx[2] = {(uint8_t)(SCD_WAKE_UP >> 8), (uint8_t)SCD_WAKE_UP};
    int rc;
    (void)xfer_rc(d, scd_port_write(d->addr, tx, 2));
    scd_port_delay_ms(SCD_MS_WAKE);
    rc = scd_cmd(d, SCD_WAKE_UP, SCD_MS_WAKE);
    if (rc == SCD_OK)
        d->state = SCD_IDLE;
    return rc;
}

data_ready. After start_periodic_measurement a new measurement appears every 5 s. To ask whether it is there — command 0xE4B8: the reply is a word, and a new measurement is available if its lower 11 bits are not zero. The upper bits must not be looked at: in the model bit 15 is set, and the word without data is 0x8000, with data 0x8006. Comparing the whole word with zero would always see data.

/* есть ли новое измерение: младшие 11 бит слова не ноль */
int scd_ready(scd_t *d, int *ready)
{
    uint16_t w;
    int rc = scd_query(d, SCD_DATA_READY, SCD_MS_SET, &w, 1);
    *ready = rc == SCD_OK && (w & SCD_READY_MASK) != 0u;
    return rc;
}
/* ждать новое измерение: спрашивать data_ready раз в poll_ms, не дольше max_ms; потом прочитать */
int scd_wait_read(scd_t *d, scd_meas_t *m, uint32_t poll_ms, uint32_t max_ms, uint32_t *polls)
{
    uint32_t waited = 0;
    int ready, rc;
    *polls = 0;
    for (;;) {
        (*polls)++;
        if ((rc = scd_ready(d, &ready)) != SCD_OK)
            return rc;
        if (ready)
            return scd_read(d, m);
        if (waited >= max_ms)
            return SCD_E_TIMEOUT;
        scd_port_delay_ms(poll_ms);
        waited += poll_ms;
    }
}

In the model an hour of periodic mode with polling once a second: measurements — 720, the first 5013 ms after the start, then exactly every 5000 ms, data_ready per measurement — 5.99 times, exchanges per hour — 10 067, not a single NACK and not a single CRC error. Low-power mode — every 30 s: 120 measurements per hour, the first after 30 054 ms; but polling once a second there means 30.95 questions per answer and 7671 exchanges: it is worth polling less often.

Reading blindly, without data_ready, does not work: in the model a read without a new measurement gets NACK (model assumption). 60 reads once a second — 12 successful and 48 NACK.

Single shot (SCD41). Command 0x219D — one measurement in 5 s, then a normal read. Between measurements the sensor can be put to sleep (power_down), and until the next time it does not measure. In the model the readings right after waking are too high (model assumption, see 103г.6): a cycle every 5 minutes, waking up and taking two measurements in a row — 834 and 810 ppm with the true 800, 10 066 ms for both. That is why the first one after waking is thrown away. The SCD40 does not have this command: the driver at once returns −8, SCD_E_MODEL, and the bare command gets NACK in the model.

Converting the words. CO₂ is the word as is, in ppm. Temperature and humidity — by the formulas of the Zephyr driver:

T = −45 + 175 · word / 65535 °C, RH = 100 · word / 65535 %.

Without floating point (the F103 has none) — in hundredths with rounding:

/* °C = −45 + 175 · слово / 65535, %RH = 100 · слово / 65535 — в сотых, с округлением */
void scd_convert(scd_meas_t *m)
{
    m->t100 = (int16_t)(-4500 + (int32_t)((17500u * m->t_raw + 32767u) / 65535u));
    m->rh100 = (uint16_t)((10000u * m->rh_raw + 32767u) / 65535u);
}

17 500 × 65 535 is less than 2³², there is no overflow. All 65 536 words were run on the stand: the largest difference from the exact formula is 0.5 hundredth, mismatches with correct rounding — 0. In the frame above 0x640F = 25 615 gives 23.40 °C, 0x6978 = 27 000 gives 41.20%. The word 0x6666 is exactly 25.00 °C and 40.00%; 0x0000 — −45 °C and 0%, 0xFFFF — 130 °C and 100%.


103г.5 Pressure and altitude

The sensor hears the number of molecules but shows a share. To convert one into the other, it needs the pressure. By default the altitude is 0: the sensor takes the pressure to be sea level. You can tell it the truth in two ways: set the installation altitude once (0x2427, 0…3000 m) or now and then pass the pressure from a barometer (0xE000, 700…1200 hPa). The limits are as in the Zephyr driver; outside them scd_set_pressure returns −7, SCD_E_ARG (in the model — for 699 and 1201).

In the model the reading is proportional to p / p_sensor, and the sensor takes the pressure for an altitude from the standard atmosphere (model assumption; how the real sensor converts it is not verified). At 1500 m this is 845.6 hPa against 1013.25 at sea level. The truth is 800 ppm, noise is off:

What the sensor is told Actual pressure, hPa Reading, ppm Error, ppm
nothing (altitude 0) 845.6 668 −132
altitude 1500 m 845.6 800 0
pressure 846 hPa 845.6 800 0
altitude 1500 m, weather −15 hPa 830.6 786 −14
altitude 1500 m, weather +15 hPa 860.6 814 14

The altitude removes the main thing — mountains and floors; the pressure from a barometer removes the weather as well. The pressure is the only setting that can be changed during a measurement: this is what the Zephyr driver does too. In the model scd_set_pressure during a measurement gives 0, and writing the altitude without stopping gives NACK, −1. The driver itself stops the measurement before a setting and starts it again:

/* высота и автокалибровка: прочитать, записать только то, что отличается; *changed — было ли что записать */
int scd_config(scd_t *d, uint16_t alt_m, uint8_t asc, uint8_t *changed)
{
    uint16_t w;
    uint8_t was;
    int rc;
    *changed = 0;
    if (alt_m > SCD_ALT_MAX || asc > 1u)
        return SCD_E_ARG;
    if ((rc = pause_meas(d, &was)) != SCD_OK)
        return rc;
    if ((rc = scd_query(d, SCD_GET_ALTITUDE, SCD_MS_SET, &w, 1)) == SCD_OK && w != alt_m) {
        rc = scd_cmd_word(d, SCD_SET_ALTITUDE, alt_m, SCD_MS_SET);
        *changed = 1;
    }
    if (rc == SCD_OK && (rc = scd_query(d, SCD_GET_ASC, SCD_MS_SET, &w, 1)) == SCD_OK && w != asc) {
        rc = scd_cmd_word(d, SCD_SET_ASC, asc, SCD_MS_SET);
        *changed = 1;
    }
    return resume_meas(d, was, rc);
}

103г.6 Warm-up, automatic calibration, FRC and EEPROM

Warm-up. How long the transient after power-up lasts in the real sensor cannot be said without the document. In the model the reading after power-up or wake-up is too high by 120 · e^(−t / 4 s) ppm (model assumption). Noise is off, the measurement is started 0.56 s after power-up (30 ms of waiting and 530 ms of scd_init):

After the measurement start, s 5 10 15 20 30 60
error, ppm 30 9 2 1 0 0

Hence the firmware rule: do not take the first measurement after power-up and wake-up, and calibrate only after long operation.

Automatic calibration (ASC). An NDIR sensor drifts with time. Automatic calibration assumes that the sensor sees fresh air from time to time and pulls its zero towards it. In the Zephyr driver it is enabled from the factory, and for the SCD41 the first period is 44 h, the following ones 156 h each. In the model at the end of a period the lowest reading of the period is declared to be 400 ppm (model assumption: the real algorithm is not disclosed). Drift +80 ppm, noise off, error of the night reading at 03:00 by day:

Room Actual at night, ppm Day 1 Day 2 Days 3–10
office, aired at night, ASC on 420 80 −20 −20
bedroom, closed at night, ASC on 650 80 −250 −250
office, ASC off 420 80 80 80

In the office automatic calibration removed the drift (−20 is 420 against 400). In the bedroom, where the air is never fresh, it decided that 730 is 400 and began to read 250 ppm low: more dangerous than without it. If the sensor never sees the outdoors — in a greenhouse, in a cellar, in an unventilated room — automatic calibration is turned off and calibration is done by hand.

Forced calibration (FRC). The sensor is taken out into fresh air, allowed to run (the Zephyr driver requires at least 3 minutes in periodic mode at a steady concentration), the measurement is stopped and 0x362F is sent with the reference concentration — 400 ppm for the outdoors. After 400 ms the sensor replies with a word: 0x8000 plus the correction, and 0xFFFF is a failure:

/* принудительная калибровка: датчик не меньше 3 минут мерил воздух с известной концентрацией ppm */
int scd_frc(scd_t *d, uint16_t ppm, int16_t *corr)
{
    uint16_t w = SCD_FRC_FAIL;
    uint8_t was;
    int rc = pause_meas(d, &was);
    if (rc == SCD_OK && (rc = scd_cmd_word(d, SCD_FRC, ppm, SCD_MS_FRC)) == SCD_OK
        && (rc = read_words(d, &w, 1)) == SCD_OK) {
        if (w == SCD_FRC_FAIL)
            rc = SCD_E_FRC;
        else
            *corr = (int16_t)((int32_t)w - (int32_t)SCD_FRC_ZERO);
    }
    return resume_meas(d, was, rc);
}

In the model FRC takes the mean of the last 12 measurements, and the sign of the correction is "reference minus measured" (model assumptions). Drift +87 ppm, noise 10 ppm, 3 minutes of operation:

Case True, ppm Before, ppm Code Correction After, ppm Error after, ppm
outdoors, reference 400 400 489.8 0 −90 401.2 1.2
without stopping the measurement 400 489.8 −1 — 491.1 91.1
a room, but reference 400 600 689.8 0 −290 401.2 −198.8

The calibration outdoors removed the drift. Without stopping the measurement the sensor did not accept the command — NACK. And a reference of 400 ppm in a room that actually has 600 spoiled the sensor by 200 ppm: FRC trusts the reference, not the air. If the sensor has not measured once since power-up, in the model the reply is 0xFFFF, and the driver returns −6, SCD_E_FRC.

EEPROM. The altitude and automatic calibration live in the sensor's RAM, and after power is removed they come back from EEPROM. persist_settings saves them there (800 ms). The Zephyr driver warns: the EEPROM is guaranteed to withstand at least 2000 writes. A firmware that sets up and saves on every start wrote the EEPROM 100 times in 100 starts in the model; the one that first reads and saves only changes — 1 time. With a restart every hour the first one will use up the 2000 writes in 83.3 days:

static void sensor_up(void)
{
    uint8_t changed;
    while (scd_init(&dev) != SCD_OK)
        scd_port_delay_ms(1000u);                           /* нет ответа: провод, адрес, питание, подтяжки */
    if (scd_config(&dev, ALTITUDE_M, ASC_ON, &changed) == SCD_OK && changed)
        (void)scd_persist(&dev);                            /* EEPROM — только когда что-то изменилось */
    (void)scd_start(&dev, 0);
}

The firmware does not save the pressure at all: it sends it on the fly.


103г.7 Pitfalls

A setting during a measurement. While periodic measurement is running, the sensor accepts only reading, data_ready, stop and pressure — this is how the Zephyr driver is built too. Altitude, ASC, FRC and persist_settings without stop_periodic_measurement get NACK in the model. The driver stops the measurement itself and then starts it again.

Not waiting out the pause. After stop_periodic_measurement — 500 ms, after data_ready and read_measurement — 1 ms, after FRC — 400 ms. Without the pauses in the model: stop and at once start — the start got NACK; data_ready without 1 ms — the read got NACK, and with the pause 0.

Reading without data_ready. There is no new measurement — NACK in the model: 48 of 60 reads once a second. Ask data_ready and look only at the lower 11 bits.

Wake-up without an answer. wake_up is not acknowledged. A driver that treats this NACK as an error will decide there is no sensor, although it has already woken up.

persist_settings on every start. EEPROM — at least 2000 writes; once an hour is less than three months. Save only changes.

Automatic calibration where there is no fresh air. In the model — minus 250 ppm in a closed bedroom. Turn ASC off or air the room.

FRC not outdoors. A reference of 400 ppm in a room with 600 — minus 200 ppm in the model. And not earlier than after 3 minutes of operation.

Altitude and pressure. Without them at 1500 m in the model −132 ppm. The altitude is set once, the pressure is sent on the fly.

No CRC. Without the check — 532 wrong of 6000 in the experiment of 103г.3 and an error of up to 32 768 ppm.


103г.8 Summary

Full sources

scd103e.h — commands, their execution times, limits, error codes and functions of the SCD40/SCD41 driver.

/* scd103e.h — раздел 103г: драйвер датчика CO₂ SCD40/SCD41 (Sensirion) по I2C без регистров микроконтроллера.
 * Драйвер говорит с датчиком только через функции порта (scd_port_*): в прошивке fw103e.c их делает I2C1 и
 * SysTick STM32F103, в стенде host103e.c — модель model103e.py.
 * Коды команд, время их выполнения, CRC-8 (0x31, начало 0xFF), маска data_ready 0x07FF, формулы °C и %RH,
 * ответ FRC (0x8000 + поправка, 0xFFFF — отказ), пределы давления и высоты — как в драйвере scd4x Zephyr 4.4.2.
 * Адрес 0x62 — по документации производителя, с датчиком не сверено. */
#ifndef SCD103E_H
#define SCD103E_H
#include <stdint.h>

#define SCD_ADDR            0x62u       /* семибитный, по документации производителя */

/* ── команды: 16 бит, старшим байтом вперёд ───────────────────────────── */
#define SCD_START_PERIODIC  0x21B1u     /* измерение раз в 5 с */
#define SCD_START_LOWPOWER  0x21ACu     /* измерение раз в 30 с */
#define SCD_STOP_PERIODIC   0x3F86u
#define SCD_READ_MEAS       0xEC05u     /* ответ — три слова: CO₂, T, RH */
#define SCD_DATA_READY      0xE4B8u     /* ответ — слово: младшие 11 бит не 0 — есть новое измерение */
#define SCD_SET_ALTITUDE    0x2427u     /* слово — высота, м */
#define SCD_GET_ALTITUDE    0x2322u
#define SCD_PRESSURE        0xE000u     /* со словом — записать давление, гПа; без слова — прочитать */
#define SCD_FRC             0x362Fu     /* слово — опорная концентрация, ppm; ответ — 0x8000 + поправка */
#define SCD_SET_ASC         0x2416u     /* слово: 1 — автокалибровка включена, 0 — выключена */
#define SCD_GET_ASC         0x2313u
#define SCD_PERSIST         0x3615u     /* настройки — в EEPROM */
#define SCD_REINIT          0x3646u     /* перечитать настройки из EEPROM */
#define SCD_SINGLE_SHOT     0x219Du     /* одно измерение — только SCD41 */
#define SCD_POWER_DOWN      0x36E0u
#define SCD_WAKE_UP         0x36F6u     /* датчик просыпается, но не подтверждает: NACK */

/* ── время выполнения, мс: до него датчик не отвечает ─────────────────── */
#define SCD_MS_STOP         500u
#define SCD_MS_READ         1u
#define SCD_MS_SET          1u          /* data_ready, высота, давление, ASC — чтение и запись */
#define SCD_MS_FRC          400u
#define SCD_MS_PERSIST      800u
#define SCD_MS_REINIT       30u
#define SCD_MS_SINGLE       5000u
#define SCD_MS_POWER_DOWN   1u
#define SCD_MS_WAKE         30u
#define SCD_MS_STARTUP      30u         /* после включения питания */

#define SCD_PERIOD_MS       5000u
#define SCD_LOWPOWER_MS     30000u
#define SCD_READY_MASK      0x07FFu
#define SCD_FRC_FAIL        0xFFFFu
#define SCD_FRC_ZERO        0x8000u
#define SCD_P_MIN           700u        /* давление, гПа */
#define SCD_P_MAX           1200u
#define SCD_ALT_MAX         3000u       /* высота, м */

enum { SCD_OK = 0, SCD_E_NACK = -1, SCD_E_BUS = -2, SCD_E_CRC = -3, SCD_E_BUSY = -4, SCD_E_TIMEOUT = -5,
       SCD_E_FRC = -6, SCD_E_ARG = -7, SCD_E_MODEL = -8 };
enum { SCD_IDLE = 0, SCD_PERIODIC = 1, SCD_LOWPOWER = 2, SCD_SLEEP = 3 };

typedef struct {
    uint16_t co2;                       /* ppm — слово как есть */
    uint16_t t_raw, rh_raw;             /* сырые слова */
    int16_t t100;                       /* °C × 100 */
    uint16_t rh100;                     /* %RH × 100 */
} scd_meas_t;

typedef struct {
    uint8_t addr;
    uint8_t scd41;                      /* 1 — SCD41: есть однократное измерение */
    uint8_t state;                      /* SCD_IDLE … SCD_SLEEP — что, по мнению драйвера, делает датчик */
    uint8_t check_crc;                  /* 1; 0 — CRC не проверять (для опыта) */
    uint8_t wait;                       /* 1; 0 — не ждать время выполнения (для опыта) */
    uint8_t guard;                      /* 1; 0 — не останавливать измерение перед настройкой (для опыта) */
    uint32_t xfers, nacks, crc_errors, persists;
} scd_t;

/* ── порт: в прошивке — I2C1 и SysTick; в стенде — строки к модели ────── */
int scd_port_write(uint8_t addr7, const uint8_t *tx, uint32_t n);    /* START, адрес + W, n байт, STOP */
int scd_port_read(uint8_t addr7, uint8_t *rx, uint32_t n);           /* START, адрес + R, n байт, STOP */
void scd_port_delay_ms(uint32_t ms);

/* ── драйвер ──────────────────────────────────────────────────────────── */
uint8_t scd_crc(const uint8_t *p, uint32_t n);
void scd_dev_init(scd_t *d, uint8_t scd41);
int scd_cmd(scd_t *d, uint16_t cmd, uint32_t ms);
int scd_cmd_word(scd_t *d, uint16_t cmd, uint16_t w, uint32_t ms);
int scd_query(scd_t *d, uint16_t cmd, uint32_t ms, uint16_t *w, uint32_t n);
int scd_init(scd_t *d);
int scd_start(scd_t *d, int lowpower);
int scd_stop(scd_t *d);
int scd_ready(scd_t *d, int *ready);
int scd_read(scd_t *d, scd_meas_t *m);
int scd_wait_read(scd_t *d, scd_meas_t *m, uint32_t poll_ms, uint32_t max_ms, uint32_t *polls);
int scd_single(scd_t *d, scd_meas_t *m);
int scd_set_pressure(scd_t *d, uint16_t hpa);
int scd_config(scd_t *d, uint16_t alt_m, uint8_t asc, uint8_t *changed);
int scd_persist(scd_t *d);
int scd_frc(scd_t *d, uint16_t ppm, int16_t *corr);
int scd_sleep(scd_t *d);
int scd_wake(scd_t *d);
void scd_convert(scd_meas_t *m);

#endif

scd103e.c — the driver: CRC-8, commands with words, checked reads, measurement, pressure, altitude, calibration and sleep.

/* scd103e.c — раздел 103г: драйвер SCD40/SCD41. Команда — два байта; слово данных — два байта и CRC-8; после
 * команды — пауза на её выполнение; настройки — только при остановленном измерении. Регистров микроконтроллера
 * здесь нет: тот же файл собирается в прошивку и в стенд на компьютере. */
#include "scd103e.h"

/* CRC-8 Sensirion: полином x^8 + x^5 + x^4 + 1 (0x31), начало 0xFF, старший бит первым, без итогового XOR */
uint8_t scd_crc(const uint8_t *p, uint32_t n)
{
    uint8_t crc = 0xFFu;
    while (n--) {
        crc ^= *p++;
        for (uint32_t b = 0; b < 8u; b++)
            crc = (crc & 0x80u) ? (uint8_t)((crc << 1) ^ 0x31u) : (uint8_t)(crc << 1);
    }
    return crc;
}

void scd_dev_init(scd_t *d, uint8_t scd41)
{
    *d = (scd_t){0};
    d->addr = SCD_ADDR;
    d->scd41 = scd41;
    d->check_crc = 1;
    d->wait = 1;
    d->guard = 1;
    d->state = SCD_IDLE;
}

static int xfer_rc(scd_t *d, int rc)
{
    d->xfers++;
    if (rc == SCD_E_NACK)
        d->nacks++;
    return rc;
}

static void pause_ms(scd_t *d, uint32_t ms)
{
    if (d->wait && ms)
        scd_port_delay_ms(ms);
}

/* команда без данных: два байта, затем время выполнения */
int scd_cmd(scd_t *d, uint16_t cmd, uint32_t ms)
{
    uint8_t tx[2] = {(uint8_t)(cmd >> 8), (uint8_t)cmd};
    int rc = xfer_rc(d, scd_port_write(d->addr, tx, 2));
    if (rc == SCD_OK)
        pause_ms(d, ms);
    return rc;
}

/* команда со словом: два байта команды, слово старшим байтом вперёд и CRC-8 этого слова */
int scd_cmd_word(scd_t *d, uint16_t cmd, uint16_t w, uint32_t ms)
{
    uint8_t tx[5] = {(uint8_t)(cmd >> 8), (uint8_t)cmd, (uint8_t)(w >> 8), (uint8_t)w, 0};
    int rc;
    tx[4] = scd_crc(&tx[2], 2);
    rc = xfer_rc(d, scd_port_write(d->addr, tx, 5));
    if (rc == SCD_OK)
        pause_ms(d, ms);
    return rc;
}

/* чтение n слов: по три байта — старший, младший, CRC; слово с неверной CRC — ошибка всего чтения */
static int read_words(scd_t *d, uint16_t *w, uint32_t n)
{
    uint8_t rx[9];
    int rc;
    if (n == 0u || n > 3u)
        return SCD_E_ARG;
    rc = xfer_rc(d, scd_port_read(d->addr, rx, 3u * n));
    if (rc != SCD_OK)
        return rc;
    for (uint32_t i = 0; i < n; i++) {
        if (d->check_crc && scd_crc(&rx[3u * i], 2) != rx[3u * i + 2u]) {
            d->crc_errors++;
            return SCD_E_CRC;
        }
        w[i] = (uint16_t)(rx[3u * i] << 8 | rx[3u * i + 1u]);
    }
    return SCD_OK;
}

/* команда, время выполнения, затем чтение ответа */
int scd_query(scd_t *d, uint16_t cmd, uint32_t ms, uint16_t *w, uint32_t n)
{
    int rc = scd_cmd(d, cmd, ms);
    return rc != SCD_OK ? rc : read_words(d, w, n);
}

static int running(const scd_t *d)
{
    return d->state == SCD_PERIODIC || d->state == SCD_LOWPOWER;
}

int scd_start(scd_t *d, int lowpower)
{
    int rc = scd_cmd(d, lowpower ? SCD_START_LOWPOWER : SCD_START_PERIODIC, 0);
    if (rc == SCD_OK)
        d->state = lowpower ? SCD_LOWPOWER : SCD_PERIODIC;
    return rc;
}

int scd_stop(scd_t *d)
{
    int rc = scd_cmd(d, SCD_STOP_PERIODIC, SCD_MS_STOP);
    if (rc == SCD_OK)
        d->state = SCD_IDLE;
    return rc;
}

/* настройки — только в покое: остановить измерение и запомнить, что оно шло */
static int pause_meas(scd_t *d, uint8_t *was)
{
    *was = d->state;
    if (running(d) && d->guard)
        return scd_stop(d);
    return SCD_OK;
}

static int resume_meas(scd_t *d, uint8_t was, int rc)
{
    if ((was == SCD_PERIODIC || was == SCD_LOWPOWER) && d->state == SCD_IDLE) {
        int r2 = scd_start(d, was == SCD_LOWPOWER);
        if (rc == SCD_OK)
            rc = r2;
    }
    return rc;
}

/* после включения: остановить измерение (если шло с прошлого запуска), разбудить (если спал), перечитать EEPROM */
int scd_init(scd_t *d)
{
    int rc = scd_cmd(d, SCD_STOP_PERIODIC, SCD_MS_STOP);
    if (rc != SCD_OK && (rc = scd_wake(d)) != SCD_OK)
        return rc;
    d->state = SCD_IDLE;
    return scd_cmd(d, SCD_REINIT, SCD_MS_REINIT);
}

/* есть ли новое измерение: младшие 11 бит слова не ноль */
int scd_ready(scd_t *d, int *ready)
{
    uint16_t w;
    int rc = scd_query(d, SCD_DATA_READY, SCD_MS_SET, &w, 1);
    *ready = rc == SCD_OK && (w & SCD_READY_MASK) != 0u;
    return rc;
}

/* °C = −45 + 175 · слово / 65535, %RH = 100 · слово / 65535 — в сотых, с округлением */
void scd_convert(scd_meas_t *m)
{
    m->t100 = (int16_t)(-4500 + (int32_t)((17500u * m->t_raw + 32767u) / 65535u));
    m->rh100 = (uint16_t)((10000u * m->rh_raw + 32767u) / 65535u);
}

int scd_read(scd_t *d, scd_meas_t *m)
{
    uint16_t w[3];
    int rc = scd_query(d, SCD_READ_MEAS, SCD_MS_READ, w, 3);
    if (rc != SCD_OK)
        return rc;
    m->co2 = w[0];
    m->t_raw = w[1];
    m->rh_raw = w[2];
    scd_convert(m);
    return SCD_OK;
}

/* ждать новое измерение: спрашивать data_ready раз в poll_ms, не дольше max_ms; потом прочитать */
int scd_wait_read(scd_t *d, scd_meas_t *m, uint32_t poll_ms, uint32_t max_ms, uint32_t *polls)
{
    uint32_t waited = 0;
    int ready, rc;
    *polls = 0;
    for (;;) {
        (*polls)++;
        if ((rc = scd_ready(d, &ready)) != SCD_OK)
            return rc;
        if (ready)
            return scd_read(d, m);
        if (waited >= max_ms)
            return SCD_E_TIMEOUT;
        scd_port_delay_ms(poll_ms);
        waited += poll_ms;
    }
}

/* SCD41: одно измерение из покоя или сна — 5 с, затем обычное чтение */
int scd_single(scd_t *d, scd_meas_t *m)
{
    int rc;
    if (!d->scd41)
        return SCD_E_MODEL;
    if (running(d))
        return SCD_E_BUSY;
    if (d->state == SCD_SLEEP && (rc = scd_wake(d)) != SCD_OK)
        return rc;
    if ((rc = scd_cmd(d, SCD_SINGLE_SHOT, SCD_MS_SINGLE)) != SCD_OK)
        return rc;
    return scd_read(d, m);
}

/* давление — единственная настройка, которую можно менять на ходу */
int scd_set_pressure(scd_t *d, uint16_t hpa)
{
    if (hpa < SCD_P_MIN || hpa > SCD_P_MAX)
        return SCD_E_ARG;
    return scd_cmd_word(d, SCD_PRESSURE, hpa, SCD_MS_SET);
}

/* высота и автокалибровка: прочитать, записать только то, что отличается; *changed — было ли что записать */
int scd_config(scd_t *d, uint16_t alt_m, uint8_t asc, uint8_t *changed)
{
    uint16_t w;
    uint8_t was;
    int rc;
    *changed = 0;
    if (alt_m > SCD_ALT_MAX || asc > 1u)
        return SCD_E_ARG;
    if ((rc = pause_meas(d, &was)) != SCD_OK)
        return rc;
    if ((rc = scd_query(d, SCD_GET_ALTITUDE, SCD_MS_SET, &w, 1)) == SCD_OK && w != alt_m) {
        rc = scd_cmd_word(d, SCD_SET_ALTITUDE, alt_m, SCD_MS_SET);
        *changed = 1;
    }
    if (rc == SCD_OK && (rc = scd_query(d, SCD_GET_ASC, SCD_MS_SET, &w, 1)) == SCD_OK && w != asc) {
        rc = scd_cmd_word(d, SCD_SET_ASC, asc, SCD_MS_SET);
        *changed = 1;
    }
    return resume_meas(d, was, rc);
}

/* настройки — в EEPROM; у EEPROM ограниченное число записей: звать, только когда что-то изменилось */
int scd_persist(scd_t *d)
{
    uint8_t was;
    int rc = pause_meas(d, &was);
    if (rc == SCD_OK && (rc = scd_cmd(d, SCD_PERSIST, SCD_MS_PERSIST)) == SCD_OK)
        d->persists++;
    return resume_meas(d, was, rc);
}

/* принудительная калибровка: датчик не меньше 3 минут мерил воздух с известной концентрацией ppm */
int scd_frc(scd_t *d, uint16_t ppm, int16_t *corr)
{
    uint16_t w = SCD_FRC_FAIL;
    uint8_t was;
    int rc = pause_meas(d, &was);
    if (rc == SCD_OK && (rc = scd_cmd_word(d, SCD_FRC, ppm, SCD_MS_FRC)) == SCD_OK
        && (rc = read_words(d, &w, 1)) == SCD_OK) {
        if (w == SCD_FRC_FAIL)
            rc = SCD_E_FRC;
        else
            *corr = (int16_t)((int32_t)w - (int32_t)SCD_FRC_ZERO);
    }
    return resume_meas(d, was, rc);
}

int scd_sleep(scd_t *d)
{
    uint8_t was;
    int rc = pause_meas(d, &was);
    if (rc == SCD_OK && (rc = scd_cmd(d, SCD_POWER_DOWN, SCD_MS_POWER_DOWN)) == SCD_OK)
        d->state = SCD_SLEEP;
    return rc;
}

/* пробуждение: первая команда будит, но датчик её не подтверждает — NACK ожидаем; вторая должна пройти */
int scd_wake(scd_t *d)
{
    uint8_t tx[2] = {(uint8_t)(SCD_WAKE_UP >> 8), (uint8_t)SCD_WAKE_UP};
    int rc;
    (void)xfer_rc(d, scd_port_write(d->addr, tx, 2));
    scd_port_delay_ms(SCD_MS_WAKE);
    rc = scd_cmd(d, SCD_WAKE_UP, SCD_MS_WAKE);
    if (rc == SCD_OK)
        d->state = SCD_IDLE;
    return rc;
}

fw103e.c — F103 firmware: I2C1 at 100 kHz, sensor start-up, a measurement every 5 s, pressure and FRC via the debugger.

/* fw103e.c — раздел 103г: датчик CO₂ SCD40/SCD41 на STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц.
 * I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 100 кГц, подтяжки — на модуле датчика). SysTick — 1 мс.
 * После включения: 30 мс, scd_init, высота и автокалибровка — записать, только если отличаются, и тогда же
 * сохранить в EEPROM; затем измерение раз в 5 с. Результат — в переменных для отладчика. Через отладчик:
 * pressure_hpa — давление (700…1200, на ходу), frc_ppm — принудительная калибровка на эту концентрацию
 * (датчик перед этим не меньше 3 минут на свежем воздухе). Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "scd103e.h"

#define CPU_HZ          72000000u
#define APB1_MHZ        36u
#define I2C_TIMEOUT_US  2000u           /* дольше любого флага на 100 кГц: байт — 90 мкс */
#define POLL_MS         1000u           /* data_ready — раз в секунду */
#define ALTITUDE_M      0u              /* высота установки над уровнем моря, м */
#define ASC_ON          1u              /* автокалибровка: только если датчик хотя бы раз в неделю видит улицу */

static scd_t dev;
static volatile uint32_t ms;
volatile uint32_t co2_ppm, readings, errors, polls, pressure_hpa, frc_ppm;
volatile int32_t t_c100, rh100, frc_corr, last_rc;

void SysTick_Handler(void)
{
    ms++;
}

void scd_port_delay_ms(uint32_t n)
{
    uint32_t t0 = ms;
    while (ms - t0 < n) {
        __WFI();
    }
}

static int expired(uint32_t t0, uint32_t us)
{
    return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}

static void i2c1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u;  /* PB6, PB7 — альтернативная функция, открытый сток */
    I2C1->CR1 = 0;                                          /* настраивать при PE = 0 */
    I2C1->CR2 = APB1_MHZ;
    I2C1->CCR = 180u;                                       /* Sm: Thigh = Tlow = 180 × 27,8 нс = 5 мкс — 100 кГц */
    I2C1->TRISE = APB1_MHZ * 1000u / 1000u + 1u;            /* 1000 нс × 36 МГц = 36 → 36 + 1 = 37 */
    I2C1->CR1 = I2C_CR1_PE;
}

static int sr1_wait(uint16_t mask)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!(I2C1->SR1 & mask)) {
        if (I2C1->SR1 & I2C_SR1_AF)
            return SCD_E_NACK;                              /* адрес или байт не подтверждён: датчик занят */
        if (expired(t0, I2C_TIMEOUT_US))
            return SCD_E_BUS;
    }
    return SCD_OK;
}

static int fail(int rc)
{
    I2C1->CR1 |= I2C_CR1_STOP;
    I2C1->SR1 = (uint16_t)~I2C_SR1_AF;                      /* AF снимается записью нуля */
    I2C1->CR1 |= I2C_CR1_ACK;
    return rc;
}

static int start(uint8_t addr_rw)
{
    int rc;
    uint32_t t0 = DWT->CYCCNT;
    while (I2C1->SR2 & I2C_SR2_BUSY) {                      /* SDA держит ведомый — раздел 17.6 */
        if (expired(t0, I2C_TIMEOUT_US))
            return SCD_E_BUS;
    }
    I2C1->CR1 |= I2C_CR1_ACK;
    I2C1->CR1 |= I2C_CR1_START;
    if ((rc = sr1_wait(I2C_SR1_SB)) != SCD_OK)
        return rc;
    I2C1->DR = addr_rw;
    return sr1_wait(I2C_SR1_ADDR);
}

int scd_port_write(uint8_t addr7, const uint8_t *tx, uint32_t n)
{
    int rc;
    if ((rc = start((uint8_t)(addr7 << 1))) != SCD_OK)
        return fail(rc);
    (void)I2C1->SR2;
    for (uint32_t i = 0; i < n; i++) {
        if ((rc = sr1_wait(I2C_SR1_TXE)) != SCD_OK)
            return fail(rc);
        I2C1->DR = tx[i];
    }
    if ((rc = sr1_wait(I2C_SR1_BTF)) != SCD_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_STOP;
    return SCD_OK;
}

/* приём n ≥ 3 байт ведущим — способ 2, RM0008, с. 762–763 (драйвер читает 3 или 9) */
int scd_port_read(uint8_t addr7, uint8_t *rx, uint32_t n)
{
    int rc;
    uint32_t i = 0;
    if (n < 3u)
        return SCD_E_ARG;
    if ((rc = start((uint8_t)(addr7 << 1 | 1u))) != SCD_OK)
        return fail(rc);
    (void)I2C1->SR2;                                        /* ADDR снят, ACK = 1 */
    for (; i + 3u < n; i++) {
        if ((rc = sr1_wait(I2C_SR1_RXNE)) != SCD_OK)
            return fail(rc);
        rx[i] = (uint8_t)I2C1->DR;
    }
    if ((rc = sr1_wait(I2C_SR1_BTF)) != SCD_OK)             /* осталось три: N−2 в DR, N−1 в сдвиговом */
        return fail(rc);
    I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
    rx[i++] = (uint8_t)I2C1->DR;                            /* N−2; пошёл приём N с NACK */
    if ((rc = sr1_wait(I2C_SR1_BTF)) != SCD_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_STOP;
    rx[i++] = (uint8_t)I2C1->DR;                            /* N−1 */
    if ((rc = sr1_wait(I2C_SR1_RXNE)) != SCD_OK)
        return fail(rc);
    rx[i] = (uint8_t)I2C1->DR;                              /* N */
    I2C1->CR1 |= I2C_CR1_ACK;
    return SCD_OK;
}

static void sensor_up(void)
{
    uint8_t changed;
    while (scd_init(&dev) != SCD_OK)
        scd_port_delay_ms(1000u);                           /* нет ответа: провод, адрес, питание, подтяжки */
    if (scd_config(&dev, ALTITUDE_M, ASC_ON, &changed) == SCD_OK && changed)
        (void)scd_persist(&dev);                            /* EEPROM — только когда что-то изменилось */
    (void)scd_start(&dev, 0);
}

int main(void)
{
    scd_meas_t m;
    uint32_t p_set = 0, n;
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    SysTick_Config(CPU_HZ / 1000u);
    i2c1_init();
    scd_port_delay_ms(SCD_MS_STARTUP);                      /* после включения датчику нужно 30 мс */
    scd_dev_init(&dev, 1);
    sensor_up();
    for (;;) {
        int rc = scd_wait_read(&dev, &m, POLL_MS, 2u * SCD_PERIOD_MS, &n);
        last_rc = rc;
        polls += n;
        if (rc == SCD_OK) {
            co2_ppm = m.co2;
            t_c100 = m.t100;
            rh100 = m.rh100;
            readings++;
        } else if (rc != SCD_E_CRC) {
            errors++;                                       /* молчит дольше двух периодов: заново */
            sensor_up();
            continue;
        } else {
            errors++;                                       /* испорченное слово: просто следующее измерение */
        }
        if (pressure_hpa != p_set && scd_set_pressure(&dev, (uint16_t)pressure_hpa) == SCD_OK)
            p_set = pressure_hpa;                           /* давление — на ходу, без остановки */
        if (frc_ppm) {
            int16_t corr = 0;
            last_rc = scd_frc(&dev, (uint16_t)frc_ppm, &corr);   /* сама остановит измерение и запустит снова */
            frc_corr = corr;
            frc_ppm = 0;
        }
    }
}

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

Open in the interactive handbook