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

198а. Mini-project: a battery-powered sensor — a year on one CR2032

The thermostat from section 196 and the clock from section 198 run from a wall socket. The fourth device is a logging thermometer that lives for a year on a single CR2032 coin cell: once a minute it wakes up on an RTC alarm, measures the temperature, adds to a log in Flash and goes back to sleep. All of section 10 turns here from theory into a line of expenses: every microamp is days of the device's life.

The heart of the project is not the code but the energy budget: work out the average current before you solder, and know which line of the budget dominates. Spoiler: not the bursts of work, but the sleep current — and the leakage you cannot see on the schematic.

Project files are in _verify/batt198a/: batt.h, batt.c (logic and the ring log), plat_f103.c (the BluePill platform), sim_host.c (a model of the Flash and the sensor for the computer), budget.py (the budget with a dimension check). The firmware is built for Cortex-M3, and a full year of operation has been run on the computer. Checked by _verify/batt198a_check.py.

Terms used in the documentation

Term Meaning
coin cell, button cell a flat disc-shaped battery
nominal capacity (mA·h) rated capacity, in milliamp-hours
continuous drain, standard load steady discharge current, the reference load
pulse drain discharge in current pulses
end (cut-off) voltage the voltage at which discharge is considered complete
internal resistance the source's own series resistance
shelf life how long the cell keeps in storage
battery life how long the device runs on one battery

198а.1 What the device does

Function How
wake up once a minute RTC alarm from a 32768 Hz crystal (section 198.5), EXTI 17 event
measure temperature 10 kΩ NTC in a divider, a 17-point table — as in section 196.3
not feed the divider for nothing the upper leg is powered from pin PA2 only during the measurement
remember a year log in Flash: a ring of 8 pages, one record every 10 minutes
know the battery state supply voltage via the internal reference VREFINT, written to the log
hand over the log a button on PA0 wakes the chip, the log goes out over USART1 at 115200 baud
sleep the rest of the time Stop, regulator in low-power mode, ≈15.4 µA together with the RTC

Wiring of the CR2032 sensor: battery with a 100 µF ceramic capacitor, NTC divider powered from pin PA2, log-read button on PA0 and a USB-UART

The battery is connected directly to the board's 3.3 V rail, bypassing the on-board regulator: the regulator and the power LED would eat it in a matter of days (198а.7). The F103 operating range is 2.0…3.6 V, so a CR2032 fits without any converters: a fresh one gives about 3 V, and the end-of-discharge voltage of a coin cell is exactly 2.0 V.

⚠️ The ADC only down to 2.4 V. The chip may be powered down to 2.0 V, but the analog supply with the ADC running is specified from 2.4 V. Below that the device still lives and writes the log, but measurement accuracy is no longer guaranteed. The divider, however, measures correctly for longer than you might think: it is powered from the same voltage as the ADC reference, and the code does not depend on the sagging supply (ratiometric measurement, section 96.2).


198а.2 Budget: calculate first, then solder

The capacity of a CR2032 from well-known manufacturers is typically 220–235 mA·h; the calculation uses a conservative 220. An honest caveat: those milliamp-hours are promised for discharge at a "steady" current of about 0.2 mA (standard load 15 kΩ) down to 2.0 V at room temperature. Milliamp pulses are hard on the battery: the internal resistance of a fresh cell is tens of ohms, near the end of discharge it is hundreds, and a current burst pulls the voltage down.

⚠️ The burst is supplied by the capacitor, not the battery. A 100 µF ceramic at the supply pin: a 4 mA × 5 ms burst takes a 0.2 V droop from it (ΔU = I·t/C), and the battery delivers the same charge smoothly, at an average current. Without the capacitor the same burst on a drained battery with hundreds of ohms inside drops the supply below reset — the device will "die" on every measurement.

All the chip's currents are taken not from thin air but from the STM32F103x8/B datasheet — the "Supply current characteristics" tables (typical values at 25 °C):

Mode Current Note
Run, HSI 8 MHz 4.0 mA peripherals off
Sleep, HSI 8 MHz 0.6 mA peripherals off
Stop, main regulator 24 µA wakeup 3.6 µs
Stop, low-power regulator 14 µA 13.5 µA at 2.4 V; wakeup 5.4 µs; up to 180 µA at 85 °C
Standby 2 µA RAM and registers are lost, wakeup 50 µs via reset
LSE crystal + RTC 1.4 µA 1.1 µA at 2.4 V
ADC on (ADON = 1) +0.8 mA analog part

