Узел на воротах дачного участка живёт на аккумуляторе и делает два дела: в будни в 7:00 включает подсветку для утренних сборов, по выходным — в 9:30, когда соседи уже встали. Держать плату включённой я не готов: активная Uno съедает десятки миллиампер, и аккумулятор уедет за пару дней. Ответ известен давно — спать и просыпаться по часам, — но в этой схеме сразу всплывает вопрос, который задают все: плату будит один-единственный пин модуля, а расписаний два. Кто из будильников сработал?
У DS3231 два независимых аппаратных будильника, и оба умеют дёргать один и тот же пин INT/SQW. Механику API мы уже проговаривали в уроке про будильники DS3231 — там setAlarm1, режимы и ловушки. Здесь планирование: как разнести два расписания по двум будильникам, как узел понимает, кто его разбудил, и почему пин остаётся низким, пока вы не сбросите флаг вручную.
База для всей этой истории уже есть: подключение модуля, adjust и чтение rtc.now() разобраны в первом уроке раздела. Если ваш модуль ещё не лежит на макетке с батарейкой, начните с него — а здесь берём следующий этаж: энергия, прерывание и регистры, которые переживают отключение питания платы.
А ещё — энергия: между событиями плата ложится в powerDown и потребляет единицы миллиампер вместо десятков. Вся затея стоит именно на этой паре: будильник в чипе часов, который не зависит от питания платы, и прерывание, которое будит процессор ровно в срок. Часы продолжают тикать от CR2032, когда всё остальное мертво — ресурс батарейки и замена разобраны в отдельном уроке про CR2032, а здесь нам важно лишь то, что расписание живёт дольше любого аккумулятора.
Карта регистров: кто за что отвечает
Будильник 1 занимает четыре байта 0x07–0x0A — секунды, минуты, часы и день/дата, — поэтому сравнивает время до секунды. Будильник 2 живёт в трёх байтах 0x0B–0x0D без секундного поля и работает с точностью до минуты. У каждого поля есть маскирующий бит: единица в A1M1–A1M4 или A2M2–A2M4 означает «это поле не сравнивай». Комбинируя маски, получают режимы от «каждую секунду» до «в назначенное число месяца», а бит DY/DT в четвёртом поле выбирает его смысл: день недели или дата.
Регистр управления 0x0E держит включатели. Биты A1IE и A2IE разрешают будильникам дёргать пин, а бит INTCN решает, что за сигнал идёт наружу: единица — импульсы будильников, ноль — меандр с частотой из битов RS2:RS1 — 1 Гц, 1,024 кГц, 4,096 кГц или 8,192 кГц. Секундный такт из урока про прерывание от RTC жил именно в нулевом положении INTCN. Пин один: либо меандр, либо будильники, и за выбор отвечает этот бит.
Статусный регистр 0x0F хранит ответ на вопрос «кто разбудил»: каждый будильник при срабатывании ставит свой флаг — A1F в бите 0, A2F в бите 1. Флаги независимы, могут встать оба, и читаются они так же, как любое поле часов, — по шине I2C. Главное свойство этих битов: сами они никогда не сбрасываются. Чип держит флаг до записи нуля со стороны скетча, и от этого свойства зависит вся логика ниже.
Цепочка пробуждения
Проводка минимальная. Пин INT/SQW модуля — на D2 Arduino, внешнее прерывание умеют только два пина Uno, и D2 с D3 — те самые; если D2 занят чем-то другим, расписания переезжают на D3 без единой правки в регистрах. Выход открытый сток, подтяжка обязательна: на большинстве модулей стоит резистор 10 кОм, но внутренняя подтяжка pinMode(2, INPUT_PULLUP) не мешает и страхует самосбор. Просыпание ловят attachInterrupt по FALLING — пин падает вниз в момент срабатывания будильника.
Дальше сон: LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF) из библиотеки LowPower выключает периферию и укладывает процессор до прерывания. Активная Uno с обвязкой берёт порядка 45 мА, в powerDown узел опускается к единицам миллиампер — на аккумуляторе это разница между двумя днями и двумя неделями. Проснувшись, узел не спешит: сначала спрашивает rtc.alarmFired(1) и rtc.alarmFired(2), потом делает дело по расписанию и в конце сбрасывает оба флага — иначе следующий звон не придёт.
Будни против выходных: режим «по дню недели» у будильников есть, но будни — это пять значений, а будильников два. Поэтому фильтр дня я держу в скетче: аппаратный будильник A1 звенит каждый день в 7:00, а софт сверяет dayOfTheWeek() — меньше пяти и не воскресенье, значит день рабочий, дело делаем; иначе молчим, сбрасываем флаг и ложимся дальше. Железо звенит всегда, софт решает, чей это день. Такую же развязку между точным моментом и делом мы делали в часах с будильником на LCD, только там флаг жил в памяти платы, а тут — в регистре чипа, который переживает отключение питания.
#include <Wire.h>#include <RTClib.h>#include <LowPower.h> RTC_DS3231 rtc;volatile bool probudil = false; void preryvanie() { probudil = true; // в обработчике - только флаг} void delaPoBudnyam() { Serial.println("7:00, den rabochij: podsvetka vorot");} void delaPoVyhodnym() { Serial.println("9:30, vyhodnoj: medlennoe utro");} void setup() { Serial.begin(9600); rtc.begin(); pinMode(2, INPUT_PULLUP); // INT/SQW - открытый сток attachInterrupt(digitalPinToInterrupt(2), preryvanie, FALLING); rtc.setAlarm1(DateTime(0, 0, 0, 7, 0, 0), DS3231_A1_Hour); // 7:00:00 каждый день rtc.setAlarm2(DateTime(0, 0, 0, 9, 30, 0), DS3231_A2_Hour); // 9:30 каждый день rtc.clearAlarm(1); rtc.clearAlarm(2);} void loop() { if (probudil) { probudil = false; DateTime now = rtc.now(); int den = now.dayOfTheWeek(); // 0 - воскресенье, 6 - суббота if (rtc.alarmFired(1) && den >= 1 && den <= 5) { delaPoBudnyam(); // будни } if (rtc.alarmFired(2) && (den == 0 || den == 6)) { delaPoVyhodnym(); // выходные } rtc.clearAlarm(1); // флаг сам не снимется никогда rtc.clearAlarm(2); } LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);}| Свойство | Alarm 1 | Alarm 2 |
|---|---|---|
| Точность срабатывания | до секунды | до минуты |
| Регистры | 0x07-0x0A, четыре байта | 0x0B-0x0D, три байта, без секунд |
| Маскирующие биты | A1M1-A1M4 | A2M2-A2M4 |
| Флаг в статусе 0x0F | A1F, бит 0 | A2F, бит 1 |
Отдельный вопрос — память расписаний. Регистры будильников живут в чипе и питаются от батарейки: выставили один раз — расписание переживёт и отключение питания платы, и перепрошивку, и зиму на даче без присмотра. Это отличает аппаратные будильники от софтовых массивов в ОЗУ, которые умирают вместе с платой, и делает их правильным местом для графика дежурного узла. Единственная оговорка — свежесть батарейки: чип с севшей литиевой клеткой потеряет всё, включая время, и молчание будильника будет первым симптомом.
Второй вопрос — тактовая дисциплина. Обработчик прерывания должен быть коротким: поднять флаг и выйти; всю работу — печать, реле, зуммер — делают в loop() после пробуждения. Длинный обработчик блокирует и I2C, и Serial, и сам механизм сна: попытка print внутри прерывания — классическая грабля, о которой стоит договориться до первого запуска. Планировку задач в loop после сна берите из урока про планировщик millis — там же про то, почему delay между делами вреден.
Два флага в симуляторе
Сна платы движок не умеет — это честно проговорено ещё в уроке про SQW и powerDown, — зато умеет живые часы и сами флаги. В симуляторе собирается софтовый двойник пары будильников: те же два флага, тот же вопрос «кто разбудил», те же правила сброса. A1 ставится на две минуты после старта, A2 — на четыре, время подводится кнопками виджета.
Следите за логом: первый сработавший флаг печатает своё дело и зажигает своё устройство — LED для буднего расписания, зуммер для выходного. Когда отработают оба, скетч сбрасывает флаги и заряжает новый цикл — ровно та дисциплина, которую на плате выполняют clearAlarm и powerDown. Попробуйте довести время сразу через +1ч: оба будильника промахнутся мимо, и узел будет молчать до следующего цикла — на плате то же самое случится, если проснуться позже обеих отметок, и это честная граница метода, а не недоработка.
- 1. Чем Alarm 2 отличается от Alarm 1 на уровне регистров?
- 2. Пин INT/SQW упал в LOW при срабатывании. Что вернёт его высоко?
- 3. Что выбирает бит INTCN в регистре управления 0x0E?
- 4. Оба будильника дёрнули один пин. Как скетч узнаёт, кто именно сработал?
- 5. A1_Hour звонит каждый день в 7:00, а надо — только в будни. Где живёт фильтр?
- У DS3231 два независимых будильника: A1 до секунды (0x07-0x0A), A2 до минуты (0x0B-0x0D), у каждого свой флаг в статусе 0x0F
- Оба будильника тянут один пин INT/SQW; кто сработал, узнают по флагам A1F/A2F, а не по пину
- Выход открытый сток: подтяжка обязательна, на модулях обычно 10 кОм; пробуждение ловят по FALLING на D2
- Флаг держит пин низким до записи нуля: clearAlarm после каждого пробуждения - обязательный шаг, иначе звонков больше не будет
- Будни и выходные раскладываются на два будильника плюс фильтр дня в скетче; powerDown опускает потребление с десятков миллиампер до единиц
Частые вопросы по теме урока
- Чем Alarm 2 отличается от Alarm 1 на уровне регистров?
- A2 занимает 0x0B–0x0D — минуты, часы, день/дата; секундного поля нет, точность — до минуты. Для почасовых расписаний секунды и не нужны. У него нет поля секунд: три регистра против четырёх
- Пин INT/SQW упал в LOW при срабатывании. Что вернёт его высоко?
- Выход открытого стока держится низким, пока жив флаг A1F или A2F. RTClib снимает флаг через rtc.clearAlarm(), и пин возвращается вверх, будучи готовым к следующему звонку. Сброс флага будильника записью в статусный регистр
- Что выбирает бит INTCN в регистре управления 0x0E?
- Единица в INTCN отдаёт пин будильникам, ноль включает генератор меандра — 1 Гц, 1,024 кГц, 4,096 кГц или 8,192 кГц из битов RS2:RS1. Пин один, приходится выбирать. Что выходит на пин: сигналы будильников или меандр фиксированной частоты
- Оба будильника дёрнули один пин. Как скетч узнаёт, кто именно сработал?
- Пин общий, а флаги у будильников раздельные. После пробуждения узел спрашивает rtc.alarmFired(1) и rtc.alarmFired(2) и делает дело того будильника, чей флаг стоит. Читает флаги A1F и A2F в статусе 0x0F
- A1_Hour звонит каждый день в 7:00, а надо — только в будни. Где живёт фильтр?
- Режим по дню недели есть, но будни — пять значений, а будильников два. Практичный путь: железо звенит ежедневно, софт сверяет день недели и молчит по выходным. В скетче: сравнение dayOfTheWeek() перед делом