Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32U585 · уровень средний · 33 мин чтения
Открыть в интерактивном справочнике

198х. Мини-проект: датчики платы B-U585I-IOT02A — климат, движение и свет

На плате B-U585I-IOT02A рядом с STM32U585 стоят пять датчиков: влажности и температуры HTS221, давления LPS22HH, ускорения и угловой скорости ISM330DHCX, магнитного поля IIS2MDC и освещённости VEML6030. Все пятеро сидят на одной шине I2C2 и все отдают сырые числа: отсчёт АЦП, счёт импульсов, 24 бита давления. Превратить их в градусы, гектопаскали, мг и люксы — дело программы, и у каждого датчика своё правило: у HTS221 — две точки калибровки внутри самого кристалла, у LPS22HH — деление на 4096, у ISM330DHCX — чувствительность, зависящая от диапазона, у VEML6030 — коэффициент от усиления и времени накопления.

Здесь всё это собрано в один прибор: раз в секунду опросить пятерых и выдать строку в USART1. Основа — пакет платы B-U585I-IOT02A_BSP 1.1.0: общий слой b_u585i_iot02a_env_sensors.c, _motion_sensors.c, _light_sensor.c, шина _bus.c и драйверы компонентов hts221, lps22hh, ism330dhcx, iis2mdc, veml6030 (лицензия BSD-3-Clause). Наш код повторяет те же регистры без HAL и читает каждый датчик одной транзакцией.

Проверено без платы — U585 в QEMU нет. Модель регистров пяти датчиков (model198h.c) крутится на ПК, и на ней работают и наш код, и драйверы ST без единой правки: те же сырые байты, два ответа, плюс независимый пересчёт на Python точными дробями. Регистры настройки после нашей инициализации и после BSP совпали все 15 из 15. Прошивка sens_u585.c собрана armclang под Cortex-M33 без предупреждений; на плате она не запускалась.

Опоры: раздел 17 (шина I²C; обмен с новым блоком — 17.10, здесь не повторяется), 96 (термометры: там датчик отдавал готовые доли градуса), 98 (давление и барометрическая формула высоты), 81 (шум датчиков и усреднение), 198г (акселерометр и его поток данных), 2д (U585 и плата). RM0456 (ред. 4) — поле SADD регистра I2C_CR2. Даташит HTS221 (DocID026333, ред. 5) — адрес, автоинкремент, калибровка; остальные даташиты не использовались, их регистры и коэффициенты — по драйверам пакета. Второй независимый источник — описание платы и драйвер veml7700 в Zephyr (github.com/zephyrproject-rtos/zephyr, v4.4.2, коммит dccb095, лицензия Apache-2.0).

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

Оригинал Перевод
two-point calibration калибровка по двум точкам: прямая через два отсчёта с известными значениями
component driver, RegisterBusIO драйвер компонента и привязка его к функциям шины
instance экземпляр: номер датчика в своей группе BSP
data ready (DRDY) готовность данных: флаг в регистре или вывод
low-noise mode (LOW_NOISE_EN) режим малого шума
integration time (ALS_IT) время накопления света
mdps/LSB, mgauss/LSB миллиградусов в секунду, миллигаусс на единицу отсчёта

198х.1 Что стоит на плате и на какой шине

Адреса и паспорта — из заголовков драйверов пакета (*_reg.h, veml6030.h), 7-битные адреса совпадают с описанием платы в Zephyr (b_u585i_iot02a-common.dtsi):

Датчик Что меряет Адрес 7 бит В BSP (8 бит) Паспорт Автоинкремент
HTS221 влажность, температура 0x5F 0xBF 0x0F = 0xBC бит 7 подадреса
LPS22HH давление, температура 0x5D 0xBB 0x0F = 0xB3 IF_ADD_INC в CTRL_REG2
ISM330DHCX ускорение, угловая скорость 0x6B 0xD7 0x0F = 0x6B IF_INC в CTRL3_C
IIS2MDC магнитное поле 0x1E 0x3D 0x4F = 0x40 BSP ставит бит 7
VEML6030 освещённость 0x10 0x21 / 0x20 нет 16-битные регистры

Пять датчиков B-U585I-IOT02A на одной шине I2C2: STM32U585 с выводами PH4 и PH5, от шины — HTS221 0x5F, LPS22HH 0x5D, ISM330DHCX 0x6B, IIS2MDC 0x1E и VEML6030 0x10 с паспортами; на той же шине VL53L5CX и EEPROM; опрос раз в секунду — пять чтений и 29 байт, строка 106 байт уходит в USART1

Шина I2C2 — выводы PH4 (SCL) и PH5 (SDA), альтернативная функция 4, открытый сток с подтяжкой, 400 кГц (BUS_I2C2_FREQUENCY в b_u585i_iot02a_bus.h). На ней же BSP держит дальномер VL53L5CX (0x29), EEPROM M24LR64 (0x56) и контроллер питания USB (TCPP03, на плате расширения); камера — на I2C1. Выходы готовности данных у ISM330DHCX, LPS22HH и IIS2MDC Zephyr ведёт на PE11, PG2 и PD10; BSP их не использует, и мы тоже — опрос раз в секунду обходится без прерываний.

У VEML6030 паспортного регистра нет вовсе: VEML6030_ReadID проверяет, что адрес ответил, и возвращает константу 0x6030.


198х.2 Слой BSP и наш слой

BSP делит датчики на три группы, в каждой — экземпляры: ENV 0 — HTS221 (температура, влажность), ENV 1 — LPS22HH (температура, давление), MOTION 0 — ISM330DHCX (гироскоп, акселерометр), MOTION 1 — IIS2MDC, LIGHT 0 — VEML6030. Вызов BSP_ENV_SENSOR_Init(0, ENV_TEMPERATURE | ENV_HUMIDITY) идёт так:

  1. HTS221_Probe заполняет структуру шины — адрес HTS221_I2C_ADDRESS, функции BSP_I2C2_ReadReg/WriteReg, BSP_GetTick — и отдаёт её драйверу компонента (HTS221_RegisterBusIO); тот сразу зовёт BSP_I2C2_Init.
  2. Читает паспорт (HTS221_ReadID) и сравнивает с HTS221_ID.
  3. Запоминает таблицу функций компонента и вызывает его Init.
  4. BSP_ENV_SENSOR_Enable включает функцию, BSP_ENV_SENSOR_GetValue возвращает float в единицах: °C, %, гПа.

