Embedded HandbookApps
All sections › XXV. Mini-projects
STM32L475 · level intermediate · 24 min read
Open in the interactive handbook

198м. Mini-project: weather station — sensors, log and display

A weather station is the calmest device in the book: the weather changes over minutes, yet it has to run on batteries for years. So what matters here is not speed but three things: converting the sensor readings correctly into degrees, percent and hectopascals, not losing the log on a reset, and sleeping between measurements so that in the average current sleep weighs more than work.

The device is built on the B-L475E-IOT01A board (STM32L475VG): it already carries the HTS221 humidity and temperature sensor, the LPS22HB barometer and the 8 MB MX25R6435F flash memory. Every 10 s the RTC wakes the chip from Stop 2, it collects the finished readings and starts new ones, and once a minute six measurements are reduced to a 16-byte record — it goes to the log in flash and, as a line, to the ST-LINK virtual COM port.

Checked without a board. The core (meteo_core.c) and the HTS221 driver from section 39в (unchanged) were run in the experiment sim_meteo.c on sensor models: a day of weather and a log in LittleFS on a flash memory model in RAM. The experiment was built with gcc on the PC and with arm-none-eabi-gcc for Cortex-M4, and in qemu-system-arm -M netduinoplus2 the output matched the PC line for line. A model in Python (meteo_host.py) repeats the day bit for bit — the CRC-32 of all records matched — and computes the bus time, the average current and the log lifetime. The firmware meteo_l475.c was built with armclang against the L4 pack headers without warnings; it has not been run on a board.

References: the board manual UM2153 and its BSP from the B-L475E-IOT01A_BSP pack (pins, addresses, the lps22hb driver, the BSP_QSPI_* functions); the LPS22HB datasheet was not used — the registers come from the comments in lps22hb.h; HTS221 — datasheet DS10866 and the driver from section 39в; datasheet DS10969 (STM32L475) — currents and wakeup times; RM0351 — table 237 of I2C settings, the PWR_CR1 and RTC_CR fields. LittleFS — section 42а, sleep modes — 10а, segment LCD — 22в. Check — _verify/meteo198m_check.py.

Terms used in the documentation

Term Meaning
one-shot a single measurement on command
ODR, output data rate how often samples are produced
BDU, block data update do not update the output until all bytes have been read
IF_ADD_INC register address auto-increment on the LPS22HB
LC_EN, low current mode the LPS22HB low-current mode
wakeup timer, WUT, ck_spre the RTC wakeup timer, the once-per-second clock
STOPWUCK which clock to wake up on from Stop
block_cycles, sync after how many erases metadata moves; committing a file

198м.1 What the device does and what it is made of

Function How
humidity and temperature HTS221 over I2C2, a one-shot measurement every 10 s
pressure LPS22HB over I2C2, a one-shot measurement every 10 s, 1/4096 hPa per LSB
sleep Stop 2, wakeup by the RTC timer from the LSE crystal
summary 6 measurements — one record per minute: mean, minimum and maximum temperature, humidity, pressure
log a 16-byte record in a LittleFS file on the MX25R6435F, committed every 15 minutes
display a line on USART1 — the ST-LINK virtual COM, 115200 bit/s

The weather station on the B-L475E-IOT01A: on the left the HTS221 and LPS22HB on I2C2, in the middle the core — a two-step measurement, six measurements into a 16-byte one-minute record; on the right the LittleFS log on the MX25R6435F over QUADSPI and a line on USART1; at the bottom the RTC on the LSE crystal wakes the chip from Stop 2 every 10 s, awake time 3.5 ms, average chip current 2.36 µA

The pins and addresses come from the BSP header stm32l475e_iot01.h and the UM2153 manual: both sensors sit on I2C2, the virtual COM is USART1 at 115200 bit/s, 8 bits, no parity.

Block Pins Peripheral Address
HTS221, LPS22HB PB10 SCL, PB11 SDA I2C2, AF4 0xBE and 0xBA as written in the BSP, 7-bit — 0x5F and 0x5D
virtual COM PB6 TX, PB7 RX USART1, AF7 —
MX25R6435F flash QUADSPI pins BSP_QSPI_* functions 8 MB: 2048 sectors of 4 KB, 256-byte page

The "display" here is a line in a terminal. The segment LCD from section 22в cannot be used on this chip: the STM32L475 has no LCD controller, and in DS10969 LCD is mentioned only as a parallel FMC interface. Its neighbour in the line, the L476, does have one — "LCD 8× 40 or 4× 44" on the first page of DS10198 — and then the output from 22в replaces the line.


198м.2 Sensors: two steps instead of waiting