The sleep mode is chosen as in section 10.1. Standby is four times more economical than Stop, but loses RAM: the accumulator of ten one-minute measurements and the ring tail would have to be hidden in the backup registers, and the program would start from scratch every time. For the first version we take Stop with the low-power regulator: 14 µA + 1.4 µA for the RTC with its crystal = 15.4 µA — RAM stays intact, and execution continues from the same line. The Standby gain is worked out in 198а.8.


198а.3 The cycle: wake up → measure → write → sleep

The clock is HSI 8 MHz without the PLL, as in section 196. This is not only about simplicity: after exiting Stop the system ends up on HSI anyway (the main pitfall of section 10.3) — and we never left it, so there is nothing to restore. Sleep is entered without interrupt handlers: the alarm and the button are set up as EXTI events, and the core is woken by a pair of __WFE:

static void enter_stop(void)
{
    EXTI->RTSR |= (1u << 17) | (1u << 0);
    EXTI->EMR |= (1u << 17) | (1u << 0);               /* события: обработчики прерываний не нужны */
    PWR->CR = (PWR->CR & ~PWR_CR_PDDS) | PWR_CR_LPDS | PWR_CR_CWUF;
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __SEV();
    __WFE();                                           /* снять взведённый флажок события */
    __WFE();                                           /* уснуть по-настоящему */
    SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
    /* система снова на HSI 8 МГц — мы с него и не уходили */
}

The main loop asks three questions after every wakeup:

    for (;;) {
        if (RTC->CRL & RTC_CRL_ALRF) {                 /* будильник: минутное измерение */
            RTC->CRL &= (uint16_t)~RTC_CRL_ALRF;
            uint32_t now = rtc_now();
            adc_on();
            batt_minute(&b, now);
            adc_off();
            rtc_write(&RTC->ALRH, &RTC->ALRL, now + BATT_PERIOD_S);
        }
        if (GPIOA->IDR & 1u) {                         /* кнопка: журнал в консоль */
            batt_dump(&b);
            while (GPIOA->IDR & 1u) {
            }
        }
        enter_stop();                                  /* ложное пробуждение безвредно: уснём снова */
    }

The measurement is as in 196, with one difference: the divider is powered from pin PA2 and switched on only for the duration of the measurement, otherwise its 150 µA would drain the battery ten times faster than the sleep itself:

    GPIOA->BSRR = GPIO_BSRR_BS2;                       /* подать питание на делитель */
    delay_ms(1);                                       /* установление: τ = 50 мкс при C = 10 нФ */
    uint32_t sum = 0;
    for (int i = 0; i < 16; i++)
        sum += adc_once(1);
    GPIOA->BSRR = GPIO_BSRR_BR2;                       /* снять питание: делитель не ест в сне */

The capacitor at the ADC input is reduced from 100 nF (section 196) to 10 nF: the node must charge within a millisecond of pause, and the noise is taken care of by averaging 16 samples. The pin's output switch holds 0.4 V at 8 mA according to the datasheet — less than 50 Ω; at the divider current that is 7.5 mV, a fraction of a percent of full scale.

Now the arithmetic of a burst. Its typical length is about 2.6 ms: Stop wakeup 5.4 µs, a millisecond for the divider, 20 ADC samples of 63 µs each (1.26 ms), and now and then a log write. The budget allows 5 ms — a twofold margin for future code. The charge of one burst:

q = 4 мА · 5 мс + 0,8 мА · 1,26 мс ≈ 20 + 1 ≈ 21 мкКл

Current profile of the sensor: 4 mA bursts once a minute above the 15.4 µA sleep level — 21 µC of charge per burst, average current about 16 µA

The average current when polling once a minute is the formula from section 10.8 plus two lines that people forget: erasing a log page (5 mA for up to 40 ms — but once per 128 records, 0.003 µA on average) and the self-discharge of the battery itself (around a percent a year — another 0.25 µA):

I_ср = 15,4 + 21 / 60 + 0,25 ≈ 16,0 мкА  →  220 000 мкА·ч / 16,0 ≈ 13 700 ч ≈ 1,6 года

A year with a margin of one and a half times. And the main conclusion of the budget: the polling period hardly matters:

Polling Average current Battery life
every 10 s 17.8 µA 1.41 years
every 60 s 16.0 µA 1.57 years
every 10 min 15.7 µA 1.60 years

The bursts account for only 0.35 µA out of 16 — two percent of consumption. This is the mirror image of the example from section 10.8, where the burst ate 72 %: there the device slept at 20 µA but woke up every second. In our device the sleep current rules — and that is why the hunt in 198а.7 is aimed at leakage.


