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

198м. Мини-проект: метеостанция — датчики, журнал и экран

Метеостанция — самый спокойный прибор в книге: погода меняется за минуты, а работать от батарей надо годами. Поэтому главное здесь не скорость, а три вещи: верно пересчитать отсчёты датчиков в градусы, проценты и гектопаскали, не потерять журнал при сбросе и спать между замерами так, чтобы в среднем токе сон весил больше, чем работа.

Прибор собран на плате B-L475E-IOT01A (STM32L475VG): на ней уже стоят датчик влажности и температуры HTS221, барометр LPS22HB и флеш-память MX25R6435F на 8 МБ. Раз в 10 с часы RTC будят кристалл из Stop 2, он забирает готовые отсчёты и запускает новые, раз в минуту шесть замеров сводятся в запись 16 байт — она уходит в журнал на флеш и строкой в виртуальный COM-порт ST-LINK.

Проверено без платы. Ядро (meteo_core.c) и драйвер HTS221 из раздела 39в (без изменений) прогнаны в опыте sim_meteo.c на модели датчиков: сутки погоды и журнал в LittleFS на модели флеш-памяти в ОЗУ. Опыт собран gcc на ПК и arm-none-eabi-gcc для Cortex-M4, в qemu-system-arm -M netduinoplus2 вывод совпал с ПК строка в строку. Модель на Python (meteo_host.py) повторяет сутки бит в бит — CRC-32 всех записей совпала — и считает время на шине, средний ток и срок журнала. Прошивка meteo_l475.c собрана armclang по заголовкам пака L4 без предупреждений; на плате не запускалась.

Опоры: руководство платы UM2153 и её BSP из пака B-L475E-IOT01A_BSP (выводы, адреса, драйвер lps22hb, функции BSP_QSPI_*); даташит LPS22HB не использовался — регистры взяты из комментариев lps22hb.h; HTS221 — даташит DS10866 и драйвер раздела 39в; даташит DS10969 (STM32L475) — токи и времена пробуждения; RM0351 — таблица 237 настроек I2C, поля PWR_CR1 и RTC_CR. LittleFS — раздел 42а, режимы сна — 10а, сегментный ЖКИ — 22в. Проверка — _verify/meteo198m_check.py.

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

Оригинал Перевод
one-shot однократный замер по команде
ODR, output data rate частота выдачи отсчётов
BDU, block data update не обновлять выход, пока не прочитаны все байты
IF_ADD_INC автоинкремент адреса регистра у LPS22HB
LC_EN, low current mode режим малого тока LPS22HB
wakeup timer, WUT, ck_spre таймер пробуждения RTC, секундный такт
STOPWUCK каким тактом проснуться из Stop
block_cycles, sync через сколько стираний переезжают метаданные; фиксация файла

198м.1 Что делает прибор и из чего он

Функция Как
влажность и температура HTS221 по I2C2, однократный замер раз в 10 с
давление LPS22HB по I2C2, однократный замер раз в 10 с, 1/4096 гПа на единицу
сон Stop 2, пробуждение таймером RTC от кварца LSE
сводка 6 замеров — одна запись в минуту: средняя, наименьшая и наибольшая температура, влажность, давление
журнал запись 16 байт в файл LittleFS на MX25R6435F, фиксация раз в 15 минут
экран строка в USART1 — виртуальный COM ST-LINK, 115 200 бит/с

Метеостанция на B-L475E-IOT01A: слева HTS221 и LPS22HB на I2C2, в середине ядро — два шага замера, шесть замеров в минутную запись 16 байт; справа журнал LittleFS на MX25R6435F по QUADSPI и строка в USART1; снизу RTC на кварце LSE будит кристалл из Stop 2 каждые 10 с, бодрствование 3,5 мс, средний ток кристалла 2,36 мкА

Выводы и адреса — из заголовка BSP stm32l475e_iot01.h и руководства UM2153: оба датчика сидят на I2C2, виртуальный COM — это USART1 на 115 200 бит/с, 8 бит, без чётности.

Узел Выводы Периферия Адрес
HTS221, LPS22HB PB10 SCL, PB11 SDA I2C2, AF4 0xBE и 0xBA в записи BSP, 7 бит — 0x5F и 0x5D
виртуальный COM PB6 TX, PB7 RX USART1, AF7 —
флеш MX25R6435F выводы QUADSPI функции BSP_QSPI_* 8 МБ: 2048 секторов по 4 КБ, страница 256 байт

«Экран» здесь — строка в терминале. Сегментный ЖКИ из раздела 22в на этом кристалле не поставить: у STM32L475 контроллера ЖКИ нет, в DS10969 LCD упоминается только как параллельный интерфейс FMC. У соседа по линейке, L476, он есть — «LCD 8× 40 or 4× 44» на первой странице DS10198, — и тогда строку заменяет вывод из 22в.