The HTS221 works through the driver from section 39в — the variant without floating point (HTS_FIXED): initialization reads the ID 0xBC and 16 bytes of calibration, and writes 0x84 to CTRL_REG1 (active, BDU, one-shot mode). The measurement itself in 39в is "start and wait": hts_measure_raw writes ONE_SHOT and polls STATUS. A weather station has no reason to wait. The core splits a measurement into two steps: on wakeup it collects the sample started 10 s earlier and immediately starts the next one. The chip does not wait for a conversion for a single microsecond, and a 10 s data delay does not matter for weather.

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;
}

The LPS22HB follows the comments in lps22hb.h from the BSP: the ID WHO_AM_I (0x0F) = 0xB1, CTRL_REG1 (0x10) — the rate in bits 6:4 (000 — one-shot) and BDU in bit 1, CTRL_REG2 (0x11) — IF_ADD_INC in bit 4 and ONE_SHOT in bit 0, RES_CONF (0x1A) — LC_EN, the low-current mode (it is also enabled by LPS22HB_Init in the BSP). Pressure is 24 bits signed in 0x28…0x2A, Pout(hPA) = … /4096; temperature is 16 bits in 0x2B…0x2C, TEMP_OUT/100. The core converts pressure to pascals without a division: × 100 / 4096 = × 25 / 1024.

The two sensors enable auto-increment in different ways. On the HTS221 it is the most significant bit of the register address (0x28 | 0x80, section 39в), on the LPS22HB it is the IF_ADD_INC bit, which is 1 by default. That is why starting an LPS22HB measurement writes 0x11 to CTRL_REG2, not 0x01: a one in ONE_SHOT and zeros in the other bits would turn auto-increment off, and reading five bytes would return the same register five times.

One measurement cycle on the bus is 6 transfers and 29 bytes: two starts, two reads of STATUS and two data reads (4 bytes from the HTS221 and 5 bytes from the LPS22HB).

A bug in the BSP driver. LPS22HB_T_ReadTemp assembles the temperature in a uint16_t and divides it as unsigned: raw_data = (tmp * 10) / 100. Everything below zero turns into hundreds of degrees:

Sample Bytes Correct 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

For a device in a room this goes unnoticed; for a weather station in winter it is three quarters of the day: in the experiment 6278 of 8640 measurements were below zero, and the BSP got all of them wrong. The core takes the sample as int16_t — that is exactly hundredths of a degree.


198м.3 Six measurements — one record

The core accumulates a sum and the extremes and once a minute outputs a record: the time (seconds since midnight), the pressure in pascals, the mean, minimum and maximum temperature and the mean humidity in hundredths — 4 + 4 + 3 · 2 + 2 = 16 bytes, least significant byte first. The line for the UART is built without printf:

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

The experiment is a winter day: −8 °C at 06:00, +3 °C at 15:00, humidity from 95 to 70 %, pressure falling from 1015 to 1003 hPa, with noise on top of everything. The HTS221 sensor is the model from section 39в with the same calibration, the LPS22HB is a model of its own based on lps22hb.h. Over the day — 8640 measurements without a single bus error and 1440 records; temperature from −8.03 to 3.05 °C, pressure from 1015.02 to 1003.01 hPa. The Python model went through the same day with its own code: the CRC-32 of the records is 0x226C42C6 — the same as the C run.

Rounding the mean. The usual (sum + n/2) / n is correct only for positive numbers: in C division truncates toward zero, and for a negative sum the result drifts upwards. Example: −7 / 6 = −1.17, but the formula gives 0; the core rounds away from zero and gets −1. In the experiment the simple formula disagreed with the correct one in 1046 records out of 1440 — everywhere the temperature was below zero — by exactly 0.01 °C. That is small, but it is not noise, it is a bias in one direction.


198м.4 The log in flash

The log is the file meteo.log in LittleFS (section 42а), with the same sizes as there: reads and writes of 16 bytes, a 256-byte cache, block_cycles = 500. The firmware's block device is four functions on top of BSP_QSPI_Read, BSP_QSPI_Write and BSP_QSPI_Erase_Sector; the argument of the last one is a sector number, not an address (the BSP multiplies it by 4096 itself).

Each record is appended to the file, but it reaches flash for good only after lfs_file_sync. The device commits the file every 15 records, that is every 15 minutes. The experiment on a flash memory model in RAM (16 blocks of 4096 bytes, programming only clears bits):

Step Result
1000 records, committed every 15 156368 bytes written to flash
power lost after record 1000 RAM lost, flash kept
mount after the reset the file holds 15840 bytes — 990 records
appended the rest of the day and read the file 1430 records, all match the originals, one gap: 16:41 comes right after 16:30, 10 records missing
wear over the day 103 erases, the most on one block — 10, all 16 blocks touched

How much is lost on a reset is set by the commit rate: here at most 14 minutes, 10 in the experiment. Committing costs writes: the useful data for the day is 23040 bytes, but 220064 went to flash — 9.55 times more, because each commit rewrites the metadata.