198а.4 The log in Flash: a ring with round-robin wear

A record is 8 bytes, with no alignment holes; times are the RTC seconds counter (section 198.2):

struct batt_rec {                                      /* 8 байт во Flash, без выравнивающих дыр */
    uint32_t t;                                        /* время RTC, секунды; 0xFFFFFFFF — пусто */
    int16_t  t_c10;                                    /* температура, десятые доли °C, среднее за 10 минут */
    uint8_t  vbat;                                     /* (напряжение в мВ − 1000) / 10: 1,00…3,55 В */
    uint8_t  crc;                                      /* CRC-8, полином 0x07, по первым 7 байтам */
};

The one-minute measurements accumulate in RAM, and once every 10 minutes the average goes to the log. The last 8 pages of Flash are given to the log (0x0800E000; the linker script leaves the firmware 56 KB) — 8 × 128 = 1024 records, a history of 6.2–7.1 days: the page being written is erased entirely, so the tail of the history "breathes". The ring is the same record log as for the settings in section 11.8, only in whole pages:

    uint32_t page = b->rec_idx / LOG_RECS_PAGE;
    uint32_t off = b->rec_idx % LOG_RECS_PAGE * LOG_REC_BYTES;
    if (off == 0u)
        plat_log_erase(page);                          /* вошли в страницу — стереть её целиком */
    plat_log_write(page, off, &r, sizeof r);
    b->rec_idx = (b->rec_idx + 1u) % LOG_RECS;

Erasing and writing in half-words is the recipe from section 11.5; where to put calibration coefficients, if they appear, is section 202.5. After a reset the ring tail is searched for again: the newest valid record is the one with the largest time, and if the next cell is occupied by the garbage of an interrupted write, the tail steps further: in Flash you cannot write over what is not erased.

Round-robin wear is back-of-the-envelope arithmetic, confirmed by a run: in a year 52560 records — 411 erases over all 8 pages, no more than 52 per page. The F103 Flash endurance is 10000 cycles: the log wears it by half a percent a year, enough for roughly 190 years. Data retention is promised by the datasheet for 30 years — it will outlive both the battery and the device.


198а.5 Reading the log: a button and the console

The UART receiver is deaf in Stop: there is no clock, and a start bit wakes nobody (later series fix this with the LPUART; the F103 does not have one). So the log is collected with a button: PA0 to the supply plus, internal pull-down (30…50 kΩ per the datasheet), the rising edge is EXTI event 0. Current through the pull-down flows only while the button is pressed.

The device knows no calendar — the log holds bare seconds of its own counter:

t=31950940 T=24.8 U=2.60

The binding to real time is done by the program on the computer at the moment of the dump: real record time = now − (current t − record t). The full ring, 1024 lines, goes out in 2.3 s — that is 9.2 mC, the price of ten minutes of sleep: press the button every day if you like.

🔧 Battery voltage — without a divider. An external divider for measuring the supply would itself become a leak. Instead, the ADC measures the internal reference VREFINT ≈ 1.20 V relative to the supply: U = 1200 · 4096 / code, mV. The reference spread of 1.16…1.24 V gives an error of up to ±0.1 V at 3 V — for the question "is it time to change the battery" that is more than enough. In a log record the voltage takes one byte: a scale of 1.00…3.55 V in 10 mV steps.


198а.6 Running on the computer and building

QEMU does not model Stop mode or currents — this device cannot be verified on it. So the section stands on two supports: the numbers come from the datasheet in the pack, the logic uses the same "logic + platform" pair as in 196–198: batt.c knows nothing about registers and builds on the computer with a model of the Flash (erase to 0xFF, write only over erased cells, an erase counter per page) and of the sensor (a daily sine wave with noise, the battery runs down from 3.05 to 2.60 V over the year).

What Result
wakeups per year 525600, log records — 52560
page erases 411, per page 51…52 — the wear is even
ring at the end of the year 976 records, 585000 s of history (6.8 days), time gaps — 0
tail after a reset found where it was
power loss in the middle of a write the half record is discarded by CRC, the tail steps past it, the dump is intact
CRC-8 of 123456789 0xF4 — the check value of the implementation
dump after a year 976 lines, the first is t=31950940 T=24.8 U=2.60

The firmware is built with arm-none-eabi-gcc -Os for Cortex-M3 without warnings:

What Bytes
whole firmware (Flash) 1852
RAM (variables) 16
of which the batt.c logic — code 632

16 bytes of RAM is one struct batt: the log lives in Flash, the buffers on the stack. Record packing was verified on the computer honestly: both x86-64 and Cortex-M are little-endian, and the structure has no fields with holes, so the bytes in the model's "Flash" and in the board's Flash are the same.