198м.2 Датчики: два шага вместо ожидания

HTS221 работает через драйвер раздела 39в — вариант без плавающей точки (HTS_FIXED): инициализация читает паспорт 0xBC и 16 байт калибровки, пишет в CTRL_REG1 0x84 (работа, BDU, однократный режим). Сам замер в 39в — «запустить и ждать»: hts_measure_raw пишет ONE_SHOT и опрашивает STATUS. Метеостанции ждать незачем. Ядро делит замер на два шага: при пробуждении забирает отсчёт, запущенный 10 с назад, и сразу запускает следующий. Кристалл не ждёт преобразования ни микросекунды, а задержка данных на 10 с погоде безразлична.

int hts_read(hts221 *d, int16_t *h_out, int16_t *t_out)
{
    uint8_t st = 0, b[4];
    if (d->bus.read(d->bus.handle, HTS221_ADDR7, HTS221_STATUS_REG, &st, 1))
        return HTS_EBUS;
    if ((st & 0x03u) != 0x03u)
        return HTS_ETIMEOUT;
    if (d->bus.read(d->bus.handle, HTS221_ADDR7, HTS221_HUMIDITY_OUT_L | HTS221_AUTOINC, b, 4))
        return HTS_EBUS;
    *h_out = (int16_t)(uint16_t)(b[0] | b[1] << 8);
    *t_out = (int16_t)(uint16_t)(b[2] | b[3] << 8);
    return HTS_OK;
}

LPS22HB — по комментариям lps22hb.h из BSP: паспорт WHO_AM_I (0x0F) = 0xB1, CTRL_REG1 (0x10) — частота в битах 6:4 (000 — однократно) и BDU в бите 1, CTRL_REG2 (0x11) — IF_ADD_INC в бите 4 и ONE_SHOT в бите 0, RES_CONF (0x1A) — LC_EN, режим малого тока (его включает и LPS22HB_Init BSP). Давление — 24 бита со знаком в 0x28…0x2A, Pout(hPA) = … /4096; температура — 16 бит в 0x2B…0x2C, TEMP_OUT/100. Ядро переводит давление в паскали без деления: × 100 / 4096 = × 25 / 1024.

Автоинкремент у двух датчиков включается по-разному. У HTS221 — старшим битом адреса регистра (0x28 | 0x80, раздел 39в), у LPS22HB — битом IF_ADD_INC, который по умолчанию 1. Поэтому запуск замера LPS22HB пишет в CTRL_REG2 0x11, а не 0x01: единица в ONE_SHOT и ноль в остальных битах выключили бы автоинкремент, и чтение пяти байтов вернуло бы пять раз один регистр.

Цикл замера на шине — 6 посылок и 29 байт: два запуска, два чтения STATUS и два чтения данных (4 байта HTS221 и 5 байтов LPS22HB).

Ошибка в драйвере BSP. LPS22HB_T_ReadTemp собирает температуру в uint16_t и делит без знака: raw_data = (tmp * 10) / 100. Всё, что ниже нуля, превращается в сотни градусов:

Отсчёт Байты Верно BSP
250 FA 00 2,50 °C 2,5 °C
0 00 00 0,00 °C 0,0 °C
−1 FF FF −0,01 °C 655,3 °C
−50 CE FF −0,50 °C 654,8 °C
−500 0C FE −5,00 °C 650,3 °C
−800 E0 FC −8,00 °C 647,3 °C

Для прибора в комнате это незаметно, для метеостанции зимой — три четверти суток: в опыте из 8640 замеров ниже нуля 6278, и BSP ошибся во всех. Ядро берёт отсчёт как int16_t — это и есть сотые градуса.


198м.3 Шесть замеров — одна запись

Ядро копит сумму и крайние значения и раз в минуту выдаёт запись: время (секунды от полуночи), давление в паскалях, средняя, наименьшая и наибольшая температура и средняя влажность в сотых — 4 + 4 + 3 · 2 + 2 = 16 байт, младший байт первым. Строка для UART собирается без printf:

06:00 T=-8.00 (-8.03..-7.95) RH=94.95 P=1012.00

Опыт — зимние сутки: −8 °C в 06:00, +3 °C в 15:00, влажность от 95 до 70 %, давление падает с 1015 до 1003 гПа, на всё наложен шум. Датчик HTS221 — модель раздела 39в с той же калибровкой, LPS22HB — своя модель по lps22hb.h. За сутки — 8640 замеров без единой ошибки шины и 1440 записей; температура от −8,03 до 3,05 °C, давление от 1015,02 до 1003,01 гПа. Модель на Python прошла те же сутки своим кодом: CRC-32 записей 0x226C42C6 — как у прогона на C.