Удобно, но дорого: каждый GetValue — отдельные транзакции, а HTS221 на каждое чтение заново перечитывает калибровку. На модели одно чтение всех пяти через драйверы ST — 21 транзакция и 47 байт, из них у HTS221 — 12 транзакций и 18 байт. Наш слой — функции i2c_rd и i2c_wr поверх регистров I2C2 (обмен тот же, что в 17.10, только NBYTES = n) и по одному чтению на датчик: 5 транзакций и 29 байт, 396 тактов SCL против 990.

static int wait_flag(uint32_t flag)                  /* 0 — флаг есть; 1 — NACK или нет ответа */
{
    for (uint32_t n = 0; n < TMO; n++) {
        uint32_t isr = I2C2->ISR;
        if (isr & I2C_ISR_NACKF)
            return 1;
        if (isr & flag)
            return 0;
    }
    return 1;
}
int i2c_rd(uint8_t a7, uint8_t reg, uint8_t *buf, uint16_t n)   /* подадрес, повторный старт, n байт */
{
    if (n == 0u || n > 255u)
        return 1;
    I2C2->CR2 = (uint32_t)a7 << 1 | 1u << I2C_CR2_NBYTES_Pos | I2C_CR2_START;
    if (wait_flag(I2C_ISR_TXIS))
        return fail();
    I2C2->TXDR = reg;
    if (wait_flag(I2C_ISR_TC))                       /* подадрес ушёл, STOP не выдан */
        return fail();
    I2C2->CR2 = (uint32_t)a7 << 1 | I2C_CR2_RD_WRN | (uint32_t)n << I2C_CR2_NBYTES_Pos
              | I2C_CR2_AUTOEND | I2C_CR2_START;
    for (uint16_t i = 0; i < n; i++) {
        if (wait_flag(I2C_ISR_RXNE))
            return fail();
        buf[i] = (uint8_t)I2C2->RXDR;
    }
    return finish();
}

TIMINGR = 0x00500A11 — не подобран руками. Три функции расчёта из b_u585i_iot02a_bus.c (I2C_GetTiming и две вспомогательные) вырезаны без правок и прогнаны на ПК: для такта блока 16 МГц и шины 400 кГц — PRESC 0, SCLDEL 5, SDADEL 0, SCLH 10, SCLL 17, по формулам BSP период 2529 нс, 395,4 кГц. От 4 МГц после сброса алгоритм тоже находит решение (0x00100003), но с SCLH = 0, поэтому I2C2 и USART1 здесь тактуются от HSI16 (CCIPR1: I2C2SEL и USART1SEL = 10), а ядро остаётся на MSIS 4 МГц. BSP же считает TIMINGR от PCLK1.

Главная проверка слоя — регистры. После sens_init на модели и после цепочки BSP Init + Enable для всех функций сравнены 15 регистров настройки — CTRL_REG1 HTS221 (0x85), IF_CTRL, CTRL_REG1, CTRL_REG2 LPS22HH (0x82, 0x32, 0x10), CTRL1_XL, CTRL2_G, CTRL3_C, CTRL9_XL ISM330DHCX (0x40, 0x4C, 0x44, 0x02), CFG_REG_A и CFG_REG_C IIS2MDC (0x0C, 0x10), ALS_CONF VEML6030 и другие. Совпали 15 из 15. Вся настройка — 24 транзакции, 900 тактов SCL, около 2,3 мс шины.


198х.3 HTS221: две точки калибровки внутри датчика

HTS221 отдаёт влажность и температуру как 16-битные отсчёты АЦП со знаком (0x28…0x2B). Чтобы получить единицы, нужны регистры 0x30…0x3F: там на заводе записаны две точки каждой прямой — отсчёт и значение. Температуры точек хранятся ×8 и занимают 10 бит: младшие 8 — в 0x32 и 0x33, старшие по 2 — в 0x35; влажности — ×2, в 0x30 и 0x31 (даташит, раздел 8, таблица 19). Дальше — прямая через две точки. В драйвере ST (hts221.c, BSD-3-Clause) она записана одной строкой: (((y1 - y0) * Coeff) + ((x1 * y0) - (x0 * y1))) / (x1 - x0).

Наш код читает калибровку один раз, всю, одной транзакцией — для этого бит 7 подадреса (0x30 | 0x80): без него HTS221 адрес не наращивает (даташит, раздел 5.1.1).

static int hts_init(void)
{
    uint8_t id, cal[16];
    if (rd1(HTS_A7, 0x0F, &id) || id != 0xBC)                      /* WHO_AM_I */
        return 1;
    if (i2c_rd(HTS_A7, 0x30 | AI, cal, 16))                        /* калибровка 0x30…0x3F одним чтением */
        return 1;
    hts_cal_parse(cal, &hts_k);
    return wr1(HTS_A7, 0x20, 0x85);                                /* CTRL_REG1: PD, BDU, 1 Гц */
}
void hts_cal_parse(const uint8_t c[16], hts_cal_t *k)  /* c[0] — регистр 0x30, c[15] — 0x3F */
{
    k->h0_x2  = c[0x00];
    k->h1_x2  = c[0x01];
    k->t0_x8  = c[0x02] | (int32_t)(c[0x05] & 0x03u) << 8;         /* старшие биты T0 — 0x35[1:0] */
    k->t1_x8  = c[0x03] | (int32_t)(c[0x05] & 0x0Cu) << 6;         /* старшие биты T1 — 0x35[3:2] */
    k->h0_out = le16(&c[0x06]);                                    /* 0x36, 0x37 */
    k->h1_out = le16(&c[0x0A]);                                    /* 0x3A, 0x3B */
    k->t0_out = le16(&c[0x0C]);                                    /* 0x3C, 0x3D */
    k->t1_out = le16(&c[0x0E]);                                    /* 0x3E, 0x3F */
}
int32_t hts_temp_c100(const hts_cal_t *k, const uint8_t out[4])   /* out — 0x28…0x2B */
{
    int64_t dx = (int64_t)k->t1_out - k->t0_out;
    if (dx == 0)
        return INT32_MIN;                                          /* испорченная калибровка */
    int64_t x = le16(&out[2]) - (int64_t)k->t0_out;                /* TEMP_OUT — 0x2A, 0x2B */
    /* T = T0 + (x − x0)·(T1 − T0)/(x1 − x0); T0 и T1 хранятся ×8, ответ — ×100 */
    return (int32_t)div_round((int64_t)k->t0_x8 * 100 * dx + x * (k->t1_x8 - k->t0_x8) * 100, 8 * dx);
}

Считаем пример — калибровка модели и один отсчёт:

Точка Отсчёт Регистры Значение
T0 T0_OUT = 250 0x32 = 0x50, 0x35[1:0] = 0 80 / 8 = 10,0 °C
T1 T1_OUT = 2140 0x33 = 0x1C, 0x35[3:2] = 1 284 / 8 = 35,5 °C
H0 H0_T0_OUT = 1200 0x30 = 66 66 / 2 = 33,0 %
H1 H1_T0_OUT = 9000 0x31 = 154 154 / 2 = 77,0 %

