103е. Датчики качества воздуха CCS811 и ENS160 по I2C: eCO₂ и TVOC, режимы, прогрев и базовая линия, компенсация температуры и влажности
В 103г и 103д углекислый газ мерили светом: NDIR-датчик считает молекулы CO₂. CCS811 и ENS160 устроены иначе — внутри у них нагретый металлооксидный слой, как у MQ из 103.5, и маленький процессор, который сам греет слой, считает и отдаёт по I2C готовые числа: TVOC — сумму летучей органики в ppb — и eCO₂ в ppm. Буква «e» здесь главная: eCO₂ не измерен, а пересчитан из органики. Здесь — что это значит на деле; как запустить CCS811 (nWAKE, APP_START, HW_ID, режим) и ENS160 (OPMODE, PART_ID); как читать результат по готовности или по nINT и что делать с растягиванием такта; зачем датчику температура и влажность и в каком порядке байт их писать; почему первым минутам и первым суткам не верят; как сохранять базовую линию во Flash раз в сутки и возвращать её после включения.
Опоры: RM0008 (DocID13902 Rev 17): ведомый держит SCL — счётчик высокого
уровня ведущего стоит, пока линия не отпущена, с. 758; приём одного и
двух байт ведущим, с. 764; больше двух — способ 2, с. 762–763; страница
Flash 1 КБ у средней плотности, таблица 5, с. 54. DocID13587 Rev 17
(STM32F103x8/xB): PB6 — I2C1_SCL, PB7 — I2C1_SDA, оба FT, PB5 — не
FT, с. 32; PB8 — FT, с. 33; ресурс Flash — 10 тысяч циклов, таблица 29, с. 58.
Шина I2C — раздел 17, настройка I2C1 на 100 кГц — 103г.2, запись во
Flash — 11.5, здесь не повторяются. Документов ams и ScioSense в проекте
нет. Карта регистров обоих чипов, HW_ID 0x81, биты STATUS и ERROR_ID,
APP_START 0xF4, коды режимов, сроки 20 мс, 1 мс, 50 и 20 мкс, раскладка
ALG_RESULT_DATA и ENV_DATA, PART_ID 0x0160, OPMODE, поля DEVICE_STATUS,
TEMP_IN и RH_IN, адреса 0x5A, 0x5B и 0x53 взяты из драйверов ccs811 и
ens160 Zephyr 4.4.2 на диске, вывод его примера ccs811 — оттуда же.
Прогрев 20 мин и 48 ч приработки CCS811, адрес ENS160 0x52, уровни
AQI 1–5 и назначение DATA_MISR — по документации производителя, не
проверено. Все числа — прогон драйвера _verify/voc103g/aq103g.c на
компьютере (gcc 12.2.0) со своей моделью обоих чипов как ведомых I2C на
Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3. Физика слоя,
алгоритм внутри чипа, прогрев, старение, растягивание такта и сроки
достоверности ENS160 в модели — допущения: числа ниже говорят о модели,
а не о датчиках.
Как это называется в документации
Оригинал Перевод MOX (metal oxide) gas sensor металлооксидный газовый датчик TVOC (total volatile organic compounds) сумма летучих органических веществ (ЛОС), ppb eCO₂ (equivalent CO₂) расчётный эквивалент CO₂, ppm drive mode (MEAS_MODE) режим и период измерения CCS811 boot / application firmware (APP_START) загрузчик / прикладная прошивка внутри датчика WAKEn / INTn pins вход «разрешить обмен» / выход «новое измерение» ENV_DATA, TEMP_IN / RH_IN температура и влажность для компенсации BASELINE register регистр базовой линии: что чип считает чистым воздухом OPMODE: deep sleep / idle / standard режим ENS160: глубокий сон / ожидание / работа VALIDITY_FLAG: initial start-up признак достоверности: начальный запуск AQI-UBA индекс качества воздуха по шкале UBA, 1–5 DATA_MISR контрольное значение прочитанных данных ENS160
103е.1 Что они мерят и почему eCO₂ — не CO₂
Слой из оксида металла, нагретый до сотен градусов, проводит ток тем лучше, чем больше вокруг восстанавливающих газов — спирта, ацетона, продуктов дыхания и готовки (по документации производителя, не проверено). Чип мерит проводимость G и сравнивает её с базовой линией Gb — проводимостью в самом чистом воздухе, который он видел. Из отношения G / Gb выходит TVOC, а из TVOC — eCO₂. Углекислый газ слой не видит вовсе: в Zephyr прямо написано, что CCS811 оценивает CO₂ там, где главный источник органики — люди. Отсюда правило: eCO₂ похож на CO₂, только пока ЛОС и CO₂ приходят из одного источника — из дыхания.
В модели проводимость слоя G = G0 · (1 + ЛОС / 100 ppb)^0,7, а чип считает TVOC = 100 · ((G / Gb)^(1/0,7) − 1) и eCO₂ = 400 + 4 · TVOC у CCS811 и 400 + 3,5 · TVOC у ENS160 (допущения модели: настоящий алгоритм не раскрыт). Люди в модели добавляют 0,25 ppb органики на каждый ppm CO₂ сверх 420. Рядом — NDIR-датчик из 103г: истинный CO₂ и шум 10 ppm, как в модели SCD41. Датчики два часа поработали на чистом воздухе, затем:
| Сцена | CO₂ на деле | NDIR | eCO₂ CCS811 | eCO₂ ENS160 | TVOC CCS811 / на деле, ppb |
|---|---|---|---|---|---|
| двое в закрытой комнате, через 2 ч | 1285 | 1280 | 1262 | 1154 | 215 / 216 |
| протёрли стол спиртом, через 5 мин | 600 | 589 | 9840 | 8538 | 2360 / 2336 |
| подкормка CO₂ из баллона в теплице, через 1 ч | 1200 | 1197 | 402 | 406 | 1 / 0 |
| свежий воздух | 420 | 417 | 402 | 406 | 1 / 0 |
С людьми eCO₂ почти совпал с CO₂ — потому что модель так связала дыхание и органику. Спирт дал почти 10 000 «ppm CO₂» при настоящих 600, а баллон с CO₂ в теплице не виден совсем: 402 против 1200. Для вентиляции по людям eCO₂ годится как грубая подсказка; для теплицы, погреба, инкубатора и всего, где CO₂ есть без органики, нужен NDIR. TVOC при этом честный: 215 против 216, 2360 против 2336. Его и стоит показывать, а eCO₂ — подписывать как оценку.
| CCS811 | ENS160 | |
|---|---|---|
| адрес | 0x5A, второй — 0x5B | 0x53, второй — 0x52 (по документации) |
| узнать чип | HW_ID (0x20) = 0x81 | PART_ID (0x00) = 0x0160, младшим вперёд |
| запуск | APP_START — запись байта 0xF4 | OPMODE (0x10) = 0x02 |
| режимы | MEAS_MODE: 0 — нет, 1 с, 10 с, 60 с, 250 мс | глубокий сон, ожидание, работа |
| результат | 8 байт с 0x02: eCO₂, TVOC старшим вперёд, STATUS, ERROR_ID, RAW | с 0x20: статус, AQI, TVOC, eCO₂ — младшим вперёд |
| «новое есть» | STATUS, бит 3 | статус, бит 1 (NEWDAT) |
| ещё выводы | nWAKE (вход), nINT (выход) | INT (выход) |
| компенсация | ENV_DATA (0x05): по байту, шаг 0,5 | TEMP_IN = 64 · K, RH_IN = 512 · %RH |
| прогрев | флага нет: 20 мин по документации | поле достоверности 0–3 |
| базовая линия | регистр BASELINE (0x11) | регистра в карте Zephyr нет |
| ошибки | бит 0 STATUS, причина — в ERROR_ID | бит 6 (STATER) |
Подключение к F103 — одна шина I2C1 на 100 кГц, как в 103г.2. Питание модулей и подтяжки — по их плате; здесь — 3,3 В:
| Вывод модуля | STM32F103 | Назначение |
|---|---|---|
| SCL (оба) | PB6 | I2C1_SCL, FT |
| SDA (оба) | PB7 | I2C1_SDA, FT |
| nWAKE (CCS811) | PB5 | выход: ноль — можно говорить |
| nINT (CCS811) | PB8 | вход с подтяжкой, EXTI8 по спаду, FT |
| VCC, GND | 3,3 В, GND | питание |
PB5 не FT (с. 32), но он только выход. Датчик температуры и влажности для компенсации — SHT31 или AHT20 из раздела 97 — садится на ту же шину.
103е.2 CCS811: nWAKE, APP_START, HW_ID и режим
nWAKE. Вход активен нулём: пока на нём единица, датчик на I2C не
отвечает (так описан вывод в привязке Zephyr). Перед каждым обменом —
ноль и 50 мкс, после — единица и 20 мкс (t_WAKE и t_DWAKE в драйвере
Zephyr). Если экономить нечего, nWAKE сажают на землю, и драйвер о нём
не думает — поле has_wake = 0:
/* ── CCS811: каждый обмен — между nWAKE = 0 и nWAKE = 1, если вывод подключён ── */
static int ccs_rd(const aq_t *d, uint8_t reg, uint8_t *p, uint32_t n)
{
int rc;
if (d->has_wake) {
aq_port_wake(1);
aq_port_delay_us(CCS_US_WAKE);
}
rc = aq_port_read(d->addr, reg, p, n);
if (d->has_wake) {
aq_port_wake(0);
aq_port_delay_us(CCS_US_DWAKE);
}
return rc;
}
В модели nWAKE, оставленный висеть (has_wake = 0, а вывод не на
земле), даёт −1, AQ_E_NACK, на первом же чтении. Тот же датчик с nWAKE
на земле запускается с кодом 0.
Загрузчик и прикладная прошивка. После включения CCS811 сидит в загрузчике. Бит 4 STATUS (APP_VALID) говорит, что прикладная прошивка есть, бит 7 (FW_MODE) — что она запущена. Запускает её запись одного байта 0xF4 — адреса регистра APP_START без данных — и 1 мс ожидания. До этого 20 мс после включения питания (t_START) — их выжидает прошивка. Дальше — HW_ID, который должен быть 0x81, версия, режим и проверка, что режим принят:
/* запуск: STATUS → APP_START (0xF4) → STATUS → HW_ID = 0x81 → версия → MEAS_MODE → STATUS без ошибки */
int ccs_init(aq_t *d, uint8_t addr, uint8_t mode, uint8_t has_wake, uint8_t burnin, uint32_t now)
{
uint8_t st, b[2], cmd = CCS_APP_START;
int rc;
*d = (aq_t){0};
d->chip = AQ_CCS811;
d->addr = addr;
d->mode = mode;
d->has_wake = has_wake;
d->burnin = burnin;
d->check_ready = 1;
d->on_ms = now;
if ((rc = ccs_rd(d, CCS_STATUS, &st, 1)) != AQ_OK)
return rc;
if (!(st & CCS_ST_APP_VALID))
return AQ_E_APP; /* в датчике нет прикладной прошивки */
if (!(st & CCS_ST_FW_MODE)) {
if ((rc = ccs_wr(d, &cmd, 1)) != AQ_OK) /* APP_START — один байт, без данных */
return rc;
aq_port_delay_us(CCS_US_APP_START);
if ((rc = ccs_rd(d, CCS_STATUS, &st, 1)) != AQ_OK)
return rc;
if (!(st & CCS_ST_FW_MODE))
return AQ_E_APP;
}
if ((rc = ccs_rd(d, CCS_HW_ID, b, 1)) != AQ_OK)
return rc;
if (b[0] != CCS_HW_ID_VAL)
return AQ_E_ID;
if ((rc = ccs_rd(d, CCS_FW_APP_VER, b, 2)) != AQ_OK)
return rc;
d->app_ver = b[0];
b[0] = CCS_MEAS_MODE;
b[1] = mode;
if ((rc = ccs_wr(d, b, 2)) != AQ_OK)
return rc;
if ((rc = ccs_rd(d, CCS_STATUS, &st, 1)) != AQ_OK)
return rc;
if (st & CCS_ST_ERROR) {
(void)ccs_rd(d, CCS_ERROR_ID, b, 1); /* чтение ERROR_ID снимает ошибку */
return AQ_E_CHIP;
}
return AQ_OK;
}
В модели запуск — 7 обменов и 4070 мкс после тех 20 мс, nWAKE переключается 14 раз:
| Обмен | Что вернул датчик |
|---|---|
| чтение STATUS | 0x10 — прошивка есть, не запущена |
запись F4 |
— |
| чтение STATUS | 0x90 — прикладная прошивка работает |
| чтение HW_ID | 0x81 |
| чтение FW_APP_VERSION | 20 00 (в модели — версия 2.0) |
запись 01 10 |
режим 1, раз в секунду |
| чтение STATUS | 0x90 — ошибки нет |
Неверный режим — например, 0x50 — датчик не примет: бит ошибки в
STATUS, а в ERROR_ID — MEASMODE_INVALID (бит 2). Драйвер читает ERROR_ID,
чтение снимает ошибку (так в Zephyr), и возвращает −6, AQ_E_CHIP. Не тот
адрес — 0x5B вместо 0x5A — −1 на первом обмене.
Режимы. MEAS_MODE, биты 6:4: 0 — не мерить, 1 — раз в 1 с, 2 — раз в 10 с, 3 — раз в 60 с, 4 — раз в 250 мс; бит 3 включает nINT. В Zephyr у кода 0x40 два имени — режим 250 мс и RAW_DATA: по документации производителя в режиме 4 есть только сырые данные (не проверено). Режим на ходу Zephyr меняет только в битах прерывания: в драйвере написано, что у смены режима работающего датчика есть условия; какие — по документации производителя, здесь не проверено. Поэтому прошивка задаёт режим один раз при запуске.
103е.3 CCS811: результат, готовность, nINT и растягивание такта
ALG_RESULT_DATA — 8 байт с регистра 0x02: eCO₂ и TVOC старшим байтом вперёд, STATUS, ERROR_ID и два байта RAW. Бит 3 STATUS (DATA_READY) говорит, новое ли измерение лежит в регистрах; чтение его снимает:
/* результат: 8 байт ALG_RESULT_DATA; eCO2 и TVOC — старшим байтом вперёд, байт 4 — STATUS, байт 5 — ERROR_ID */
int ccs_read(aq_t *d, aq_meas_t *m, uint32_t now)
{
uint8_t b[8];
uint32_t up = now - d->on_ms;
int rc = ccs_rd(d, CCS_ALG_RESULT, b, 8);
if (rc != AQ_OK) {
d->n_err++;
return rc;
}
m->eco2 = (uint16_t)(b[0] << 8 | b[1]);
m->tvoc = (uint16_t)(b[2] << 8 | b[3]);
m->status = b[4];
m->error = b[5];
m->raw = (uint16_t)(b[6] << 8 | b[7]);
m->aqi = 0;
if (m->status & CCS_ST_ERROR) {
d->n_err++;
return AQ_E_CHIP;
}
if (d->check_ready && !(m->status & CCS_ST_DATA_READY)) {
d->n_stale++; /* нового измерения нет: числа — прошлые */
return AQ_E_STALE;
}
m->validity = (d->burnin && up < CCS_BURNIN_MS) ? 2u : (up < CCS_WARM_MS ? 1u : 0u);
m->valid = m->validity == 0u;
d->n_read++;
return AQ_OK;
}
RAW — то, что чип мерит на слое: младшие 10 бит — напряжение, по
1 650 000 / 1023 = 1612 мкВ на единицу, старшие 6 бит — ток в мкА. В
примере Zephyr после старта — 0,677040 В и 14 мкА; это код 420: 420 ·
1612 = 677 040 мкВ, и ccs_raw_uv даёт ровно это. Там же видно, как
датчик начинает: первые два чтения — 65 021 ppm и 65 021 ppb, потом
«нового нет», и с пятой секунды — 400 ppm и 0 ppb. Первые числа после
запуска брать нельзя.
Сколько измерений. В модели часы датчика медленнее на 0,3 % (допущение модели). За 10 минут:
| Режим | 1 (1 с) | 2 (10 с) | 3 (60 с) | 4 (250 мс) | 0 |
|---|---|---|---|---|---|
| измерений | 598 | 59 | 9 | 2392 | 0 |
В режиме 0 чтение возвращает −5, AQ_E_STALE: готовых данных нет.
Опрос против готовности. Час опроса раз в секунду:
| Режим | DATA_READY смотрим | Измерений | Принято | «Нового нет» | Принято повторно |
|---|---|---|---|---|---|
| 2 (10 с) | да | 359 | 360 | 3240 | 0 |
| 2 (10 с) | нет | 359 | 3600 | 0 | 3240 |
| 1 (1 с) | да | 3594 | 3594 | 6 | 0 |
Без проверки DATA_READY драйвер в режиме 10 с девять раз из десяти принимает прошлое измерение за новое — 3240 повторов за час. Среднее по таким числам — среднее по одним и тем же. Принятых 360 при 359 измерениях — не ошибка: первое чтение забрало измерение, сделанное до начала часа. Даже в режиме 1 с опрос раз в секунду шесть раз застал регистры пустыми: часы датчика и микроконтроллера разные.
nINT. С битом 3 в MEAS_MODE nINT падает, когда готово новое измерение, и держится нулём, пока результат не прочитан (так в привязке Zephyr). Прошивка ловит спад на PB8 и читает в главном цикле:
void EXTI9_5_IRQHandler(void)
{
EXTI->PR = EXTI_PR_PR8; /* снимается записью единицы */
nint_flag = 1; /* читать — в главном цикле */
}
В модели за час в режиме 1 с — 3589 измерений, 3589 чтений, ни одного
пустого, и шаг подстраивается под часы датчика: 1003 мс. Если спад
пропущен — линия так и стоит в нуле, и главный цикл дочитывает по уровню
GPIOB->IDR.
Растягивание такта. CCS811 любит держать SCL в нуле, пока готовит ответ. В модели при чтении результата так бывает всегда: в 90 % случаев на 20–100 мкс, в 10 % — на 0,5–1,5 мс (допущение модели: сколько держит настоящий датчик, без документа не проверено). Аппаратный I2C у F103 растягивание понимает: ведущий видит низкий SCL после своего фронта и останавливает счёт высокого уровня, пока линия не отпущена (с. 758). Беда в тайм-аутах ожидания флагов. 10 000 чтений в режиме 4:
| Тайм-аут порта | 0,2 мс | 0,5 мс | 1 мс | 2 мс | 25 мс |
|---|---|---|---|---|---|
| не дождались | 981 | 982 | 506 | 0 | 0 |
В прошивке — 25 мс: дольше любого растягивания в модели больше чем вдесятеро, а оборванный провод всё равно ловится.
/* ждать флаг; ведомый держит SCL — блок ждёт сам (RM0008, с. 758), а тайм-аут ловит обрыв и залипание */
static int sr1_wait(uint16_t mask)
{
uint32_t t0 = DWT->CYCCNT;
while (!(I2C1->SR1 & mask)) {
if (I2C1->SR1 & I2C_SR1_AF)
return AQ_E_NACK;
if (expired(t0, I2C_TIMEOUT_US))
return AQ_E_TIMEOUT;
}
return AQ_OK;
}
Хуже — программный I2C на ногодрыге, который отпускает SCL и не смотрит, поднялась ли линия. Ведущий снимает биты, пока датчик ещё держит такт и SDA отпущена, — читает единицы, а данные датчика потом сдвигаются. Модель на уровне битов, 10 000 чтений eCO₂:
| Программный I2C | 100 кГц | 10 кГц |
|---|---|---|
| не ждёт SCL | 10 000 неверных | 6537 неверных |
| ждёт, пока SCL станет единицей | 0 | 0 |
Замедлить шину не помогает: растягивание идёт в микросекундах, а не в тактах. Помогают аппаратный I2C с разумным тайм-аутом или программный, который после каждого отпускания SCL ждёт единицу на линии.
Чтение одного и двух байт у F1 — свои последовательности (с. 764): один байт — снять ACK до сброса ADDR и сразу STOP; два — POS и ACK до адреса, затем BTF и STOP. Порт делает все три случая:
if (n == 1u) {
I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK; /* один байт: NACK и STOP сразу после ADDR */
(void)I2C1->SR2;
I2C1->CR1 |= I2C_CR1_STOP;
if ((rc = sr1_wait(I2C_SR1_RXNE)) != AQ_OK)
return fail(rc);
p[0] = (uint8_t)I2C1->DR;
return AQ_OK;
}
103е.4 ENS160: OPMODE, PART_ID, статус и данные
У ENS160 в драйвере Zephyr нет ни APP_START, ни nWAKE: всё делает
регистр OPMODE (0x10).
0x00 — глубокий сон, 0x01 — ожидание, 0x02 — работа, 0xF0 — сброс;
после каждой смены — 10 мс (так в Zephyr). PART_ID — два байта
младшим вперёд, 60 01 = 0x0160. Версию прошивки чип кладёт в
GPR_READ4…6 после команды 0x0E в регистр COMMAND; Zephyr спрашивает её
в режиме ожидания, так же делает и этот драйвер:
/* запуск: сброс → PART_ID = 0x0160 → IDLE → версия прошивки → STANDARD; прогрев не ждём, его видно по статусу */
int ens_init(aq_t *d, uint8_t addr, uint32_t *appver)
{
uint8_t b[3];
int rc;
*d = (aq_t){0};
d->chip = AQ_ENS160;
d->addr = addr;
d->check_ready = 1;
if ((rc = ens_opmode(d, ENS_OP_RESET)) != AQ_OK)
return rc;
if ((rc = aq_port_read(addr, ENS_PART_ID, b, 2)) != AQ_OK)
return rc;
if ((uint16_t)(b[1] << 8 | b[0]) != ENS_PART_ID_VAL) /* младшим байтом вперёд: 60 01 */
return AQ_E_ID;
if ((rc = ens_opmode(d, ENS_OP_IDLE)) != AQ_OK)
return rc;
if ((rc = ens_wr1(d, ENS_COMMAND, ENS_CMD_GET_APPVER)) != AQ_OK)
return rc;
aq_port_delay_us(ENS_US_BOOT);
if ((rc = aq_port_read(addr, ENS_GPR_READ4, b, 3)) != AQ_OK)
return rc;
*appver = (uint32_t)b[2] << 16 | (uint32_t)b[1] << 8 | b[0];
return ens_opmode(d, ENS_OP_STANDARD);
}
В модели запуск — 6 обменов и 42 230 мкс, почти всё — четыре паузы по 10 мс. Прошивка модели — 5.4.6 (допущение модели).
Статус и данные. Регистр 0x20 (в Zephyr — DEVICE_STATUS): бит 6 (STATER) — ошибка, биты 3:2 — достоверность, бит 1 (NEWDAT) — новое измерение. Достоверность: 0 — норма, 1 — прогрев, 2 — начальный запуск, 3 — негодно. Сразу за статусом лежат AQI (0x21, биты 2:0), TVOC (0x22) и eCO₂ (0x24), оба младшим байтом вперёд, — поэтому одно чтение 6 байт забирает всё:
/* результат одним чтением с 0x20: статус, AQI, TVOC и eCO2 (оба младшим байтом вперёд) */
int ens_read(aq_t *d, aq_meas_t *m)
{
uint8_t b[6];
int rc = aq_port_read(d->addr, ENS_DEV_STATUS, b, 6);
if (rc != AQ_OK) {
d->n_err++;
return rc;
}
m->status = b[0];
m->aqi = (uint8_t)(b[1] & 0x07u);
m->tvoc = (uint16_t)(b[3] << 8 | b[2]);
m->eco2 = (uint16_t)(b[5] << 8 | b[4]);
m->error = 0;
m->raw = 0;
m->validity = (uint8_t)((b[0] >> ENS_VALID_SHIFT) & 3u);
m->valid = m->validity == 0u;
if (b[0] & ENS_ST_STATER) {
d->n_err++;
return AQ_E_CHIP;
}
if (d->check_ready && !(b[0] & ENS_ST_NEWDAT)) {
d->n_stale++;
return AQ_E_STALE;
}
d->n_read++;
return AQ_OK;
}
Число при прогреве драйвер не выбрасывает, а помечает: valid = 0.
Драйвер Zephyr поступает строже — после перехода в работу читает статус
и, если достоверность не ноль, отказывает в запуске (-EINVAL). В
модели сразу после запуска статус 0x08: достоверность 2, начальный
запуск — и запуск в духе Zephyr не прошёл бы ни у нового датчика, ни
после каждого включения, пока идёт прогрев.
AQI — индекс по шкале UBA, по документации производителя 1–5 (не проверено); в модели уровни взяты по TVOC: до 65, 220, 660 и 2200 ppb (допущение модели). В сценах 103е.1 люди дали AQI 2, спирт — 5.
DATA_MISR. Регистр 0x38 в карте Zephyr есть, но драйвер его не читает. По документации производителя это контрольное значение, которое чип считает по отданным данным, чтобы микроконтроллер мог проверить чтение (не проверено). Как именно оно считается, на диске нет, поэтому драйвер его не проверяет; защита от порчи — проверка NEWDAT, достоверности и правдоподобия чисел.
103е.5 Компенсация температуры и влажности
Влажность и тепло тоже поднимают проводимость слоя. Чип делит G на поправку по тем °C и %RH, которые ему сообщили; не сообщили — считает 25 °C и 50 % (это же пишет Zephyr, когда данных нет). В модели поправка — e^(0,012 · (RH − 50) + 0,02 · (T − 25)) (допущение модели). Откуда брать T и RH — датчик из раздела 97 (SHT31 или AHT20) рядом с модулем: прошивка шлёт их обоим чипам раз в минуту.
CCS811, ENV_DATA. Четыре байта: влажность, ноль, температура, ноль. В Zephyr описано, почему так: младшая часть — дробь, из которой чип смотрит только старший бит, то есть половинку; поэтому значащие байты — первый и третий, по 0,5 % и 0,5 °C. Температура со сдвигом +25 °C, ниже −25 °C — ноль:
/* ENV_DATA: влажность и температура по байту, шаг 0,5; температура со сдвигом +25 °C; младшие байты — нули */
void ccs_env_bytes(int32_t t100, int32_t rh100, uint8_t out[4])
{
int32_t h = (rh100 * 2 + 50) / 100, t;
if (h < 0)
h = 0;
if (h > 200)
h = 200;
if (t100 < -2500)
t = 0; /* ниже −25 °C — ноль */
else
t = ((t100 + 2500) * 2 + 50) / 100;
if (t > 255)
t = 255;
out[0] = (uint8_t)h;
out[1] = 0;
out[2] = (uint8_t)t;
out[3] = 0;
}
ENS160, TEMP_IN и RH_IN. Два слова младшим байтом вперёд: 64 · кельвины и 512 · %RH. Zephyr принимает только −5…60 °C и 20…80 %, вне — ошибка; этот драйвер прижимает к краю:
/* TEMP_IN = 64 · (°C + 273,15), RH_IN = 512 · %RH, к ближайшему; пределы −5…60 °C и 20…80 % — как в Zephyr */
void ens_env_words(int32_t t100, int32_t rh100, uint16_t *t_in, uint16_t *rh_in)
{
if (t100 < -500)
t100 = -500;
if (t100 > 6000)
t100 = 6000;
if (rh100 < 2000)
rh100 = 2000;
if (rh100 > 8000)
rh100 = 8000;
*t_in = (uint16_t)((64 * (t100 + 27315) + 50) / 100);
*rh_in = (uint16_t)((512 * rh100 + 50) / 100);
}
| °C, %RH | ENV_DATA CCS811 | TEMP_IN | RH_IN |
|---|---|---|---|
| 25, 50 | 64 00 64 00 |
19 082 | 25 600 |
| 22, 60 | 78 00 5E 00 |
18 890 | 30 720 |
| 22,4, 43,8 | 58 00 5F 00 |
18 915 | 22 426 |
| −26, 50 | 64 00 00 00 |
17 162 (−5 °C) | 25 600 |
| 60, 90 | B4 00 AA 00 |
21 322 | 40 960 (80 %) |
Что даёт компенсация. Воздух чистый, 2 ч датчики учились при 25 °C и 50 %, затем час при других условиях. TVOC в конце часа, ppb (на деле 0):
| Час при | Без компенсации, CCS811 / ENS160 | С компенсацией, CCS811 / ENS160 |
|---|---|---|
| 25 °C, 30 % | 1 / 1, а после возврата к 50 % — 39 | 2 / 2, после возврата — 0 |
| 25 °C, 50 % | 2 / 2 | 2 / 2 |
| 25 °C, 70 % | 43 / 42 | 2 / 2 |
| 25 °C, 85 % | 85 / 84 | 2 / 10 |
| 35 °C, 50 % | 35 / 35 | 2 / 2 |
Сырость без компенсации — до 85 ppb «органики» из ничего. Сухой час коварнее: TVOC не растёт, но базовая линия съезжает вниз за «чистым» сухим воздухом, и после возврата к 50 % ещё час — 39 ppb. 10 ppb у ENS160 при 85 % — это предел 80 % из Zephyr: остаток в 5 % компенсация не видит.
Порядок байт. 22 °C и 60 %, датчики выучили линию с верной компенсацией, затем в воздухе 200 ppb органики и в датчик записали:
| Что записали | Чип понял | TVOC, ppb | eCO₂, ppm |
|---|---|---|---|
| CCS811, верно | 22 °C, 60 % | 199 | 1198 |
| CCS811, температура и влажность местами | 35 °C, 47 % | 159 | 1034 |
| CCS811, значащие байты на месте нулей | −25 °C, 0 % | 2875 | 11 900 |
| ENS160, слова старшим байтом вперёд | 536 °C, 0,2 % | 0 | — |
Перепутанные местами поля дают ошибку в 20 %. Байты не на своих местах превращают 200 ppb в 2875. А ENS160 со словами «старшим вперёд» получил 536 °C и ослеп: TVOC — ноль при 200 ppb.
103е.6 Прогрев, приработка и базовая линия во Flash
Прогрев. По документации производителя CCS811 после каждого
включения нужно 20 минут, а новому датчику — 48 часов работы, прежде
чем числа станут верными (не проверено). Флага прогрева у CCS811 нет —
его ведёт драйвер по времени от включения: в aq_meas_t поле
validity = 1 первые 20 мин и 2 первые 48 ч у нового датчика, а
valid = 0. В модели проводимость после включения завышена в
1 + e^(−t / 240 с) раз (допущение модели). Воздух — 100 ppb, базовую
линию вернули сразу при включении:
| После включения | 1 мин | 5 мин | 10 мин | 20 мин | 30 мин |
|---|---|---|---|---|---|
| TVOC, ppb | 359 | 187 | 124 | 101 | 101 |
valid |
0 | 0 | 0 | 1 | 1 |
Поэтому прошивка возвращает базовую линию не сразу, а после 20 минут.
У ENS160 прогрев видно в статусе. В модели — 3 минуты прогрева после каждого включения и час начального запуска у нового датчика (допущение модели):
| ENS160 | Достоверность по времени от запуска |
|---|---|
| новый | 2 (начальный запуск) до 3600 с, потом 0 |
| отработавший | 1 (прогрев) до 180 с, потом 0 |
Базовая линия. Gb — что чип считает чистым воздухом. В модели после включения без подсказки он берёт за чистый тот воздух, что вокруг, затем догоняет более чистый за 10 минут, а более грязный — за 7 суток; слой стареет, и чистый воздух даёт на 0,5 % в сутки больше проводимости; у нового датчика проводимость первые часы выше в 1 + 0,6 · e^(−t / 10 ч) раз (допущения модели). CCS811 отдаёт Gb в регистре BASELINE — два байта, смысл которых чип не раскрывает: их читают, хранят и пишут обратно как есть (так и в Zephyr). Офис: днём с 9 до 18 — 300 ppb, ночью проветрено; датчику 40 суток, его включили в 14:00. TVOC / на деле, ppb:
| Базовая линия при включении | Через 30 мин | Через 2 ч | Назавтра в полдень |
|---|---|---|---|
| нет, чип учится заново | 0 / 300 | 1 / 300 | 285 / 300 |
| сохранена вчера | 301 / 300 | 295 / 300 | 285 / 300 |
| сохранена месяц назад | 383 / 300 | 373 / 300 | 327 / 300 |
| сохранена через 2 ч после первого включения | 193 / 300 | 190 / 300 | 285 / 300 |
Без базовой линии датчик принял 300 ppb за чистый воздух и до вечера показывал ноль — правда вернулась только после ночного проветривания. Вчерашняя линия дала верные числа через 20 минут. Месячная — +83 ppb, и назавтра ещё +27: слой постарел, а вверх линия ползёт неделями. Линия, сохранённая в первые часы жизни датчика, занижает на треть. Отсюда правило: сохранять раз в сутки, у нового датчика — не раньше 48 часов.
Журнал во Flash. Запись — 8 байт: метка, линия, номер, проверка. На
странице 1 КБ (таблица 5, с. 54) — 128 записей; раз в сутки — одно
стирание за 128 суток, и 10 тысяч стираний (таблица 29, с. 58) хватит
на 3507 лет. Метка пишется первой: оборвалось питание посреди записи —
место занято, а неполную запись bl_load пропустит. Подробно о
журналах — в 11.8:
int bl_save(uint16_t bl, uint32_t *erases)
{
uint16_t old = 0, s = 0;
uint32_t off = 0;
int rc;
if (bl_load(&old, &s) == 0)
s = 0;
while (off < BL_PAGE && aq_port_flash_read16(off) != 0xFFFFu)
off += BL_REC;
if (off >= BL_PAGE) { /* страница кончилась — стереть, раз в 128 записей */
if ((rc = aq_port_flash_erase()) != AQ_OK)
return rc;
(*erases)++;
off = 0;
}
s++;
if ((rc = aq_port_flash_write16(off, BL_TAG)) != AQ_OK || (rc = aq_port_flash_write16(off + 2u, bl)) != AQ_OK
|| (rc = aq_port_flash_write16(off + 4u, s)) != AQ_OK)
return rc;
return aq_port_flash_write16(off + 6u, chk(bl, s));
}
На стенде — 400 суток сохранений: 3 стирания, 1600 записей полусловами,
ни одной записи в нестёртую ячейку, последняя — 0x8190 под номером 400.
Обрыв после двух полуслов: bl_save вернула −7, bl_load по-прежнему
отдала 0x8190 номер 400, а следующее сохранение легло в следующее место.
В прошивке — так:
if (have_bl && !restored && now - C.on_ms >= CCS_WARM_MS) {
restored = ccs_set_baseline(&C, bl) == AQ_OK; /* вернуть линию — после прогрева 20 мин */
}
if (now - t_save >= DAY_MS && !(C.burnin && now - C.on_ms < CCS_BURNIN_MS)) {
t_save = now;
if (ccs_get_baseline(&C, &bl) == AQ_OK && bl_save(bl, &er) == AQ_OK) {
baseline = bl; /* раз в сутки: страница 1 КБ — на 128 суток */
bl_erases = er;
have_bl = 1;
}
}
Пустой журнал прошивка считает признаком нового датчика: burnin = 1, и
первые 48 ч линия не сохраняется. Заменили датчик — журнал стирают.
У ENS160 регистра базовой линии в карте Zephyr нет: по документации
производителя он ведёт её сам (не проверено).
103е.7 Грабли
eCO₂ приняли за CO₂. Слой не видит углекислый газ. В модели спирт дал 9840 ppm «CO₂» при 600, а баллон с CO₂ — 402 при 1200. Где важен настоящий CO₂ — NDIR из 103г или 103д.
nWAKE висит в воздухе. Пока на нём не ноль, CCS811 молчит: −1 на первом же обмене. Либо вывод МК и 50 мкс перед обменом, либо на землю.
APP_START забыт. CCS811 после включения — в загрузчике, STATUS = 0x10, данных нет. Запись байта 0xF4, 1 мс, STATUS = 0x90.
Опрос без DATA_READY или NEWDAT. Режим 10 с, опрос раз в секунду — 3240 повторов прошлого измерения за час. Смотреть бит готовности или читать по nINT.
Растягивание такта. Тайм-аут ожидания флага 0,2 мс — 981 неудача на 10 000 чтений, 1 мс — 506; 2 мс и больше — ноль. Программный I2C, который не ждёт SCL, на 100 кГц портит все чтения, на 10 кГц — 6537 из 10 000.
Порядок байт ENV_DATA и TEMP_IN. CCS811: влажность — первый байт, температура — третий, со сдвигом +25 °C; ENS160: слова младшим вперёд. Ошибка в порядке — 2875 ppb вместо 200 или ослепший датчик.
Первые минуты и первые сутки. Первые чтения CCS811 после запуска в примере Zephyr — 65 021. 20 минут после включения и 48 ч у нового — по документации; в модели через минуту после включения 359 ppb при 100. У ENS160 смотреть поле достоверности, а не отказывать в запуске, как Zephyr.
Базовая линия: не вернули, вернули рано, сохранили старую. Без возврата — ноль до вечера при 300 ppb; линия месячной давности — +83 ppb; сохранённая в первые часы — −107. Сохранять раз в сутки, не раньше 48 ч, возвращать после 20 мин прогрева.
103е.8 Итоги
- CCS811 и ENS160 — металлооксидные датчики летучей органики: TVOC мерят, eCO₂ считают из него. В модели спирт — 9840 ppm eCO₂ при 600 ppm CO₂, баллон с CO₂ — 402 при 1200.
- CCS811: адрес 0x5A или 0x5B; nWAKE в ноль на каждый обмен (50 и 20 мкс); STATUS 0x10 → запись 0xF4 → 0x90; HW_ID 0x81; режим — MEAS_MODE, 1 с, 10 с, 60 с или 250 мс; результат — 8 байт старшим вперёд, готовность — бит 3 STATUS или nINT.
- ENS160: адрес 0x53 или 0x52; OPMODE 0xF0 → 0x01 → 0x02, по 10 мс;
PART_ID
60 01; результат одним чтением 6 байт с 0x20 младшим вперёд; достоверность — биты 3:2 статуса. - Растягивание такта: аппаратный I2C F1 его ждёт (с. 758), тайм-аут флага — не меньше 2 мс в модели, в прошивке 25 мс.
- Компенсация: ENV_DATA —
влажность·2, 0, (T + 25)·2, 0; TEMP_IN = 64 · K, RH_IN = 512 · %RH младшим вперёд; без неё при 85 % — 85 ppb из ничего. - Базовая линия CCS811 — во Flash раз в сутки, не раньше 48 ч у нового датчика, вернуть через 20 мин после включения; 128 записей на страницу 1 КБ — стирание раз в 128 суток.
Полные исходники
aq103g.h — регистры и биты обоих чипов, сроки, коды ошибок, структуры и функции драйвера.
/* aq103g.h — раздел 103е: драйвер датчиков качества воздуха CCS811 и ENS160 по I2C без регистров микроконтроллера.
* Обмен — функции порта (aq_port_*): в прошивке fw103g.c это I2C1, вывод nWAKE и Flash STM32F103, в стенде
* host103g.c — модель model103g.py.
* Регистры, биты STATUS и ERROR_ID, коды MEAS_MODE, HW_ID 0x81, APP_START 0xF4, сроки t_START, t_APP_START, t_WAKE и
* t_DWAKE, формат ENV_DATA, PART_ID 0x0160, OPMODE, поля DEVICE_STATUS, TEMP_IN = 64 · K и RH_IN = 512 · %RH младшим
* байтом вперёд, 10 мс после смены OPMODE — как в драйверах ccs811 и ens160 Zephyr 4.4.2. Прогрев 20 мин и 48 ч
* первого включения CCS811 — по документации производителя, с датчиком не сверено. */
#ifndef AQ103G_H
#define AQ103G_H
#include <stdint.h>
/* ── CCS811 ─────────────────────────────────────────────────────────────── */
#define CCS_ADDR 0x5Au /* второй адрес — 0x5B */
#define CCS_STATUS 0x00u
#define CCS_MEAS_MODE 0x01u
#define CCS_ALG_RESULT 0x02u /* 8 байт: eCO2, TVOC (старшим вперёд), STATUS, ERROR_ID, RAW */
#define CCS_ENV_DATA 0x05u /* 4 байта: влажность, 0, температура, 0 */
#define CCS_BASELINE 0x11u
#define CCS_HW_ID 0x20u
#define CCS_FW_APP_VER 0x24u
#define CCS_ERROR_ID 0xE0u
#define CCS_APP_START 0xF4u
#define CCS_HW_ID_VAL 0x81u
#define CCS_ST_ERROR 0x01u
#define CCS_ST_DATA_READY 0x08u
#define CCS_ST_APP_VALID 0x10u
#define CCS_ST_FW_MODE 0x80u
#define CCS_MODE_IDLE 0x00u /* режим 0 — не мерит */
#define CCS_MODE_1S 0x10u /* режим 1 — раз в 1 с */
#define CCS_MODE_10S 0x20u /* режим 2 — раз в 10 с */
#define CCS_MODE_60S 0x30u /* режим 3 — раз в 60 с */
#define CCS_MODE_250MS 0x40u /* режим 4 — раз в 250 мс */
#define CCS_MODE_INT_DRDY 0x08u /* nINT — при новом измерении */
#define CCS_US_START 20000u /* t_START после включения питания */
#define CCS_US_APP_START 1000u /* t_APP_START */
#define CCS_US_WAKE 50u /* t_WAKE: nWAKE в ноль — и только потом обмен */
#define CCS_US_DWAKE 20u /* t_DWAKE: после обмена nWAKE в единицу */
#define CCS_WARM_MS 1200000u /* 20 мин после каждого включения (по документации) */
#define CCS_BURNIN_MS 172800000u /* 48 ч первого включения нового датчика (по документации) */
/* ── ENS160 ─────────────────────────────────────────────────────────────── */
#define ENS_ADDR 0x53u /* второй адрес — 0x52 */
#define ENS_PART_ID 0x00u /* 2 байта, младшим вперёд: 0x0160 */
#define ENS_OPMODE 0x10u
#define ENS_CONFIG 0x11u
#define ENS_COMMAND 0x12u
#define ENS_TEMP_IN 0x13u /* 64 · K, младшим вперёд */
#define ENS_RH_IN 0x15u /* 512 · %RH, младшим вперёд */
#define ENS_DEV_STATUS 0x20u /* за ним подряд AQI (0x21), TVOC (0x22), eCO2 (0x24) */
#define ENS_DATA_MISR 0x38u
#define ENS_GPR_READ4 0x4Cu
#define ENS_PART_ID_VAL 0x0160u
#define ENS_OP_SLEEP 0x00u /* глубокий сон */
#define ENS_OP_IDLE 0x01u
#define ENS_OP_STANDARD 0x02u
#define ENS_OP_RESET 0xF0u
#define ENS_CMD_GET_APPVER 0x0Eu
#define ENS_ST_STATER 0x40u /* ошибка */
#define ENS_ST_NEWDAT 0x02u
#define ENS_VALID_SHIFT 2u /* биты 3:2: 0 — норма, 1 — прогрев, 2 — начальный запуск, 3 — негодно */
#define ENS_US_BOOT 10000u /* 10 мс после смены OPMODE */
enum { AQ_OK = 0, AQ_E_NACK = -1, AQ_E_TIMEOUT = -2, AQ_E_ID = -3, AQ_E_APP = -4, AQ_E_STALE = -5, AQ_E_CHIP = -6,
AQ_E_ARG = -7 };
enum { AQ_CCS811 = 1, AQ_ENS160 = 2 };
typedef struct {
uint16_t eco2, tvoc, raw; /* eCO2 — ppm, TVOC — ppb */
uint8_t status, error, aqi; /* error — ERROR_ID CCS811; aqi — AQI-UBA ENS160 */
uint8_t validity; /* ENS160: 0…3; CCS811: 0 — норма, 1 — прогрев, 2 — первые 48 ч */
uint8_t valid; /* числу можно верить */
} aq_meas_t;
typedef struct {
uint8_t chip, addr, mode, has_wake; /* has_wake: nWAKE подключён к выводу, а не к земле */
uint8_t app_ver, check_ready; /* check_ready: 1; 0 — не смотреть DATA_READY/NEWDAT (для опыта) */
uint8_t burnin; /* 1 — датчик новый, ещё не отработал 48 ч */
uint32_t on_ms; /* когда включилось питание датчика */
uint32_t n_read, n_stale, n_err;
} aq_t;
/* ── порт: в прошивке — I2C1, PB5 (nWAKE) и Flash; в стенде — строки к модели ── */
int aq_port_write(uint8_t addr7, const uint8_t *p, uint32_t n);
int aq_port_read(uint8_t addr7, uint8_t reg, uint8_t *p, uint32_t n); /* запись адреса регистра, повторный старт, чтение */
void aq_port_wake(uint8_t active); /* 1 — nWAKE в ноль */
void aq_port_delay_us(uint32_t us);
int aq_port_flash_erase(void);
int aq_port_flash_write16(uint32_t off, uint16_t v);
uint16_t aq_port_flash_read16(uint32_t off);
/* ── CCS811 ── */
int ccs_init(aq_t *d, uint8_t addr, uint8_t mode, uint8_t has_wake, uint8_t burnin, uint32_t now);
int ccs_read(aq_t *d, aq_meas_t *m, uint32_t now);
int ccs_env(aq_t *d, int32_t t100, int32_t rh100);
void ccs_env_bytes(int32_t t100, int32_t rh100, uint8_t out[4]);
int ccs_get_baseline(aq_t *d, uint16_t *bl);
int ccs_set_baseline(aq_t *d, uint16_t bl);
uint32_t ccs_raw_uv(uint16_t raw);
/* ── ENS160 ── */
int ens_init(aq_t *d, uint8_t addr, uint32_t *appver);
int ens_read(aq_t *d, aq_meas_t *m);
int ens_env(aq_t *d, int32_t t100, int32_t rh100);
void ens_env_words(int32_t t100, int32_t rh100, uint16_t *t_in, uint16_t *rh_in);
int ens_opmode(aq_t *d, uint8_t op);
/* ── базовая линия CCS811 во Flash: запись 8 байт, страница 1 КБ — 128 записей ── */
#define BL_PAGE 1024u
#define BL_REC 8u
#define BL_TAG 0xB15Eu
int bl_load(uint16_t *bl, uint16_t *seq);
int bl_save(uint16_t bl, uint32_t *erases);
#endif
aq103g.c — драйвер: запуск CCS811 и ENS160, чтение результата с проверкой готовности, компенсация, базовая линия и её журнал во Flash.
/* aq103g.c — раздел 103е: драйвер CCS811 и ENS160. Запуск, чтение результата с проверкой готовности, компенсация
* температуры и влажности, базовая линия CCS811 и её журнал во Flash. Регистров микроконтроллера здесь нет: тот же
* файл собирается в прошивку и в стенд на компьютере. */
#include "aq103g.h"
/* ── CCS811: каждый обмен — между nWAKE = 0 и nWAKE = 1, если вывод подключён ── */
static int ccs_rd(const aq_t *d, uint8_t reg, uint8_t *p, uint32_t n)
{
int rc;
if (d->has_wake) {
aq_port_wake(1);
aq_port_delay_us(CCS_US_WAKE);
}
rc = aq_port_read(d->addr, reg, p, n);
if (d->has_wake) {
aq_port_wake(0);
aq_port_delay_us(CCS_US_DWAKE);
}
return rc;
}
static int ccs_wr(const aq_t *d, const uint8_t *p, uint32_t n)
{
int rc;
if (d->has_wake) {
aq_port_wake(1);
aq_port_delay_us(CCS_US_WAKE);
}
rc = aq_port_write(d->addr, p, n);
if (d->has_wake) {
aq_port_wake(0);
aq_port_delay_us(CCS_US_DWAKE);
}
return rc;
}
/* запуск: STATUS → APP_START (0xF4) → STATUS → HW_ID = 0x81 → версия → MEAS_MODE → STATUS без ошибки */
int ccs_init(aq_t *d, uint8_t addr, uint8_t mode, uint8_t has_wake, uint8_t burnin, uint32_t now)
{
uint8_t st, b[2], cmd = CCS_APP_START;
int rc;
*d = (aq_t){0};
d->chip = AQ_CCS811;
d->addr = addr;
d->mode = mode;
d->has_wake = has_wake;
d->burnin = burnin;
d->check_ready = 1;
d->on_ms = now;
if ((rc = ccs_rd(d, CCS_STATUS, &st, 1)) != AQ_OK)
return rc;
if (!(st & CCS_ST_APP_VALID))
return AQ_E_APP; /* в датчике нет прикладной прошивки */
if (!(st & CCS_ST_FW_MODE)) {
if ((rc = ccs_wr(d, &cmd, 1)) != AQ_OK) /* APP_START — один байт, без данных */
return rc;
aq_port_delay_us(CCS_US_APP_START);
if ((rc = ccs_rd(d, CCS_STATUS, &st, 1)) != AQ_OK)
return rc;
if (!(st & CCS_ST_FW_MODE))
return AQ_E_APP;
}
if ((rc = ccs_rd(d, CCS_HW_ID, b, 1)) != AQ_OK)
return rc;
if (b[0] != CCS_HW_ID_VAL)
return AQ_E_ID;
if ((rc = ccs_rd(d, CCS_FW_APP_VER, b, 2)) != AQ_OK)
return rc;
d->app_ver = b[0];
b[0] = CCS_MEAS_MODE;
b[1] = mode;
if ((rc = ccs_wr(d, b, 2)) != AQ_OK)
return rc;
if ((rc = ccs_rd(d, CCS_STATUS, &st, 1)) != AQ_OK)
return rc;
if (st & CCS_ST_ERROR) {
(void)ccs_rd(d, CCS_ERROR_ID, b, 1); /* чтение ERROR_ID снимает ошибку */
return AQ_E_CHIP;
}
return AQ_OK;
}
/* результат: 8 байт ALG_RESULT_DATA; eCO2 и TVOC — старшим байтом вперёд, байт 4 — STATUS, байт 5 — ERROR_ID */
int ccs_read(aq_t *d, aq_meas_t *m, uint32_t now)
{
uint8_t b[8];
uint32_t up = now - d->on_ms;
int rc = ccs_rd(d, CCS_ALG_RESULT, b, 8);
if (rc != AQ_OK) {
d->n_err++;
return rc;
}
m->eco2 = (uint16_t)(b[0] << 8 | b[1]);
m->tvoc = (uint16_t)(b[2] << 8 | b[3]);
m->status = b[4];
m->error = b[5];
m->raw = (uint16_t)(b[6] << 8 | b[7]);
m->aqi = 0;
if (m->status & CCS_ST_ERROR) {
d->n_err++;
return AQ_E_CHIP;
}
if (d->check_ready && !(m->status & CCS_ST_DATA_READY)) {
d->n_stale++; /* нового измерения нет: числа — прошлые */
return AQ_E_STALE;
}
m->validity = (d->burnin && up < CCS_BURNIN_MS) ? 2u : (up < CCS_WARM_MS ? 1u : 0u);
m->valid = m->validity == 0u;
d->n_read++;
return AQ_OK;
}
/* ENV_DATA: влажность и температура по байту, шаг 0,5; температура со сдвигом +25 °C; младшие байты — нули */
void ccs_env_bytes(int32_t t100, int32_t rh100, uint8_t out[4])
{
int32_t h = (rh100 * 2 + 50) / 100, t;
if (h < 0)
h = 0;
if (h > 200)
h = 200;
if (t100 < -2500)
t = 0; /* ниже −25 °C — ноль */
else
t = ((t100 + 2500) * 2 + 50) / 100;
if (t > 255)
t = 255;
out[0] = (uint8_t)h;
out[1] = 0;
out[2] = (uint8_t)t;
out[3] = 0;
}
int ccs_env(aq_t *d, int32_t t100, int32_t rh100)
{
uint8_t b[5] = {CCS_ENV_DATA};
ccs_env_bytes(t100, rh100, &b[1]);
return ccs_wr(d, b, 5);
}
/* базовая линия — два байта как есть: что в них, датчик не раскрывает; сохранить и вернуть тем же порядком */
int ccs_get_baseline(aq_t *d, uint16_t *bl)
{
uint8_t b[2];
int rc = ccs_rd(d, CCS_BASELINE, b, 2);
if (rc == AQ_OK)
*bl = (uint16_t)(b[0] << 8 | b[1]);
return rc;
}
int ccs_set_baseline(aq_t *d, uint16_t bl)
{
uint8_t b[3] = {CCS_BASELINE, (uint8_t)(bl >> 8), (uint8_t)bl};
return ccs_wr(d, b, 3);
}
/* напряжение на слое: младшие 10 бит RAW, 1 650 000 мкВ / 1023 = 1612 мкВ на единицу; ток — старшие 6 бит, мкА */
uint32_t ccs_raw_uv(uint16_t raw)
{
return (uint32_t)(raw & 0x3FFu) * (1650000u / 1023u);
}
/* ── ENS160 ─────────────────────────────────────────────────────────────── */
static int ens_wr1(const aq_t *d, uint8_t reg, uint8_t v)
{
uint8_t b[2] = {reg, v};
return aq_port_write(d->addr, b, 2);
}
int ens_opmode(aq_t *d, uint8_t op)
{
int rc = ens_wr1(d, ENS_OPMODE, op);
aq_port_delay_us(ENS_US_BOOT); /* 10 мс после смены режима */
return rc;
}
/* запуск: сброс → PART_ID = 0x0160 → IDLE → версия прошивки → STANDARD; прогрев не ждём, его видно по статусу */
int ens_init(aq_t *d, uint8_t addr, uint32_t *appver)
{
uint8_t b[3];
int rc;
*d = (aq_t){0};
d->chip = AQ_ENS160;
d->addr = addr;
d->check_ready = 1;
if ((rc = ens_opmode(d, ENS_OP_RESET)) != AQ_OK)
return rc;
if ((rc = aq_port_read(addr, ENS_PART_ID, b, 2)) != AQ_OK)
return rc;
if ((uint16_t)(b[1] << 8 | b[0]) != ENS_PART_ID_VAL) /* младшим байтом вперёд: 60 01 */
return AQ_E_ID;
if ((rc = ens_opmode(d, ENS_OP_IDLE)) != AQ_OK)
return rc;
if ((rc = ens_wr1(d, ENS_COMMAND, ENS_CMD_GET_APPVER)) != AQ_OK)
return rc;
aq_port_delay_us(ENS_US_BOOT);
if ((rc = aq_port_read(addr, ENS_GPR_READ4, b, 3)) != AQ_OK)
return rc;
*appver = (uint32_t)b[2] << 16 | (uint32_t)b[1] << 8 | b[0];
return ens_opmode(d, ENS_OP_STANDARD);
}
/* результат одним чтением с 0x20: статус, AQI, TVOC и eCO2 (оба младшим байтом вперёд) */
int ens_read(aq_t *d, aq_meas_t *m)
{
uint8_t b[6];
int rc = aq_port_read(d->addr, ENS_DEV_STATUS, b, 6);
if (rc != AQ_OK) {
d->n_err++;
return rc;
}
m->status = b[0];
m->aqi = (uint8_t)(b[1] & 0x07u);
m->tvoc = (uint16_t)(b[3] << 8 | b[2]);
m->eco2 = (uint16_t)(b[5] << 8 | b[4]);
m->error = 0;
m->raw = 0;
m->validity = (uint8_t)((b[0] >> ENS_VALID_SHIFT) & 3u);
m->valid = m->validity == 0u;
if (b[0] & ENS_ST_STATER) {
d->n_err++;
return AQ_E_CHIP;
}
if (d->check_ready && !(b[0] & ENS_ST_NEWDAT)) {
d->n_stale++;
return AQ_E_STALE;
}
d->n_read++;
return AQ_OK;
}
/* TEMP_IN = 64 · (°C + 273,15), RH_IN = 512 · %RH, к ближайшему; пределы −5…60 °C и 20…80 % — как в Zephyr */
void ens_env_words(int32_t t100, int32_t rh100, uint16_t *t_in, uint16_t *rh_in)
{
if (t100 < -500)
t100 = -500;
if (t100 > 6000)
t100 = 6000;
if (rh100 < 2000)
rh100 = 2000;
if (rh100 > 8000)
rh100 = 8000;
*t_in = (uint16_t)((64 * (t100 + 27315) + 50) / 100);
*rh_in = (uint16_t)((512 * rh100 + 50) / 100);
}
int ens_env(aq_t *d, int32_t t100, int32_t rh100)
{
uint16_t t, h;
uint8_t b[3];
int rc;
ens_env_words(t100, rh100, &t, &h);
b[0] = ENS_TEMP_IN;
b[1] = (uint8_t)t; /* младшим байтом вперёд */
b[2] = (uint8_t)(t >> 8);
if ((rc = aq_port_write(d->addr, b, 3)) != AQ_OK)
return rc;
b[0] = ENS_RH_IN;
b[1] = (uint8_t)h;
b[2] = (uint8_t)(h >> 8);
return aq_port_write(d->addr, b, 3);
}
/* ── журнал базовой линии: запись — метка, линия, номер, проверка; метку пишем первой ── */
static uint16_t chk(uint16_t bl, uint16_t seq)
{
return (uint16_t)(BL_TAG ^ bl ^ seq ^ 0x5A5Au);
}
/* последняя годная запись: 1 — есть, 0 — журнал пуст */
int bl_load(uint16_t *bl, uint16_t *seq)
{
int found = 0;
for (uint32_t off = 0; off < BL_PAGE; off += BL_REC) {
uint16_t tag = aq_port_flash_read16(off);
uint16_t b = aq_port_flash_read16(off + 2u), s = aq_port_flash_read16(off + 4u);
if (tag == 0xFFFFu)
break; /* дальше свободно */
if (tag == BL_TAG && aq_port_flash_read16(off + 6u) == chk(b, s)) {
*bl = b;
*seq = s;
found = 1; /* недописанную при обрыве питания пропускаем */
}
}
return found;
}
int bl_save(uint16_t bl, uint32_t *erases)
{
uint16_t old = 0, s = 0;
uint32_t off = 0;
int rc;
if (bl_load(&old, &s) == 0)
s = 0;
while (off < BL_PAGE && aq_port_flash_read16(off) != 0xFFFFu)
off += BL_REC;
if (off >= BL_PAGE) { /* страница кончилась — стереть, раз в 128 записей */
if ((rc = aq_port_flash_erase()) != AQ_OK)
return rc;
(*erases)++;
off = 0;
}
s++;
if ((rc = aq_port_flash_write16(off, BL_TAG)) != AQ_OK || (rc = aq_port_flash_write16(off + 2u, bl)) != AQ_OK
|| (rc = aq_port_flash_write16(off + 4u, s)) != AQ_OK)
return rc;
return aq_port_flash_write16(off + 6u, chk(bl, s));
}
fw103g.c — прошивка F103: I2C1 с повторным стартом и тайм-аутом, nWAKE на PB5, nINT на PB8, журнал на последней странице Flash.
/* fw103g.c — раздел 103е: CCS811 и ENS160 на одной шине I2C1 STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц.
* PB6 — SCL, PB7 — SDA (открытый сток, 100 кГц, подтяжки — на модулях). PB5 — nWAKE CCS811 (выход, 1 — сон),
* PB8 — nINT CCS811 (вход с подтяжкой, EXTI8 по спаду). CCS811 — 0x5A, режим 2 (раз в 10 с) с прерыванием;
* ENS160 — 0x53, стандартный режим, опрос раз в секунду. Температура и влажность для компенсации — из датчика
* раздела 97 (SHT31 или AHT20 на той же шине): здесь они приходят готовыми в env_t100 и env_rh100.
* Базовая линия CCS811: вернуть из Flash через 20 мин после включения, сохранять раз в сутки работы, у нового
* датчика — не раньше 48 ч. Журнал — последняя страница Flash (0x0800FC00, 1 КБ). Результат — в переменных для
* отладчика. Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "aq103g.h"
#define CPU_HZ 72000000u
#define APB1_MHZ 36u
#define I2C_TIMEOUT_US 25000u /* растягивание такта CCS811: в модели до 1,5 мс — запас больше чем вдесятеро */
#define BL_ADDR 0x0800FC00u /* последняя страница 64 КБ Flash */
#define ENV_MS 60000u
#define ENS_MS 1000u
#define DAY_MS 86400000u
static aq_t C, E;
static volatile uint32_t ms;
static volatile uint8_t nint_flag;
volatile int32_t env_t100 = 2500, env_rh100 = 5000; /* °C и %RH в сотых — от датчика раздела 97 */
volatile uint16_t ccs_eco2, ccs_tvoc, ens_eco2, ens_tvoc, baseline;
volatile uint8_t ens_aqi, ccs_valid, ens_validity;
volatile uint32_t appver, bl_erases, errors;
volatile int32_t last_rc;
void SysTick_Handler(void)
{
ms++;
}
void EXTI9_5_IRQHandler(void)
{
EXTI->PR = EXTI_PR_PR8; /* снимается записью единицы */
nint_flag = 1; /* читать — в главном цикле */
}
static int expired(uint32_t t0, uint32_t us)
{
return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}
void aq_port_delay_us(uint32_t us)
{
uint32_t t0 = DWT->CYCCNT;
while (!expired(t0, us)) {
}
}
void aq_port_wake(uint8_t active)
{
GPIOB->BSRR = active ? GPIO_BSRR_BR5 : GPIO_BSRR_BS5; /* nWAKE активен нулём */
}
static void i2c1_init(void)
{
I2C1->CR1 = 0; /* настраивать при PE = 0 */
I2C1->CR2 = APB1_MHZ;
I2C1->CCR = 180u; /* Sm: Thigh = Tlow = 180 × 27,8 нс = 5 мкс — 100 кГц */
I2C1->TRISE = APB1_MHZ + 1u; /* 1000 нс × 36 МГц = 36 → 37 */
I2C1->CR1 = I2C_CR1_PE;
}
/* ждать флаг; ведомый держит SCL — блок ждёт сам (RM0008, с. 758), а тайм-аут ловит обрыв и залипание */
static int sr1_wait(uint16_t mask)
{
uint32_t t0 = DWT->CYCCNT;
while (!(I2C1->SR1 & mask)) {
if (I2C1->SR1 & I2C_SR1_AF)
return AQ_E_NACK;
if (expired(t0, I2C_TIMEOUT_US))
return AQ_E_TIMEOUT;
}
return AQ_OK;
}
static int fail(int rc)
{
I2C1->CR1 |= I2C_CR1_STOP;
I2C1->SR1 = (uint16_t)~I2C_SR1_AF; /* AF снимается записью нуля */
I2C1->CR1 &= (uint16_t)~I2C_CR1_POS;
I2C1->CR1 |= I2C_CR1_ACK;
return rc;
}
static int start(uint8_t addr_rw, uint8_t restart)
{
uint32_t t0 = DWT->CYCCNT;
int rc;
while (!restart && (I2C1->SR2 & I2C_SR2_BUSY)) { /* SDA держит ведомый — раздел 17.6 */
if (expired(t0, I2C_TIMEOUT_US))
return AQ_E_TIMEOUT;
}
I2C1->CR1 |= I2C_CR1_START;
if ((rc = sr1_wait(I2C_SR1_SB)) != AQ_OK)
return rc;
I2C1->DR = addr_rw;
return sr1_wait(I2C_SR1_ADDR);
}
int aq_port_write(uint8_t addr7, const uint8_t *p, uint32_t n)
{
int rc;
I2C1->CR1 |= I2C_CR1_ACK;
if ((rc = start((uint8_t)(addr7 << 1), 0)) != AQ_OK)
return fail(rc);
(void)I2C1->SR2;
for (uint32_t i = 0; i < n; i++) {
if ((rc = sr1_wait(I2C_SR1_TXE)) != AQ_OK)
return fail(rc);
I2C1->DR = p[i];
}
if ((rc = sr1_wait(I2C_SR1_BTF)) != AQ_OK)
return fail(rc);
I2C1->CR1 |= I2C_CR1_STOP;
return AQ_OK;
}
/* адрес регистра, повторный старт и приём: 1 байт, 2 байта (POS) и больше двух — RM0008, с. 763–764 */
int aq_port_read(uint8_t addr7, uint8_t reg, uint8_t *p, uint32_t n)
{
uint32_t i = 0;
int rc;
if (n == 0u)
return AQ_E_ARG;
I2C1->CR1 |= I2C_CR1_ACK;
if ((rc = start((uint8_t)(addr7 << 1), 0)) != AQ_OK)
return fail(rc);
(void)I2C1->SR2;
I2C1->DR = reg;
if ((rc = sr1_wait(I2C_SR1_BTF)) != AQ_OK)
return fail(rc);
if (n == 2u)
I2C1->CR1 |= I2C_CR1_POS; /* ACK относится к следующему байту */
if ((rc = start((uint8_t)(addr7 << 1 | 1u), 1)) != AQ_OK)
return fail(rc);
if (n == 1u) {
I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK; /* один байт: NACK и STOP сразу после ADDR */
(void)I2C1->SR2;
I2C1->CR1 |= I2C_CR1_STOP;
if ((rc = sr1_wait(I2C_SR1_RXNE)) != AQ_OK)
return fail(rc);
p[0] = (uint8_t)I2C1->DR;
return AQ_OK;
}
if (n == 2u) {
(void)I2C1->SR2;
I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
if ((rc = sr1_wait(I2C_SR1_BTF)) != AQ_OK) /* оба байта приняты: в DR и в сдвиговом */
return fail(rc);
I2C1->CR1 |= I2C_CR1_STOP;
p[0] = (uint8_t)I2C1->DR;
p[1] = (uint8_t)I2C1->DR;
I2C1->CR1 &= (uint16_t)~I2C_CR1_POS;
return AQ_OK;
}
(void)I2C1->SR2;
for (; i + 3u < n; i++) {
if ((rc = sr1_wait(I2C_SR1_RXNE)) != AQ_OK)
return fail(rc);
p[i] = (uint8_t)I2C1->DR;
}
if ((rc = sr1_wait(I2C_SR1_BTF)) != AQ_OK) /* осталось три: N−2 в DR, N−1 в сдвиговом */
return fail(rc);
I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
p[i++] = (uint8_t)I2C1->DR;
if ((rc = sr1_wait(I2C_SR1_BTF)) != AQ_OK)
return fail(rc);
I2C1->CR1 |= I2C_CR1_STOP;
p[i++] = (uint8_t)I2C1->DR;
if ((rc = sr1_wait(I2C_SR1_RXNE)) != AQ_OK)
return fail(rc);
p[i] = (uint8_t)I2C1->DR;
return AQ_OK;
}
/* ── Flash: страница журнала (раздел 11.5) ──────────────────────────────── */
static void flash_unlock(void)
{
if (FLASH->CR & FLASH_CR_LOCK) {
FLASH->KEYR = 0x45670123u;
FLASH->KEYR = 0xCDEF89ABu;
}
}
static void flash_wait(void)
{
while (FLASH->SR & FLASH_SR_BSY) {
}
}
int aq_port_flash_erase(void)
{
flash_unlock();
flash_wait();
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR | FLASH_SR_EOP;
FLASH->CR |= FLASH_CR_PER;
FLASH->AR = BL_ADDR;
FLASH->CR |= FLASH_CR_STRT;
flash_wait();
FLASH->CR &= ~FLASH_CR_PER;
FLASH->CR |= FLASH_CR_LOCK;
return (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) ? AQ_E_ARG : AQ_OK;
}
int aq_port_flash_write16(uint32_t off, uint16_t v)
{
int rc;
flash_unlock();
flash_wait();
FLASH->SR = FLASH_SR_PGERR | FLASH_SR_WRPRTERR | FLASH_SR_EOP;
FLASH->CR |= FLASH_CR_PG;
*(volatile uint16_t *)(BL_ADDR + off) = v; /* у F1 — только полусловами */
flash_wait();
rc = (FLASH->SR & (FLASH_SR_PGERR | FLASH_SR_WRPRTERR)) ? AQ_E_ARG : AQ_OK;
FLASH->CR &= ~FLASH_CR_PG;
FLASH->CR |= FLASH_CR_LOCK;
return rc;
}
uint16_t aq_port_flash_read16(uint32_t off)
{
return *(const volatile uint16_t *)(BL_ADDR + off);
}
static void board_init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
GPIOB->BSRR = GPIO_BSRR_BS5 | GPIO_BSRR_BS8; /* nWAKE = 1 (сон); подтяжка PB8 — вверх */
GPIOB->CRL = (GPIOB->CRL & 0x000FFFFFu)
| 0x2u << 20 /* PB5 — выход 2 МГц, nWAKE */
| 0xFFu << 24; /* PB6, PB7 — альтернативная функция, открытый сток */
GPIOB->CRH = (GPIOB->CRH & ~0xFu) | 0x8u; /* PB8 — вход с подтяжкой, nINT */
AFIO->EXTICR[2] = (AFIO->EXTICR[2] & ~AFIO_EXTICR3_EXTI8) | AFIO_EXTICR3_EXTI8_PB;
EXTI->FTSR |= EXTI_FTSR_TR8; /* nINT падает — есть новое измерение */
EXTI->IMR |= EXTI_IMR_MR8;
NVIC_SetPriority(EXTI9_5_IRQn, 6);
NVIC_EnableIRQ(EXTI9_5_IRQn);
i2c1_init();
SysTick_Config(CPU_HZ / 1000u);
}
static void env_update(void)
{
(void)ccs_env(&C, env_t100, env_rh100); /* без них MOX-слой путает влажность с газом */
(void)ens_env(&E, env_t100, env_rh100);
}
int main(void)
{
aq_meas_t m;
uint16_t bl = 0, seq = 0;
uint32_t t_env = 0, t_ens = 0, t_save, av = 0, er = 0;
uint8_t have_bl, restored = 0;
board_init();
aq_port_delay_us(CCS_US_START); /* t_START: 20 мс после включения */
have_bl = (uint8_t)bl_load(&bl, &seq); /* журнала нет — датчик новый: первые 48 ч не сохранять */
while ((last_rc = ccs_init(&C, CCS_ADDR, CCS_MODE_10S | CCS_MODE_INT_DRDY, 1, (uint8_t)!have_bl, ms)) != AQ_OK) {
errors++;
aq_port_delay_us(100000u); /* нет ответа: nWAKE, адрес, питание, подтяжки */
}
while ((last_rc = ens_init(&E, ENS_ADDR, &av)) != AQ_OK) {
errors++;
aq_port_delay_us(100000u);
}
appver = av;
env_update();
t_save = ms;
for (;;) {
uint32_t now = ms;
if (nint_flag || !(GPIOB->IDR & GPIO_IDR_IDR8)) { /* nINT держится нулём, пока результат не прочитан */
nint_flag = 0;
last_rc = ccs_read(&C, &m, now);
if (last_rc == AQ_OK) {
ccs_eco2 = m.eco2;
ccs_tvoc = m.tvoc;
ccs_valid = m.valid;
} else if (last_rc != AQ_E_STALE) {
errors++;
}
}
if (now - t_ens >= ENS_MS) {
t_ens = now;
if (ens_read(&E, &m) == AQ_OK) {
ens_eco2 = m.eco2;
ens_tvoc = m.tvoc;
ens_aqi = m.aqi;
}
ens_validity = m.validity;
}
if (now - t_env >= ENV_MS) {
t_env = now;
env_update();
}
if (have_bl && !restored && now - C.on_ms >= CCS_WARM_MS) {
restored = ccs_set_baseline(&C, bl) == AQ_OK; /* вернуть линию — после прогрева 20 мин */
}
if (now - t_save >= DAY_MS && !(C.burnin && now - C.on_ms < CCS_BURNIN_MS)) {
t_save = now;
if (ccs_get_baseline(&C, &bl) == AQ_OK && bl_save(bl, &er) == AQ_OK) {
baseline = bl; /* раз в сутки: страница 1 КБ — на 128 суток */
bl_erases = er;
have_bl = 1;
}
}
__WFI(); /* до тика SysTick или спада nINT */
}
}
Дальше: раздел 104 «Термопара: MAX31855 и MAX6675» — термопара, холодный спай и готовые преобразователи на SPI.