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

103г. Датчик CO₂ SCD40/SCD41 по I2C: команды и CRC-8, периодическое и однократное измерение, ppm, °C и %RH, давление и калибровка

В разделе 103 MQ мерил газ нагретым слоем, в 103в PMS5003 считал пылинки лазером. SCD40 и SCD41 мерят углекислый газ светом: сколько инфракрасного излучения поглотил воздух в маленькой камере. Ответ — готовое число в миллионных долях (ppm), а рядом температура и влажность. Здесь — как устроен обмен по I2C: команды по 16 бит, слова с CRC-8 и паузы на выполнение; как проверить CRC на примере 0xBEEF → 0x92; как мерить раз в 5 с, раз в 30 с или по одному разу (SCD41); как перевести сырые слова в °C и %RH; зачем датчику давление и высота; как работают автоматическая и принудительная калибровка и почему сохранять настройки в EEPROM на каждом запуске нельзя.

Опоры: RM0008 (DocID13902 Rev 17): приём больше двух байт ведущим, способ 2, с. 762–763; I2C_CCR, с. 783–784; I2C_TRISE, с. 784. DocID13587 Rev 17 (STM32F103x8/xB): PB6 — I2C1_SCL, PB7 — I2C1_SDA, оба FT, с. 32. Шина I2C и её настройка — раздел 17, здесь не повторяются. Документа Sensirion на SCD40/SCD41 в проекте нет. Коды команд, время их выполнения, CRC-8 (полином 0x31, начало 0xFF), маска слова data_ready, формулы °C и %RH, ответ принудительной калибровки, пределы давления и высоты, 2000 записей EEPROM, периоды автокалибровки и периоды измерения 5 с и 30 с взяты из драйвера scd4x Zephyr 4.4.2 на диске, пример 0xBEEF → 0x92 — из его теста CRC. Адрес 0x62 и принцип измерения — по документации производителя, не проверено. Все числа — прогон драйвера _verify/co2_103e/scd103e.c на компьютере (gcc 12.2.0) со своей моделью SCD41 как ведомого I2C на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3. Как датчик отвечает на ошибки, прогрев, дрейф, шум и сами алгоритмы калибровки в модели — допущения: числа ниже говорят о модели, а не о датчике.

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

Оригинал Перевод
NDIR (non-dispersive infrared) недисперсионный инфракрасный метод
photoacoustic фотоакустический: поглощённый свет слышен как звук
ppm (parts per million) миллионная доля, 1 ppm = 0,0001 %
periodic / low power periodic measurement периодическое (5 с) / экономное (30 с) измерение
single shot однократное измерение
data ready status «есть новое измерение»
command execution time время выполнения команды
ambient pressure / sensor altitude давление окружающего воздуха / высота установки
automatic self-calibration (ASC) автоматическая калибровка
forced recalibration (FRC) принудительная калибровка
persist settings сохранить настройки в EEPROM

103г.1 Принцип: фотоакустический NDIR и что выходит наружу

Молекулы CO₂ поглощают инфракрасный свет в узкой полосе. По документации производителя (не проверено) внутри SCD4x — источник ИК, камера с воздухом и микрофон: источник светит импульсами, CO₂ поглощает часть света и чуть нагревается, газ в камере расширяется и сжимается в такт импульсам — получается звук. Чем больше CO₂, тем громче. Отсюда «фотоакустический»: поглощение слышно, а не видно на фотоприёмнике.

Микрофон слышит число молекул CO₂ в камере, а не их долю. При том же воздухе на высоте молекул в камере меньше, и показание падает — поэтому датчику нужно знать давление (103г.5). Рядом в корпусе — датчик температуры и влажности: его слова приходят вместе с CO₂.

SCD40 SCD41
периодическое измерение раз в 5 с раз в 5 с
экономное по документации производителя, не проверено раз в 30 с
однократное (0x219D, 5 с) нет есть
сон и пробуждение (0x36E0, 0x36F6) — для однократного режима
периоды автокалибровки (44 и 156 ч) в Zephyr недоступны меняются командами

Так разделяет их драйвер Zephyr 4.4.2: режим «однократно» и периоды автокалибровки там есть только у SCD41. На улице CO₂ — около 400 ppm, в закрытой комнате с людьми — в разы больше; насколько точен сам датчик, без документа сказать нельзя.


