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 бит/с |
Выводы и адреса — из заголовка 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. Проверки при сборке:
TIMINGR— поля PRESC 3, SCLDEL 4, SDADEL 2, SCLH 0x0F, SCLL 0x13 из таблицы 237;- делитель USART1: 16 МГц / 115 200 = 138,9 → 139;
- шесть пробуждений по 10 с — ровно минута;
- 16 МГц не больше 26 МГц — предела диапазона 2.
Перед сном строка должна уйти целиком: 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 Итоги
- Два датчика на I2C2: 6 посылок и 29 байт на замер, 2,77 мс на 100 кГц; замер в два шага — без ожидания преобразования.
- Сутки опыта: 8640 замеров, 1440 записей по 16 байт, температура от −8,03 до 3,05 °C; модель на Python совпала с C по CRC-32 (0x226C42C6).
- BSP LPS22HB ошибается на всех отрицательных температурах: в опыте 6278 замеров из 8640, −0,01 °C → 655,3 °C.
- LittleFS с фиксацией раз в 15 записей: после сброса на 1000-й записи осталось 990, потеряно 10; запись на флеш в 9,55 раза больше данных; 10 стираний в сутки у самого загруженного блока — 27,4 года.
- 8 МБ флеш — 364,1 суток журнала.
- Stop 2 между замерами: кристалл в среднем 2,36 мкА (при 80 МГц — 5,28), с датчиками 7,36 мкА — срок больше, чем у батареи.
- armclang: 1214 + 626 + 1398 байт кода, 1372 байта ОЗУ прошивки.
Полные исходники
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, <) == 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 и точное время».