198а.7 What eats the battery unnoticed

Each line comes from the budget.py budget; the method of finding such leaks with a multimeter is in section 10.8.

What was forgotten Extra current The battery lasts
a 40 kΩ pull-up on a pin held to ground 75 µA 101 days
NTC divider powered from the rail, not from PA2 150 µA 55 days
"I'll blink the LED once a minute" — and it stayed on 1 mA 9 days
debugger: DBG_* bits left over after debugging ≈0.6 mA 15 days
a 100 µF electrolytic instead of ceramic ≈3 µA 482 days

Notes on the lines:


198а.8 What to improve


198а.9 Pitfalls

The device lives two weeks instead of a year and a half. After debugging, DBG_* bits were left in DBGMCU->CR: the chip keeps being clocked in "Stop", the current is 0.6 mA instead of 15 µA. Even the reset button does not clear them — only cycling the power (section 10.8).

Sleep current is 90 µA, although everything is off in the program. One forgotten pull-up on a pin held low costs 75 µA. Unused pins go to analog mode, and to measure honestly use the method of 10.8: the multimeter in the chip's supply line, not the whole board's.

The device does not answer UART commands. In Stop the receiver is deaf: there is no clock, and a start bit is not a wakeup source (section 10.6). Wake it with a button, as here, or take a chip with an LPUART that can wake up on a start bit.

Near the end of the battery the device reboots — once a day, then more often. This is the log page erase: up to 5 mA for tens of milliseconds. On the internal resistance of a drained coin cell — hundreds of ohms — such a current drops a volt: 5 mA × 200 Ω = 1.0 V, and the supply dives below the reset threshold. Use a larger capacitor, and erase only while the voltage is healthy.

The temperature reads consistently low by a degree or two. The capacitor at the ADC input is larger than calculated: the divider node does not have time to charge during the pause after PA2 is switched on. With 10 nF the time constant is 50 µs and a millisecond is enough; with the 100 nF from section 196 it is not. Either wait longer or use a smaller capacitance.

After a battery change the log is intact, but the time starts from zero. That is by design: the log is in Flash, while the RTC counter ran from the same battery and was reset together with it. The binding to the calendar is done on the computer anyway at the moment of the dump — the next read simply starts a new epoch.


198а.10 Summary

Full source code

batt.h — the interface of the device logic and the platform functions.

/* Мини-проект «Датчик на батарейке»: логика прибора без железа.
 * Измерение, накопление и кольцевой журнал во Flash не знают про регистры:
 * всё железное — в функциях платформы (plat_*), которые на BluePill написаны
 * поверх регистров, а на компьютере — поверх модели Flash и термометра. */
#ifndef BATT_H
#define BATT_H

#include <stdint.h>

#define LOG_PAGES      8u                              /* журнал: 8 страниц по 1 КБ в конце Flash */
#define LOG_PAGE_BYTES 1024u
#define LOG_REC_BYTES  8u
#define LOG_RECS_PAGE  (LOG_PAGE_BYTES / LOG_REC_BYTES)   /* 128 записей на страницу */
#define LOG_RECS       (LOG_PAGES * LOG_RECS_PAGE)        /* 1024 записи в кольце */

#define BATT_PERIOD_S  60u                             /* будильник RTC: раз в минуту */
#define BATT_PER_REC   10u                             /* запись в журнал: раз в 10 измерений */

#define REC_EMPTY_T    0xFFFFFFFFu                     /* время в стёртой ячейке */

struct batt_rec {                                      /* 8 байт во Flash, без выравнивающих дыр */
    uint32_t t;                                        /* время RTC, секунды; 0xFFFFFFFF — пусто */
    int16_t  t_c10;                                    /* температура, десятые доли °C, среднее за 10 минут */
    uint8_t  vbat;                                     /* (напряжение в мВ − 1000) / 10: 1,00…3,55 В */
    uint8_t  crc;                                      /* CRC-8, полином 0x07, по первым 7 байтам */
};

struct batt {
    uint32_t rec_idx;                                  /* куда ляжет следующая запись, 0…LOG_RECS−1 */
    uint32_t n_acc;                                    /* сколько поминутных измерений накоплено */
    int32_t  sum_c10;                                  /* их сумма */
    uint16_t last_vdd_mv;                              /* последнее напряжение батарейки */
    int16_t  last_c10;                                 /* последняя температура */
};