Отсчёт T_OUT = 1250: 10,0 + (1250 − 250) · 25,5 / 1890 = 23,492 °C (0,013492 °C на единицу), в строке — T=23.49. H_OUT = 3500: 33,0 + 2300 · 44 / 7800 = 45,974 %, в строке — RH=46.0. Драйвер ST на тех же байтах дал 23,4921 и 45,9744 — то же самое во float. Пример из даташита (рисунки 9 и 10: отсчёты 300 и 500 при 10 и 20 °C, 400 → 15 °C; 0x4000 и 0x6000 при 20 и 40 %, 0x5000 → 30 %) оба кода повторяют ровно: 15,00 °C и 30,0 %.

Влажность по прямой может выйти за 0…100 %, и даташит велит обрезать программно. Драйвер ST обрезает, наш код — тоже: отсчёт 20 000 даёт 100,0, отсчёт −8000 — 0. Шум по умолчанию задан усреднением AV_CONF = 011 — 16 отсчётов температуры и 32 влажности, 0,03 °C и 0,15 % (даташит, таблица 16; о том, что даёт усреднение, — раздел 81). Ни BSP, ни мы этот регистр не трогаем. CTRL_REG1 = 0x85: включён, BDU, 1 Гц — как после HTS221_Init и Enable.


198х.4 LPS22HH: давление 24 бита и высота

Давление — три байта PRESS_OUT_XL, _L, _H (0x28…0x2A), за ними температура 0x2B, 0x2C. Пересчёт в драйвере ST (lps22hh_reg.c): 24 бита сдвигаются влево на 8 и делятся на 1 048 576, то есть делятся на 4096 — шаг 0,000244 гПа (0,0244 Па); температура — делится на 100. Всё это — одно чтение пяти байт; адрес наращивает сам датчик, если в CTRL_REG2 стоит IF_ADD_INC (бит 4). Мы, как и BSP, ставим его явно.

static int lps_init(void)
{
    uint8_t id;
    if (rd1(LPS_A7, 0x0F, &id) || id != 0xB3)
        return 1;
    return set_bits(LPS_A7, 0x0E, 0, 0x82)                         /* IF_CTRL: I3C_DISABLE и INT_EN_I3C, как BSP */
        || wr1(LPS_A7, 0x11, 0x10)                                 /* CTRL_REG2: IF_ADD_INC */
        || wr1(LPS_A7, 0x10, 0x32);                                /* CTRL_REG1: 25 Гц, BDU */
}
uint32_t lps_press_pa(const uint8_t b[5])             /* b — 0x28…0x2C: давление 3 байта, температура 2 */
{
    uint32_t raw = b[0] | (uint32_t)b[1] << 8 | (uint32_t)b[2] << 16;
    return (uint32_t)((raw * 25u + 512u) / 1024u);                 /* raw/4096 гПа = raw·100/4096 Па */
}
int32_t alt_dm(uint32_t p_pa, uint32_t p0_pa)         /* барометрическая формула из раздела 98 */
{
    float h = 44330.0f * (1.0f - powf((float)p_pa / (float)p0_pa, 1.0f / 5.255f));
    return (int32_t)lroundf(h * 10.0f);
}

Сцена модели: байты 0x00, 0xE0, 0x3D — 0x3DE000 = 4 055 040, делённое на 4096, — 990,00 гПа, в строке P=990.00; температура 2410 — TP=24.10. Высота по формуле раздела 98 от 1013,25 гПа — 195,4 м (Python — 195,391 м), для 1012,00 гПа — 10,4 м. Это высота относительно выбранного p0; как погода сдвигает её на сотни метров — в разделе 98.

Находка в драйвере ST. LPS22HH_Init включает режим малого шума: после него CTRL_REG2 = 0x12 (LOW_NOISE_EN и IF_ADD_INC). Но LPS22HH_TEMP_Enable записывает частоту 25 Гц (LPS22HH_25_Hz = 0x03), а вместе с ней — бит малого шума из того же числа, то есть 0. После Enable в регистре 0x10: малого шума нет. Наш код повторяет итог BSP (0x10), чтобы сравнение регистров было честным; нужен малый шум — надо писать CTRL_REG2 после выбора частоты. И IF_CTRL = 0x82, а не 0x02: значение LPS22HH_I3C_DISABLE = 0x11 ставит заодно INT_EN_I3C — мы пишем то же.


198х.5 ISM330DHCX и IIS2MDC: диапазон решает всё

ISM330DHCX отдаёт шесть осей подряд: гироскоп 0x22…0x27, акселерометр 0x28…0x2D — 12 байт одним чтением при IF_INC в CTRL3_C. Чувствительность — в ism330dhcx.h:

Акселерометр мг на единицу Гироскоп мград/с на единицу
±2g 0,061 ±125 °/с 4,375
±4g 0,122 ±250 °/с 8,75
±8g 0,244 ±500 °/с 17,5
±16g 0,488 ±1000 °/с 35
±2000 °/с 70
±4000 °/с 140

BSP ставит ±2g и ±2000 °/с, частоту 104 Гц (CTRL1_XL = 0x40, CTRL2_G = 0x4C). При ±2g 1 g — это 16 393 единицы. Коды поля FS_XL идут не по порядку: 00 — ±2g, 01 — ±16g, 10 — ±4g, 11 — ±8g (перечисление ism330dhcx_fs_xl_t). У гироскопа четыре бита: FS_4000, FS_125 и два бита FS_G.

