Уличный термометр на Nano я запитал от банки 18650 и первые сутки любовался аккуратными строками журнала раз в секунду. Через восемь дней батарейка села, и вдуматься в это было полезнее, чем жалеть банку: скетч работал по схеме «печать — delay(1000)» — и все эти восемь дней процессор на 16 МГц молотил полные обороты, хотя полезной работы было меньше процента. Ножки мультиметра показали честные 44 мА: обычная плата Uno или Nano ест столько же и при активной работе, и с delay, потому что delay не усыпляет процессор — он просто крутит цикл в ожидании. При 45 мА банка на 2600 мАч живёт около двух суток, а мне обещал неделю минимум.
Экономия на ладони: между делами процессор нужно усыплять целиком, и будить его по факту события, а не по программному будильнику. Событие «прошла секунда» идеально отдаёт та же коробочка, что и ведёт календарь, — модуль часов. У DS3231 для этого есть отдельный вывод, и в этом уроке мы повесим его на внешнее прерывание: каждую секунду чип сам щёлкает ножкой, а плата в это время спит и ест микроамперы вместо миллиампер. Про то, чем программный счёт времени принципиально отличается от аппаратного, я уже писал в уроке про millis и RTC — сейчас та же разница превратится в дни жизни батареи.
SQW: бесплатный метроном изнутри модуля
На плате модуля DS3231, кроме привычных SDA/SCL и батарейки, обычно разведены два дополнительных вывода. 32K выдаёт непрерывные 32768 Гц — эталон для внешних счётчиков, его мы трогать не будем. А вот SQW — наш герой: микросхема умеет отдавать наружу прямоугольник на 1 Гц, 1, 4 или 8 кГц. Принципиально то, откуда берётся эта частота: из того же термокомпенсированного генератора, что крутит календарь, — а его уход мы считали в уроке про точность DS3231 и получили порядка ±2 ppm, то есть секунды за год. Сравните с керамическим резонатором самой Arduino, у которого допуск до ±0,5%: программный delay(1000) плывёт на минуты в сутки, а секундный тик SQW стоит как вкопанный.
Выбор частоты — это биты RS2 и RS1 в регистре управления 0x0E, и руками туда лезть не обязательно: у RTClib есть метод rtc.writeSqwPinMode(). В свежих версиях библиотеки константа называется DS3231_SquareWave1Hz, в старых — DS3231_1Hz; если компилятор не узнал имя, загляните в enum Ds3231SqwPinMode внутри RTC_DS3231.h и возьмите написание оттуда. Один нюанс на перспективу: эта же ножка у DS3231 совмещена с выходом будильников — когда поднят бит INTCN, на ней вместо прямоугольника появляется сигнал будильника из урока про часы с будильником. Один пин — две роли, и writeSqwPinMode как раз переключает режим целиком.
Где такой метроном пригождается на практике? В настольных часах на LCD тик раз в секунду будит плату для перерисовки двоеточия — между обновлениями экран можно не трогать вовсе. В логгере тик заменяет частый опрос: запрос now() с чтением семи регистров занимает около миллисекунды на стандартных 100 кГц шины — делаете его по факту тика, а не каждые 50 мс в свободное время, и I2C дышит в двадцать раз реже. И третий случай — счётчики наработки: тик идёт независимо от вашего кода, поэтому сумма тиков за сутки честнее суммы delay-интервалов, каждая из которых плывёт по резонатору платы.
Подключаем тик к пину прерывания
Схема — три провода: SQW модуля на пин 2 Arduino, общая земля, питание остаётся как было. Почему именно пин 2: на Uno аппаратные внешние прерывания есть только на D2 и D3 — это мы разбирали в уроке про attachInterrupt, здесь правило то же. А вот режим стоит выбрать осознанно: вывод INT/SQW у DS3231 сделан по схеме открытого стока, то есть он умеет только притягивать линию к земле, а в HIGH её должен тянуть кто-то другой. Подтяжку ставим внутреннюю — pinMode(2, INPUT_PULLUP), и линию внешней парой резисторов нагружать не нужно. Итоговая картинка сигнала: спокойная секунда на HIGH, затем чип роняет линию в LOW — и так каждую секунду. Ловим не нажатие, а именно падение: attachInterrupt(digitalPinToInterrupt(2), обработчик, FALLING).
Дальше — сон. Библиотека LowPower ставится из менеджера библиотек одной кнопкой, и её вызов LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF) усыпляет ATmega328P до внешнего прерывания: loop останавливается прямо на этой строке, чип просыпается на фронте с SQW, доигрывает loop до конца и засыпает снова. С токами честно так: голый микроконтроллер в power-down берёт единицы микроампер, а вот готовая Nano или Uno продолжит есть свои 40–50 мА даже во сне — стабилизатор, USB-чип и светодиод питания не умеют спать. Поэтому для настоящих батареечных проектов берут Pro Mini и выпаивают у него LED и стабилизатор: в стоке он тоже молотит миллиамперы, а после операции опускается к десяткам микроампер. Соседний раздел про watchdog и сон покажет ещё пару ступеней экономии без внешнего модуля.
Считаем скважность, чтобы понять, зачем всё затевалось: проснувшись, скетч читает DateTime по I2C (около миллисекунды) и может что-то напечатать — на 9600 бод символ стоит около миллисекунды, строка из двадцати символов отнимает двадцать. Итого десятки миллисекунд работы на секунду между тиками: процессор бодрствует меньше двух процентов времени, остальные девяносто восемь спит. Мой уличный термометр после переделки прожил на банке 18650 шесть недель вместо восьми дней — почти ровно пропорционально скважности, остаток съедают утечки обвязки и сам модуль часов с его микроамперами.
#include <Wire.h>#include <RTClib.h>#include <LowPower.h> RTC_DS3231 rtc; volatile int tiki = 0; // меняется из обработчика — обязательно volatile void onTik() { tiki = tiki + 1; // обработчик короткий: только счёт} void setup() { Serial.begin(9600); rtc.begin(); rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // время с часов ПК pinMode(2, INPUT_PULLUP); // открытый сток SQW rtc.writeSqwPinMode(DS3231_SquareWave1Hz); // секунда наружу attachInterrupt(digitalPinToInterrupt(2), onTik, FALLING);} void loop() { LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF); // спим до фронта // проснулись: фронт с SQW уже пришёл int kopiya = tiki; // копия, чтобы число не менялось при печати tiki = 0; DateTime now = rtc.now(); if (kopiya > 0 && now.second() % 15 == 0) { // печать раз в 15 секунд Serial.print(now.hour()); Serial.print(":"); Serial.print(now.minute()); Serial.print(":"); Serial.println(now.second()); }}| Источник секундного такта | Точность | Ток | Когда брать |
|---|---|---|---|
| delay(1000) | резонатор платы с допуском до ±0,5% — минуты ухода в сутки | те же 40–50 мА: процессор не спит | мигалка и учебные опыты |
| millis-опрос раз в секунду | тот же резонатор, но loop не блокируется | 40–50 мА | когда в loop нужен отклик на кнопки и датчики |
| SQW 1 Гц с DS3231 + powerDown | ±2 ppm: секунды в год, тик не зависит от платы | единицы мкА у голого чипа; плата — десятки мА без переделки | батареечные датчики и логгеры |
| Будильник DS3231 вместо SQW | та же база ±2 ppm, но момент задаёте вы | во сне единицы мкА | редкие события: раз в час или в день по расписанию |
Строки из симулятора читаются так: tiki_za_son — сколько фронтов пришло за три секунды «сна» loop, vsego — суммарный счётчик, front_na_ms — миллисекунда последнего фронта. Щёлкните кнопкой в момент сна и убедитесь, что строка всё равно появляется: обработчик отработал, пока основной код стоял в delay. На плате механика та же, только delay заменён powerDown: фронт с SQW дёргает прерывание даже в глубоком сне — это единственный будильник, который его там слышит.
Два правила из кода переезжают на плату без правок. Обработчик занимает две строки: инкремент и запоминание millis() — все медленные дела (чтение DateTime, печать, решения) остаются loop, а прерывание только сообщает «было событие». И volatile на обоих счётчиках: без него компилятор имеет право закэшировать значение в loop, и вы вечность будете смотреть на ноль при живом счётчике — та же ловушка, что и в уроке про attachInterrupt, и слово volatile обязано стоять на тех же местах.
- Смените FALLING на RISING и щёлкните кнопкой: счёт будет ловить отпускание, а не нажатие. На плате скетч продолжит считать тики — просто на каждый тик будет приходить не туда, куда вы думаете, пока не вспомните, что линия роняется в LOW.
- Уберите INPUT_PULLUP (оставьте INPUT) и запустите: в симуляторе счёт уедет на отпускание кнопки, а на реальной плате линия без подтяжки повиснет в воздухе, и чистых фронтов не будет вовсе — ровно то, о чём был абзац про открытый сток.
- Поставьте CHANGE и щёлкните один раз: счётчик вырастет на два — фронт и спад. Для секундного метронома это удвоение лишнее, но для счёта обоих фронтов пригождается.
- Увеличьте сон до 5000 мс и накопите за один сон три-четыре клика: в строке появится tiki_za_son=4 — обработчик скопил всё, пока loop спал.
- SQW у DS3231 — секундный такт из термокомпенсированного генератора: точнее любого программного delay на резонаторе платы
- Вывод INT/SQW — открытый сток: нужна подтяжка INPUT_PULLUP или внешняя 4,7–10 кОм, и ловим FALLING
- Частоту SQW включает rtc.writeSqwPinMode(DS3231_SquareWave1Hz); имя константы зависит от версии RTClib
- LowPower.powerDown(SLEEP_FOREVER, ...) будит только внешнее прерывание — связка с SQW даёт сон между делами
- Плата в сборе во сне ест десятки мА (LED, USB-чип, стабилизатор); микроамперы начинаются с Pro Mini или голого чипа
- 1. Чем секундный тик SQW лучше программного delay(1000)?
- 2. Зачем при подключении INT/SQW включать INPUT_PULLUP?
- 3. Какой режим attachInterrupt ловит момент, когда SQW роняет линию в LOW?
- 4. Почему delay(1000) не экономит батарею?
- 5. Сколько внешних прерываний есть у Arduino Uno и на каких пинах?
Секундный метроном — это фундамент, на который уже хорошо ложится календарная логика. Тик говорит «прошла секунда, можно делать дела», а дальше решает расписание: какие дни недели поливные, какое окно утром, был ли уже полив. В следующем уроке соберём именно это — таймер полива на неделю поверх DateTime — и выясним, зачем окно из двух часов надёжнее точного момента «в 7:00:00». А если ваш проект будит плату не каждую секунду, а раз в сутки, перечитайте будильники DS3231: тот же сон, но срабатывание по календарю, а не по метроному.
Частые вопросы по теме урока
- Чем секундный тик SQW лучше программного delay(1000)?
- Частота SQW снимается с того же компенсированного генератора, что ведёт календарь: уход в секунды за год. Резонатор платы даёт до ±0,5% — минуты ухода в сутки. Тик идёт от термокомпенсированного генератора с уходом около ±2 ppm
- Зачем при подключении INT/SQW включать INPUT_PULLUP?
- INT/SQW умеет только притягивать линию к земле. Без подтяжки (внутренней или внешних 4,7–10 кОм) между фронтами линия висит в воздухе, и фронты не образуются. Выход открытый сток: линию к питанию должен тянуть кто-то другой
- Какой режим attachInterrupt ловит момент, когда SQW роняет линию в LOW?
- Открытый сток между тиками держит линию в HIGH через подтяжку, а каждую секунду притягивает её к земле. Это переход HIGH в LOW — режим FALLING. FALLING — падение уровня
- Почему delay(1000) не экономит батарею?
- delay — программное ожидание: тактовый генератор молотит на 16 МГц. Усыпить процессор умеют только режимы сна вроде LowPower.powerDown. Процессор во время delay не спит и плата продолжает брать те же 40–50 мА
- Сколько внешних прерываний есть у Arduino Uno и на каких пинах?
- ATmega328P в корпусе Uno держит два внешних прерывания: INT0 на D2 и INT1 на D3. digitalPinToInterrupt() переводит номер пина в номер прерывания. Два — на D2 и D3 (INT0 и INT1)