Округление среднего. Привычное (сумма + n/2) / n верно только для положительных чисел: в C деление отбрасывает дробь к нулю, и для отрицательной суммы результат съезжает вверх. Пример: −7 / 6 = −1,17, а формула даёт 0; ядро округляет от нуля и получает −1. В опыте простая формула разошлась с верной в 1046 записях из 1440 — везде, где температура ниже нуля, — ровно на 0,01 °C. Мало, но это не шум, а сдвиг в одну сторону.


198м.4 Журнал на флеш

Журнал — файл meteo.log в LittleFS (раздел 42а), те же размеры, что там: чтение и запись по 16 байт, кэш 256 байт, block_cycles = 500. Блочное устройство прошивки — четыре функции поверх BSP_QSPI_Read, BSP_QSPI_Write и BSP_QSPI_Erase_Sector; у последней аргумент — номер сектора, а не адрес (BSP умножает его на 4096 сам).

Каждая запись дописывается в файл, но на флеш она попадает окончательно только после lfs_file_sync. Прибор фиксирует файл раз в 15 записей, то есть раз в 15 минут. Опыт на модели флеш-памяти в ОЗУ (16 блоков по 4096 байт, запись только сбрасывает биты):

Шаг Что вышло
1000 записей, фиксация раз в 15 записано на флеш 156 368 байт
питание пропало после 1000-й записи ОЗУ потеряно, флеш осталась
монтирование после сброса в файле 15 840 байт — 990 записей
дописали остаток суток и прочитали файл 1430 записей, все совпали с исходными, одна дыра: после 16:30 сразу 16:41, нет 10 записей
износ за сутки 103 стирания, больше всего у одного блока — 10, задеты все 16 блоков

Сколько теряется при сбросе, задаёт частота фиксации: здесь не больше 14 минут, в опыте — 10. Фиксация стоит записей: полезных данных за сутки 23 040 байт, а на флеш ушло 220 064 — в 9,55 раза больше, потому что каждая фиксация переписывает метаданные.

Срок журнала — простой счёт: сутки — 23 040 байт, 8 МБ MX25R6435F хватает на 364,1 суток, дальше старые данные надо удалять (например, файл на каждые сутки). Износ: 10 стираний в сутки у самого загруженного блока при ресурсе 100 000 циклов (42а) — 10 000 суток, 27,4 года.


198м.5 Сон между замерами

Кристалл работает от HSI16 на 16 МГц в диапазоне напряжения 2 (до 26 МГц). Бит STOPWUCK в RCC_CFGR велит просыпаться из Stop сразу на HSI16. Часы RTC идут от кварца LSE платы, таймер пробуждения считает секунды (WUCKSEL = 100 — ck_spre, 1 Гц) до WUTR + 1 = 10 и будит кристалл через линию EXTI 20. Режим — Stop 2: LPMS = 010 в PWR_CR1.

static void stop2(void)
{
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __DSB();
    __WFI();                                    /* проснёмся по RTC через 10 с на HSI16 */
    SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
}

Сколько кристалл не спит. I2C2 на 100 кГц от 16 МГц настраивается по таблице 237 RM0351 (TIMINGR = 0x30420F13, период около 10 мкс). Цикл замера — 29 байт по 9 тактов плюс старты и стопы — 277 тактов, 2,77 мс. Строка в UART — 49 байт по 10 бит на 115 200 бит/с — 4,25 мс раз в минуту. Выход из Stop 2 на HSI16 — 7,3 мкс (DS10969, табл. 42). Итого в среднем 3,486 мс бодрствования на 10 с.

Средний ток по DS10969 при 3 В и 25 °C: Stop 2 с RTC на кварце — 1,67 мкА (табл. 35), работа на 16 МГц в диапазоне 2 — 1,83 мА (табл. 27; она снята с тактом от HSE, поэтому добавлен ток генератора HSI16 — 155 мкА). Батарея 2500 мА·ч, как в 10а:

Вариант Кристалл Вместе с датчиками Срок
замер раз в 10 с, 16 МГц 2,36 мкА 7,36 мкА 38,8 года
замер раз в 10 с, 80 МГц 5,28 мкА 10,28 мкА 27,8 года
замер раз в 60 с, 16 МГц 1,90 мкА 6,90 мкА 41,3 года

Датчики в расчёте — числа документов: HTS221 — 2 мкА (DS10866, при 1 Гц; для замера раз в 10 с числа нет), LPS22HB — «down to 3 μA» по UM2153. Ток флеш-памяти при записи не учтён — его берут из даташита MX25R6435F. Срок в десятки лет означает одно: упрёмся в саморазряд батареи, а не в кристалл, как и в 10а.

Два вывода из таблицы. Шина I2C при 100 кГц — 23 % тока кристалла, и разогнать её частотой ядра нельзя: при 80 МГц бодрствование то же, а ток работы 10,2 мА — средний ток кристалла вырос в 2,2 раза. И кристалл уже не главный: датчики берут больше, чем он.