static int ism_init(void)
{
    uint8_t id, v = 1;
    if (rd1(ISM_A7, 0x0F, &id) || id != 0x6B)
        return 1;
    if (wr1(ISM_A7, 0x12, 0x01))                                   /* CTRL3_C: SW_RESET */
        return 1;
    for (int n = 0; n < 1000 && (v & 1u); n++)                     /* бит снимается сам */
        if (rd1(ISM_A7, 0x12, &v))
            return 1;
    return (v & 1u)
        || wr1(ISM_A7, 0x12, 0x44)                                 /* CTRL3_C: BDU, IF_INC */
        || set_bits(ISM_A7, 0x18, 0, 0x02)                         /* CTRL9_XL: DEVICE_CONF */
        || set_bits(ISM_A7, 0x0A, 0x07, 0)                         /* FIFO_CTRL4: FIFO выключен */
        || wr1(ISM_A7, 0x10, 0x40 | ISM_FS_XL << 2)                /* CTRL1_XL: 104 Гц, ±2g */
        || wr1(ISM_A7, 0x11, 0x40 | ISM_FS_G);                     /* CTRL2_G: 104 Гц, ±2000 °/с */
}
int32_t ism_acc_mg(int16_t raw, uint8_t fs_xl)        /* FS_XL: 00 ±2g, 01 ±16g, 10 ±4g, 11 ±8g */
{
    static const int32_t ug_per_lsb[4] = {61, 488, 122, 244};
    return (int32_t)div_round((int64_t)raw * ug_per_lsb[fs_xl & 3u], 1000);
}
int32_t ism_gyr_mdps(int16_t raw, uint8_t fs_g)       /* 4 бита CTRL2_G[3:0]: FS_G[1:0], FS_125, FS_4000 */
{
    int32_t udps;                                                  /* микроградусы в секунду на единицу */
    if (fs_g & 1u)       udps = 140000;                            /* ±4000 */
    else if (fs_g & 2u)  udps = 4375;                              /* ±125 */
    else                 udps = 8750 << (fs_g >> 2);               /* ±250, 500, 1000, 2000 */
    return (int32_t)div_round((int64_t)raw * udps, 1000);
}

Драйвер ST умножает отсчёт на float и приводит к int32_t — дробь отбрасывается к нулю. Мы округляем:

Отсчёт Точно, мг Наш код Драйвер ST
328 20,008 20 20
−205 −12,505 −13 −12
16 393 999,973 1000 999

Разница не больше единицы, но «999 мг» на столе, лежащем ровно, сбивает с толку. Гироскоп при ±2000 °/с — ровно 70 мград/с на единицу: отсчёт 16 — 1120 мград/с (G=1.12), −3 — −210.

У IIS2MDC диапазон один — 50 Гс, 1,5 мГс на единицу (IIS2MDC_MAG_SENSITIVITY_FS_50GAUSS), шесть байт с 0x68. BSP включает BDU в CFG_REG_C (0x10) и непрерывный режим 100 Гц в CFG_REG_A (0x0C). Отсчёт −101: точно −151,5, у нас −152, у ST −151.


198х.6 VEML6030: люксы от усиления и времени, строка в UART

VEML6030 — не датчик ST, и устроен иначе: регистры 16-битные, у каждого код команды (0x00 — ALS_CONF, 0x04 — ALS, 0x05 — WHITE), два байта на регистр. Драйвер пакета люксы не считает вовсе: VEML6030_GetValues отдаёт счёт. Коэффициент есть в драйвере Zephyr veml7700 — его же Zephyr назначает для VEML6030 этой платы: таблица 4 × 6, от 0,0036 лк на единицу (усиление ×2, 800 мс) до 1,8432 (×1/8, 25 мс). Таблица целиком сворачивается в одну формулу — 0,0036 · (800 / время) · (2 / усиление); прогон сверил все 24 числа.

uint32_t veml_res_ulx(uint8_t gain, uint8_t it)       /* микролюксы на единицу счёта */
{
    static const uint8_t gmul[4] = {2, 1, 16, 8};                  /* ALS_GAIN: 00 ×1, 01 ×2, 10 ×1/8, 11 ×1/4 */
    uint32_t tmul;
    switch (it) {                                                  /* ALS_IT, мс: 25, 50, 100, 200, 400, 800 */
    case 0x0C: tmul = 32; break;
    case 0x08: tmul = 16; break;
    case 0x00: tmul = 8;  break;
    case 0x01: tmul = 4;  break;
    case 0x02: tmul = 2;  break;
    case 0x03: tmul = 1;  break;
    default:   return 0;
    }
    return 3600u * gmul[gain & 3u] * tmul;                         /* 0,0036 лк при ×2 и 800 мс */
}

BSP оставляет усиление ×1 и 100 мс (BSP_LIGHT_SENSOR_Init записывает их только в свою структуру, в датчик не пишет; наш veml_init пишет явно): 0,0576 лк на единицу, счёт 6000 — 345,6 лк (L=345.6), потолок 65 535 — 3774,8 лк. Для яркого солнца — усиление ×1/8 и 25 мс: потолок 120 794 лк.

Сводный опрос — пять чтений подряд, по одному на датчик:

void sens_poll(uint8_t mask, sens_t *v)
{
    uint8_t b[12];
    v->ok = 0;
    if ((mask & 1u) && !i2c_rd(HTS_A7, 0x28 | AI, b, 4)) {        /* HUMIDITY_OUT, TEMP_OUT */
        v->rh_x10 = hts_rh_x10(&hts_k, b);
        v->t_c100 = hts_temp_c100(&hts_k, b);
        v->ok |= 1u;
    }
    if ((mask & 2u) && !i2c_rd(LPS_A7, 0x28, b, 5)) {             /* PRESS_OUT_XL…TEMP_OUT_H */
        v->p_pa = lps_press_pa(b);
        v->tp_c100 = lps_temp_c100(b);
        v->alt_dm = alt_dm(v->p_pa, P0_PA);
        v->ok |= 2u;
    }
    if ((mask & 4u) && !i2c_rd(ISM_A7, 0x22, b, 12)) {            /* OUTX_L_G…OUTZ_H_A */
        for (int i = 0; i < 3; i++) {
            v->gyr_mdps[i] = ism_gyr_mdps(le16(&b[2 * i]), ISM_FS_G);
            v->acc_mg[i] = ism_acc_mg(le16(&b[6 + 2 * i]), ISM_FS_XL);
        }
        v->ok |= 4u;
    }
    if ((mask & 8u) && !i2c_rd(MAG_A7, 0x68 | AI, b, 6)) {        /* OUTX_L_REG…OUTZ_H_REG */
        for (int i = 0; i < 3; i++)
            v->mag_mg[i] = mag_mg(le16(&b[2 * i]));
        v->ok |= 8u;
    }
    if ((mask & 16u) && !i2c_rd(VEML_A7, 0x04, b, 2)) {           /* ALS */
        v->lux_mlx = veml_mlx((uint16_t)le16(b), VEML_GAIN, VEML_IT);
        v->ok |= 16u;
    }
}
int main(void)
{
    static sens_t v;
    static char line[160];
    board_init();
    uint8_t mask = sens_init();
    uint32_t next = ms + 1000u;
    for (;;) {
        if ((int32_t)(ms - next) < 0) {
            __WFI();                                 /* SysTick будит раз в миллисекунду */
            continue;
        }
        next += 1000u;
        sens_poll(mask, &v);
        uart_send(line, fmt_line(line, &v));
    }
}

Строка модели — 106 байт вместе с \r\n:

S=1F T=23.49 RH=46.0 P=990.00 TP=24.10 ALT=195.4 A=20,-13,1000 G=1.12,-0.21,0.00 M=300,-152,-600 L=345.6

S=1F — ответили все пять. При 115 200 бод (BRR = 139 от 16 МГц, 115 108 бод, −0,08 %) строка уходит за 9,2 мс; шина на весь опрос — 396 тактов SCL, около 1,0 мс. Прошивку armclang 6.24 собрал без предупреждений: код sens_u585.c — 840 байт, sens_dev.c — 946, sens_conv.c — 1348 (всего 3134 байта), ОЗУ — 256 байт. _Static_assert держат BRR = 139 и поля TIMINGR: копия с 9600 бод не собирается.


198х.7 Грабли

Адрес 8 бит вместо 7. BSP хранит адреса со сдвигом и битом чтения: HTS221 — 0xBF, а на шине он 0x5F. Отдать 0xBF функции, которая сама сдвигает адрес (как наш i2c_rd), — в поле SADD окажется 0x17E, а блок в 7-битном режиме смотрит только SADD[7:1] (RM0456, регистр I2C_CR2) — на шину уйдёт 0x3F. На модели все пять 8-битных адресов BSP, поданные как 7-битные, — 0 ответов из 5. Обратная ошибка — 0x5F в HAL_I2C_Mem_Read, который ждёт уже сдвинутый адрес: на шину уйдёт 0x2F.

Чтение пачкой без автоинкремента. HTS221 наращивает адрес только при бите 7 подадреса. Без него четыре байта с 0x28 — четыре копии одного регистра (0xAC), и модель показывает −281,19 °C. Калибровка без бита 7 — 16 копий 0x30: T0_OUT = T1_OUT, делитель прямой — ноль (наш код возвращает INT32_MIN и не делит). У LPS22HH и ISM330DHCX правило другое — бит в регистре управления; LPS22HH со сброшенным IF_ADD_INC на модели отдаёт пять одинаковых байт — 0,00 гПа.

Порядок байт. У датчиков ST младший байт первым. Собрать старшим первым — отсчёт 16 393 (байты 0x09, 0x40) превращается в 2368, и 1 g читается как 144 мг. У IIS2MDC есть бит BLE в CFG_REG_C: 0 — младший байт по младшему адресу, 1 — старший (iis2mdc_ble_t); держать его в нуле.

VEML6030: драйверы спорят о порядке байт. Zephyr собирает регистр младшим байтом первым (sys_get_le16), драйвер ST после чтения байты переставляет (veml6030_read_reg). На модели с младшим первым драйвер ST читает 6000 как 0x7017 = 28 695 (1652,8 лк); если прав ST, то ошибается наш код — ровно так же. С даташитом Vishay это не сверено, рассудит плата.

Половина uint32_t. VEML6030_GetValues пишет 16 бит через (uint16_t *) в массив uint32_t — старшая половина остаётся, какой была. Массив, заполненный 0xDEADBEEF, после вызова — 0xDEAD7017. Обнулять массив перед вызовом.

Код 01 в FS_XL — это ±16g. Кто ждёт ±4g по порядку, получит 1 g как 2049 единиц и, умножив на 0,122, — 250 мг.

Калибровку — один раз. Драйвер ST перечитывает её при каждом замере: 12 транзакций на одно чтение HTS221 вместо одной.

Малый шум LPS22HH пропадает в Enable. См. 198х.4: после LPS22HH_Init бит стоит, после Enable с 25 Гц — снят.


198х.8 Итоги

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

sens_dev.c — настройка и опрос датчиков B-U585I-IOT02A.

/* sens_dev.c — раздел 198х: настройка и опрос датчиков B-U585I-IOT02A.
   Регистры и значения — как у драйверов компонентов пакета B-U585I-IOT02A_BSP (BSD-3-Clause):
   после sens_init регистры настройки совпадают с тем, что оставляют BSP_*_Init + BSP_*_Enable. */
#include "sens_dev.h"

#define AI 0x80u                 /* бит 7 подадреса: автоинкремент у HTS221 (и так же делает BSP для IIS2MDC) */
#define ISM_FS_XL   0u           /* ±2g */
#define ISM_FS_G    12u          /* ±2000 °/с */
#define VEML_GAIN   0u           /* ×1 */
#define VEML_IT     0u           /* 100 мс */

static hts_cal_t hts_k;

static int wr1(uint8_t a7, uint8_t reg, uint8_t v)
{
    return i2c_wr(a7, reg, &v, 1);
}

static int rd1(uint8_t a7, uint8_t reg, uint8_t *v)
{
    return i2c_rd(a7, reg, v, 1);
}

static int set_bits(uint8_t a7, uint8_t reg, uint8_t clr, uint8_t set)   /* чтение-правка-запись */
{
    uint8_t v;
    if (rd1(a7, reg, &v))
        return 1;
    return wr1(a7, reg, (uint8_t)((v & ~clr) | set));
}

static int hts_init(void)
{
    uint8_t id, cal[16];
    if (rd1(HTS_A7, 0x0F, &id) || id != 0xBC)                      /* WHO_AM_I */
        return 1;
    if (i2c_rd(HTS_A7, 0x30 | AI, cal, 16))                        /* калибровка 0x30…0x3F одним чтением */
        return 1;
    hts_cal_parse(cal, &hts_k);
    return wr1(HTS_A7, 0x20, 0x85);                                /* CTRL_REG1: PD, BDU, 1 Гц */
}

static int lps_init(void)
{
    uint8_t id;
    if (rd1(LPS_A7, 0x0F, &id) || id != 0xB3)
        return 1;
    return set_bits(LPS_A7, 0x0E, 0, 0x82)                         /* IF_CTRL: I3C_DISABLE и INT_EN_I3C, как BSP */
        || wr1(LPS_A7, 0x11, 0x10)                                 /* CTRL_REG2: IF_ADD_INC */
        || wr1(LPS_A7, 0x10, 0x32);                                /* CTRL_REG1: 25 Гц, BDU */
}

static int ism_init(void)
{
    uint8_t id, v = 1;
    if (rd1(ISM_A7, 0x0F, &id) || id != 0x6B)
        return 1;
    if (wr1(ISM_A7, 0x12, 0x01))                                   /* CTRL3_C: SW_RESET */
        return 1;
    for (int n = 0; n < 1000 && (v & 1u); n++)                     /* бит снимается сам */
        if (rd1(ISM_A7, 0x12, &v))
            return 1;
    return (v & 1u)
        || wr1(ISM_A7, 0x12, 0x44)                                 /* CTRL3_C: BDU, IF_INC */
        || set_bits(ISM_A7, 0x18, 0, 0x02)                         /* CTRL9_XL: DEVICE_CONF */
        || set_bits(ISM_A7, 0x0A, 0x07, 0)                         /* FIFO_CTRL4: FIFO выключен */
        || wr1(ISM_A7, 0x10, 0x40 | ISM_FS_XL << 2)                /* CTRL1_XL: 104 Гц, ±2g */
        || wr1(ISM_A7, 0x11, 0x40 | ISM_FS_G);                     /* CTRL2_G: 104 Гц, ±2000 °/с */
}