The log lifetime is simple arithmetic: a day is 23040 bytes, the 8 MB of the MX25R6435F are enough for 364.1 days, after that old data has to be deleted (for example, one file per day). Wear: 10 erases per day on the most loaded block with an endurance of 100000 cycles (42а) — 10000 days, 27.4 years.


198м.5 Sleep between measurements

The chip runs from HSI16 at 16 MHz in voltage range 2 (up to 26 MHz). The STOPWUCK bit in RCC_CFGR makes it wake up from Stop directly on HSI16. The RTC runs from the board's LSE crystal, the wakeup timer counts seconds (WUCKSEL = 100 — ck_spre, 1 Hz) up to WUTR + 1 = 10 and wakes the chip through EXTI line 20. The mode is Stop 2: LPMS = 010 in PWR_CR1.

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

How long the chip stays awake. I2C2 at 100 kHz from 16 MHz is configured per table 237 of RM0351 (TIMINGR = 0x30420F13, a period of about 10 µs). The measurement cycle is 29 bytes of 9 clocks each plus starts and stops — 277 clocks, 2.77 ms. The UART line is 49 bytes of 10 bits at 115200 bit/s — 4.25 ms once a minute. Wakeup from Stop 2 on HSI16 takes 7.3 µs (DS10969, table 42). In total, on average 3.486 ms awake per 10 s.

The average current per DS10969 at 3 V and 25 °C: Stop 2 with the RTC on the crystal — 1.67 µA (table 35), running at 16 MHz in range 2 — 1.83 mA (table 27; it was measured with the clock from HSE, so the HSI16 oscillator current is added — 155 µA). A 2500 mAh battery, as in 10а:

Option Chip With sensors Lifetime
measurement every 10 s, 16 MHz 2.36 µA 7.36 µA 38.8 years
measurement every 10 s, 80 MHz 5.28 µA 10.28 µA 27.8 years
measurement every 60 s, 16 MHz 1.90 µA 6.90 µA 41.3 years

The sensors in the calculation use the document figures: HTS221 — 2 µA (DS10866, at 1 Hz; there is no figure for one measurement every 10 s), LPS22HB — "down to 3 μA" per UM2153. The flash memory current during writes is not included — take it from the MX25R6435F datasheet. A lifetime of decades means only one thing: the battery's self-discharge is the limit, not the chip, as in 10а.

Two conclusions from the table. The I2C bus at 100 kHz is 23 % of the chip current, and it cannot be sped up by the core clock: at 80 MHz the awake time is the same, but the run current is 10.2 mA — the average chip current grew 2.2 times. And the chip is no longer the main consumer: the sensors draw more than it does.


198м.6 Firmware and build

meteo_l475.c is all CMSIS registers: clocks, pins, polled I2C2, USART1, LSE and RTC, Stop 2, LittleFS on top of the BSP. Build-time checks:

Before sleeping, the line must go out completely: uart_puts waits for the TC flag, not just TXE: USART1 does not work in Stop 2 (DS10969, table 5 — only the LPUART keeps running there), and the last byte would be cut off.

armclang 6.24 built three files without warnings: the core — 1214 bytes of code, the HTS221 driver — 626, the firmware — 1398 bytes of code and 1372 bytes of RAM (LittleFS buffers). LittleFS is not included in the size: it is built from the Zephyr tree, as in 42а.


198м.7 Pitfalls

Below-zero temperature from the BSP. LPS22HB_T_ReadTemp divides an unsigned number: −0.01 °C becomes 655.3 °C. The sample is an int16_t in hundredths of a degree.

Auto-increment turned off by your own hand. On the LPS22HB it lives in CTRL_REG2 next to ONE_SHOT. Write 0x11, not 0x01.

Waiting for the conversion. Polling STATUS until ready keeps the chip running. Two steps — collect the previous one, start a new one — and there is nothing to wait for.

Rounding negatives. (sum + n/2) / n shifts means below zero upwards: 1046 records out of 1440 by 0.01 °C.

Committing the log. Without lfs_file_sync a reset takes away everything written after the last commit. More frequent commits mean less loss but more writing: at 15 minutes — 9.55 times more than the useful data.

A number instead of an address. BSP_QSPI_Erase_Sector expects a sector number, BSP_QSPI_Erase_Block an address. Mix them up and you erase someone else's block.

The line was cut off in sleep. USART1 does not work in Stop 2. Before sleeping, wait for TC.

Faster does not mean more economical. I2C at 100 kHz takes just as long at 80 MHz, and the chip current is 2.2 times higher.

Board current is not chip current. The B-L475E-IOT01A carries radio modules and an ST-LINK; the figures in 198м.5 are for your own board with the same sensors.


198м.8 Summary

Full source code

meteo_core.c — the weather station core.

/* 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 — the weather station on the B-L475E-IOT01A: a measurement every 10 s, Stop 2 between measurements.

/* 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));
        }
    }
}

Next: section 198н "Mini-project: a GPS receiver — NMEA, PPS and precise time".

Open in the interactive handbook