198м.6 Прошивка и сборка

meteo_l475.c — всё на регистрах CMSIS: такты, выводы, I2C2 опросом, USART1, LSE и RTC, Stop 2, LittleFS поверх BSP. Проверки при сборке:

Перед сном строка должна уйти целиком: uart_puts ждёт флага TC, а не только TXE: в Stop 2 USART1 не работает (DS10969, табл. 5 — там остаётся только LPUART), и последний байт оборвётся.

armclang 6.24 собрал три файла без предупреждений: ядро — 1214 байт кода, драйвер HTS221 — 626, прошивка — 1398 байт кода и 1372 байта ОЗУ (буферы LittleFS). LittleFS в размер не входит: её собирают из дерева Zephyr, как в 42а.


198м.7 Грабли

Температура ниже нуля из BSP. LPS22HB_T_ReadTemp делит беззнаковое число: −0,01 °C становится 655,3 °C. Отсчёт — int16_t в сотых градуса.

Автоинкремент выключили сами. У LPS22HB он живёт в CTRL_REG2 рядом с ONE_SHOT. Писать 0x11, а не 0x01.

Ждать преобразования. Опрос STATUS до готовности держит кристалл в работе. Два шага — забрать прошлый, запустить новый — и ждать не надо.

Округление отрицательных. (сумма + n/2) / n сдвигает средние ниже нуля вверх: 1046 записей из 1440 на 0,01 °C.

Фиксация журнала. Без lfs_file_sync сброс уносит всё, что записано после последней фиксации. Чаще фиксация — меньше потерь, но больше записи: при 15 минутах — в 9,55 раза больше полезных данных.

Номер вместо адреса. BSP_QSPI_Erase_Sector ждёт номер сектора, BSP_QSPI_Erase_Block — адрес. Перепутать — стереть чужой блок.

Строка оборвалась во сне. В Stop 2 USART1 не работает. Перед сном ждать TC.

Быстрее — не значит экономнее. I2C на 100 кГц идёт столько же при 80 МГц, ток кристалла — в 2,2 раза больше.

Ток платы — не ток кристалла. На B-L475E-IOT01A стоят радиомодули и ST-LINK; числа 198м.5 — для своей платы с теми же датчиками.


198м.8 Итоги

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

meteo_core.c — ядро метеостанции.

/* meteo_core.c — раздел 198м: ядро метеостанции (см. meteo_core.h). */
#include "meteo_core.h"

_Static_assert(LPS22HB_ADDR7 == (0xBAu >> 1), "BSP пишет адрес LPS22HB в 8-битном виде: 0xBA");
_Static_assert((LPS22HB_IF_ADD_INC | LPS22HB_ONE_SHOT) == 0x11u, "CTRL_REG2: IF_ADD_INC (бит 4) + ONE_SHOT (бит 0)");
_Static_assert(METEO_REC_BYTES == 4u + 4u + 2u * 3u + 2u, "запись: время, давление, три температуры, влажность");

/* ── LPS22HB ───────────────────────────────────────────────────────────── */
int lps_init(lps22hb *d)
{
    uint8_t id = 0, v;
    if (d->bus.read(d->bus.handle, LPS22HB_ADDR7, LPS22HB_WHO_AM_I, &id, 1))
        return HTS_EBUS;
    if (id != LPS22HB_ID)
        return HTS_EID;
    v = 0x01u;                                                   /* RES_CONF: LC_EN — малый ток, как в BSP */
    if (d->bus.write(d->bus.handle, LPS22HB_ADDR7, LPS22HB_RES_CONF, &v, 1))
        return HTS_EBUS;
    v = LPS22HB_BDU;                                             /* ODR = 000: только по ONE_SHOT */
    if (d->bus.write(d->bus.handle, LPS22HB_ADDR7, LPS22HB_CTRL_REG1, &v, 1))
        return HTS_EBUS;
    return HTS_OK;
}

int lps_start(lps22hb *d)
{
    uint8_t v = LPS22HB_IF_ADD_INC | LPS22HB_ONE_SHOT;           /* не сбросить автоинкремент */
    return d->bus.write(d->bus.handle, LPS22HB_ADDR7, LPS22HB_CTRL_REG2, &v, 1) ? HTS_EBUS : HTS_OK;
}