static int mag_init(void)
{
    uint8_t id;
    if (rd1(MAG_A7, 0x4F | AI, &id) || id != 0x40)
        return 1;
    return set_bits(MAG_A7, 0x62 | AI, 0x02, 0x10)                 /* CFG_REG_C: BDU, без самотеста */
        || wr1(MAG_A7, 0x60 | AI, 0x0C);                           /* CFG_REG_A: 100 Гц, непрерывно */
}

static int veml_init(void)
{
    uint8_t conf[2];
    if (i2c_rd(VEML_A7, 0x00, conf, 2))                            /* паспорта нет: хватит ответа на адрес */
        return 1;
    conf[0] = (uint8_t)(VEML_GAIN << 11 | VEML_IT << 6);           /* ALS_CONF, младший байт первым; SD = 0 */
    conf[1] = (uint8_t)((VEML_GAIN << 11 | VEML_IT << 6) >> 8);
    return i2c_wr(VEML_A7, 0x00, conf, 2);
}

uint8_t sens_init(void)
{
    uint8_t m = 0;
    m |= (uint8_t)(!hts_init()  << 0);
    m |= (uint8_t)(!lps_init()  << 1);
    m |= (uint8_t)(!ism_init()  << 2);
    m |= (uint8_t)(!mag_init()  << 3);
    m |= (uint8_t)(!veml_init() << 4);
    return m;
}

void sens_poll(uint8_t mask, sens_t *v)
{
    uint8_t b[12];
    v->ok = 0;
    if ((mask & 1u) && !i2c_rd(HTS_A7, 0x28 | AI, b, 4)) {        /* HUMIDITY_OUT, TEMP_OUT */
        v->rh_x10 = hts_rh_x10(&hts_k, b);
        v->t_c100 = hts_temp_c100(&hts_k, b);
        v->ok |= 1u;
    }
    if ((mask & 2u) && !i2c_rd(LPS_A7, 0x28, b, 5)) {             /* PRESS_OUT_XL…TEMP_OUT_H */
        v->p_pa = lps_press_pa(b);
        v->tp_c100 = lps_temp_c100(b);
        v->alt_dm = alt_dm(v->p_pa, P0_PA);
        v->ok |= 2u;
    }
    if ((mask & 4u) && !i2c_rd(ISM_A7, 0x22, b, 12)) {            /* OUTX_L_G…OUTZ_H_A */
        for (int i = 0; i < 3; i++) {
            v->gyr_mdps[i] = ism_gyr_mdps(le16(&b[2 * i]), ISM_FS_G);
            v->acc_mg[i] = ism_acc_mg(le16(&b[6 + 2 * i]), ISM_FS_XL);
        }
        v->ok |= 4u;
    }
    if ((mask & 8u) && !i2c_rd(MAG_A7, 0x68 | AI, b, 6)) {        /* OUTX_L_REG…OUTZ_H_REG */
        for (int i = 0; i < 3; i++)
            v->mag_mg[i] = mag_mg(le16(&b[2 * i]));
        v->ok |= 8u;
    }
    if ((mask & 16u) && !i2c_rd(VEML_A7, 0x04, b, 2)) {           /* ALS */
        v->lux_mlx = veml_mlx((uint16_t)le16(b), VEML_GAIN, VEML_IT);
        v->ok |= 16u;
    }
}

sens_conv.c — сырые байты датчиков → единицы, целыми числами.

/* sens_conv.c — раздел 198х: сырые байты датчиков → единицы, целыми числами.
   Формулы и коэффициенты — из драйверов компонентов пакета B-U585I-IOT02A_BSP (BSD-3-Clause),
   коэффициенты VEML6030 — из драйвера veml7700 проекта Zephyr (Apache-2.0). */
#include <math.h>
#include "sens_conv.h"

static int64_t div_round(int64_t a, int64_t b)       /* деление с округлением к ближайшему, b > 0 */
{
    return (a >= 0 ? a + b / 2 : a - b / 2) / b;
}

int16_t le16(const uint8_t *p)                        /* младший байт первым */
{
    return (int16_t)(uint16_t)(p[0] | (uint16_t)p[1] << 8);
}

void hts_cal_parse(const uint8_t c[16], hts_cal_t *k)  /* c[0] — регистр 0x30, c[15] — 0x3F */
{
    k->h0_x2  = c[0x00];
    k->h1_x2  = c[0x01];
    k->t0_x8  = c[0x02] | (int32_t)(c[0x05] & 0x03u) << 8;         /* старшие биты T0 — 0x35[1:0] */
    k->t1_x8  = c[0x03] | (int32_t)(c[0x05] & 0x0Cu) << 6;         /* старшие биты T1 — 0x35[3:2] */
    k->h0_out = le16(&c[0x06]);                                    /* 0x36, 0x37 */
    k->h1_out = le16(&c[0x0A]);                                    /* 0x3A, 0x3B */
    k->t0_out = le16(&c[0x0C]);                                    /* 0x3C, 0x3D */
    k->t1_out = le16(&c[0x0E]);                                    /* 0x3E, 0x3F */
}

int32_t hts_temp_c100(const hts_cal_t *k, const uint8_t out[4])   /* out — 0x28…0x2B */
{
    int64_t dx = (int64_t)k->t1_out - k->t0_out;
    if (dx == 0)
        return INT32_MIN;                                          /* испорченная калибровка */
    int64_t x = le16(&out[2]) - (int64_t)k->t0_out;                /* TEMP_OUT — 0x2A, 0x2B */
    /* T = T0 + (x − x0)·(T1 − T0)/(x1 − x0); T0 и T1 хранятся ×8, ответ — ×100 */
    return (int32_t)div_round((int64_t)k->t0_x8 * 100 * dx + x * (k->t1_x8 - k->t0_x8) * 100, 8 * dx);
}

int32_t hts_rh_x10(const hts_cal_t *k, const uint8_t out[4])
{
    int64_t dx = (int64_t)k->h1_out - k->h0_out;
    if (dx == 0)
        return INT32_MIN;
    int64_t x = le16(&out[0]) - (int64_t)k->h0_out;                /* HUMIDITY_OUT — 0x28, 0x29 */
    int64_t rh = div_round((int64_t)k->h0_x2 * 10 * dx + x * (k->h1_x2 - k->h0_x2) * 10, 2 * dx);
    return (int32_t)(rh < 0 ? 0 : rh > 1000 ? 1000 : rh);          /* как драйвер ST: 0…100 % */
}

