Балконный метеодатчик из урока с энергобюджетом на 18650 живёт на банке 3400 мА·ч, и мне нужно знать, сколько вольт у неё осталось, не подходя с мультиметром. Классическая дорога — резисторный делитель на A0, как в вольтметре 0–30 В: два резистора, арифметика, готово. Но делитель висит на батарее постоянно: «мёртвые» 100 кОм сверху и снизу при 4 В тянут с себя 20 мкА круглосуточно — за полгода набегает заметная доля ёмкости, которую я планировал отдать датчику.
Дорог три. Первая — делитель, его цена уже посчитана. Вторая — внешний ADC, но это лишняя микросхема и лишний провод там, где задача решается ресурсами самого чипа. Третья — трюк, о котором редко пишут: внутри ATmega328P сидит образцовый источник 1,1 В (bandgap), который не зависит от питания. Обычный analogRead меряет сигнал против опоры; я разворачиваю схему: опорой делаю само VCC, а «сигналом» — bandgap. ADC возвращает число, обратно пропорциональное питанию, и VCC вычисляется одной формулой. Ноль деталей, ноль лишних микроампер.
В этом уроке: откуда берётся формула VCC = 1023 · 1,1 / raw, почему raw надо защищать от нуля, как прошить точную калибровочную константу в EEPROM с магическим числом и где поставить порог «пора менять батарею». Кратко пройду по трём режимам analogReference и по шуму канала bandgap — его показания я усредняю приёмом из урока про фильтр ADC.
Bandgap наоборот: опора и измеряемый сигнал меняются местами
Сначала обычная схема. В режиме INTERNAL опорой служат те самые 1,1 В, и ADC считает, какую долю опоры занимает вход: при входе 0,55 В raw около 512. В режиме DEFAULT опорой становится само питание VCC — и вот тут дверца: bandgap-источник подключён к мультиплексору ADC как внутренний канал (номер 1110 в ADMUX). Я держу опорой VCC и «измеряю» нечто, что всегда равно 1,1 В: raw = 1023 · 1,1 / VCC. Опора в формуле и в измерении — одна и та же величина, поэтому из raw она выводится назад: VCC = 1023 · 1,1 / raw. Свежая банка 3,9–4,2 В даёт raw в районе 268–288; при 3,0 В raw подрастает до 375. Чем ниже питание, тем больше raw.
Формула в целых числах: VCC_мВ = 1 125 300 / raw (это 1023 × 1100 мВ). Два места, где она ломается. Первое — raw = 0 или близко к нему: целочисленное деление на ноль на AVR возвращает мусор, поэтому всё, что меньше 40, я считаю сбоем замера и выкидываю. Второе — шум: канал bandgap шумит на пару-тройку единиц LSB, а на батарее 4 В один LSB — это почти 4 мВ скачка. Лечение — среднее из 8–16 замеров, как в уроке про фильтр ADC. Хочется быстрее — канал можно читать регистрами из урока про FastADC: механика та же, только окно замера короче.
Про analogReference — коротко. DEFAULT — опора VCC: показания плывут вместе с питанием, и для bandgap-трюка это не беда, а само условие работы. INTERNAL — опора 1,1 В: так меряют сигналы до 1,1 В, например ток через шунт. EXTERNAL — опора на пине AREF: точность ровно такая, как у источника, который ты на AREF подал. После каждого переключения опоры ADC нужны 1–2 холостых замера и пара миллисекунд паузы: первые показания после смены режима врут, потому что внутренняя ёмкость выборки ещё не зарядилась от новой опоры.
// Bandgap-вольтметр: VCC в милливольтах без делителя и мультиметра.// ADMUX и ADC движок симулятора не исполняет - скетч для Arduino IDE.#include <EEPROM.h> const int PIN_LED = 8; // маячок "пора менять батарею"const long MAGIC = 20260214; // магия калибровки из EEPROM-урокаlong kalibr = 1125300UL; // 1023 x 1100 мВ; уточняется при пуске long chitatVcc() { analogReference(DEFAULT); // опора = VCC: меряем bandgap "наоборот" ADMUX = 0b01001110; // REFS0=1 (опора VCC), канал 1110 = bandgap delay(5); // опоре нужен такт на стабилизацию bitSet(ADCSRA, ADSC); // старт преобразования while (bitRead(ADCSRA, ADSC)) { } // ждём флаг готовности long raw = ADC; // 0..1023, тем больше, чем ниже VCC return raw < 40 ? -1 : kalibr / raw; // -1 = замер сорвался, на ноль не делим} void setup() { Serial.begin(9600); pinMode(PIN_LED, OUTPUT); long magiya; EEPROM.get(0, magiya); if (magiya == MAGIC) { // калибровка прошита - берём её EEPROM.get(4, kalibr); }} void loop() { long summa = 0; int godnyh = 0; for (int i = 0; i < 16; i++) { // усреднение 16 замеров против шума long mv = chitatVcc(); if (mv > 0) { summa = summa + mv; godnyh = godnyh + 1; } delay(2); } if (godnyh == 0) return; // все замеры сорвались - молчим long vcc = summa / godnyh; digitalWrite(PIN_LED, vcc < 3000 ? HIGH : LOW); // порог замены банки Serial.print("VCC="); Serial.print(vcc); Serial.println(" mV"); delay(2000);}Разбор по строкам. ADMUX = 0b01001110 собирает два поля сразу: старшие биты выбирают опору VCC (REFS0 = 1), младший ниббл 1110 подключает к мультиплексору bandgap — тот самый внутренний канал, которого в списке A0–A5 нет. delay(5) после переключения — страховка от холодных показаний, о которых я упоминал выше. bitSet и bitRead дают одно одиночное преобразование вместо analogRead, который канал 1110 сам не умеет выбирать. Тернарник в возврате — защита от деления на ноль: raw меньше 40 означает сорванный замер, возвращаю -1 и выкидываю его из среднего.
Зачем калибровка: у каждого экземпляра чипа bandgap свой — datasheet обещает от 1,0 до 1,2 В, разброс почти двадцать процентов. Формула с константой 1125300 даёт вольтметр с точностью «плюс-минус кофе». Лечение: один раз замеряешь настоящие 4,02 В мультиметром, делишь показание скетча на него, получаешь персональный коэффициент и подшиваешь его в EEPROM по соседству с магическим числом — чтобы чистый чип отличался от калиброванного. После перепрошивок и ресетов калибровка живёт, скетч подхватывает её в setup().
Порог «пора менять» подбирается под банку. Литий-ион резко просядает ниже 3,3 В и глубоко разряжаться ниже 3,0 В ему вредно, поэтому мой LED на пине 8 загорается при VCC меньше 3000 мВ — сигнал ставить банку на зарядку. Для трёх батареек АА арифметика другая: примерно 1,0 В на элемент, то есть 3,0 В на банку из трёх. Цена вопроса — около 100 мс на усреднение из 16 замеров раз в две секунды: для контроля батареи это ничто, энергобюджет из соседнего урока такой налог даже не заметит.
| Режим analogReference | Опора ADC | Что меряем | raw при входе 1,0 В |
|---|---|---|---|
| DEFAULT | VCC (3,0–5,5 В, плавает) | внешний сигнал; bandgap-трюк даёт само VCC | зависит от VCC: 4 В → 281, 3 В → 375 |
| INTERNAL | bandgap 1,1 В | сигналы до 1,1 В: шунты, диодные переходы | около 930 |
| EXTERNAL | напряжение на пине AREF | прецизионные замеры с внешним источником | 1023 × 1,0 / U_AREF |
- 1. Батарея свежая, VCC высокое. Какое raw вернёт чтение bandgap-канала?
- 2. Почему после analogReference() выбрасывают первый замер?
- 3. Зачем калибровочный коэффициент прошивают в EEPROM, а не держат константой в скетче?
- bandgap-трюк: опорой служит само VCC, меряем внутренние 1,1 В — из raw = 1023·1,1/VCC выводится VCC = 1125300/raw в милливольтах
- ноль деталей: ни делителя, ни лишних микроампер — контроль батареи ресурсами чипа
- raw меньше 40 — сбой замера: целочисленное деление на ноль на AVR даёт мусор
- bandgap у каждого чипа свой (1,0–1,2 В): раз поверить мультиметром и прошить коэффициент в EEPROM
Частые вопросы по теме урока
- Батарея свежая, VCC высокое. Какое raw вернёт чтение bandgap-канала?
- raw = 1023 · 1,1 / VCC. Чем выше опора (VCC), тем меньшую долю шкалы занимают 1,1 В: при 4 В raw около 281, при 3 В — около 375. raw растёт по мере разряда. маленькое, в районе 270–290: против высокой опоры 1,1 В занимают меньше шагов шкалы
- Почему после analogReference() выбрасывают первый замер?
- После смены опоры конденсатору выборки-хранения нужно время на заряд от нового источника: 1–2 холостых замера плюс пара миллисекунд паузы, иначе показания уплывают. внутренняя ёмкость выборки не успела зарядиться от новой опоры — показания врут
- Зачем калибровочный коэффициент прошивают в EEPROM, а не держат константой в скетче?
- Разброс bandgap между экземплярами — почти 20%: точный коэффициент измеряют мультиметром для конкретного чипа и хранят в EEPROM, часто с магическим числом, отделяющим калиброванный чип от чистого. у каждого экземпляра чипа свой bandgap 1,0–1,2 В — коэффициент индивидуален