Батарейный трекер в сарае умер молча: приехал весной — на экране ноль, аккумулятор 18650 сел под отсечку за зиму. Цифра текущего напряжения, которую он показывал, ничего не рассказала: когда начался спад, как быстро падал, мороз это был или саморазряд. С тех пор мой батарейный монитор — не цифра, а цифра плюс график за окно и честный статус «полный / норм / низко».
Собираем такой монитор на OLED: делитель 100к/10к на вход A0, пересчёт чтения в милливольты, пороги 3.3 и 3.0 В для Li-ion, кольцевой буфер на 32 точки и график линиями. Кнопка на D2 выводит страницу минимума и максимума за окно — она отвечает на главный вопрос «сколько просело за ночь» без ноутбука и без SD-карты.
Потенциометр в макетке изображает банку: крутишь ручку — напряжение садится и растёт, все пороги и статусы отрабатывают на глаз; чтение A0 — то же, что в уроке про потенциометр. Механику кольца и полилинии я подробно разбирал в уроке про самописец на 64 точки — здесь буфер вдвое короче, а всё остальное: делитель, пороги, статусы — специфика именно батареи.
Делитель 100к/10к: считаем обратно в вольты
Резисторы делят напряжение банки в одиннадцать раз: сверху 100 кОм, снизу 10 кОм, средняя точка — на A0. Обратно умножаем на 11: чтение АЦП переводится в милливольты входа (умножить на 5000 и разделить на 1023 — шаг АЦП выходит 4,9 мВ), а результат × 11 даёт напряжение банки. Шаг по банке выходит 54 мВ; пороги разнесены на 300 мВ — пять с половиной шагов, и даже с шумом в пару единиц АЦП статус не пляшет.
Зачем делитель, если 4,2 В влезают в диапазон АЦП напрямую? У моего прибора плата питается от той же банки через повышайку до 5 В, а заряжается от солнечной панели с рыхлым контроллером: выбросы на шине доходят до шести вольт, и подавать такое прямо на вход АЦП я не готов. Делитель заодно готовит вход к двум банкам — их 8,4 В делятся до 0,76 В. Цена решения — утечка: через 110 кОм течёт 38 мкА, за месяц около 27 мА·ч; для 18650 на 2500 мА·ч это мизер, но для батарейки-таблетки уже заметно.
| Напряжение банки | Статус | Что делать |
|---|---|---|
| 3.3 В и выше | POLN | заряд свежий — работа в обычном ритме |
| 3.0-3.3 В | NORM | рабочая зона Li-ion, следить чаще |
| ниже 3.0 В | NIZKO | пора на зарядку, ёмкость на исходе |
| около 2.5 В | отсечка | глубокий разряд: банка деградирует |
Буфер — кольцо на 32 ячейки и указатель записи: buf[head] = v; head = (head + 1) % N. Перезаписывается всегда самое старое значение, сдвигать массив циклом не нужно. Выборка раз в 500 мс даёт окно в 16 секунд — годится для отладки на столе; для зимнего сарая я ставлю раз в 10 минут — окно растягивается на пять с лишним часов, и ночь видна целиком. Период выбирается под вопрос, на который отвечает график: переходные процессы — сотни миллисекунд, саморазряд — часы.
Рисуется график полилинией: между соседними точками drawLine, шаг по горизонтали 4 пикселя, 32 точки закрывают всю ширину экрана. Под графиком — две горизонтальные черты-порога: 3.3 и 3.0 В переводятся в ту же шкалу, что и точки, и рисуются drawLine через весь экран — глаз сразу видит, где линия коснулась порога. Про map в высоту зоны и оговорку про vmin, равный vmax, — в уроке про примитивы; здесь те же строки кода, смасштабированные под зону высотой 16 пикселей.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1);const int BTN = 2;const int N = 32; // 32 точки с шагом 4 px - вся ширина int buf[32];int head = 0;int n = 0; // сколько точек собраноlong lastSample = 0;long lastDraw = 0;long minMaxUntil = 0;int prevBtn = HIGH; long chitajBat() { long pin = (long)analogRead(A0) * 5000 / 1023; // мВ на входе АЦП return pin * 11 / 1000; // делитель 100к/10к: x11} String statusOf(long u) { if (u >= 3300) return "POLN"; if (u >= 3000) return "NORM"; return "NIZKO";} long extrem(bool malyi) { long m = 0; if (malyi) m = 9999; for (int i = 0; i < n; i++) { int v = buf[i]; if (malyi && v < m) m = v; if (!malyi && v > m) m = v; } return m;} void risuj() { long u = chitajBat(); display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); display.print("BATAREYA"); if (millis() < minMaxUntil) { // страница min/max на три секунды display.setCursor(0, 20); display.print("MIN " + String(extrem(true)) + " mV"); display.setCursor(0, 34); display.print("MAX " + String(extrem(false)) + " mV"); } else { display.setTextSize(2); display.setCursor(0, 16); String ustroki = String(u / 1000) + "."; int sotye = (u % 1000) / 10; if (sotye < 10) ustroki += "0"; ustroki += String(sotye) + " V"; display.print(ustroki); display.setTextSize(1); display.setCursor(0, 36); display.print("STATUS: " + statusOf(u)); // график: полилиния по кольцу и черты порогов, зона y=48..62 int y33 = map(3300, 2600, 4400, 62, 48); int y30 = map(3000, 2600, 4400, 62, 48); display.drawLine(0, y33, 127, y33, SSD1306_WHITE); display.drawLine(0, y30, 127, y30, SSD1306_WHITE); for (int i = 0; i < n - 1; i++) { long v1 = buf[(head + i) % N]; long v2 = buf[(head + i + 1) % N]; int y1 = map(v1, 2600, 4400, 62, 48); int y2 = map(v2, 2600, 4400, 62, 48); display.drawLine(i * 4, y1, i * 4 + 4, y2, SSD1306_WHITE); } } display.display();} void setup() { Serial.begin(9600); pinMode(BTN, INPUT_PULLUP); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE);} void loop() { if (millis() - lastSample >= 500) { // окно 32 точки = 16 секунд lastSample = millis(); buf[head] = chitajBat(); head = (head + 1) % N; if (n < N) n++; long u = buf[(head + N - 1) % N]; Serial.print("u=" + String(u) + " st=" + statusOf(u)); Serial.println(); } int btn = digitalRead(BTN); if (btn == LOW && prevBtn == HIGH) minMaxUntil = millis() + 3000; prevBtn = btn; if (millis() - lastDraw >= 250) { // перерисовка не чаще 4 раз в секунду lastDraw = millis(); risuj(); }}Статус без дёрганья: текст, гистерезис, кнопка
На монохромном OLED цвета статуса нет — красным не помигаешь, поэтому статус живёт текстом: POLN, NORM, NIZKO рядом с напряжением. Слово выбирается теми же порогами, что и черты на графике, и в этом главный приём прибора: цифру 3.42 нужно читать, а слово POLN видно с прохода. На цветном экране те же три зоны красятся зелёным, жёлтым и красным — логика порогов не меняется ни на бит.
Гистерезис, на котором стоит статус: смена слова происходит не ровно на пороге, а с запасом 50 мВ — вниз при 3250, вверх при 3350. Без запаса банка на границе 3.3 В даёт чтения туда-сюда в пределах шума АЦП, и статус мигает NORM-POLN каждый такт. Кнопка — по фронту, как в уроке про кнопку; страница min/max держится три секунды и гаснет сама: у прибора одна кнопка, и удерживать ничего не нужно.
Минимум за окно — главный индикатор зимы: просадка ниже 3.0 В на морозе означает, что ёмкость упала и банка скоро уйдёт под отсечку; максимум сразу после солнечного полудня показывает, до чего дотянул контроллер. Пара min/max по кнопке заменяет логгер для девяноста процентов вопросов, а полный лог на SD ставится, когда вопрос уже нашёлся. Порог NIZKO удобно вешать на зуммер — прибор сам попросит зарядки, не дожидаясь вашего визита.
Монитор встраивается в любой прибор за десять строк: буфер, функция пересчёта, зона графика. В симуляторе ниже потяните ползунок вниз — следите, как статус пройдёт все три слова и как столбиковый график съедет влево; нажмите кнопку — страница min/max подхватит крайние значения за собранное к этому моменту окно.
- Делитель 100к/10к делит банку в 11 раз: шаг АЦП 4,9 мВ превращается в 54 мВ по банке — пороги с разносом 300 мВ держат пять с половиной шагов запаса
- Статус на монохроме — текст POLN/NORM/NIZKO с гистерезисом 50 мВ: без запаса шум АЦП мигает словом на границе порога
- Кольцо на 32 точки: buf[head] = v; head = (head + 1) % N — перезаписывается самое старое; длину окна выбирает период выборки, а не размер буфера
- График — drawLine между соседними точками с шагом 4 px; черты порогов 3.3 и 3.0 В рисуются в той же шкале
- Кнопка по фронту показывает min/max за окно три секунды — просадка за ночь видна без ноутбука и логгера
- 1. Чему равен шаг измерения по банке при делителе 100к/10к?
- 2. Зачем статусу гистерезис 50 мВ?
- 3. Кольцо на 32 точки с выборкой раз в 10 минут даёт окно…
- 4. Что показывает страница MIN/MAX, которую вызывает кнопка?
- 5. Сколько держится страница min/max после нажатия?
Частые вопросы по теме урока
- Чему равен шаг измерения по банке при делителе 100к/10к?
- АЦП Uno шагает по 4,9 мВ на входе; делитель умножает это на 11 — по банке выходит 54 мВ. Между порогами 3.3 и 3.0 В — 300 мВ, то есть пять с половиной шагов. 54 мВ: шаг АЦП × 11
- Зачем статусу гистерезис 50 мВ?
- У границы порога чтения гуляют туда-сюда в пределах шума, и жёсткое сравнение дёргает статус на каждой выборке. Запас 50 мВ держит слово неподвижным. чтобы шум АЦП у границы не мигал словом каждый такт
- Кольцо на 32 точки с выборкой раз в 10 минут даёт окно…
- 32 × 10 минут = 320 минут — пять часов двадцать. Период выборки, а не размер буфера, выбирает, что вы увидите: переходы — секунды, саморазряд — часы. 5 часов 20 минут
- Что показывает страница MIN/MAX, которую вызывает кнопка?
- Минимум и максимум сканируются по кольцу за один проход — 32 ячейки, мгновенно. Это ответ на вопрос «сколько просело за ночь» без логгера. крайние напряжения за собранное окно истории
- Сколько держится страница min/max после нажатия?
- Страница живёт по разности millis три секунды и гаснет сама: у прибора одна кнопка, и она не должна превращаться в выключатель страниц. три секунды, потом гаснет сама