103г.2 Подключение к F103 и обмен: команда, слово, CRC, пауза

Вывод модуля STM32F103 Назначение
SCL PB6 такт I2C1_SCL
SDA PB7 данные I2C1_SDA
VDD 3,3 В питание
GND GND земля

Подтяжки SCL и SDA обычно стоят на модуле. PB6 и PB7 — FT (с. 32): если модуль подтянут к 5 В, вывод F103 это выдержит. Обмен редкий — девять байт раз в 5 с, поэтому шина на 100 кГц, стандартный режим. I2C1 сидит на APB1 = 36 МГц: в стандартном режиме Thigh = Tlow = CCR · TPCLK1 (с. 784), CCR = 36 МГц / (2 × 100 кГц) = 180 — по 5 мкс на половину такта; TRISE = 1000 нс × 36 МГц + 1 = 37 (там же: в стандартном режиме фронт до 1000 нс):

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

У SCD4x нет регистров. Есть команды по 16 бит, старшим байтом вперёд. Команда бывает трёх видов: просто два байта; два байта и слово данных с его CRC; два байта, пауза и чтение ответа отдельной транзакцией. Пока датчик выполняет команду, он не отвечает — ни на запись, ни на чтение; сколько ждать, записано в таблице драйвера Zephyr:

Команда Код Ждать, мс Ответ
start_periodic_measurement 0x21B1 0 —
start_low_power_periodic_measurement 0x21AC 0 —
stop_periodic_measurement 0x3F86 500 —
read_measurement 0xEC05 1 3 слова: CO₂, T, RH
get_data_ready_status 0xE4B8 1 1 слово
set / get_sensor_altitude 0x2427 / 0x2322 1 — / 1 слово
set / get_ambient_pressure 0xE000 1 — / 1 слово
perform_forced_recalibration 0x362F 400 1 слово
set / get_automatic_self_calibration 0x2416 / 0x2313 1 — / 1 слово
persist_settings 0x3615 800 —
reinit 0x3646 30 —
measure_single_shot 0x219D 5000 —
power_down / wake_up 0x36E0 / 0x36F6 1 / 30 —

Слово данных идёт тремя байтами: старший, младший и CRC-8 этих двух. Запись со словом — пять байт:

