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 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:
TIMINGR— fields PRESC 3, SCLDEL 4, SDADEL 2, SCLH 0x0F, SCLL 0x13 from table 237;- the USART1 divider: 16 MHz / 115200 = 138.9 → 139;
- six wakeups of 10 s each — exactly one minute;
- 16 MHz is no more than 26 MHz — the limit of range 2.
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
- Two sensors on I2C2: 6 transfers and 29 bytes per measurement, 2.77 ms at 100 kHz; a two-step measurement — no waiting for the conversion.
- A day of the experiment: 8640 measurements, 1440 records of 16 bytes, temperature from −8.03 to 3.05 °C; the Python model matched C by CRC-32 (0x226C42C6).
- The LPS22HB BSP is wrong for all negative temperatures: in the experiment 6278 measurements out of 8640, −0.01 °C → 655.3 °C.
- LittleFS committed every 15 records: after a reset at record 1000, 990 remained, 10 were lost; flash writes are 9.55 times the data; 10 erases per day on the most loaded block — 27.4 years.
- 8 MB of flash — 364.1 days of log.
- Stop 2 between measurements: the chip averages 2.36 µA (at 80 MHz — 5.28), 7.36 µA with the sensors — longer than the battery lasts.
- armclang: 1214 + 626 + 1398 bytes of code, 1372 bytes of firmware RAM.
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, <) == 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".