/* платформа */
int16_t  plat_temp_c10(void);                          /* подать питание на делитель, измерить, снять */
uint16_t plat_vdd_mv(void);                            /* напряжение питания по внутренней опоре */
const uint8_t *plat_log_page(uint32_t page);           /* страница журнала для чтения */
void plat_log_erase(uint32_t page);
void plat_log_write(uint32_t page, uint32_t off, const void *src, uint32_t n);  /* n кратно 2 */
void plat_puts(const char *s);

/* логика */
uint8_t  batt_crc8(const void *p, uint32_t n);
void     batt_pack(struct batt_rec *r, uint32_t t, int16_t c10, uint16_t vdd_mv);
int      batt_rec_ok(const struct batt_rec *r);
void     batt_rec_str(const struct batt_rec *r, char *out);   /* «t=… T=… U=…», без printf */
void     batt_init(struct batt *b);                    /* найти хвост кольца после сброса */
void     batt_minute(struct batt *b, uint32_t now);    /* вызов по будильнику, раз в минуту */
uint32_t batt_dump(const struct batt *b);              /* журнал в консоль от старой записи к новой */

#endif

batt.c — logic: measurement, accumulation, the ring log in Flash and the dump to the console.

/* Мини-проект «Датчик на батарейке»: логика — измерение раз в минуту,
 * запись раз в десять минут, кольцевой журнал во Flash, дамп в консоль.
 * Ни одного регистра: собирается и для Cortex-M3, и для компьютера. */
#include "batt.h"

/* CRC-8, полином 0x07, начальное значение 0: от строки «123456789» даёт 0xF4 */
uint8_t batt_crc8(const void *p, uint32_t n)
{
    const uint8_t *b = (const uint8_t *)p;
    uint8_t c = 0;
    while (n--) {
        c ^= *b++;
        for (int i = 0; i < 8; i++)
            c = (uint8_t)((c & 0x80u) ? (unsigned)(c << 1) ^ 0x07u : (unsigned)(c << 1));
    }
    return c;
}

void batt_pack(struct batt_rec *r, uint32_t t, int16_t c10, uint16_t vdd_mv)
{
    r->t = t;
    r->t_c10 = c10;
    uint32_t mv = vdd_mv < 1000u ? 1000u : vdd_mv;     /* шкала 1,00…3,55 В шагом 10 мВ */
    uint32_t v = (mv - 1000u) / 10u;
    r->vbat = v > 255u ? 255u : (uint8_t)v;
    r->crc = batt_crc8(r, 7);
}

int batt_rec_ok(const struct batt_rec *r)
{
    return r->t != REC_EMPTY_T && batt_crc8(r, 7) == r->crc;
}

/* ── печать без printf: прошивка собирается с -nostdlib ── */
static char *put_u32(char *s, uint32_t v)
{
    char tmp[10];
    int n = 0;
    do {
        tmp[n++] = (char)('0' + v % 10u);
        v /= 10u;
    } while (v);
    while (n)
        *s++ = tmp[--n];
    return s;
}

void batt_rec_str(const struct batt_rec *r, char *out)   /* out не меньше 40 байт */
{
    char *s = out;
    *s++ = 't'; *s++ = '=';
    s = put_u32(s, r->t);
    *s++ = ' '; *s++ = 'T'; *s++ = '=';
    int32_t c = r->t_c10;
    if (c < 0) {
        *s++ = '-';
        c = -c;
    }
    s = put_u32(s, (uint32_t)c / 10u);
    *s++ = '.';
    *s++ = (char)('0' + (uint32_t)c % 10u);
    *s++ = ' '; *s++ = 'U'; *s++ = '=';
    uint32_t mv = 1000u + (uint32_t)r->vbat * 10u;
    s = put_u32(s, mv / 1000u);
    *s++ = '.';
    *s++ = (char)('0' + mv / 100u % 10u);
    *s++ = (char)('0' + mv / 10u % 10u);
    *s++ = '\n';
    *s = 0;
}

/* ── кольцо ── */
static const struct batt_rec *rec_at(uint32_t idx)
{
    const uint8_t *pg = plat_log_page(idx / LOG_RECS_PAGE);
    return (const struct batt_rec *)(const void *)(pg + (idx % LOG_RECS_PAGE) * LOG_REC_BYTES);
}

static int rec_blank(const struct batt_rec *r)
{
    const uint8_t *b = (const uint8_t *)(const void *)r;
    for (unsigned i = 0; i < LOG_REC_BYTES; i++)
        if (b[i] != 0xFFu)
            return 0;
    return 1;
}