int lps_read(lps22hb *d, int32_t *p_raw, int16_t *t_raw)
{
    uint8_t st = 0, b[5];
    if (d->bus.read(d->bus.handle, LPS22HB_ADDR7, LPS22HB_STATUS, &st, 1))
        return HTS_EBUS;
    if ((st & 0x03u) != 0x03u)                                   /* T_DA и P_DA */
        return HTS_ETIMEOUT;
    if (d->bus.read(d->bus.handle, LPS22HB_ADDR7, LPS22HB_PRESS_XL, b, 5))
        return HTS_EBUS;
    uint32_t u = (uint32_t)b[0] | (uint32_t)b[1] << 8 | (uint32_t)b[2] << 16;
    *p_raw = (int32_t)(u ^ 0x800000u) - 0x800000;                /* 24 бита со знаком */
    *t_raw = (int16_t)(uint16_t)(b[3] | b[4] << 8);              /* сотые °C */
    return HTS_OK;
}

int32_t lps_pa(int32_t p_raw)
{
    return avg_round(p_raw * 25, 1024);                          /* ×100/4096 = ×25/1024 */
}

int16_t lps_bsp_temp_x10(uint8_t lo, uint8_t hi)
{
    uint16_t tmp = (uint16_t)(((uint16_t)hi << 8) + (uint16_t)lo);   /* дословно как в lps22hb.c */
    int16_t raw_data = (int16_t)((tmp * 10) / 100);
    return raw_data;
}

/* ── HTS221 в два шага ─────────────────────────────────────────────────── */
int hts_start(hts221 *d)
{
    uint8_t v = HTS221_CTRL2_ONE_SHOT;
    return d->bus.write(d->bus.handle, HTS221_ADDR7, HTS221_CTRL_REG2, &v, 1) ? HTS_EBUS : HTS_OK;
}

int hts_read(hts221 *d, int16_t *h_out, int16_t *t_out)
{
    uint8_t st = 0, b[4];
    if (d->bus.read(d->bus.handle, HTS221_ADDR7, HTS221_STATUS_REG, &st, 1))
        return HTS_EBUS;
    if ((st & 0x03u) != 0x03u)
        return HTS_ETIMEOUT;
    if (d->bus.read(d->bus.handle, HTS221_ADDR7, HTS221_HUMIDITY_OUT_L | HTS221_AUTOINC, b, 4))
        return HTS_EBUS;
    *h_out = (int16_t)(uint16_t)(b[0] | b[1] << 8);
    *t_out = (int16_t)(uint16_t)(b[2] | b[3] << 8);
    return HTS_OK;
}

/* ── усреднение ────────────────────────────────────────────────────────── */
int32_t avg_round(int32_t n, int32_t d)
{
    return n >= 0 ? (n + d / 2) / d : -((-n + d / 2) / d);
}

int32_t avg_naive(int32_t n, int32_t d)
{
    return (n + d / 2) / d;
}

void acc_reset(meteo_acc *a)
{
    a->n = 0;
    a->sum_t = a->sum_rh = a->sum_p = 0;
    a->t_min = INT16_MAX;
    a->t_max = INT16_MIN;
}

void acc_add(meteo_acc *a, int16_t t_c, uint16_t rh_c, int32_t p_pa)
{
    a->n++;
    a->sum_t += t_c;
    a->sum_rh += rh_c;
    a->sum_p += p_pa;
    if (t_c < a->t_min)
        a->t_min = t_c;
    if (t_c > a->t_max)
        a->t_max = t_c;
}

void acc_take(meteo_acc *a, uint32_t t, meteo_rec *r, int naive)
{
    int32_t n = (int32_t)a->n;
    int32_t (*avg)(int32_t, int32_t) = naive ? avg_naive : avg_round;
    r->t = t;
    r->t_avg = (int16_t)avg(a->sum_t, n);
    r->rh = (uint16_t)avg(a->sum_rh, n);
    r->p_pa = avg(a->sum_p, n);
    r->t_min = a->t_min;
    r->t_max = a->t_max;
    acc_reset(a);
}

/* ── запись 16 байт, младший байт первым ───────────────────────────────── */
static void put(uint8_t *b, uint32_t v, unsigned n)
{
    for (unsigned i = 0; i < n; i++)
        b[i] = (uint8_t)(v >> (8u * i));
}

static uint32_t get(const uint8_t *b, unsigned n)
{
    uint32_t v = 0;
    for (unsigned i = 0; i < n; i++)
        v |= (uint32_t)b[i] << (8u * i);
    return v;
}

void rec_pack(const meteo_rec *r, uint8_t b[METEO_REC_BYTES])
{
    put(b, r->t, 4);
    put(b + 4, (uint32_t)r->p_pa, 4);
    put(b + 8, (uint16_t)r->t_avg, 2);
    put(b + 10, (uint16_t)r->t_min, 2);
    put(b + 12, (uint16_t)r->t_max, 2);
    put(b + 14, r->rh, 2);
}

