198а. Мини-проект: датчик на батарейке — год на одной CR2032
Термостат из раздела 196 и часы из раздела 198 живут от розетки. Четвёртый прибор — термометр-регистратор, который год живёт от одной «таблетки» CR2032: раз в минуту просыпается по будильнику RTC, измеряет температуру, копит журнал во Flash и снова засыпает. Весь раздел 10 здесь превращается из теории в статью расхода: каждый микроампер — это дни жизни прибора.
Главное в проекте — не код, а бюджет энергии: посчитать средний ток до того, как паять, и знать, какая строка бюджета главная. Спойлер: не вспышки работы, а ток сна — и утечки, которых не видно на схеме.
Файлы проекта — _verify/batt198a/: batt.h, batt.c (логика и кольцевой
журнал), plat_f103.c (платформа BluePill), sim_host.c (модель Flash
и датчика для компьютера), budget.py (бюджет с проверкой размерностей).
Прошивка собрана для Cortex-M3, год работы прогнан на компьютере.
Сверяет _verify/batt198a_check.py.
Как это называется в документации
Оригинал Перевод coin cell, button cell дисковый элемент питания, «таблетка» nominal capacity (mA·h) номинальная ёмкость, миллиампер-часы continuous drain, standard load постоянный ток разряда, стандартная нагрузка pulse drain импульсный ток разряда end (cut-off) voltage конечное напряжение разряда internal resistance внутреннее сопротивление источника shelf life срок хранения элемента battery life срок службы от одной батарейки
198а.1 Что делает прибор
| Функция | Как |
|---|---|
| просыпаться раз в минуту | будильник RTC от кварца 32 768 Гц (раздел 198.5), событие EXTI 17 |
| измерять температуру | NTC 10 кОм в делителе, таблица из 17 точек — как в разделе 196.3 |
| не кормить делитель зря | верхнее плечо запитано от вывода PA2 только на время измерения |
| помнить год | журнал во Flash: кольцо из 8 страниц, запись раз в 10 минут |
| знать состояние батарейки | напряжение питания по внутренней опоре VREFINT, пишется в журнал |
| отдавать журнал | кнопка на PA0 будит камень, журнал уходит в USART1, 115 200 бод |
| спать всё остальное время | Stop, регулятор в экономичном режиме, ≈15,4 мкА вместе с RTC |
Батарейка подключена прямо к шине 3,3 В платы, мимо штатного стабилизатора: он и светодиод питания съели бы её за считаные дни (198а.7). Рабочий диапазон F103 — 2,0…3,6 В, так что CR2032 подходит без преобразователей: свежая даёт около 3 В, а конечное напряжение разряда «таблетки» — как раз 2,0 В.
⚠️ АЦП — только до 2,4 В. Питание камню разрешено до 2,0 В, но аналоговое питание с работающим АЦП нормировано от 2,4 В. Ниже прибор ещё живёт и пишет журнал, а точность измерений уже не обещана. Делитель при этом меряется верно дольше, чем кажется: он питается тем же напряжением, что и опора АЦП, и код от просевшего питания не зависит (логометрическое измерение, раздел 96.2).
198а.2 Бюджет: сначала посчитать, потом паять
Ёмкость CR2032 у известных производителей — типовые 220–235 мА·ч; в расчёте — консервативные 220. Честная оговорка: эти миллиампер-часы обещаны при разряде «ровным» током порядка 0,2 мА (стандартная нагрузка 15 кОм) до 2,0 В при комнатной температуре. Миллиамперные импульсы батарейке тяжелы: внутреннее сопротивление свежего элемента — десятки ом, к концу разряда — сотни, и вспышка тока просаживает напряжение.
⚠️ Вспышку отдаёт конденсатор, не батарейка. Керамический 100 мкФ у вывода питания: вспышка 4 мА × 5 мс возьмёт из него 0,2 В просадки (ΔU = I·t/C), а батарейка отдаст тот же заряд плавно, средним током. Без конденсатора та же вспышка на севшей батарейке с сотнями ом внутри уронит питание ниже сброса — прибор будет «умирать» при каждом измерении.
Все токи камня берутся не из головы, а из даташита STM32F103x8/B — таблицы «Supply current characteristics» (типовые значения при 25 °C):
| Режим | Ток | Примечание |
|---|---|---|
| Run, HSI 8 МГц | 4,0 мА | периферия выключена |
| Sleep, HSI 8 МГц | 0,6 мА | периферия выключена |
| Stop, регулятор основной | 24 мкА | выход 3,6 мкс |
| Stop, регулятор экономичный | 14 мкА | 13,5 мкА при 2,4 В; выход 5,4 мкс; при 85 °C — до 180 мкА |
| Standby | 2 мкА | ОЗУ и регистры теряются, выход 50 мкс через сброс |
| кварц LSE + RTC | 1,4 мкА | 1,1 мкА при 2,4 В |
| АЦП включён (ADON = 1) | +0,8 мА | аналоговая часть |
Выбор режима сна — по разделу 10.1. Standby в четыре раза экономнее Stop, но теряет ОЗУ: накопитель десяти минутных измерений и хвост кольца пришлось бы прятать в резервные регистры, а программа каждый раз стартовала бы с нуля. Для первой версии берём Stop с экономичным регулятором: 14 мкА + 1,4 мкА на RTC с кварцем = 15,4 мкА — и ОЗУ целое, и продолжение с той же строки. Выигрыш Standby посчитан в 198а.8.
198а.3 Цикл: проснулся → измерил → записал → уснул
Тактирование — HSI 8 МГц без ФАПЧ, как в разделе 196. Это не только
простота: после выхода из Stop система и так оказывается на HSI
(главная грабля раздела 10.3) — а мы с него и не уходили, восстанавливать
нечего. Вход в сон — без обработчиков прерываний: будильник и кнопка
заведены событиями EXTI, ядро будится парой __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 МГц — мы с него и не уходили */
}
Главный цикл — три вопроса после каждого пробуждения:
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(); /* ложное пробуждение безвредно: уснём снова */
}
Измерение — как в 196, с одним отличием: делитель питается от вывода PA2 и включён только на время замера, иначе его 150 мкА съедали бы батарейку в десять раз быстрее самого сна:
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; /* снять питание: делитель не ест в сне */
Конденсатор у входа АЦП уменьшен со 100 нФ (раздел 196) до 10 нФ: узел должен зарядиться за миллисекунду паузы, а шум добирается усреднением 16 выборок. Выходной ключ вывода по даташиту держит 0,4 В при 8 мА — меньше 50 Ом; на токе делителя это 7,5 мВ, доли процента шкалы.
Теперь арифметика вспышки. Типовая длительность — около 2,6 мс: выход из Stop 5,4 мкс, миллисекунда на делитель, 20 выборок АЦП по 63 мкс (1,26 мс), изредка запись в журнал. В бюджет закладываем 5 мс — двойной запас на будущий код. Заряд одной вспышки:
q = 4 мА · 5 мс + 0,8 мА · 1,26 мс ≈ 20 + 1 ≈ 21 мкКл
Средний ток при опросе раз в минуту — формула из раздела 10.8 плюс две строки, о которых забывают: стирание страницы журнала (5 мА до 40 мс — но раз в 128 записей, в среднем 0,003 мкА) и саморазряд самой батарейки (порядка процента в год — ещё 0,25 мкА):
I_ср = 15,4 + 21 / 60 + 0,25 ≈ 16,0 мкА → 220 000 мкА·ч / 16,0 ≈ 13 700 ч ≈ 1,6 года
Год — с полуторным запасом. И главный вывод бюджета — период опроса почти не важен:
| Опрос | Средний ток | Срок службы |
|---|---|---|
| раз в 10 с | 17,8 мкА | 1,41 года |
| раз в 60 с | 16,0 мкА | 1,57 года |
| раз в 10 мин | 15,7 мкА | 1,60 года |
Вспышки дают лишь 0,35 мкА из 16 — два процента расхода. Это зеркальное отражение примера из раздела 10.8, где всплеск съедал 72 %: там прибор спал при 20 мкА, но просыпался каждую секунду. У нашего прибора правит ток сна — и потому охота в 198а.7 идёт именно на утечки.
198а.4 Журнал во Flash: кольцо с износом по кругу
Запись — 8 байт, без выравнивающих дыр, времена — счётчик секунд RTC (раздел 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 байтам */
};
Поминутные измерения копятся в ОЗУ, в журнал раз в 10 минут уходит среднее. Под журнал отданы последние 8 страниц Flash (0x0800E000, в скрипте компоновщика у прошивки осталось 56 КБ) — 8 × 128 = 1024 записи, история на 6,2–7,1 суток: страница, в которую пишем, стёрта целиком, поэтому хвост истории «дышит». Кольцо — тот же журнал записей, что для настроек в разделе 11.8, только целыми страницами:
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;
Стирание и запись полусловами — рецепт раздела 11.5; куда класть калибровочные коэффициенты, если появятся, — раздел 202.5. После сброса хвост кольца ищется заново: самая свежая корректная запись — по наибольшему времени, а если следующая ячейка занята мусором оборванной записи, хвост шагает дальше: во Flash нельзя писать поверх не стёртого.
Износ по кругу — арифметика на салфетке, проверенная прогоном: за год 52 560 записей — 411 стираний на все 8 страниц, на каждую — не больше 52. Ресурс Flash у F103 — 10 000 циклов: журнал изнашивает её на полпроцента в год, хватит примерно на 190 лет. Хранение данных даташит обещает 30 лет — переживёт и батарейку, и прибор.
198а.5 Чтение журнала: кнопка и консоль
Приёмник UART в Stop глух: тактов нет, старт-бит никого не разбудит (у поздних серий это чинит LPUART, у F103 его нет). Поэтому журнал забирается кнопкой: PA0 к плюсу питания, внутренняя подтяжка вниз (30…50 кОм по даташиту), фронт — событие EXTI 0. Ток через подтяжку течёт только пока кнопка нажата.
Прибор не знает календаря — в журнале голые секунды его собственного счётчика:
t=31950940 T=24.8 U=2.60
Привязку к настоящему времени делает программа на компьютере в момент дампа: реальное время записи = сейчас − (t текущее − t записи). Полное кольцо, 1024 строки, уходит за 2,3 с — это 9,2 мКл, по цене десяти минут сна: жать кнопку хоть каждый день.
🔧 Напряжение батарейки — без делителя. Внешний делитель на измерение питания сам стал бы утечкой. Вместо него АЦП меряет внутреннюю опору VREFINT ≈ 1,20 В относительно питания: U = 1200 · 4096 / код, мВ. Разброс опоры 1,16…1,24 В даёт погрешность до ±0,1 В у 3 В — для вопроса «пора ли менять батарейку» хватает с запасом. В записи журнала напряжение занимает один байт: шкала 1,00…3,55 В шагом 10 мВ.
198а.6 Прогон на компьютере и сборка
QEMU режим Stop и токи не моделирует — на нём этот прибор не проверишь.
Поэтому раздел держится на двух опорах: числа — из даташита пакета,
логика — та же пара «логика + платформа», что в 196–198: batt.c
не знает про регистры и собирается на компьютере с моделью Flash
(стирание в 0xFF, запись только поверх стёртого, счётчик стираний
на страницу) и датчика (суточная синусоида с шумом, батарейка садится
с 3,05 до 2,60 В за год).
| Что | Результат |
|---|---|
| пробуждений за год | 525 600, записей в журнал — 52 560 |
| стираний страниц | 411, по страницам 51…52 — износ ровный |
| кольцо в конце года | 976 записей, 585 000 с истории (6,8 суток), разрывов времени — 0 |
| хвост после сброса | найден там же, где был |
| обрыв питания посреди записи | половинная запись отброшена по CRC, хвост шагнул за неё, дамп цел |
CRC-8 от 123456789 |
0xF4 — контрольное значение реализации |
| дамп после года | 976 строк, первая t=31950940 T=24.8 U=2.60 |
Прошивка собрана arm-none-eabi-gcc -Os для Cortex-M3 без предупреждений:
| Что | Байт |
|---|---|
| вся прошивка (Flash) | 1852 |
| ОЗУ (переменные) | 16 |
из них логика batt.c — код |
632 |
16 байт ОЗУ — это один struct batt: журнал живёт во Flash, буферы —
на стеке. Упаковка записи проверена на компьютере честно: и x86-64,
и Cortex-M — little-endian, полей с дырами в структуре нет, поэтому
байты во «Flash» модели и во Flash платы совпадают.
198а.7 Что съедает батарейку незаметно
Каждая строка — из бюджета budget.py; методика поиска таких утечек
мультиметром — раздел 10.8.
| Что забыли | Лишний ток | Батарейки хватит на |
|---|---|---|
| подтяжка 40 кОм на выводе, прижатом к земле | 75 мкА | 101 день |
| делитель NTC запитан от шины, а не от PA2 | 150 мкА | 55 дней |
| «раз в минуту мигну светодиодом» — а он остался гореть | 1 мА | 9 дней |
| отладчик: биты DBG_* остались после отладки | ≈0,6 мА | 15 дней |
| электролит 100 мкФ вместо керамики | ≈3 мкА | 482 дня |
Пояснения к строкам:
- Подтяжка. 30…50 кОм внутри камня: 3 В / 40 кОм = 75 мкА — впятеро больше всего тока сна. Неиспользуемые выводы — в аналоговый режим (рецепт в 10.8), наша подтяжка PA0 вниз не тратит ничего, пока кнопка отпущена.
- Светодиод. Даже скромный: (3 В − 2 В) / 1 кОм = 1 мА. У прибора на батарейке индикатор либо вспыхивает на миллисекунды, либо его нет.
- Отладчик. С битами
DBG_STOP/DBG_STANDBYкамень в «Stop» держит такты — ток порядка Sleep, 0,6 мА. Биты переживают кнопку сброса, снимает их только снятие питания (раздел 10.8). - Штатный стабилизатор платы и его светодиод питания — миллиамперы сами по себе: батарейка подключена мимо них.
- Конденсаторы. У алюминиевого электролита ток утечки оценивают как 0,01·C·U — для 100 мкФ на 3 В это 3 мкА, минус четверть срока службы. Керамика X7R течёт на порядки меньше.
198а.8 Что улучшить
- Standby вместо Stop (раздел 10.4): база 2 + 1,4 = 3,4 мкА, средний
ток 4,0 мкА — 6,3 года. Цена: накопитель и хвост кольца — в резервные
регистры BKP, каждый подъём — через сброс и
Reset_Handler, отладка неудобнее. Так делают приборы «на всю жизнь батарейки». - Не стирать страницу при севшей батарейке: свой же замер напряжения есть в каждой записи — при U ниже 2,2 В отложить стирание, журнал замрёт, но прибор доживёт до замены. Детектор PVD (раздел 10.7) сделает то же без опроса.
- Коррекция хода (раздел 198.4): кварц ±20 ppm уводит время на 1,7 с в сутки; для журнала за год это около десяти минут.
- Калибровка датчика в одной точке (разделы 202.3 и 202.5) — таблица NTC из 196.3 общая, а экземпляры терморезисторов разные.
- Цифровой датчик DS18B20 (раздел 96.4) вместо делителя: точнее и без аналоговых оговорок, но 750 мс преобразования — бюджет вспышки пересчитать заново.
198а.9 Грабли
Прибор живёт две недели вместо полутора лет. После отладки остались
биты DBG_* в DBGMCU->CR: камень в «Stop» продолжает тактоваться,
ток — 0,6 мА вместо 15 мкА. Их не снимает даже кнопка сброса — только
передёрнуть питание (раздел 10.8).
Ток сна 90 мкА, хотя в программе всё выключено. Одна забытая подтяжка на прижатом выводе — 75 мкА. Неиспользуемые выводы — в аналоговый режим, а замерить честно — по методике 10.8: мультиметр в разрыв питания камня, не всей платы.
Прибор не отвечает на команды по UART. В Stop приёмник глух: тактов нет, старт-бит — не источник пробуждения (раздел 10.6). Будить кнопкой, как здесь, или брать камень с LPUART, умеющим просыпаться от старт-бита.
Ближе к концу батарейки прибор перезагружается — раз в сутки, потом чаще. Это стирание страницы журнала: до 5 мА на десятки миллисекунд. На внутреннем сопротивлении севшей «таблетки» — сотни ом — такой ток роняет вольт: 5 мА × 200 Ом = 1,0 В, и питание ныряет под порог сброса. Конденсатор ёмче, стирание — только при бодром напряжении.
Температура стабильно занижена на градус-другой. Конденсатор у входа АЦП больше расчётного: узел делителя не успевает зарядиться за паузу после включения PA2. У 10 нФ постоянная времени 50 мкс и миллисекунды хватает; у 100 нФ из раздела 196 — уже нет. Либо ждать дольше, либо ёмкость меньше.
После смены батарейки журнал цел, а время — с нуля. Так и задумано: журнал — во Flash, а счётчик RTC жил от той же батарейки и обнулился вместе с ней. Привязка к календарю всё равно делается на компьютере в момент дампа — очередное чтение просто начнёт новую эпоху.
198а.10 Итоги
- Год на CR2032 — это бюджет, а не подвиг: сон 15,4 мкА (Stop 14 + RTC 1,4), вспышка 21 мкКл раз в минуту, саморазряд 0,25 мкА — средний ток 16,0 мкА, 220 мА·ч хватает на ≈13 700 ч — 1,6 года.
- Правит ток сна: опрос раз в 10 с или раз в 10 мин двигает срок службы лишь между 1,41 и 1,60 года — вспышки дают два процента расхода.
- Журнал: 8 страниц × 128 записей по 8 байт, история 6,2–7,1 суток; за год — 52 560 записей и не больше 52 стираний на страницу — 0,5 % ресурса в 10 000 циклов.
- Прогон года на компьютере: 525 600 пробуждений, 0 разрывов времени в кольце, оборванная запись отброшена по CRC-8 (контрольное 0xF4).
- Прошивка — 1852 байта Flash и 16 байт ОЗУ; логика без регистров — 632 байта.
- Утечки страшнее работы: одна подтяжка (75 мкА) превращает 1,6 года в 101 день, светодиод — в 9 дней.
Полные исходники
batt.h — интерфейс логики прибора и функций платформы.
/* Мини-проект «Датчик на батарейке»: логика прибора без железа.
* Измерение, накопление и кольцевой журнал во 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 — логика: измерение, накопление, кольцевой журнал во Flash и дамп в консоль.
/* Мини-проект «Датчик на батарейке»: логика — измерение раз в минуту,
* запись раз в десять минут, кольцевой журнал во 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 — платформа BluePill на регистрах CMSIS.
/* Мини-проект «Датчик на батарейке»: платформа 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(); /* ложное пробуждение безвредно: уснём снова */
}
}
Дальше: раздел 198б «Мини-проект: USB-самописец и программа на ПК».