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 мкс.
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 Итоги
- SCD40 и SCD41 мерят CO₂ фотоакустическим NDIR и отдают слова: CO₂ в ppm, T и RH; команды по 16 бит, слово — три байта с CRC-8.
- CRC-8: полином 0x31, начало 0xFF; 0xBEEF → 0x92; любые 1–3 испорченных бита в слове ловит всегда, из 10 626 четверных пропускает 80.
- T = −45 + 175 · слово / 65535, RH = 100 · слово / 65535; в сотых без плавающей точки — на всех 65 536 словах ошибка не больше 0,5 сотой.
- Периодическое измерение — раз в 5 с, экономное — раз в 30 с, однократное — 5 с, только SCD41; новое измерение — по младшим 11 битам data_ready.
- После команды — пауза: 500 мс после стопа, 1 мс после чтения, 400 мс
после FRC, 800 мс после
persist_settings. - Настройки — только после остановки, кроме давления; без высоты на 1500 м в модели −132 ppm.
- ASC — только если датчик видит свежий воздух; FRC — на улице с опорой 400 ppm после 3 минут работы; EEPROM — 2000 записей, сохранять только изменения.
Полные исходники
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.