103г. SCD40/SCD41 CO₂ sensor over I2C: commands and CRC-8, periodic and single-shot measurement, ppm, °C and %RH, pressure and calibration
In section 103 the MQ measured gas with a heated layer, in 103в the PMS5003 counted dust particles with a laser. The SCD40 and SCD41 measure carbon dioxide with light: how much infrared radiation the air in a small chamber has absorbed. The answer is a ready number in parts per million (ppm), with temperature and humidity next to it. Here — how the I2C exchange works: 16-bit commands, words with CRC-8 and pauses for execution; how to check the CRC on the example 0xBEEF → 0x92; how to measure every 5 s, every 30 s or once at a time (SCD41); how to convert raw words into °C and %RH; why the sensor needs pressure and altitude; how automatic and forced calibration work and why the settings must not be saved to EEPROM on every start-up.
Sources: RM0008 (DocID13902 Rev 17): master reception of more than two bytes,
method 2, pp. 762–763; I2C_CCR, pp. 783–784; I2C_TRISE, p. 784.
DocID13587 Rev 17 (STM32F103x8/xB): PB6 — I2C1_SCL, PB7 — I2C1_SDA,
both FT, p. 32. The I2C bus and its setup are section 17 and are not
repeated here. There is no Sensirion SCD40/SCD41 document in the project. The
command codes, their execution times, the CRC-8 (polynomial 0x31, initial value
0xFF), the mask of the data_ready word, the °C and %RH formulas, the forced
calibration reply, the pressure and altitude limits, the 2000 EEPROM writes,
the automatic calibration periods and the 5 s and 30 s measurement periods are
taken from the scd4x driver of Zephyr 4.4.2 on disk, the example 0xBEEF → 0x92
from its CRC test. The address 0x62 and the measuring principle are per the
manufacturer's documentation, not verified. All numbers are a run of the driver
_verify/co2_103e/scd103e.c on a PC (gcc 12.2.0) with our own model of the
SCD41 as an I2C slave in Python 3.11; the firmware was built with armclang 6.24
for Cortex-M3. How the sensor reacts to errors, the warm-up, drift, noise and
the calibration algorithms themselves are assumptions in the model: the numbers
below describe the model, not the sensor.
What it is called in the documentation
Original Meaning NDIR (non-dispersive infrared) non-dispersive infrared method photoacoustic the absorbed light is heard as sound ppm (parts per million) millionth part, 1 ppm = 0.0001% periodic / low power periodic measurement periodic (5 s) / low-power (30 s) measurement single shot one measurement on request data ready status "a new measurement is available" command execution time time the command takes to execute ambient pressure / sensor altitude pressure of the surrounding air / installation altitude automatic self-calibration (ASC) automatic calibration forced recalibration (FRC) forced calibration persist settings save the settings to EEPROM
103г.1 Principle: photoacoustic NDIR and what comes out
CO₂ molecules absorb infrared light in a narrow band. Per the manufacturer's documentation (not verified), inside the SCD4x there are an IR source, a chamber with air and a microphone: the source shines in pulses, CO₂ absorbs part of the light and warms up slightly, the gas in the chamber expands and contracts in step with the pulses — this is sound. The more CO₂, the louder. Hence "photoacoustic": the absorption is heard rather than seen by a photodetector.
The microphone hears the number of CO₂ molecules in the chamber, not their share. With the same air at altitude there are fewer molecules in the chamber, and the reading drops — so the sensor has to know the pressure (103г.5). Next to it in the package is a temperature and humidity sensor: its words arrive together with CO₂.
| SCD40 | SCD41 | |
|---|---|---|
| periodic measurement | every 5 s | every 5 s |
| low-power | per the manufacturer's documentation, not verified | every 30 s |
single shot (0x219D, 5 s) |
no | yes |
sleep and wake-up (0x36E0, 0x36F6) |
— | for the single-shot mode |
| automatic calibration periods (44 and 156 h) | not available in Zephyr | changed by commands |
This is how the Zephyr 4.4.2 driver separates them: the "single shot" mode and the automatic calibration periods exist there only for the SCD41. Outdoors CO₂ is about 400 ppm, in a closed room with people it is several times higher; how accurate the sensor itself is cannot be said without the document.
103г.2 Connecting to the F103 and the exchange: command, word, CRC, pause
| Module pin | STM32F103 | Purpose |
|---|---|---|
| SCL | PB6 | clock I2C1_SCL |
| SDA | PB7 | data I2C1_SDA |
| VDD | 3.3 V | supply |
| GND | GND | ground |
The SCL and SDA pull-ups are usually on the module. PB6 and PB7 are FT (p. 32):
if the module is pulled up to 5 V, the F103 pin will survive it. The exchange is
rare — nine bytes every 5 s, so the bus runs at 100 kHz, standard mode. I2C1
sits on APB1 = 36 MHz: in standard mode Thigh = Tlow = CCR · TPCLK1 (p. 784),
CCR = 36 MHz / (2 × 100 kHz) = 180 — 5 µs per half period;
TRISE = 1000 ns × 36 MHz + 1 = 37 (same page: in standard mode the edge is
up to 1000 ns):
static void i2c1_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u; /* PB6, PB7 — альтернативная функция, открытый сток */
I2C1->CR1 = 0; /* настраивать при PE = 0 */
I2C1->CR2 = APB1_MHZ;
I2C1->CCR = 180u; /* Sm: Thigh = Tlow = 180 × 27,8 нс = 5 мкс — 100 кГц */
I2C1->TRISE = APB1_MHZ * 1000u / 1000u + 1u; /* 1000 нс × 36 МГц = 36 → 36 + 1 = 37 */
I2C1->CR1 = I2C_CR1_PE;
}
The SCD4x has no registers. It has 16-bit commands, high byte first. A command comes in three kinds: just two bytes; two bytes plus a data word with its CRC; two bytes, a pause and reading the reply in a separate transaction. While the sensor executes a command it does not answer — neither a write nor a read; how long to wait is written in the table of the Zephyr driver:
| Command | Code | Wait, ms | Reply |
|---|---|---|---|
| start_periodic_measurement | 0x21B1 |
0 | — |
| start_low_power_periodic_measurement | 0x21AC |
0 | — |
| stop_periodic_measurement | 0x3F86 |
500 | — |
| read_measurement | 0xEC05 |
1 | 3 words: CO₂, T, RH |
| get_data_ready_status | 0xE4B8 |
1 | 1 word |
| set / get_sensor_altitude | 0x2427 / 0x2322 |
1 | — / 1 word |
| set / get_ambient_pressure | 0xE000 |
1 | — / 1 word |
| perform_forced_recalibration | 0x362F |
400 | 1 word |
| set / get_automatic_self_calibration | 0x2416 / 0x2313 |
1 | — / 1 word |
| persist_settings | 0x3615 |
800 | — |
| reinit | 0x3646 |
30 | — |
| measure_single_shot | 0x219D |
5000 | — |
| power_down / wake_up | 0x36E0 / 0x36F6 |
1 / 30 | — |
A data word goes as three bytes: high, low and the CRC-8 of these two. A write with a word is five bytes:
/* команда со словом: два байта команды, слово старшим байтом вперёд и CRC-8 этого слова */
int scd_cmd_word(scd_t *d, uint16_t cmd, uint16_t w, uint32_t ms)
{
uint8_t tx[5] = {(uint8_t)(cmd >> 8), (uint8_t)cmd, (uint8_t)(w >> 8), (uint8_t)w, 0};
int rc;
tx[4] = scd_crc(&tx[2], 2);
rc = xfer_rc(d, scd_port_write(d->addr, tx, 5));
if (rc == SCD_OK)
pause_ms(d, ms);
return rc;
}
The reply is read in a separate transaction — without a repeated start, as with register sensors in 17.2, but after a pause. The CRC is checked for every word:
/* чтение n слов: по три байта — старший, младший, CRC; слово с неверной CRC — ошибка всего чтения */
static int read_words(scd_t *d, uint16_t *w, uint32_t n)
{
uint8_t rx[9];
int rc;
if (n == 0u || n > 3u)
return SCD_E_ARG;
rc = xfer_rc(d, scd_port_read(d->addr, rx, 3u * n));
if (rc != SCD_OK)
return rc;
for (uint32_t i = 0; i < n; i++) {
if (d->check_crc && scd_crc(&rx[3u * i], 2) != rx[3u * i + 2u]) {
d->crc_errors++;
return SCD_E_CRC;
}
w[i] = (uint16_t)(rx[3u * i] << 8 | rx[3u * i + 1u]);
}
return SCD_OK;
}
Receiving 3 and 9 bytes in the firmware is method 2 from RM0008
(pp. 762–763), as in 103б. Reading a measurement in the model at 800 ppm,
23.4 °C and 41.2%: write EC 05, pause 1 ms, then
03 20 2A 64 0F 79 69 78 56 — three words 0x0320, 0x640F, 0x6978, each
followed by its CRC. At 100 kHz writing the command takes 290 µs, reading nine
bytes 920 µs, the whole read together with the pause 2210 µs.
103г.3 Sensirion CRC-8: polynomial 0x31, initial value 0xFF
The CRC is computed over the two bytes of a word, most significant bit first, with initial value 0xFF and no final XOR. The polynomial 0x31 is x⁸ + x⁵ + x⁴ + 1:
/* CRC-8 Sensirion: полином x^8 + x^5 + x^4 + 1 (0x31), начало 0xFF, старший бит первым, без итогового XOR */
uint8_t scd_crc(const uint8_t *p, uint32_t n)
{
uint8_t crc = 0xFFu;
while (n--) {
crc ^= *p++;
for (uint32_t b = 0; b < 8u; b++)
crc = (crc & 0x80u) ? (uint8_t)((crc << 1) ^ 0x31u) : (uint8_t)(crc << 1);
}
return crc;
}
The test example is the word 0xBEEF: CRC = 0x92. Exactly this is expected by
the CRC test in Zephyr 4.4.2 (crc8 with polynomial 0x31 and initial value
0xFF), and the same is given by scd_crc on the stand and an independent
Python computation. A few more words:
| Word | 0x0000 | 0x0001 | 0x0190 | 0x0320 | 0x8000 | 0xFFFF |
|---|---|---|---|---|---|---|
| CRC | 0x81 | 0xB0 | 0x4C | 0x2A | 0xA2 | 0xAC |
The CRC of zero is not zero — that is what the initial value 0xFF gives: a broken line that reads as all zeros or all ones will not pass the CRC. The word 0x0320 is the very 800 ppm from the frame above.
Which corruptions the CRC catches: an enumeration of all sets of corrupted bits in the three bytes "word + CRC" for 0xBEEF:
| Bits corrupted | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| sets | 24 | 276 | 2024 | 10 626 |
| CRC missed | 0 | 0 | 0 | 80 |
Any one, two and three errors in a word the CRC always catches; it misses only some of the quadruple ones. This is stronger than the simple sum of the PMS5003 from 103в, which missed every 33rd double corruption. The same class of protection is the PEC of the MLX90614 in 104б, but there the polynomial is 0x07 and the initial value 0.
Corruption experiment. In the model every bit read from the sensor is flipped with a probability of 0.2% (so in the model; a real bus errs far less often). 6000 attempts to read a measurement:
| CRC check | Correct accepted | Wrong accepted | Rejected by CRC | Other errors | Largest CO₂ error, ppm |
|---|---|---|---|---|---|
| on | 3983 | 0 | 2017 | 0 | 0 |
| off | 4920 | 532 | 0 | 548 | 32 768 |
With the check not a single wrong measurement was accepted — the corrupted one simply disappeared, and the firmware waits for the next one 5 s later. Without the check 532 wrong ones went into the program, and one flipped high bit gave an error of 32 768 ppm. Much is rejected because the data_ready replies were corrupted too: the driver asks it up to six times per measurement.
103г.4 Measurement: start, data_ready, conversion into ppm, °C and %RH
Start-up. After power-up the sensor needs 30 ms (a constant of the Zephyr
driver), and the firmware waits them out. Then scd_init: stop the
measurement — it may be running since the previous start, with the
microcontroller merely reset; no answer — then it is asleep, wake it up; then
reinit — reread the settings from EEPROM. In the model on a fresh sensor this
is two commands and 530.58 ms: 500 ms stop, 30 ms reinit. After a
microcontroller reset while the sensor kept measuring — the same. A sleeping
sensor does not acknowledge the stop, and the path is different: 4 exchanges,
2 of them NACK, 91.16 ms.
Wake-up. The sensor executes the wake_up command but does not acknowledge
it — this is also written in the Zephyr driver, which sends it twice. The first
NACK is expected, the second command must pass:
/* пробуждение: первая команда будит, но датчик её не подтверждает — NACK ожидаем; вторая должна пройти */
int scd_wake(scd_t *d)
{
uint8_t tx[2] = {(uint8_t)(SCD_WAKE_UP >> 8), (uint8_t)SCD_WAKE_UP};
int rc;
(void)xfer_rc(d, scd_port_write(d->addr, tx, 2));
scd_port_delay_ms(SCD_MS_WAKE);
rc = scd_cmd(d, SCD_WAKE_UP, SCD_MS_WAKE);
if (rc == SCD_OK)
d->state = SCD_IDLE;
return rc;
}
data_ready. After start_periodic_measurement a new measurement appears
every 5 s. To ask whether it is there — command 0xE4B8: the reply is a word,
and a new measurement is available if its lower 11 bits are not zero. The upper
bits must not be looked at: in the model bit 15 is set, and the word without
data is 0x8000, with data 0x8006. Comparing the whole word with zero would
always see data.
/* есть ли новое измерение: младшие 11 бит слова не ноль */
int scd_ready(scd_t *d, int *ready)
{
uint16_t w;
int rc = scd_query(d, SCD_DATA_READY, SCD_MS_SET, &w, 1);
*ready = rc == SCD_OK && (w & SCD_READY_MASK) != 0u;
return rc;
}
/* ждать новое измерение: спрашивать data_ready раз в poll_ms, не дольше max_ms; потом прочитать */
int scd_wait_read(scd_t *d, scd_meas_t *m, uint32_t poll_ms, uint32_t max_ms, uint32_t *polls)
{
uint32_t waited = 0;
int ready, rc;
*polls = 0;
for (;;) {
(*polls)++;
if ((rc = scd_ready(d, &ready)) != SCD_OK)
return rc;
if (ready)
return scd_read(d, m);
if (waited >= max_ms)
return SCD_E_TIMEOUT;
scd_port_delay_ms(poll_ms);
waited += poll_ms;
}
}
In the model an hour of periodic mode with polling once a second: measurements — 720, the first 5013 ms after the start, then exactly every 5000 ms, data_ready per measurement — 5.99 times, exchanges per hour — 10 067, not a single NACK and not a single CRC error. Low-power mode — every 30 s: 120 measurements per hour, the first after 30 054 ms; but polling once a second there means 30.95 questions per answer and 7671 exchanges: it is worth polling less often.
Reading blindly, without data_ready, does not work: in the model a read without a new measurement gets NACK (model assumption). 60 reads once a second — 12 successful and 48 NACK.
Single shot (SCD41). Command 0x219D — one measurement in 5 s, then a
normal read. Between measurements the sensor can be put to sleep
(power_down), and until the next time it does not measure. In the model the
readings right after waking are too high (model assumption, see 103г.6): a
cycle every 5 minutes, waking up and taking two measurements in a row — 834 and
810 ppm with the true 800, 10 066 ms for both. That is why the first one after
waking is thrown away. The SCD40 does not have this command: the driver at once
returns −8, SCD_E_MODEL, and the bare command gets NACK in the model.
Converting the words. CO₂ is the word as is, in ppm. Temperature and humidity — by the formulas of the Zephyr driver:
T = −45 + 175 · word / 65535 °C, RH = 100 · word / 65535 %.
Without floating point (the F103 has none) — in hundredths with rounding:
/* °C = −45 + 175 · слово / 65535, %RH = 100 · слово / 65535 — в сотых, с округлением */
void scd_convert(scd_meas_t *m)
{
m->t100 = (int16_t)(-4500 + (int32_t)((17500u * m->t_raw + 32767u) / 65535u));
m->rh100 = (uint16_t)((10000u * m->rh_raw + 32767u) / 65535u);
}
17 500 × 65 535 is less than 2³², there is no overflow. All 65 536 words were run on the stand: the largest difference from the exact formula is 0.5 hundredth, mismatches with correct rounding — 0. In the frame above 0x640F = 25 615 gives 23.40 °C, 0x6978 = 27 000 gives 41.20%. The word 0x6666 is exactly 25.00 °C and 40.00%; 0x0000 — −45 °C and 0%, 0xFFFF — 130 °C and 100%.
103г.5 Pressure and altitude
The sensor hears the number of molecules but shows a share. To convert one into
the other, it needs the pressure. By default the altitude is 0: the sensor takes
the pressure to be sea level. You can tell it the truth in two ways: set the
installation altitude once (0x2427, 0…3000 m) or now and then pass the
pressure from a barometer (0xE000, 700…1200 hPa). The limits are as in the
Zephyr driver; outside them scd_set_pressure returns −7, SCD_E_ARG (in the
model — for 699 and 1201).
In the model the reading is proportional to p / p_sensor, and the sensor takes the pressure for an altitude from the standard atmosphere (model assumption; how the real sensor converts it is not verified). At 1500 m this is 845.6 hPa against 1013.25 at sea level. The truth is 800 ppm, noise is off:
| What the sensor is told | Actual pressure, hPa | Reading, ppm | Error, ppm |
|---|---|---|---|
| nothing (altitude 0) | 845.6 | 668 | −132 |
| altitude 1500 m | 845.6 | 800 | 0 |
| pressure 846 hPa | 845.6 | 800 | 0 |
| altitude 1500 m, weather −15 hPa | 830.6 | 786 | −14 |
| altitude 1500 m, weather +15 hPa | 860.6 | 814 | 14 |
The altitude removes the main thing — mountains and floors; the pressure from a
barometer removes the weather as well. The pressure is the only setting that can
be changed during a measurement: this is what the Zephyr driver does too. In the
model scd_set_pressure during a measurement gives 0, and writing the altitude
without stopping gives NACK, −1. The driver itself stops the measurement before
a setting and starts it again:
/* высота и автокалибровка: прочитать, записать только то, что отличается; *changed — было ли что записать */
int scd_config(scd_t *d, uint16_t alt_m, uint8_t asc, uint8_t *changed)
{
uint16_t w;
uint8_t was;
int rc;
*changed = 0;
if (alt_m > SCD_ALT_MAX || asc > 1u)
return SCD_E_ARG;
if ((rc = pause_meas(d, &was)) != SCD_OK)
return rc;
if ((rc = scd_query(d, SCD_GET_ALTITUDE, SCD_MS_SET, &w, 1)) == SCD_OK && w != alt_m) {
rc = scd_cmd_word(d, SCD_SET_ALTITUDE, alt_m, SCD_MS_SET);
*changed = 1;
}
if (rc == SCD_OK && (rc = scd_query(d, SCD_GET_ASC, SCD_MS_SET, &w, 1)) == SCD_OK && w != asc) {
rc = scd_cmd_word(d, SCD_SET_ASC, asc, SCD_MS_SET);
*changed = 1;
}
return resume_meas(d, was, rc);
}
103г.6 Warm-up, automatic calibration, FRC and EEPROM
Warm-up. How long the transient after power-up lasts in the real sensor
cannot be said without the document. In the model the reading after power-up or
wake-up is too high by 120 · e^(−t / 4 s) ppm (model assumption). Noise is
off, the measurement is started 0.56 s after power-up (30 ms of waiting and
530 ms of scd_init):
| After the measurement start, s | 5 | 10 | 15 | 20 | 30 | 60 |
|---|---|---|---|---|---|---|
| error, ppm | 30 | 9 | 2 | 1 | 0 | 0 |
Hence the firmware rule: do not take the first measurement after power-up and wake-up, and calibrate only after long operation.
Automatic calibration (ASC). An NDIR sensor drifts with time. Automatic calibration assumes that the sensor sees fresh air from time to time and pulls its zero towards it. In the Zephyr driver it is enabled from the factory, and for the SCD41 the first period is 44 h, the following ones 156 h each. In the model at the end of a period the lowest reading of the period is declared to be 400 ppm (model assumption: the real algorithm is not disclosed). Drift +80 ppm, noise off, error of the night reading at 03:00 by day:
| Room | Actual at night, ppm | Day 1 | Day 2 | Days 3–10 |
|---|---|---|---|---|
| office, aired at night, ASC on | 420 | 80 | −20 | −20 |
| bedroom, closed at night, ASC on | 650 | 80 | −250 | −250 |
| office, ASC off | 420 | 80 | 80 | 80 |
In the office automatic calibration removed the drift (−20 is 420 against 400). In the bedroom, where the air is never fresh, it decided that 730 is 400 and began to read 250 ppm low: more dangerous than without it. If the sensor never sees the outdoors — in a greenhouse, in a cellar, in an unventilated room — automatic calibration is turned off and calibration is done by hand.
Forced calibration (FRC). The sensor is taken out into fresh air, allowed
to run (the Zephyr driver requires at least 3 minutes in periodic mode at a
steady concentration), the measurement is stopped and 0x362F is sent with the
reference concentration — 400 ppm for the outdoors. After 400 ms the sensor
replies with a word: 0x8000 plus the correction, and 0xFFFF is a failure:
/* принудительная калибровка: датчик не меньше 3 минут мерил воздух с известной концентрацией ppm */
int scd_frc(scd_t *d, uint16_t ppm, int16_t *corr)
{
uint16_t w = SCD_FRC_FAIL;
uint8_t was;
int rc = pause_meas(d, &was);
if (rc == SCD_OK && (rc = scd_cmd_word(d, SCD_FRC, ppm, SCD_MS_FRC)) == SCD_OK
&& (rc = read_words(d, &w, 1)) == SCD_OK) {
if (w == SCD_FRC_FAIL)
rc = SCD_E_FRC;
else
*corr = (int16_t)((int32_t)w - (int32_t)SCD_FRC_ZERO);
}
return resume_meas(d, was, rc);
}
In the model FRC takes the mean of the last 12 measurements, and the sign of the correction is "reference minus measured" (model assumptions). Drift +87 ppm, noise 10 ppm, 3 minutes of operation:
| Case | True, ppm | Before, ppm | Code | Correction | After, ppm | Error after, ppm |
|---|---|---|---|---|---|---|
| outdoors, reference 400 | 400 | 489.8 | 0 | −90 | 401.2 | 1.2 |
| without stopping the measurement | 400 | 489.8 | −1 | — | 491.1 | 91.1 |
| a room, but reference 400 | 600 | 689.8 | 0 | −290 | 401.2 | −198.8 |
The calibration outdoors removed the drift. Without stopping the measurement the
sensor did not accept the command — NACK. And a reference of 400 ppm in a room
that actually has 600 spoiled the sensor by 200 ppm: FRC trusts the reference,
not the air. If the sensor has not measured once since power-up, in the model
the reply is 0xFFFF, and the driver returns −6, SCD_E_FRC.
EEPROM. The altitude and automatic calibration live in the sensor's RAM, and
after power is removed they come back from EEPROM. persist_settings saves them
there (800 ms). The Zephyr driver warns: the EEPROM is guaranteed to withstand
at least 2000 writes. A firmware that sets up and saves on every start wrote the
EEPROM 100 times in 100 starts in the model; the one that first reads and saves
only changes — 1 time. With a restart every hour the first one will use up the
2000 writes in 83.3 days:
static void sensor_up(void)
{
uint8_t changed;
while (scd_init(&dev) != SCD_OK)
scd_port_delay_ms(1000u); /* нет ответа: провод, адрес, питание, подтяжки */
if (scd_config(&dev, ALTITUDE_M, ASC_ON, &changed) == SCD_OK && changed)
(void)scd_persist(&dev); /* EEPROM — только когда что-то изменилось */
(void)scd_start(&dev, 0);
}
The firmware does not save the pressure at all: it sends it on the fly.
103г.7 Pitfalls
A setting during a measurement. While periodic measurement is running, the
sensor accepts only reading, data_ready, stop and pressure — this is how the
Zephyr driver is built too. Altitude, ASC, FRC and persist_settings without
stop_periodic_measurement get NACK in the model. The driver stops the
measurement itself and then starts it again.
Not waiting out the pause. After stop_periodic_measurement — 500 ms,
after data_ready and read_measurement — 1 ms, after FRC — 400 ms. Without
the pauses in the model: stop and at once start — the start got NACK;
data_ready without 1 ms — the read got NACK, and with the pause 0.
Reading without data_ready. There is no new measurement — NACK in the model: 48 of 60 reads once a second. Ask data_ready and look only at the lower 11 bits.
Wake-up without an answer. wake_up is not acknowledged. A driver that
treats this NACK as an error will decide there is no sensor, although it has
already woken up.
persist_settings on every start. EEPROM — at least 2000 writes; once an hour is less than three months. Save only changes.
Automatic calibration where there is no fresh air. In the model — minus 250 ppm in a closed bedroom. Turn ASC off or air the room.
FRC not outdoors. A reference of 400 ppm in a room with 600 — minus 200 ppm in the model. And not earlier than after 3 minutes of operation.
Altitude and pressure. Without them at 1500 m in the model −132 ppm. The altitude is set once, the pressure is sent on the fly.
No CRC. Without the check — 532 wrong of 6000 in the experiment of 103г.3 and an error of up to 32 768 ppm.
103г.8 Summary
- The SCD40 and SCD41 measure CO₂ by photoacoustic NDIR and return words: CO₂ in ppm, T and RH; commands are 16 bits, a word is three bytes with CRC-8.
- CRC-8: polynomial 0x31, initial value 0xFF; 0xBEEF → 0x92; any 1–3 corrupted bits in a word it always catches, of 10 626 quadruple ones it misses 80.
- T = −45 + 175 · word / 65535, RH = 100 · word / 65535; in hundredths without floating point — on all 65 536 words the error is at most 0.5 hundredth.
- Periodic measurement — every 5 s, low-power — every 30 s, single shot — 5 s, SCD41 only; a new measurement — by the lower 11 bits of data_ready.
- After a command — a pause: 500 ms after stop, 1 ms after a read, 400 ms after
FRC, 800 ms after
persist_settings. - Settings — only after stopping, except the pressure; without the altitude at 1500 m the model gives −132 ppm.
- ASC — only if the sensor sees fresh air; FRC — outdoors with a reference of 400 ppm after 3 minutes of operation; EEPROM — 2000 writes, save only changes.
Full sources
scd103e.h — commands, their execution times, limits, error codes and functions of the SCD40/SCD41 driver.
/* scd103e.h — раздел 103г: драйвер датчика CO₂ SCD40/SCD41 (Sensirion) по I2C без регистров микроконтроллера.
* Драйвер говорит с датчиком только через функции порта (scd_port_*): в прошивке fw103e.c их делает I2C1 и
* SysTick STM32F103, в стенде host103e.c — модель model103e.py.
* Коды команд, время их выполнения, CRC-8 (0x31, начало 0xFF), маска data_ready 0x07FF, формулы °C и %RH,
* ответ FRC (0x8000 + поправка, 0xFFFF — отказ), пределы давления и высоты — как в драйвере scd4x Zephyr 4.4.2.
* Адрес 0x62 — по документации производителя, с датчиком не сверено. */
#ifndef SCD103E_H
#define SCD103E_H
#include <stdint.h>
#define SCD_ADDR 0x62u /* семибитный, по документации производителя */
/* ── команды: 16 бит, старшим байтом вперёд ───────────────────────────── */
#define SCD_START_PERIODIC 0x21B1u /* измерение раз в 5 с */
#define SCD_START_LOWPOWER 0x21ACu /* измерение раз в 30 с */
#define SCD_STOP_PERIODIC 0x3F86u
#define SCD_READ_MEAS 0xEC05u /* ответ — три слова: CO₂, T, RH */
#define SCD_DATA_READY 0xE4B8u /* ответ — слово: младшие 11 бит не 0 — есть новое измерение */
#define SCD_SET_ALTITUDE 0x2427u /* слово — высота, м */
#define SCD_GET_ALTITUDE 0x2322u
#define SCD_PRESSURE 0xE000u /* со словом — записать давление, гПа; без слова — прочитать */
#define SCD_FRC 0x362Fu /* слово — опорная концентрация, ppm; ответ — 0x8000 + поправка */
#define SCD_SET_ASC 0x2416u /* слово: 1 — автокалибровка включена, 0 — выключена */
#define SCD_GET_ASC 0x2313u
#define SCD_PERSIST 0x3615u /* настройки — в EEPROM */
#define SCD_REINIT 0x3646u /* перечитать настройки из EEPROM */
#define SCD_SINGLE_SHOT 0x219Du /* одно измерение — только SCD41 */
#define SCD_POWER_DOWN 0x36E0u
#define SCD_WAKE_UP 0x36F6u /* датчик просыпается, но не подтверждает: NACK */
/* ── время выполнения, мс: до него датчик не отвечает ─────────────────── */
#define SCD_MS_STOP 500u
#define SCD_MS_READ 1u
#define SCD_MS_SET 1u /* data_ready, высота, давление, ASC — чтение и запись */
#define SCD_MS_FRC 400u
#define SCD_MS_PERSIST 800u
#define SCD_MS_REINIT 30u
#define SCD_MS_SINGLE 5000u
#define SCD_MS_POWER_DOWN 1u
#define SCD_MS_WAKE 30u
#define SCD_MS_STARTUP 30u /* после включения питания */
#define SCD_PERIOD_MS 5000u
#define SCD_LOWPOWER_MS 30000u
#define SCD_READY_MASK 0x07FFu
#define SCD_FRC_FAIL 0xFFFFu
#define SCD_FRC_ZERO 0x8000u
#define SCD_P_MIN 700u /* давление, гПа */
#define SCD_P_MAX 1200u
#define SCD_ALT_MAX 3000u /* высота, м */
enum { SCD_OK = 0, SCD_E_NACK = -1, SCD_E_BUS = -2, SCD_E_CRC = -3, SCD_E_BUSY = -4, SCD_E_TIMEOUT = -5,
SCD_E_FRC = -6, SCD_E_ARG = -7, SCD_E_MODEL = -8 };
enum { SCD_IDLE = 0, SCD_PERIODIC = 1, SCD_LOWPOWER = 2, SCD_SLEEP = 3 };
typedef struct {
uint16_t co2; /* ppm — слово как есть */
uint16_t t_raw, rh_raw; /* сырые слова */
int16_t t100; /* °C × 100 */
uint16_t rh100; /* %RH × 100 */
} scd_meas_t;
typedef struct {
uint8_t addr;
uint8_t scd41; /* 1 — SCD41: есть однократное измерение */
uint8_t state; /* SCD_IDLE … SCD_SLEEP — что, по мнению драйвера, делает датчик */
uint8_t check_crc; /* 1; 0 — CRC не проверять (для опыта) */
uint8_t wait; /* 1; 0 — не ждать время выполнения (для опыта) */
uint8_t guard; /* 1; 0 — не останавливать измерение перед настройкой (для опыта) */
uint32_t xfers, nacks, crc_errors, persists;
} scd_t;
/* ── порт: в прошивке — I2C1 и SysTick; в стенде — строки к модели ────── */
int scd_port_write(uint8_t addr7, const uint8_t *tx, uint32_t n); /* START, адрес + W, n байт, STOP */
int scd_port_read(uint8_t addr7, uint8_t *rx, uint32_t n); /* START, адрес + R, n байт, STOP */
void scd_port_delay_ms(uint32_t ms);
/* ── драйвер ──────────────────────────────────────────────────────────── */
uint8_t scd_crc(const uint8_t *p, uint32_t n);
void scd_dev_init(scd_t *d, uint8_t scd41);
int scd_cmd(scd_t *d, uint16_t cmd, uint32_t ms);
int scd_cmd_word(scd_t *d, uint16_t cmd, uint16_t w, uint32_t ms);
int scd_query(scd_t *d, uint16_t cmd, uint32_t ms, uint16_t *w, uint32_t n);
int scd_init(scd_t *d);
int scd_start(scd_t *d, int lowpower);
int scd_stop(scd_t *d);
int scd_ready(scd_t *d, int *ready);
int scd_read(scd_t *d, scd_meas_t *m);
int scd_wait_read(scd_t *d, scd_meas_t *m, uint32_t poll_ms, uint32_t max_ms, uint32_t *polls);
int scd_single(scd_t *d, scd_meas_t *m);
int scd_set_pressure(scd_t *d, uint16_t hpa);
int scd_config(scd_t *d, uint16_t alt_m, uint8_t asc, uint8_t *changed);
int scd_persist(scd_t *d);
int scd_frc(scd_t *d, uint16_t ppm, int16_t *corr);
int scd_sleep(scd_t *d);
int scd_wake(scd_t *d);
void scd_convert(scd_meas_t *m);
#endif
scd103e.c — the driver: CRC-8, commands with words, checked reads, measurement, pressure, altitude, calibration and sleep.
/* scd103e.c — раздел 103г: драйвер SCD40/SCD41. Команда — два байта; слово данных — два байта и CRC-8; после
* команды — пауза на её выполнение; настройки — только при остановленном измерении. Регистров микроконтроллера
* здесь нет: тот же файл собирается в прошивку и в стенд на компьютере. */
#include "scd103e.h"
/* CRC-8 Sensirion: полином x^8 + x^5 + x^4 + 1 (0x31), начало 0xFF, старший бит первым, без итогового XOR */
uint8_t scd_crc(const uint8_t *p, uint32_t n)
{
uint8_t crc = 0xFFu;
while (n--) {
crc ^= *p++;
for (uint32_t b = 0; b < 8u; b++)
crc = (crc & 0x80u) ? (uint8_t)((crc << 1) ^ 0x31u) : (uint8_t)(crc << 1);
}
return crc;
}
void scd_dev_init(scd_t *d, uint8_t scd41)
{
*d = (scd_t){0};
d->addr = SCD_ADDR;
d->scd41 = scd41;
d->check_crc = 1;
d->wait = 1;
d->guard = 1;
d->state = SCD_IDLE;
}
static int xfer_rc(scd_t *d, int rc)
{
d->xfers++;
if (rc == SCD_E_NACK)
d->nacks++;
return rc;
}
static void pause_ms(scd_t *d, uint32_t ms)
{
if (d->wait && ms)
scd_port_delay_ms(ms);
}
/* команда без данных: два байта, затем время выполнения */
int scd_cmd(scd_t *d, uint16_t cmd, uint32_t ms)
{
uint8_t tx[2] = {(uint8_t)(cmd >> 8), (uint8_t)cmd};
int rc = xfer_rc(d, scd_port_write(d->addr, tx, 2));
if (rc == SCD_OK)
pause_ms(d, ms);
return rc;
}
/* команда со словом: два байта команды, слово старшим байтом вперёд и CRC-8 этого слова */
int scd_cmd_word(scd_t *d, uint16_t cmd, uint16_t w, uint32_t ms)
{
uint8_t tx[5] = {(uint8_t)(cmd >> 8), (uint8_t)cmd, (uint8_t)(w >> 8), (uint8_t)w, 0};
int rc;
tx[4] = scd_crc(&tx[2], 2);
rc = xfer_rc(d, scd_port_write(d->addr, tx, 5));
if (rc == SCD_OK)
pause_ms(d, ms);
return rc;
}
/* чтение n слов: по три байта — старший, младший, CRC; слово с неверной CRC — ошибка всего чтения */
static int read_words(scd_t *d, uint16_t *w, uint32_t n)
{
uint8_t rx[9];
int rc;
if (n == 0u || n > 3u)
return SCD_E_ARG;
rc = xfer_rc(d, scd_port_read(d->addr, rx, 3u * n));
if (rc != SCD_OK)
return rc;
for (uint32_t i = 0; i < n; i++) {
if (d->check_crc && scd_crc(&rx[3u * i], 2) != rx[3u * i + 2u]) {
d->crc_errors++;
return SCD_E_CRC;
}
w[i] = (uint16_t)(rx[3u * i] << 8 | rx[3u * i + 1u]);
}
return SCD_OK;
}
/* команда, время выполнения, затем чтение ответа */
int scd_query(scd_t *d, uint16_t cmd, uint32_t ms, uint16_t *w, uint32_t n)
{
int rc = scd_cmd(d, cmd, ms);
return rc != SCD_OK ? rc : read_words(d, w, n);
}
static int running(const scd_t *d)
{
return d->state == SCD_PERIODIC || d->state == SCD_LOWPOWER;
}
int scd_start(scd_t *d, int lowpower)
{
int rc = scd_cmd(d, lowpower ? SCD_START_LOWPOWER : SCD_START_PERIODIC, 0);
if (rc == SCD_OK)
d->state = lowpower ? SCD_LOWPOWER : SCD_PERIODIC;
return rc;
}
int scd_stop(scd_t *d)
{
int rc = scd_cmd(d, SCD_STOP_PERIODIC, SCD_MS_STOP);
if (rc == SCD_OK)
d->state = SCD_IDLE;
return rc;
}
/* настройки — только в покое: остановить измерение и запомнить, что оно шло */
static int pause_meas(scd_t *d, uint8_t *was)
{
*was = d->state;
if (running(d) && d->guard)
return scd_stop(d);
return SCD_OK;
}
static int resume_meas(scd_t *d, uint8_t was, int rc)
{
if ((was == SCD_PERIODIC || was == SCD_LOWPOWER) && d->state == SCD_IDLE) {
int r2 = scd_start(d, was == SCD_LOWPOWER);
if (rc == SCD_OK)
rc = r2;
}
return rc;
}
/* после включения: остановить измерение (если шло с прошлого запуска), разбудить (если спал), перечитать EEPROM */
int scd_init(scd_t *d)
{
int rc = scd_cmd(d, SCD_STOP_PERIODIC, SCD_MS_STOP);
if (rc != SCD_OK && (rc = scd_wake(d)) != SCD_OK)
return rc;
d->state = SCD_IDLE;
return scd_cmd(d, SCD_REINIT, SCD_MS_REINIT);
}
/* есть ли новое измерение: младшие 11 бит слова не ноль */
int scd_ready(scd_t *d, int *ready)
{
uint16_t w;
int rc = scd_query(d, SCD_DATA_READY, SCD_MS_SET, &w, 1);
*ready = rc == SCD_OK && (w & SCD_READY_MASK) != 0u;
return rc;
}
/* °C = −45 + 175 · слово / 65535, %RH = 100 · слово / 65535 — в сотых, с округлением */
void scd_convert(scd_meas_t *m)
{
m->t100 = (int16_t)(-4500 + (int32_t)((17500u * m->t_raw + 32767u) / 65535u));
m->rh100 = (uint16_t)((10000u * m->rh_raw + 32767u) / 65535u);
}
int scd_read(scd_t *d, scd_meas_t *m)
{
uint16_t w[3];
int rc = scd_query(d, SCD_READ_MEAS, SCD_MS_READ, w, 3);
if (rc != SCD_OK)
return rc;
m->co2 = w[0];
m->t_raw = w[1];
m->rh_raw = w[2];
scd_convert(m);
return SCD_OK;
}
/* ждать новое измерение: спрашивать data_ready раз в poll_ms, не дольше max_ms; потом прочитать */
int scd_wait_read(scd_t *d, scd_meas_t *m, uint32_t poll_ms, uint32_t max_ms, uint32_t *polls)
{
uint32_t waited = 0;
int ready, rc;
*polls = 0;
for (;;) {
(*polls)++;
if ((rc = scd_ready(d, &ready)) != SCD_OK)
return rc;
if (ready)
return scd_read(d, m);
if (waited >= max_ms)
return SCD_E_TIMEOUT;
scd_port_delay_ms(poll_ms);
waited += poll_ms;
}
}
/* SCD41: одно измерение из покоя или сна — 5 с, затем обычное чтение */
int scd_single(scd_t *d, scd_meas_t *m)
{
int rc;
if (!d->scd41)
return SCD_E_MODEL;
if (running(d))
return SCD_E_BUSY;
if (d->state == SCD_SLEEP && (rc = scd_wake(d)) != SCD_OK)
return rc;
if ((rc = scd_cmd(d, SCD_SINGLE_SHOT, SCD_MS_SINGLE)) != SCD_OK)
return rc;
return scd_read(d, m);
}
/* давление — единственная настройка, которую можно менять на ходу */
int scd_set_pressure(scd_t *d, uint16_t hpa)
{
if (hpa < SCD_P_MIN || hpa > SCD_P_MAX)
return SCD_E_ARG;
return scd_cmd_word(d, SCD_PRESSURE, hpa, SCD_MS_SET);
}
/* высота и автокалибровка: прочитать, записать только то, что отличается; *changed — было ли что записать */
int scd_config(scd_t *d, uint16_t alt_m, uint8_t asc, uint8_t *changed)
{
uint16_t w;
uint8_t was;
int rc;
*changed = 0;
if (alt_m > SCD_ALT_MAX || asc > 1u)
return SCD_E_ARG;
if ((rc = pause_meas(d, &was)) != SCD_OK)
return rc;
if ((rc = scd_query(d, SCD_GET_ALTITUDE, SCD_MS_SET, &w, 1)) == SCD_OK && w != alt_m) {
rc = scd_cmd_word(d, SCD_SET_ALTITUDE, alt_m, SCD_MS_SET);
*changed = 1;
}
if (rc == SCD_OK && (rc = scd_query(d, SCD_GET_ASC, SCD_MS_SET, &w, 1)) == SCD_OK && w != asc) {
rc = scd_cmd_word(d, SCD_SET_ASC, asc, SCD_MS_SET);
*changed = 1;
}
return resume_meas(d, was, rc);
}
/* настройки — в EEPROM; у EEPROM ограниченное число записей: звать, только когда что-то изменилось */
int scd_persist(scd_t *d)
{
uint8_t was;
int rc = pause_meas(d, &was);
if (rc == SCD_OK && (rc = scd_cmd(d, SCD_PERSIST, SCD_MS_PERSIST)) == SCD_OK)
d->persists++;
return resume_meas(d, was, rc);
}
/* принудительная калибровка: датчик не меньше 3 минут мерил воздух с известной концентрацией ppm */
int scd_frc(scd_t *d, uint16_t ppm, int16_t *corr)
{
uint16_t w = SCD_FRC_FAIL;
uint8_t was;
int rc = pause_meas(d, &was);
if (rc == SCD_OK && (rc = scd_cmd_word(d, SCD_FRC, ppm, SCD_MS_FRC)) == SCD_OK
&& (rc = read_words(d, &w, 1)) == SCD_OK) {
if (w == SCD_FRC_FAIL)
rc = SCD_E_FRC;
else
*corr = (int16_t)((int32_t)w - (int32_t)SCD_FRC_ZERO);
}
return resume_meas(d, was, rc);
}
int scd_sleep(scd_t *d)
{
uint8_t was;
int rc = pause_meas(d, &was);
if (rc == SCD_OK && (rc = scd_cmd(d, SCD_POWER_DOWN, SCD_MS_POWER_DOWN)) == SCD_OK)
d->state = SCD_SLEEP;
return rc;
}
/* пробуждение: первая команда будит, но датчик её не подтверждает — NACK ожидаем; вторая должна пройти */
int scd_wake(scd_t *d)
{
uint8_t tx[2] = {(uint8_t)(SCD_WAKE_UP >> 8), (uint8_t)SCD_WAKE_UP};
int rc;
(void)xfer_rc(d, scd_port_write(d->addr, tx, 2));
scd_port_delay_ms(SCD_MS_WAKE);
rc = scd_cmd(d, SCD_WAKE_UP, SCD_MS_WAKE);
if (rc == SCD_OK)
d->state = SCD_IDLE;
return rc;
}
fw103e.c — F103 firmware: I2C1 at 100 kHz, sensor start-up, a measurement every 5 s, pressure and FRC via the debugger.
/* fw103e.c — раздел 103г: датчик CO₂ SCD40/SCD41 на STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц.
* I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 100 кГц, подтяжки — на модуле датчика). SysTick — 1 мс.
* После включения: 30 мс, scd_init, высота и автокалибровка — записать, только если отличаются, и тогда же
* сохранить в EEPROM; затем измерение раз в 5 с. Результат — в переменных для отладчика. Через отладчик:
* pressure_hpa — давление (700…1200, на ходу), frc_ppm — принудительная калибровка на эту концентрацию
* (датчик перед этим не меньше 3 минут на свежем воздухе). Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "scd103e.h"
#define CPU_HZ 72000000u
#define APB1_MHZ 36u
#define I2C_TIMEOUT_US 2000u /* дольше любого флага на 100 кГц: байт — 90 мкс */
#define POLL_MS 1000u /* data_ready — раз в секунду */
#define ALTITUDE_M 0u /* высота установки над уровнем моря, м */
#define ASC_ON 1u /* автокалибровка: только если датчик хотя бы раз в неделю видит улицу */
static scd_t dev;
static volatile uint32_t ms;
volatile uint32_t co2_ppm, readings, errors, polls, pressure_hpa, frc_ppm;
volatile int32_t t_c100, rh100, frc_corr, last_rc;
void SysTick_Handler(void)
{
ms++;
}
void scd_port_delay_ms(uint32_t n)
{
uint32_t t0 = ms;
while (ms - t0 < n) {
__WFI();
}
}
static int expired(uint32_t t0, uint32_t us)
{
return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}
static void i2c1_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u; /* PB6, PB7 — альтернативная функция, открытый сток */
I2C1->CR1 = 0; /* настраивать при PE = 0 */
I2C1->CR2 = APB1_MHZ;
I2C1->CCR = 180u; /* Sm: Thigh = Tlow = 180 × 27,8 нс = 5 мкс — 100 кГц */
I2C1->TRISE = APB1_MHZ * 1000u / 1000u + 1u; /* 1000 нс × 36 МГц = 36 → 36 + 1 = 37 */
I2C1->CR1 = I2C_CR1_PE;
}
static int sr1_wait(uint16_t mask)
{
uint32_t t0 = DWT->CYCCNT;
while (!(I2C1->SR1 & mask)) {
if (I2C1->SR1 & I2C_SR1_AF)
return SCD_E_NACK; /* адрес или байт не подтверждён: датчик занят */
if (expired(t0, I2C_TIMEOUT_US))
return SCD_E_BUS;
}
return SCD_OK;
}
static int fail(int rc)
{
I2C1->CR1 |= I2C_CR1_STOP;
I2C1->SR1 = (uint16_t)~I2C_SR1_AF; /* AF снимается записью нуля */
I2C1->CR1 |= I2C_CR1_ACK;
return rc;
}
static int start(uint8_t addr_rw)
{
int rc;
uint32_t t0 = DWT->CYCCNT;
while (I2C1->SR2 & I2C_SR2_BUSY) { /* SDA держит ведомый — раздел 17.6 */
if (expired(t0, I2C_TIMEOUT_US))
return SCD_E_BUS;
}
I2C1->CR1 |= I2C_CR1_ACK;
I2C1->CR1 |= I2C_CR1_START;
if ((rc = sr1_wait(I2C_SR1_SB)) != SCD_OK)
return rc;
I2C1->DR = addr_rw;
return sr1_wait(I2C_SR1_ADDR);
}
int scd_port_write(uint8_t addr7, const uint8_t *tx, uint32_t n)
{
int rc;
if ((rc = start((uint8_t)(addr7 << 1))) != SCD_OK)
return fail(rc);
(void)I2C1->SR2;
for (uint32_t i = 0; i < n; i++) {
if ((rc = sr1_wait(I2C_SR1_TXE)) != SCD_OK)
return fail(rc);
I2C1->DR = tx[i];
}
if ((rc = sr1_wait(I2C_SR1_BTF)) != SCD_OK)
return fail(rc);
I2C1->CR1 |= I2C_CR1_STOP;
return SCD_OK;
}
/* приём n ≥ 3 байт ведущим — способ 2, RM0008, с. 762–763 (драйвер читает 3 или 9) */
int scd_port_read(uint8_t addr7, uint8_t *rx, uint32_t n)
{
int rc;
uint32_t i = 0;
if (n < 3u)
return SCD_E_ARG;
if ((rc = start((uint8_t)(addr7 << 1 | 1u))) != SCD_OK)
return fail(rc);
(void)I2C1->SR2; /* ADDR снят, ACK = 1 */
for (; i + 3u < n; i++) {
if ((rc = sr1_wait(I2C_SR1_RXNE)) != SCD_OK)
return fail(rc);
rx[i] = (uint8_t)I2C1->DR;
}
if ((rc = sr1_wait(I2C_SR1_BTF)) != SCD_OK) /* осталось три: N−2 в DR, N−1 в сдвиговом */
return fail(rc);
I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
rx[i++] = (uint8_t)I2C1->DR; /* N−2; пошёл приём N с NACK */
if ((rc = sr1_wait(I2C_SR1_BTF)) != SCD_OK)
return fail(rc);
I2C1->CR1 |= I2C_CR1_STOP;
rx[i++] = (uint8_t)I2C1->DR; /* N−1 */
if ((rc = sr1_wait(I2C_SR1_RXNE)) != SCD_OK)
return fail(rc);
rx[i] = (uint8_t)I2C1->DR; /* N */
I2C1->CR1 |= I2C_CR1_ACK;
return SCD_OK;
}
static void sensor_up(void)
{
uint8_t changed;
while (scd_init(&dev) != SCD_OK)
scd_port_delay_ms(1000u); /* нет ответа: провод, адрес, питание, подтяжки */
if (scd_config(&dev, ALTITUDE_M, ASC_ON, &changed) == SCD_OK && changed)
(void)scd_persist(&dev); /* EEPROM — только когда что-то изменилось */
(void)scd_start(&dev, 0);
}
int main(void)
{
scd_meas_t m;
uint32_t p_set = 0, n;
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
SysTick_Config(CPU_HZ / 1000u);
i2c1_init();
scd_port_delay_ms(SCD_MS_STARTUP); /* после включения датчику нужно 30 мс */
scd_dev_init(&dev, 1);
sensor_up();
for (;;) {
int rc = scd_wait_read(&dev, &m, POLL_MS, 2u * SCD_PERIOD_MS, &n);
last_rc = rc;
polls += n;
if (rc == SCD_OK) {
co2_ppm = m.co2;
t_c100 = m.t100;
rh100 = m.rh100;
readings++;
} else if (rc != SCD_E_CRC) {
errors++; /* молчит дольше двух периодов: заново */
sensor_up();
continue;
} else {
errors++; /* испорченное слово: просто следующее измерение */
}
if (pressure_hpa != p_set && scd_set_pressure(&dev, (uint16_t)pressure_hpa) == SCD_OK)
p_set = pressure_hpa; /* давление — на ходу, без остановки */
if (frc_ppm) {
int16_t corr = 0;
last_rc = scd_frc(&dev, (uint16_t)frc_ppm, &corr); /* сама остановит измерение и запустит снова */
frc_corr = corr;
frc_ppm = 0;
}
}
}
Next: section 104 "Thermocouple: MAX31855 and MAX6675" — the thermocouple, the cold junction and ready-made converters on SPI.