void rec_unpack(meteo_rec *r, const uint8_t b[METEO_REC_BYTES])
{
    r->t = get(b, 4);
    r->p_pa = (int32_t)get(b + 4, 4);
    r->t_avg = (int16_t)(uint16_t)get(b + 8, 2);
    r->t_min = (int16_t)(uint16_t)get(b + 10, 2);
    r->t_max = (int16_t)(uint16_t)get(b + 12, 2);
    r->rh = (uint16_t)get(b + 14, 2);
}

/* ── строка для UART: «07:15 T=-3.21 (-3.40..-3.05) RH=84.50 P=1012.93» ─── */
static char *num(char *p, int32_t v, unsigned frac)             /* v — в сотых, frac — 2 знака */
{
    char tmp[12];
    unsigned n = 0;
    uint32_t u;
    if (v < 0) {
        *p++ = '-';
        u = (uint32_t)(-v);
    } else {
        u = (uint32_t)v;
    }
    do {
        tmp[n++] = (char)('0' + u % 10u);
        u /= 10u;
    } while (u || n <= frac);
    while (n) {
        *p++ = tmp[--n];
        if (n == frac && frac)
            *p++ = '.';
    }
    return p;
}

static char *two(char *p, uint32_t v)
{
    *p++ = (char)('0' + v / 10u % 10u);
    *p++ = (char)('0' + v % 10u);
    return p;
}

unsigned rec_line(const meteo_rec *r, char *buf)
{
    char *p = buf;
    p = two(p, r->t / 3600u);
    *p++ = ':';
    p = two(p, r->t / 60u % 60u);
    *p++ = ' '; *p++ = 'T'; *p++ = '=';
    p = num(p, r->t_avg, 2);
    *p++ = ' '; *p++ = '(';
    p = num(p, r->t_min, 2);
    *p++ = '.'; *p++ = '.';
    p = num(p, r->t_max, 2);
    *p++ = ')'; *p++ = ' '; *p++ = 'R'; *p++ = 'H'; *p++ = '=';
    p = num(p, r->rh, 2);
    *p++ = ' '; *p++ = 'P'; *p++ = '=';
    p = num(p, r->p_pa, 2);
    *p++ = '\r'; *p++ = '\n';
    *p = 0;
    return (unsigned)(p - buf);
}

meteo_l475.c — метеостанция на B-L475E-IOT01A: замер раз в 10 с, между замерами Stop 2.

/* meteo_l475.c — раздел 198м: метеостанция на B-L475E-IOT01A (STM32L475VG), регистры CMSIS.
 * HSI16 16 МГц, диапазон 2; замер раз в 10 с по таймеру пробуждения RTC (LSE), между замерами — Stop 2;
 * HTS221 и LPS22HB на I2C2 (PB10, PB11, AF4), строка в USART1 (PB6, PB7, AF7 — виртуальный COM ST-LINK);
 * журнал — LittleFS (раздел 42а) на MX25R6435F через функции BSP_QSPI_* из BSP платы (здесь не собираются). */
#include "stm32l4xx.h"
#include "meteo_core.h"
#include "lfs.h"

#define HSI_HZ      16000000u
#define I2C_TIMING  0x30420F13u      /* RM0351, табл. 237: 16 МГц, 100 кГц — PRESC 3, SCLDEL 4, SDADEL 2, SCLH 0x0F, SCLL 0x13 */
#define UART_BAUD   115200u
#define UART_BRR    ((HSI_HZ + UART_BAUD / 2u) / UART_BAUD)
#define WAKE_S      10u              /* WUCKSEL = 10x: ck_spre 1 Гц, период WUT + 1 */

_Static_assert(HSI_HZ <= 26000000u, "диапазон 2 — до 26 МГц (DS10969, табл. 27)");
_Static_assert(((I2C_TIMING >> 28) & 0xFu) == 3u && ((I2C_TIMING >> 20) & 0xFu) == 4u && ((I2C_TIMING >> 16) & 0xFu) == 2u
               && ((I2C_TIMING >> 8) & 0xFFu) == 0x0Fu && (I2C_TIMING & 0xFFu) == 0x13u, "поля TIMINGR");
_Static_assert(UART_BRR == 139u, "16 МГц / 115 200 = 138,9 → 139");
_Static_assert(METEO_AVG * WAKE_S == 60u, "6 замеров по 10 с — минута");

/* ── I2C2: посылки по 7-битному адресу, опросом ─────────────────────────── */
static int i2c_wait(uint32_t flag)
{
    for (uint32_t n = 0; n < 100000u; n++) {
        uint32_t isr = I2C2->ISR;
        if (isr & I2C_ISR_NACKF) {
            I2C2->ICR = I2C_ICR_NACKCF | I2C_ICR_STOPCF;
            return -1;
        }
        if (isr & flag)
            return 0;
    }
    return -1;
}