/* Хвост кольца: следующая ячейка после записи с наибольшим временем.
 * Ячейка занята мусором (оборванная запись) — шагнуть дальше: во Flash
 * нельзя писать поверх не стёртого, нуль обратно в единицу не превратить. */
void batt_init(struct batt *b)
{
    uint32_t best = REC_EMPTY_T, tail = 0;
    int found = 0;
    for (uint32_t i = 0; i < LOG_RECS; i++) {
        const struct batt_rec *r = rec_at(i);
        if (batt_rec_ok(r) && (!found || r->t > best)) {
            best = r->t;
            tail = (i + 1u) % LOG_RECS;
            found = 1;
        }
    }
    if (found)
        while (tail % LOG_RECS_PAGE != 0u && !rec_blank(rec_at(tail)))
            tail = (tail + 1u) % LOG_RECS;             /* начало страницы решит стирание */
    b->rec_idx = found ? tail : 0;
    b->n_acc = 0;
    b->sum_c10 = 0;
    b->last_vdd_mv = 0;
    b->last_c10 = 0;
}

void batt_minute(struct batt *b, uint32_t now)
{
    b->last_c10 = plat_temp_c10();
    b->last_vdd_mv = plat_vdd_mv();
    b->sum_c10 += b->last_c10;
    b->n_acc++;
    if (b->n_acc < BATT_PER_REC)
        return;
    struct batt_rec r;
    batt_pack(&r, now, (int16_t)(b->sum_c10 / (int32_t)b->n_acc), b->last_vdd_mv);
    b->sum_c10 = 0;
    b->n_acc = 0;
    uint32_t page = b->rec_idx / LOG_RECS_PAGE;
    uint32_t off = b->rec_idx % LOG_RECS_PAGE * LOG_REC_BYTES;
    if (off == 0u)
        plat_log_erase(page);                          /* вошли в страницу — стереть её целиком */
    plat_log_write(page, off, &r, sizeof r);
    b->rec_idx = (b->rec_idx + 1u) % LOG_RECS;
}

uint32_t batt_dump(const struct batt *b)
{
    char line[40];
    uint32_t n = 0;
    for (uint32_t i = 0; i < LOG_RECS; i++) {
        const struct batt_rec *r = rec_at((b->rec_idx + i) % LOG_RECS);
        if (!batt_rec_ok(r))
            continue;                                  /* пустые и оборванные — молча мимо */
        batt_rec_str(r, line);
        plat_puts(line);
        n++;
    }
    return n;
}

plat_f103.c — the BluePill platform on CMSIS registers.

/* Мини-проект «Датчик на батарейке»: платформа BluePill (STM32F103C8), регистры через CMSIS.
 *   Питание — CR2032 прямо на шину 3,3 В платы, мимо штатного стабилизатора.
 *   PA0  — кнопка «прочитать журнал»: к 3,3 В, внутренняя подтяжка вниз, событие EXTI0
 *   PA1  — АЦП1, канал 1: средняя точка делителя NTC 10 кОм / 10 кОм (таблица — раздел 196)
 *   PA2  — питание делителя: высокий уровень только на время измерения
 *   PA9/PA10 — USART1, 115 200 бод: журнал по кнопке
 *   PC14/PC15 — кварц 32 768 Гц, RTC и будильник (раздел 198)
 *   Flash 0x0800E000…0x0800FFFF — кольцевой журнал, 8 страниц по 1 КБ
 * Такт — HSI 8 МГц: после выхода из Stop система и так на HSI, восстанавливать нечего. */
#include "stm32f10x.h"
#include "batt.h"

#define LOG_BASE 0x0800E000u
#define MAGIC 0xB007u                                  /* в BKP_DR1: RTC уже настроен */

/* ── задержка: SysTick без прерываний ── */
static void delay_ms(uint32_t n)
{
    SysTick->LOAD = 8000u - 1u;                        /* 8 МГц → 1 мс */
    SysTick->VAL = 0;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;
    while (n--)
        while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)) {
        }
    SysTick->CTRL = 0;
}

/* ── АЦП: включается на одно пробуждение ── */
static void adc_on(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    ADC1->SMPR2 = 7u << 3;                             /* канал 1: 239,5 такта выборки */
    ADC1->SMPR1 = 7u << 21;                            /* канал 17 (VREFINT): тоже 239,5 — нужно ≥ 17,1 мкс */
    ADC1->CR2 = ADC_CR2_ADON | ADC_CR2_TSVREFE;        /* включить АЦП и внутреннюю опору */
    for (volatile int d = 0; d < 100; d++) {
    }
    ADC1->CR2 |= ADC_CR2_CAL;                          /* калибровка после каждого включения */
    while (ADC1->CR2 & ADC_CR2_CAL) {
    }
}