uint32_t lps_press_pa(const uint8_t b[5])             /* b — 0x28…0x2C: давление 3 байта, температура 2 */
{
    uint32_t raw = b[0] | (uint32_t)b[1] << 8 | (uint32_t)b[2] << 16;
    return (uint32_t)((raw * 25u + 512u) / 1024u);                 /* raw/4096 гПа = raw·100/4096 Па */
}

int32_t lps_temp_c100(const uint8_t b[5])
{
    return le16(&b[3]);                                            /* уже в сотых °C */
}

int32_t alt_dm(uint32_t p_pa, uint32_t p0_pa)         /* барометрическая формула из раздела 98 */
{
    float h = 44330.0f * (1.0f - powf((float)p_pa / (float)p0_pa, 1.0f / 5.255f));
    return (int32_t)lroundf(h * 10.0f);
}

int32_t ism_acc_mg(int16_t raw, uint8_t fs_xl)        /* FS_XL: 00 ±2g, 01 ±16g, 10 ±4g, 11 ±8g */
{
    static const int32_t ug_per_lsb[4] = {61, 488, 122, 244};
    return (int32_t)div_round((int64_t)raw * ug_per_lsb[fs_xl & 3u], 1000);
}

int32_t ism_gyr_mdps(int16_t raw, uint8_t fs_g)       /* 4 бита CTRL2_G[3:0]: FS_G[1:0], FS_125, FS_4000 */
{
    int32_t udps;                                                  /* микроградусы в секунду на единицу */
    if (fs_g & 1u)       udps = 140000;                            /* ±4000 */
    else if (fs_g & 2u)  udps = 4375;                              /* ±125 */
    else                 udps = 8750 << (fs_g >> 2);               /* ±250, 500, 1000, 2000 */
    return (int32_t)div_round((int64_t)raw * udps, 1000);
}

int32_t mag_mg(int16_t raw)                           /* 1,5 мГс на единицу */
{
    return (int32_t)div_round((int64_t)raw * 3, 2);
}

uint32_t veml_res_ulx(uint8_t gain, uint8_t it)       /* микролюксы на единицу счёта */
{
    static const uint8_t gmul[4] = {2, 1, 16, 8};                  /* ALS_GAIN: 00 ×1, 01 ×2, 10 ×1/8, 11 ×1/4 */
    uint32_t tmul;
    switch (it) {                                                  /* ALS_IT, мс: 25, 50, 100, 200, 400, 800 */
    case 0x0C: tmul = 32; break;
    case 0x08: tmul = 16; break;
    case 0x00: tmul = 8;  break;
    case 0x01: tmul = 4;  break;
    case 0x02: tmul = 2;  break;
    case 0x03: tmul = 1;  break;
    default:   return 0;
    }
    return 3600u * gmul[gain & 3u] * tmul;                         /* 0,0036 лк при ×2 и 800 мс */
}

uint32_t veml_mlx(uint16_t counts, uint8_t gain, uint8_t it)
{
    return (uint32_t)(((uint64_t)counts * veml_res_ulx(gain, it) + 500u) / 1000u);
}

/* ── строка в UART ──────────────────────────────────────────────────────── */
static char *put_u(char *p, uint32_t v)
{
    char t[10];
    int n = 0;
    do {
        t[n++] = (char)('0' + v % 10u);
        v /= 10u;
    } while (v);
    while (n)
        *p++ = t[--n];
    return p;
}

static char *put_fix(char *p, int32_t v, unsigned dec)  /* v в единицах 10^-dec */
{
    uint32_t a = v < 0 ? 0u - (uint32_t)v : (uint32_t)v, d = 1;
    for (unsigned i = 0; i < dec; i++)
        d *= 10u;
    if (v < 0)
        *p++ = '-';
    p = put_u(p, a / d);
    if (dec) {
        uint32_t f = a % d;
        *p++ = '.';
        for (uint32_t q = d / 10u; q; q /= 10u) {
            *p++ = (char)('0' + f / q);
            f %= q;
        }
    }
    return p;
}

static char *put_s(char *p, const char *s)
{
    while (*s)
        *p++ = *s++;
    return p;
}

static char *put_xyz(char *p, const char *name, const int32_t v[3], unsigned dec)
{
    p = put_s(p, name);
    for (int i = 0; i < 3; i++) {
        if (i)
            *p++ = ',';
        p = put_fix(p, v[i], dec);
    }
    return p;
}

int fmt_line(char *s, const sens_t *v)                /* нужно до 160 байт */
{
    static const char hex[] = "0123456789ABCDEF";
    int32_t g[3];
    char *p = put_s(s, "S=");
    *p++ = hex[v->ok >> 4 & 0xFu];
    *p++ = hex[v->ok & 0xFu];
    p = put_s(p, " T=");   p = put_fix(p, v->t_c100, 2);
    p = put_s(p, " RH=");  p = put_fix(p, v->rh_x10, 1);
    p = put_s(p, " P=");   p = put_fix(p, (int32_t)v->p_pa, 2);
    p = put_s(p, " TP=");  p = put_fix(p, v->tp_c100, 2);
    p = put_s(p, " ALT="); p = put_fix(p, v->alt_dm, 1);
    p = put_xyz(p, " A=", v->acc_mg, 0);
    for (int i = 0; i < 3; i++)
        g[i] = (int32_t)div_round(v->gyr_mdps[i], 10);             /* сотые °/с */
    p = put_xyz(p, " G=", g, 2);
    p = put_xyz(p, " M=", v->mag_mg, 0);
    p = put_s(p, " L=");   p = put_fix(p, (int32_t)((v->lux_mlx + 50u) / 100u), 1);
    p = put_s(p, "\r\n");
    *p = 0;
    return (int)(p - s);
}

sens_u585.c — прошивка опроса датчиков регистрами, без HAL.

/* sens_u585.c — раздел 198х: прошивка опроса датчиков B-U585I-IOT02A, регистрами, без HAL.
   Ядро на MSIS 4 МГц после сброса; I2C2 и USART1 тактуются от HSI16. Раз в секунду — строка в USART1.
   Выводы — как в BSP платы: I2C2 — PH4 (SCL) и PH5 (SDA), AF4; COM1 — USART1, PA9 (TX), AF7. */
#include "stm32u585xx.h"
#include "sens_dev.h"