static int i2c_write(void *h, uint8_t a7, uint8_t sub, const uint8_t *d, uint16_t n)
{
    (void)h;
    I2C2->CR2 = (uint32_t)a7 << 1 | (uint32_t)(n + 1u) << I2C_CR2_NBYTES_Pos | I2C_CR2_AUTOEND | I2C_CR2_START;
    if (i2c_wait(I2C_ISR_TXIS))
        return -1;
    I2C2->TXDR = sub;
    for (uint16_t i = 0; i < n; i++) {
        if (i2c_wait(I2C_ISR_TXIS))
            return -1;
        I2C2->TXDR = d[i];
    }
    if (i2c_wait(I2C_ISR_STOPF))
        return -1;
    I2C2->ICR = I2C_ICR_STOPCF;
    return 0;
}

static int i2c_read(void *h, uint8_t a7, uint8_t sub, uint8_t *d, uint16_t n)
{
    (void)h;
    I2C2->CR2 = (uint32_t)a7 << 1 | 1u << I2C_CR2_NBYTES_Pos | I2C_CR2_START;          /* без AUTOEND: повторный старт */
    if (i2c_wait(I2C_ISR_TXIS))
        return -1;
    I2C2->TXDR = sub;
    if (i2c_wait(I2C_ISR_TC))
        return -1;
    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 (i2c_wait(I2C_ISR_RXNE))
            return -1;
        d[i] = (uint8_t)I2C2->RXDR;
    }
    if (i2c_wait(I2C_ISR_STOPF))
        return -1;
    I2C2->ICR = I2C_ICR_STOPCF;
    return 0;
}

static void no_delay(void *h, uint32_t ms) { (void)h; (void)ms; }

/* ── USART1 ─────────────────────────────────────────────────────────────── */
static void uart_puts(const char *s, unsigned n)
{
    while (n--) {
        while (!(USART1->ISR & USART_ISR_TXE))
            ;
        USART1->TDR = (uint8_t)*s++;
    }
    while (!(USART1->ISR & USART_ISR_TC))       /* дождаться последнего бита: иначе Stop 2 обрежет строку */
        ;
}

/* ── LittleFS поверх BSP_QSPI_* (MX25R6435F: сектор 4 КБ, страница 256 байт) ── */
extern uint8_t BSP_QSPI_Read(uint8_t *pData, uint32_t ReadAddr, uint32_t Size);
extern uint8_t BSP_QSPI_Write(uint8_t *pData, uint32_t WriteAddr, uint32_t Size);
extern uint8_t BSP_QSPI_Erase_Sector(uint32_t Sector);
extern uint8_t BSP_QSPI_GetStatus(void);

static int bd_read(const struct lfs_config *c, lfs_block_t b, lfs_off_t off, void *buf, lfs_size_t n)
{
    return BSP_QSPI_Read(buf, b * c->block_size + off, n) ? LFS_ERR_IO : 0;
}
static int bd_prog(const struct lfs_config *c, lfs_block_t b, lfs_off_t off, const void *buf, lfs_size_t n)
{
    return BSP_QSPI_Write((uint8_t *)(uintptr_t)buf, b * c->block_size + off, n) ? LFS_ERR_IO : 0;
}
static int bd_erase(const struct lfs_config *c, lfs_block_t b)
{
    (void)c;
    if (BSP_QSPI_Erase_Sector(b))               /* номер сектора, а не адрес */
        return LFS_ERR_IO;
    while (BSP_QSPI_GetStatus() != 0u)          /* QSPI_OK = 0: стирание закончилось */
        ;
    return 0;
}
static int bd_sync(const struct lfs_config *c) { (void)c; return 0; }

static uint8_t rbuf[256], pbuf[256], fbuf[256], lbuf[256];
static const struct lfs_config lcfg = {
    .read = bd_read, .prog = bd_prog, .erase = bd_erase, .sync = bd_sync,
    .read_size = 16, .prog_size = 16, .block_size = 4096, .block_count = 2048,
    .cache_size = 256, .lookahead_size = 256, .block_cycles = 500,
    .read_buffer = rbuf, .prog_buffer = pbuf, .lookahead_buffer = lbuf,
};
static const struct lfs_file_config fcfg = { .buffer = fbuf };
static lfs_t fs;
static lfs_file_t lf;