static void adc_off(void)
{
    ADC1->CR2 = 0;                                     /* ADON = 0: аналоговая часть обесточена */
    RCC->APB2ENR &= ~RCC_APB2ENR_ADC1EN;
}

static uint16_t adc_once(uint32_t ch)
{
    ADC1->SQR3 = ch;
    ADC1->CR2 |= ADC_CR2_ADON;                         /* повторная запись ADON запускает преобразование */
    while (!(ADC1->SR & ADC_SR_EOC)) {
    }
    return (uint16_t)ADC1->DR;
}

/* таблица NTC 10 кОм (B = 3950) — посчитана в разделе 196 */
static const int16_t T_C[17] = {-200, -150, -100, -50, 0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600};
static const uint16_t CODE[17] = {355, 466, 600, 758, 939, 1140, 1357, 1585, 1817, 2048, 2270, 2481, 2676, 2854, 3014, 3155, 3280};

int16_t plat_temp_c10(void)
{
    GPIOA->BSRR = GPIO_BSRR_BS2;                       /* подать питание на делитель */
    delay_ms(1);                                       /* установление: τ = 50 мкс при C = 10 нФ */
    uint32_t sum = 0;
    for (int i = 0; i < 16; i++)
        sum += adc_once(1);
    GPIOA->BSRR = GPIO_BSRR_BR2;                       /* снять питание: делитель не ест в сне */
    uint32_t c = sum / 16;
    if (c < CODE[0])
        return T_C[0];                                 /* упор в край шкалы — виден в журнале */
    if (c > CODE[16])
        return T_C[16];
    int i = 0;
    while (c > CODE[i + 1])
        i++;
    return (int16_t)(T_C[i] + (int32_t)(T_C[i + 1] - T_C[i]) * (int32_t)(c - CODE[i]) / (CODE[i + 1] - CODE[i]));
}

uint16_t plat_vdd_mv(void)
{
    uint32_t sum = 0;
    for (int i = 0; i < 4; i++)
        sum += adc_once(17);                           /* VREFINT ≈ 1,20 В относительно VDDA */
    uint32_t code = sum / 4;
    return (uint16_t)(1200u * 4096u / (code ? code : 1u));
}

/* ── журнал во Flash: стирание страницы и запись полусловами (раздел 11.5) ── */
static void flash_unlock(void)
{
    FLASH->KEYR = 0x45670123u;
    FLASH->KEYR = 0xCDEF89ABu;
}

const uint8_t *plat_log_page(uint32_t page)
{
    return (const uint8_t *)(LOG_BASE + page * LOG_PAGE_BYTES);
}

void plat_log_erase(uint32_t page)
{
    flash_unlock();
    FLASH->CR |= FLASH_CR_PER;
    FLASH->AR = LOG_BASE + page * LOG_PAGE_BYTES;
    FLASH->CR |= FLASH_CR_STRT;
    while (FLASH->SR & FLASH_SR_BSY) {
    }
    FLASH->CR &= ~FLASH_CR_PER;
    FLASH->CR |= FLASH_CR_LOCK;
}

void plat_log_write(uint32_t page, uint32_t off, const void *src, uint32_t n)
{
    flash_unlock();
    FLASH->CR |= FLASH_CR_PG;
    const uint16_t *s = (const uint16_t *)src;
    volatile uint16_t *dst = (volatile uint16_t *)(LOG_BASE + page * LOG_PAGE_BYTES + off);
    for (uint32_t i = 0; i < n / 2u; i++) {
        dst[i] = s[i];
        while (FLASH->SR & FLASH_SR_BSY) {
        }
    }
    FLASH->CR &= ~FLASH_CR_PG;
    FLASH->CR |= FLASH_CR_LOCK;
}

void plat_puts(const char *s)
{
    for (; *s; s++) {
        if (*s == '\n') {
            while (!(USART1->SR & USART_SR_TXE)) {
            }
            USART1->DR = '\r';
        }
        while (!(USART1->SR & USART_SR_TXE)) {
        }
        USART1->DR = (uint8_t)*s;
    }
    while (!(USART1->SR & USART_SR_TC)) {              /* дожать последний байт до сна */
    }
}

/* ── RTC: как в разделе 198 — запись через RTOFF и CNF ── */
static void rtc_wait(void)
{
    while (!(RTC->CRL & RTC_CRL_RTOFF)) {
    }
}