#define HSI_HZ    16000000u
#define BAUD      115200u
#define UART_BRR  ((HSI_HZ + BAUD / 2u) / BAUD)
#define I2C_TIMING 0x00500A11u    /* 400 кГц от 16 МГц — алгоритм I2C_GetTiming из BSP (timing198h.py) */
#define TMO       100000u         /* предел ожидания флага, итераций */

_Static_assert(UART_BRR == 139u, "USART1: 16 МГц / 115200");
_Static_assert((I2C_TIMING >> 28) == 0u && (I2C_TIMING & 0xFFu) == 17u && ((I2C_TIMING >> 8) & 0xFFu) == 10u, "поля TIMINGR");

static volatile uint32_t ms;

void SysTick_Handler(void)
{
    ms++;
}

static void board_init(void)
{
    RCC->CR |= RCC_CR_HSION;
    while (!(RCC->CR & RCC_CR_HSIRDY)) {
    }
    RCC->CCIPR1 = (RCC->CCIPR1 & ~(RCC_CCIPR1_USART1SEL | RCC_CCIPR1_I2C2SEL))
                | 2u << RCC_CCIPR1_USART1SEL_Pos | 2u << RCC_CCIPR1_I2C2SEL_Pos;       /* 10 — HSI16 */
    RCC->AHB2ENR1 |= RCC_AHB2ENR1_GPIOAEN | RCC_AHB2ENR1_GPIOHEN;
    RCC->APB1ENR1 |= RCC_APB1ENR1_I2C2EN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    (void)RCC->APB2ENR;

    /* PH4, PH5: альтернативная функция 4, открытый сток, подтяжка вверх, высокая скорость — как BSP */
    GPIOH->AFR[0] = (GPIOH->AFR[0] & ~(0xFFu << 16)) | 4u << 16 | 4u << 20;
    GPIOH->OTYPER |= 3u << 4;
    GPIOH->OSPEEDR = (GPIOH->OSPEEDR & ~(0xFu << 8)) | 0xAu << 8;
    GPIOH->PUPDR = (GPIOH->PUPDR & ~(0xFu << 8)) | 0x5u << 8;
    GPIOH->MODER = (GPIOH->MODER & ~(0xFu << 8)) | 0xAu << 8;
    /* PA9: USART1_TX, альтернативная функция 7 */
    GPIOA->AFR[1] = (GPIOA->AFR[1] & ~(0xFu << 4)) | 7u << 4;
    GPIOA->MODER = (GPIOA->MODER & ~(3u << 18)) | 2u << 18;

    I2C2->CR1 = 0;                                   /* PE = 0: TIMINGR меняется только выключенным */
    I2C2->TIMINGR = I2C_TIMING;
    I2C2->CR1 = I2C_CR1_PE;                          /* аналоговый фильтр включён (ANFOFF = 0) */

    USART1->BRR = UART_BRR;
    USART1->CR1 = USART_CR1_TE | USART_CR1_UE;

    SysTick->LOAD = 4000u - 1u;                      /* 1 мс при MSIS 4 МГц */
    SysTick->VAL = 0;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
}

static int wait_flag(uint32_t flag)                  /* 0 — флаг есть; 1 — NACK или нет ответа */
{
    for (uint32_t n = 0; n < TMO; n++) {
        uint32_t isr = I2C2->ISR;
        if (isr & I2C_ISR_NACKF)
            return 1;
        if (isr & flag)
            return 0;
    }
    return 1;
}

static int fail(void)                                /* после NACK блок сам выдаёт STOP; иначе — просим */
{
    if (!(I2C2->ISR & I2C_ISR_NACKF))
        I2C2->CR2 |= I2C_CR2_STOP;
    for (uint32_t n = 0; n < TMO && !(I2C2->ISR & I2C_ISR_STOPF); n++) {
    }
    I2C2->ICR = I2C_ICR_NACKCF | I2C_ICR_STOPCF;
    return 1;
}

static int finish(void)
{
    if (wait_flag(I2C_ISR_STOPF))
        return fail();
    I2C2->ICR = I2C_ICR_STOPCF;                      /* пока STOPF стоит, блок занят */
    return 0;
}

int i2c_rd(uint8_t a7, uint8_t reg, uint8_t *buf, uint16_t n)   /* подадрес, повторный старт, n байт */
{
    if (n == 0u || n > 255u)
        return 1;
    I2C2->CR2 = (uint32_t)a7 << 1 | 1u << I2C_CR2_NBYTES_Pos | I2C_CR2_START;
    if (wait_flag(I2C_ISR_TXIS))
        return fail();
    I2C2->TXDR = reg;
    if (wait_flag(I2C_ISR_TC))                       /* подадрес ушёл, STOP не выдан */
        return fail();
    I2C2->CR2 = (uint32_t)a7 << 1 | I2C_CR2_RD_WRN | (uint32_t)n << I2C_CR2_NBYTES_Pos
              | I2C_CR2_AUTOEND | I2C_CR2_START;
    for (uint16_t i = 0; i < n; i++) {
        if (wait_flag(I2C_ISR_RXNE))
            return fail();
        buf[i] = (uint8_t)I2C2->RXDR;
    }
    return finish();
}

int i2c_wr(uint8_t a7, uint8_t reg, const uint8_t *buf, uint16_t n)  /* подадрес и n байт одной посылкой */
{
    if (n > 254u)
        return 1;
    I2C2->CR2 = (uint32_t)a7 << 1 | (uint32_t)(n + 1u) << I2C_CR2_NBYTES_Pos | I2C_CR2_AUTOEND | I2C_CR2_START;
    for (uint16_t i = 0; i <= n; i++) {
        if (wait_flag(I2C_ISR_TXIS))
            return fail();
        I2C2->TXDR = i ? buf[i - 1u] : reg;
    }
    return finish();
}

static void uart_send(const char *s, int n)
{
    for (int i = 0; i < n; i++) {
        while (!(USART1->ISR & USART_ISR_TXE_TXFNF)) {
        }
        USART1->TDR = (uint8_t)s[i];
    }
}

int main(void)
{
    static sens_t v;
    static char line[160];
    board_init();
    uint8_t mask = sens_init();
    uint32_t next = ms + 1000u;
    for (;;) {
        if ((int32_t)(ms - next) < 0) {
            __WFI();                                 /* SysTick будит раз в миллисекунду */
            continue;
        }
        next += 1000u;
        sens_poll(mask, &v);
        uart_send(line, fmt_line(line, &v));
    }
}

Дальше: раздел 198ц «Мини-проект: журнал во внешней Octo-SPI флешке B-U585I-IOT02A» — строки этого опроса ложатся в MX25LM51245G на OCTOSPI2 и переживают выключение.

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