/* ── такты, выводы, RTC ────────────────────────────────────────────────── */
static void hw_init(void)
{
    RCC->CR |= RCC_CR_HSION;
    while (!(RCC->CR & RCC_CR_HSIRDY))
        ;
    RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_HSI | RCC_CFGR_STOPWUCK;   /* и после Stop — HSI16 */
    RCC->APB1ENR1 |= RCC_APB1ENR1_PWREN | RCC_APB1ENR1_I2C2EN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOBEN;
    PWR->CR1 = (PWR->CR1 & ~PWR_CR1_VOS) | PWR_CR1_VOS_1;                          /* диапазон 2 */
    RCC->CCIPR = (RCC->CCIPR & ~(RCC_CCIPR_I2C2SEL | RCC_CCIPR_USART1SEL)) | RCC_CCIPR_I2C2SEL_1 | RCC_CCIPR_USART1SEL_1;
    /* PB6, PB7 — AF7; PB10, PB11 — AF4, открытый сток */
    GPIOB->MODER = (GPIOB->MODER & ~(0xFu << 12 | 0xFu << 20)) | 0xAu << 12 | 0xAu << 20;
    GPIOB->OTYPER |= 3u << 10;
    GPIOB->AFR[0] = (GPIOB->AFR[0] & ~(0xFFu << 24)) | 0x77u << 24;
    GPIOB->AFR[1] = (GPIOB->AFR[1] & ~(0xFFu << 8)) | 0x44u << 8;
    I2C2->TIMINGR = I2C_TIMING;
    I2C2->CR1 = I2C_CR1_PE;
    USART1->BRR = UART_BRR;
    USART1->CR1 = USART_CR1_TE | USART_CR1_UE;
    /* LSE и RTC: домен резервного питания открыт битом DBP */
    PWR->CR1 |= PWR_CR1_DBP;
    RCC->BDCR |= RCC_BDCR_LSEON;
    while (!(RCC->BDCR & RCC_BDCR_LSERDY))
        ;
    RCC->BDCR |= RCC_BDCR_RTCSEL_0 | RCC_BDCR_RTCEN;                               /* RTCSEL = 01: LSE */
    RTC->WPR = 0xCA;
    RTC->WPR = 0x53;
    RTC->CR &= ~RTC_CR_WUTE;
    while (!(RTC->ISR & RTC_ISR_WUTWF))
        ;
    RTC->WUTR = WAKE_S - 1u;
    RTC->CR = (RTC->CR & ~RTC_CR_WUCKSEL) | RTC_CR_WUCKSEL_2 | RTC_CR_WUTIE | RTC_CR_WUTE;   /* 100: ck_spre */
    RTC->WPR = 0xFF;
    EXTI->IMR1 |= EXTI_IMR1_IM20;                                                   /* пробуждение RTC — линия 20 */
    EXTI->RTSR1 |= EXTI_RTSR1_RT20;
    NVIC_EnableIRQ(RTC_WKUP_IRQn);
    PWR->CR1 = (PWR->CR1 & ~PWR_CR1_LPMS) | PWR_CR1_LPMS_STOP2;
}

void RTC_WKUP_IRQHandler(void)
{
    RTC->ISR &= ~RTC_ISR_WUTF;
    EXTI->PR1 = EXTI_PR1_PIF20;
}

static void stop2(void)
{
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __DSB();
    __WFI();                                    /* проснёмся по RTC через 10 с на HSI16 */
    SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
}

int main(void)
{
    static hts221 hd;
    static lps22hb ld;
    static meteo_acc acc;
    static char line[64];
    hts_bus bus = { i2c_write, i2c_read, no_delay, 0 };
    uint32_t t = 0, synced = 0;

    hw_init();
    hd.bus = bus;
    ld.bus = bus;
    if (hts_init(&hd) || lps_init(&ld))
        uart_puts("sensor?\r\n", 9);
    if (lfs_mount(&fs, &lcfg)) {
        lfs_format(&fs, &lcfg);
        lfs_mount(&fs, &lcfg);
    }
    lfs_file_opencfg(&fs, &lf, "meteo.log", LFS_O_WRONLY | LFS_O_CREAT | LFS_O_APPEND, &fcfg);
    acc_reset(&acc);
    hts_start(&hd);
    lps_start(&ld);
    for (;;) {
        stop2();
        t += WAKE_S;
        int16_t ho, to, lt;
        int32_t praw;
        if (hts_read(&hd, &ho, &to) == HTS_OK && lps_read(&ld, &praw, &lt) == HTS_OK)
            acc_add(&acc, (int16_t)hts_temp_centi(&hd.cal, to), (uint16_t)hts_hum_centi(&hd.cal, ho), lps_pa(praw));
        hts_start(&hd);                         /* следующий замер будет готов к следующему пробуждению */
        lps_start(&ld);
        if (t % 60u == 0u && acc.n) {
            meteo_rec r;
            uint8_t b[METEO_REC_BYTES];
            acc_take(&acc, t % 86400u, &r, 0);
            rec_pack(&r, b);
            lfs_file_write(&fs, &lf, b, sizeof b);
            if (++synced % 15u == 0u)
                lfs_file_sync(&fs, &lf);        /* раз в 15 минут: при сбросе теряется не больше 14 записей */
            uart_puts(line, rec_line(&r, line));
        }
    }
}

Дальше: раздел 198н «Мини-проект: приёмник GPS — NMEA, PPS и точное время».

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