static uint32_t rtc_now(void)
{
    uint16_t hi, lo;
    do {
        hi = RTC->CNTH;
        lo = RTC->CNTL;
    } while (hi != RTC->CNTH);
    return (uint32_t)hi << 16 | lo;
}

static void rtc_write(volatile uint16_t *rh, volatile uint16_t *rl, uint32_t v)
{
    rtc_wait();
    RTC->CRL |= RTC_CRL_CNF;
    *rh = (uint16_t)(v >> 16);
    *rl = (uint16_t)v;
    RTC->CRL &= (uint16_t)~RTC_CRL_CNF;
    rtc_wait();
}

static void rtc_init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_PWREN | RCC_APB1ENR_BKPEN;
    PWR->CR |= PWR_CR_DBP;                             /* запись в домен резервного питания */
    if (BKP->DR1 != MAGIC) {
        RCC->BDCR |= RCC_BDCR_BDRST;
        RCC->BDCR &= ~RCC_BDCR_BDRST;
        RCC->BDCR |= RCC_BDCR_LSEON;
        while (!(RCC->BDCR & RCC_BDCR_LSERDY)) {
        }
        RCC->BDCR |= RCC_BDCR_RTCSEL_LSE | RCC_BDCR_RTCEN;
        rtc_write(&RTC->PRLH, &RTC->PRLL, 32767u);     /* 32 768 / (32 767 + 1) = 1 Гц */
        rtc_write(&RTC->CNTH, &RTC->CNTL, 0u);
        BKP->DR1 = MAGIC;
    } else {
        RTC->CRL &= (uint16_t)~RTC_CRL_RSF;
        while (!(RTC->CRL & RTC_CRL_RSF)) {
        }
    }
}

/* ── Stop: регулятор в экономичный режим, будят события EXTI 17 (будильник) и 0 (кнопка) ── */
static void enter_stop(void)
{
    EXTI->RTSR |= (1u << 17) | (1u << 0);
    EXTI->EMR |= (1u << 17) | (1u << 0);               /* события: обработчики прерываний не нужны */
    PWR->CR = (PWR->CR & ~PWR_CR_PDDS) | PWR_CR_LPDS | PWR_CR_CWUF;
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __SEV();
    __WFE();                                           /* снять взведённый флажок события */
    __WFE();                                           /* уснуть по-настоящему */
    SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
    /* система снова на HSI 8 МГц — мы с него и не уходили */
}

static void hw_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_USART1EN;
    /* PA0 — вход с подтяжкой вниз (1000), PA1 — аналог (0000), PA2 — выход 2 МГц (0010),
     * PA3…PA7 — аналог: висящий вход с триггером Шмитта ест ток (раздел 10.8) */
    GPIOA->CRL = 0x00000208u;
    GPIOA->ODR = 0;                                    /* подтяжка PA0 — вниз */
    /* PA9 — альтернативный выход 2 МГц (1010), PA10 — вход (0100); PA8, PA11, PA12, PA15 — аналог;
     * PA13/PA14 (SWD) — не трогать, иначе отладчик теряет камень */
    GPIOA->CRH = (GPIOA->CRH & ~(0xFu | 0xFF0u | 0xF000u | 0xF0000u | 0xF0000000u)) | 0x4A0u;
    GPIOB->CRL = 0;
    GPIOB->CRH = 0;
    GPIOC->CRL = 0;
    GPIOC->CRH &= 0xFF000000u;                         /* PC13 в аналог; PC14/PC15 — кварц, не трогать */
    USART1->BRR = 69;                                  /* 8 МГц / 69 = 115 942 бод */
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE;         /* приёмник не нужен: в Stop он всё равно глух */
}

int main(void)
{
    static struct batt b;
    hw_init();
    rtc_init();
    batt_init(&b);
    rtc_write(&RTC->ALRH, &RTC->ALRL, rtc_now() + BATT_PERIOD_S);
    for (;;) {
        if (RTC->CRL & RTC_CRL_ALRF) {                 /* будильник: минутное измерение */
            RTC->CRL &= (uint16_t)~RTC_CRL_ALRF;
            uint32_t now = rtc_now();
            adc_on();
            batt_minute(&b, now);
            adc_off();
            rtc_write(&RTC->ALRH, &RTC->ALRL, now + BATT_PERIOD_S);
        }
        if (GPIOA->IDR & 1u) {                         /* кнопка: журнал в консоль */
            batt_dump(&b);
            while (GPIOA->IDR & 1u) {
            }
        }
        enter_stop();                                  /* ложное пробуждение безвредно: уснём снова */
    }
}

Next: section 198б "Mini-project: a USB data logger and a PC program".

Open in the interactive handbook