/* команда со словом: два байта команды, слово старшим байтом вперёд и 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;
}

Ответ читается отдельной транзакцией — без повторного старта, как у регистровых датчиков в 17.2, а после паузы. CRC проверяется у каждого слова:

/* чтение 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;
}

Приём 3 и 9 байт в прошивке — способ 2 из RM0008 (с. 762–763), как в 103б. Чтение измерения в модели при 800 ppm, 23,4 °C и 41,2 %: запись EC 05, пауза 1 мс, затем 03 20 2A 64 0F 79 69 78 56 — три слова 0x0320, 0x640F, 0x6978, за каждым его CRC. На 100 кГц запись команды идёт 290 мкс, чтение девяти байт — 920 мкс, всё чтение вместе с паузой — 2210 мкс.

Вверху — подключение SCD41 к STM32F103: PB6 — SCL, PB7 — SDA на I2C1 на 100 кГц, подтяжки на модуле; внутри датчика ИК-источник, камера и микрофон, рядом датчик температуры и влажности; фотоакустический NDIR, измерение раз в 5 с или экономно раз в 30 с. В середине — чтение измерения: адрес с записью, команда EC 05, пауза 1 мс, адрес с чтением и девять байт 03 20 2A, 64 0F 79, 69 78 56 — слова CO₂, T и RH, у каждого своя CRC-8; 0x0320 — 800 ppm, T = −45 + 175 · 0x640F / 65535 = 23,40 °C, RH = 100 · 0x6978 / 65535 = 41,20 %; CRC-8 с полиномом 0x31 и началом 0xFF, 0xBEEF → 0x92. Внизу — порядок команд: start_periodic 0x21B1, data_ready 0xE4B8, read_measurement 0xEC05, stop_periodic 0x3F86 с паузой 500 мс, затем калибровка FRC 0x362F 400 мс, высота или ASC, persist_settings 0x3615 800 мс — только при изменении; однократное измерение 0x219D 5 с — только у SCD41


103г.3 CRC-8 Sensirion: полином 0x31, начало 0xFF

CRC считается по двум байтам слова, старшим битом вперёд, с началом 0xFF и без итогового XOR. Полином 0x31 — 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;
}

Проверочный пример — слово 0xBEEF: CRC = 0x92. Ровно это ждёт тест CRC в Zephyr 4.4.2 (crc8 с полиномом 0x31 и началом 0xFF), и это же дают scd_crc на стенде и независимый расчёт на Python. Ещё несколько слов:

Слово 0x0000 0x0001 0x0190 0x0320 0x8000 0xFFFF
CRC 0x81 0xB0 0x4C 0x2A 0xA2 0xAC

У нуля CRC не ноль — это и даёт начало 0xFF: оборванная линия, которая читается как сплошные нули или сплошные единицы, CRC не пройдёт. Слово 0x0320 — те самые 800 ppm из кадра выше.

Какие порчи CRC ловит: перебор всех наборов испорченных бит в трёх байтах «слово + CRC» для 0xBEEF:

Испорчено бит 1 2 3 4
наборов 24 276 2024 10 626
CRC не заметила 0 0 0 80

Любые одну, две и три ошибки в слове CRC ловит всегда; пропускает только часть четверных. Это сильнее простой суммы PMS5003 из 103в, которая пропускала каждую 33-ю двойную порчу. Тот же класс защиты — PEC у MLX90614 в 104б, но полином там 0x07, а начало 0.

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

Проверка CRC Принято верных Принято неверных Отвергнуто по CRC Прочие ошибки Наибольшая ошибка CO₂, ppm
есть 3983 0 2017 0 0
нет 4920 532 0 548 32 768

С проверкой ни одного неверного измерения не принято — испорченное просто пропало, прошивка ждёт следующего через 5 с. Без проверки 532 неверных ушли в программу, и один перевёрнутый старший бит дал ошибку 32 768 ppm. Отвергнуто много, потому что портились и ответы data_ready: их драйвер спрашивает до шести раз на одно измерение.


103г.4 Измерение: старт, data_ready, перевод в ppm, °C и %RH

Запуск. После включения питания датчику нужно 30 мс (константа драйвера Zephyr), прошивка их выжидает. Затем scd_init: остановить измерение — вдруг оно идёт с прошлого запуска, а микроконтроллер просто перезагрузился; не ответил — значит, спит, будить; затем reinit — перечитать настройки из EEPROM. В модели на свежем датчике — две команды и 530,58 мс: 500 мс стоп, 30 мс reinit. После сброса микроконтроллера, когда датчик продолжал мерить, — то же самое. Спящий датчик стоп не подтверждает, и путь другой: 4 обмена, из них 2 NACK, 91,16 мс.

Пробуждение. Команду wake_up датчик выполняет, но не подтверждает — так написано и в драйвере Zephyr, который шлёт её дважды. Первый NACK ожидаем, вторая команда должна пройти:

/* пробуждение: первая команда будит, но датчик её не подтверждает — 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. После start_periodic_measurement новое измерение появляется раз в 5 с. Спросить, есть ли оно, — команда 0xE4B8: ответ — слово, и новое измерение есть, если его младшие 11 бит не ноль. Старшие биты смотреть нельзя: в модели взведён бит 15, и слово без данных — 0x8000, с данными — 0x8006. Сравнение всего слова с нулём видело бы данные всегда.

/* есть ли новое измерение: младшие 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;
    }
}

В модели час периодического режима с опросом раз в секунду: измерений — 720, первое — через 5013 мс после старта, дальше ровно через 5000 мс, data_ready на одно измерение — 5,99 раза, обменов за час — 10 067, ни одного NACK и ни одной ошибки CRC. Экономный режим — раз в 30 с: за час 120 измерений, первое через 30 054 мс; но опрос раз в секунду там — 30,95 вопроса на ответ и 7671 обмен: опрашивать стоит реже.

Читать вслепую, без data_ready, нельзя: в модели чтение без нового измерения получает NACK (допущение модели). 60 чтений раз в секунду — 12 удачных и 48 NACK.

Однократное измерение (SCD41). Команда 0x219D — одно измерение за 5 с, потом обычное чтение. Между измерениями датчик можно усыпить (power_down), и до следующего раза он не мерит. В модели сразу после пробуждения показания завышены (допущение модели, см. 103г.6): цикл раз в 5 минут, проснуться и взять два измерения подряд — 834 и 810 ppm при истинных 800, на оба 10 066 мс. Поэтому первое после пробуждения выбрасывают. У SCD40 этой команды нет: драйвер сразу возвращает −8, SCD_E_MODEL, а голая команда в модели получает NACK.

Перевод слов. CO₂ — слово как есть, в ppm. Температура и влажность — по формулам драйвера Zephyr:

T = −45 + 175 · слово / 65535 °C, RH = 100 · слово / 65535 %.

Без плавающей точки (у F103 её нет) — в сотых с округлением:

/* °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 меньше 2³², переполнения нет. На стенде прогнаны все 65 536 слов: наибольшая разница с точной формулой — 0,5 сотой, расхождений с правильным округлением — 0. В кадре выше 0x640F = 25 615 даёт 23,40 °C, 0x6978 = 27 000 — 41,20 %. Слово 0x6666 — ровно 25,00 °C и 40,00 %; 0x0000 — −45 °C и 0 %, 0xFFFF — 130 °C и 100 %.


103г.5 Давление и высота

Датчик слышит число молекул, а показывает долю. Чтобы пересчитать одно в другое, ему нужно давление. По умолчанию высота — 0: датчик считает давление уровнем моря. Сказать ему правду можно двумя способами: один раз задать высоту установки (0x2427, 0…3000 м) или время от времени передавать давление с барометра (0xE000, 700…1200 гПа). Пределы — как в драйвере Zephyr; вне их scd_set_pressure возвращает −7, SCD_E_ARG (в модели — для 699 и 1201).

В модели показание пропорционально p / p_датчика, а давление по высоте датчик берёт из стандартной атмосферы (допущение модели; как пересчитывает настоящий датчик, не проверено). На 1500 м это 845,6 гПа против 1013,25 у моря. Истинно — 800 ppm, шум выключен:

Что сказано датчику Давление на деле, гПа Показание, ppm Ошибка, ppm
ничего (высота 0) 845,6 668 −132
высота 1500 м 845,6 800 0
давление 846 гПа 845,6 800 0
высота 1500 м, погода −15 гПа 830,6 786 −14
высота 1500 м, погода +15 гПа 860,6 814 14

Высота убирает главное — горы и этажи; давление с барометра убирает ещё и погоду. Давление — единственная настройка, которую можно менять во время измерения: так делает и драйвер Zephyr. В модели scd_set_pressure во время измерения — 0, а запись высоты без остановки — NACK, −1. Драйвер сам останавливает измерение перед настройкой и запускает снова:

/* высота и автокалибровка: прочитать, записать только то, что отличается; *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 Прогрев, автокалибровка, FRC и EEPROM

Прогрев. Сколько у настоящего датчика длится переходный процесс после включения, без документа не сказать. В модели показание после включения или пробуждения завышено на 120 · e^(−t / 4 с) ppm (допущение модели). Шум выключен, измерение запущено через 0,56 с после включения (30 мс ожидания и 530 мс scd_init):

После старта измерения, с 5 10 15 20 30 60
ошибка, ppm 30 9 2 1 0 0

Отсюда правило прошивки: первое измерение после включения и пробуждения не брать, а калибровать — только после долгой работы.

Автокалибровка (ASC). NDIR-датчик со временем уходит. Автокалибровка исходит из того, что датчик время от времени видит свежий воздух, и подтягивает к нему свой ноль. В драйвере Zephyr она включена с завода, а у SCD41 первый период — 44 ч, следующие — по 156 ч. В модели в конце периода наименьшее показание за период объявляется 400 ppm (допущение модели: настоящий алгоритм не раскрыт). Дрейф +80 ppm, шум выключен, ошибка ночного показания в 03:00 по дням:

Помещение Ночью на деле, ppm День 1 День 2 Дни 3–10
офис, ночью проветрено, ASC включена 420 80 −20 −20
спальня, ночью закрыта, ASC включена 650 80 −250 −250
офис, ASC выключена 420 80 80 80

В офисе автокалибровка убрала дрейф (−20 — это 420 против 400). В спальне, где воздух ни разу не бывает свежим, она решила, что 730 — это 400, и стала занижать на 250 ppm: опаснее, чем было без неё. Если датчик никогда не видит улицу — в теплице, в погребе, в непроветриваемой комнате, — автокалибровку выключают и калибруют руками.

Принудительная калибровка (FRC). Датчик выносят на свежий воздух, дают поработать (драйвер Zephyr требует не меньше 3 минут в периодическом режиме при ровной концентрации), останавливают измерение и шлют 0x362F с опорной концентрацией — для улицы 400 ppm. Через 400 мс датчик отвечает словом: 0x8000 плюс поправка, а 0xFFFF — отказ:

/* принудительная калибровка: датчик не меньше 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);
}

В модели FRC берёт среднее последних 12 измерений, а знак поправки — «опорная минус измеренная» (допущения модели). Дрейф +87 ppm, шум 10 ppm, 3 минуты работы:

Случай Истинно, ppm До, ppm Код Поправка После, ppm Ошибка после, ppm
улица, опора 400 400 489,8 0 −90 401,2 1,2
без остановки измерения 400 489,8 −1 — 491,1 91,1
комната, а опора 400 600 689,8 0 −290 401,2 −198,8

Калибровка на улице сняла дрейф. Без остановки измерения датчик команду не принял — NACK. А опора 400 ppm в комнате, где на деле 600, испортила датчик на 200 ppm: FRC верит опоре, а не воздуху. Если датчик после включения ни разу не мерил, в модели ответ — 0xFFFF, и драйвер возвращает −6, SCD_E_FRC.

EEPROM. Высота и автокалибровка живут в ОЗУ датчика, а после снятия питания возвращаются из EEPROM. Сохраняет их туда persist_settings (800 мс). Драйвер Zephyr предупреждает: EEPROM гарантированно держит не меньше 2000 записей. Прошивка, которая на каждом запуске настраивает и сохраняет, на 100 запусков в модели записала EEPROM 100 раз; та, что сначала читает и сохраняет только изменения, — 1 раз. При перезапуске раз в час первая исчерпает 2000 записей за 83,3 суток:

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

Давление прошивка не сохраняет вовсе: она шлёт его на ходу.


103г.7 Грабли

Настройка во время измерения. Пока идёт периодическое измерение, датчик принимает только чтение, data_ready, остановку и давление — так устроен и драйвер Zephyr. Высота, ASC, FRC и persist_settings без stop_periodic_measurement в модели получают NACK. Драйвер останавливает измерение сам и потом запускает снова.

Не выждать паузу. После stop_periodic_measurement — 500 мс, после data_ready и read_measurement — 1 мс, после FRC — 400 мс. Без пауз в модели: стоп и сразу старт — старт получил NACK; data_ready без 1 мс — чтение получило NACK, а с паузой — 0.

Читать без data_ready. Нового измерения нет — в модели NACK: 48 из 60 чтений раз в секунду. Спрашивать data_ready и смотреть только младшие 11 бит.

Пробуждение без ответа. wake_up не подтверждается. Драйвер, который считает этот NACK ошибкой, решит, что датчика нет, хотя тот уже проснулся.

persist_settings на каждом запуске. EEPROM — не меньше 2000 записей; раз в час — меньше трёх месяцев. Сохранять только изменения.

Автокалибровка там, где не бывает свежего воздуха. В модели — минус 250 ppm в закрытой спальне. Выключить ASC или проветривать.

FRC не на улице. Опора 400 ppm в комнате с 600 — минус 200 ppm в модели. И не раньше чем через 3 минуты работы.

Высота и давление. Без них на 1500 м в модели −132 ppm. Высоту задают один раз, давление шлют на ходу.

Нет CRC. Без проверки — 532 неверных из 6000 в опыте 103г.3 и ошибка до 32 768 ppm.


103г.8 Итоги

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

scd103e.h — команды, время их выполнения, пределы, коды ошибок и функции драйвера SCD40/SCD41.

/* 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 — драйвер: CRC-8, команды со словами, чтение с проверкой, измерение, давление, высота, калибровка и сон.

/* 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: I2C1 на 100 кГц, запуск датчика, измерение раз в 5 с, давление и FRC через отладчик.

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

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

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