Хитрости, которые я узнал об использовании часов реального времени DS3231 для прерывания работы Arduino
Вы хотите определить точное время, когда Arduino будет запускать специальные сегменты кода в эскизе, без использования таймеров и счетчиков в оборудовании Arduino. У вас есть некоторые навыки и опыт написания кода с помощью Arduino IDE, и вы хотели бы избегать использования таких операторов кода, как delay()
.
Вместо этого вы хотели бы подключить очень точный модуль часов реального времени DS3231 в качестве источника внешних прерываний. Для вас может быть важно, чтобы DS3231 мог использовать батарею для поддержания точного времени, даже если Arduino временно теряет питание.
Наконец, вы хотите узнать, как использовать библиотеку DS3231.h Эндрю Викерта, о которой говорится в онлайн-справке Arduino: https://www.arduino.cc/reference/en/libraries/ds3231/. Он содержит несколько трюков, овладение которыми стоит того. Вы можете импортировать эту библиотеку в свою среду разработки Arduino с помощью диспетчера библиотек (Инструменты > Управление библиотеками...).
Идите шаг за шагом. В этом уроке показаны следующие шаги:
Если вы хотите, чтобы специальный код запускался более одного раза с указанными вами интервалами, ваш код может выполнить шаг 3 еще раз и установить новый сигнал тревоги. Пример эскиза, который прилагается к этому руководству, неоднократно прерывает работу Arduino с 10-секундными интервалами.
Это руководство основано на «официальных» источниках, в том числе:
attachInterrupt()
: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/. Специальный код в этом примере тривиален: он выводит только текущее время из DS3231. Реальный пример может сделать что-то полезное, например полить растение или записать измерение температуры. Какой бы ни была задача, код должен перейти в отдельный специальный блок, который будет запускаться только тогда, когда сигнал тревоги DS3231 прерывает работу Arduino.
Я считаю, что хорошей практикой является разбивать код на короткие функции, где каждая функция обрабатывает только одну задачу или один набор связанных задач. Имя функции может быть любым; почему бы не описать, что делает функция? Вот часть моей функции, которая запускает специальный код в этом примере.
void runTheSpecialCode() {
// get the current time
// using the DateTime and RTClib classes
// defined in DS3231.h
DateTime dt = RTClib::now();
// print the current time
Serial.print(dt.hour()); Serial.print(":");
if (dt.minute() < 10) Serial.print("0");
Serial.print(dt.minute()); Serial.print(":");
if (dt.second() < 10) Serial.print("0");
Serial.println(dt.second());
// There will be more to do here, as you will see.
// This is enough, for now, to illustrate the idea:
// put special code in its own, special function
}
Вы проложите провода между пятью парами контактов. Каждая пара выполняет одну электрическую функцию и сопоставляет контакт Arduino с соответствующим контактом DS3231. Медленно соедините каждую пару, затем проверьте оба конца, чтобы убедиться, что каждый провод идет туда, куда должен. В таблице перечислены пары в порядке их подключения слева направо к DS3231 от Arduino Uno.
Цель | DS3231 Контактный | вывод ардуино |
---|---|---|
Тревога | ПКВ | 3* |
СКЛ | СКЛ | вероятность нежелательной почты** |
ПДД | ПДД | ПДД** |
мощность 5 вольт | ВКК | 5В |
Земля | Земля | Земля |
Как я уже сказал, не торопитесь устанавливать эти связи. Медленно и уверенно часто оказывается самый быстрый способ выполнить что-либо правильно.
Мы будем общаться с модулем DS3231 посредством «объекта» DS3231, своего рода набора программных инструментов с именем. Библиотека DS3231 определяет множество встроенных функций (считайте их инструментами). Когда мы хотим использовать функцию, мы пишем имя набора инструментов, затем точку, а затем имя инструмента. В примере скетча для этой цели создается переменная «часы». Тогда эскиз сможет получить доступ к инструментам в поле «часы». Все инструменты объявлены в файле DS3231.h, упомянутом выше.
#include <DS3231.h>
DS3231 clock;
Serial.println(clock.getMinute()); // the current minute, 0..59
Мы будем использовать инструменты в нашем объекте «часы», чтобы установить будильник. Но сначала нам нужно рассчитать время будильника.
На этом этапе предполагается, что вы предварительно установили фактическое время на DS3231. Пример эскиза содержит код, который вы можете использовать для установки времени на часах, если вам это понадобится. Просто удалите окружающие его разделители комментариев /* и */.
Пример скетча в этом руководстве вычисляет время будильника в будущем путем добавления интервала в секундах к текущему времени. В примере добавляется десять секунд. Минута добавит 60 секунд. Час добавит 3600 секунд. День, 86 400 секунд. И так далее.
В библиотеке DS3231 есть скрытая «хитрость», позволяющая легко добавлять время в секундах. Вы не найдете этот трюк среди доступных функций на странице README библиотеки DS3231. Это также не совсем очевидно, если посмотреть на файл DS3231.h. Некоторые подробности можно найти в файле кода DS3231.cpp. Вот шаги для выполнения вычислений.
now()
, доступ к которой должен осуществляться особым образом.Шаги 4 и 5 можно объединить.
const Uint32_t interval = 10; // number of seconds to add
DateTime currentTime; // default declaration
currentTime = RTClib::now(); // RTClib is defined in DS3231.h
uint32_t currentSeconds = currentTime.unixtime(); // express the date in seconds
DateTime alarmTime(currentSeconds + interval); // add 10 seconds and create a new date
Несмотря на то, что объект AlarmTime создается на основе количества секунд, в его наборе инструментов имеются инструменты для выражения года, месяца, дня, часа, минуты и секунды. Мы устанавливаем время будильника на DS3231, как описано далее, используя объект AlarmTime в качестве источника необходимых нам значений.
Предположим, например, что текущее время, сообщенное модулем DS3231, было на 7 секунд после 10:42 утра в среду, 27 октября 2021 г. Вычисленное выше значение AlarmTime будет 10:42:17 в тот же день, десять секунд спустя.
DS3231 выдает два различных сигнала тревоги: сигнал № 1 (A1) и сигнал № 2 (A2). Для обоих будильников можно указать день и время с точностью до минуты. Разница в том, что A1 можно уточнить с точностью до секунды. Каждый сигнал тревоги имеет свою собственную пару функций в библиотеке DS3231 для установки времени сигнала тревоги и считывания этого времени. Доступ ко всем функциям осуществляется через объект DS3231, например тот, который мы назвали «часы»:
clock.setA1Time(), clock.getA1Time(), clock.setA2Time() и clock.getA2Time()
Функция setA1Time() принимает восемь параметров, как указано в цитате из файла DS3231.h:
void setA1Time(byte A1Day, byte A1Hour, byte A1Minute, byte A1Second, byte AlarmBits, bool A1Dy, bool A1h12, bool A1PM);
Внимательное чтение заголовочного файла DS3231.h может объяснить параметры. Далее следуют мои попытки объяснить их самому себе своими словами. Если читатель обнаружит какое-либо несоответствие между моей версией и файлом заголовка, считайте, что заголовок правильный.
Первые пять параметров имеют тип «байт». Веб-сайт cppreference определяет тип байта следующим образом: https://en.cppreference.com/w/cpp/types/byte:
std::byte — это отдельный тип, реализующий концепцию байта, указанную в определении языка C++.
Подобно char и unsigned char, его можно использовать для доступа к необработанной памяти, занятой другими объектами (представление объекта), но в отличие от этих типов он не является символьным типом и не является арифметическим типом. Байт — это всего лишь набор битов, и для него определены только побитовые операторы.
В этой конкретной ситуации мы можем позволить себе думать о переменных байтового типа для дня и времени, как если бы они были целыми числами без знака. Они могут содержать целочисленные значения от 0 до 255. ВНИМАНИЕ: автор кода должен избегать бессмысленных значений. Например, значение 102 не имеет смысла ни для одного из этих параметров. Ваша задача как автора кода — предоставлять разумные значения.
Давайте продолжим с AlarmTime, созданным на предыдущем шаге: 27-й день месяца, в 17 секунд 10:42 утра. В листинге ниже показано, как можно передать эти значения в функцию. Я перечисляю каждый параметр в отдельной строке, чтобы сделать их более читабельными для людей и оставить место для комментариев. Пример здесь неполный; он демонстрирует только значения байтового типа для даты и времени. Для функции требуется больше параметров, как описано ниже, и она не будет работать в показанной здесь форме.
Кстати, обратите внимание, что переменные «clock» и «alarmTime» являются объектами, то есть наборами программных инструментов. Как видите, мы используем инструменты из соответствующих наборов инструментов для доступа к информации, содержащейся в объектах.
clock.setA1Time(
alarmTime.day(), // the day of the month: 27
alarmTime.hour(), // the hour of the day: 10
alarmTime.minute(), // the minute of the hour: 42
alarmTime.second(), // the second of the minute: 17
// ... the remaining parameters are explained below
);
Следующий параметр байтового типа, называемый AlarmBits, на самом деле представляет собой всего лишь набор битов. Биты имеют имена, определенные в таблице данных DS3231 (на стр. 11).
Бит 7 | Бит 6 | Бит 5 | Бит 4 | Бит 3 | Бит 2 | Бит 1 | Бит 0 |
---|---|---|---|---|---|---|---|
-- | -- | -- | ДыДт | А1М4 | А1М3 | А1М2 | А1М1 |
Вместе биты образуют «маску» или шаблон, который сообщает DS3231, когда и как часто подавать сигнал тревоги. Таблица на странице 12 таблицы данных дает значение различных наборов битов. На основе этой таблицы в примере эскиза в этом руководстве используется следующий набор битов:
-- | -- | -- | ДыДт | А1М4 | А1М3 | А1М2 | А1М1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
Такое расположение битов можно явно выразить в коде: 0x00001110
. Он сообщает DS3231 подавать сигнал тревоги всякий раз, когда «секунды совпадают», то есть когда значение «секунды» настройки будильника совпадает со значением «секунды» текущего времени.
Последние три параметра функции setA1Time()
имеют логические значения или значения true/false. Они сообщают DS3231 дополнительную информацию о том, как оценить настройку сигнализации. Следующий сегмент кода показывает завершенный вызов setA1Time(), продолжая пример, начатый выше:
clock.setA1Time(
alarmTime.day(), // the day of the month: 27
alarmTime.hour(), // the hour of the day: 10
alarmTime.minute(), // the minute of the hour: 42
alarmTime.second(), // the second of the minute: 17
0x00001110, // AlarmBits = signal when the seconds match
false, // A1Dy false = A1Day means the date in the month;
// true = A1Day means the day of the week
false, // A1h12 false = A1Hour in range 0..23;
// true = A1Hour in range 1..12 AM or PM
false // A1PM false = A1Hour is a.m.;
// true = A1Hour is p.m.
);
В примере скетча, где мы установили прерывание будильника каждые 10 секунд, имеют значение только параметры A1Second и AlarmBits. Однако нам необходимо предоставить их все, когда мы вызываем функцию setA1Time()
. Правильные значения предоставить не сложнее, чем ненужные; мы могли бы также проявить к ним заботу.
Функция setA2Time()
работает аналогично, но без параметра секунд. Уделите некоторое время просмотру строк 119–145 файла DS3231.h в библиотеке и страниц 11–12 таблицы данных. Терпеливо изучайте эти ссылки, пока не найдете в них информацию, необходимую для установки времени будильника.
После установки времени скетч должен выполнить дополнительные действия, чтобы включить сигнализацию в DS3231. Я призываю читателя последовательно следовать последовательности из трех шагов, даже если какой-то шаг в какой-то момент может показаться менее необходимым по какой-то причине. Если ваш код должен ошибаться, пусть он ошибается в сторону уверенности.
Для сигнала тревоги A1 инструкции в библиотеке DS3231 будут следующими:
turnOffAlarm(1); // clear the A1 enable bit in register 0Eh
checkIfAlarm(1); // clear the A1 alarm flag bit in register 0Fh
turnOnAlarm(1); // set the A1 enable bit in register 0Eh
Для сигнала тревоги A2 просто измените параметр на 2. Например: checkIfAlarm(2); // clear A2 flag bit in register 0Fh
.
Проблема, описанная в этом репозитории пользователем @flowmeter, подчеркивает, что оба флага сигнализации должны быть сброшены, прежде чем DS3231 сможет подать сигнал тревоги. Чтобы быть уверенным, рассмотрите возможность вызова checkIfAlarm() дважды, по одному разу для каждого сигнала тревоги, даже если вы используете только один из сигналов тревоги :
checkIfAlarm(1);
checkIfAlarm(2);
Почему авторы кода решили «проверить» сигнал тревоги, который, по их мнению, в данный момент не отправляет сигнал? Причина в том, что функция checkIfAlarm()
имеет неочевидный побочный эффект. Он очищает бит флага тревоги. Мы используем функцию checkIfAlarm()
, поскольку она единственная в библиотеке DS3231, которая выполняет необходимую операцию.
Подумайте об этом. По причинам, которые будут объяснены ниже, аппаратному обеспечению Arduino, распознающему прерывания, необходимо, чтобы напряжение на выводе SQW DS3231 было ВЫСОКИМ до момента возникновения аварийного сигнала. Событие тревоги меняет две вещи внутри DS3231:
На выводе SQW будет оставаться НИЗКИЙ уровень до тех пор, пока один из битов флага сигнализации остается установленным. Пока бит флага тревоги внутри DS3231 удерживает на выводе SQW НИЗКИЙ уровень, Arduino больше не может обнаруживать сигналы тревоги. Оба бита флага тревоги должны быть сброшены, чтобы DS3231 восстановил ВЫСОКОЕ напряжение на своем сигнальном выводе SQW. См. обсуждение битов 1 и 0 в «Регистре состояния (0Fh)» на странице 14 таблицы данных DS3231.
Каждый сигнал тревоги имеет свой собственный бит флага тревоги внутри DS3231. Любой из битов флага тревоги может удерживать на выводе SQW НИЗКИЙ уровень. DS3231 не будет очищать бит флага тревоги по собственной инициативе. Задача автора кода — очистить биты флага тревоги после возникновения тревоги .
В вашем основном цикле нет необходимости измерять время. Достаточно лишь проверить флаг, чтобы увидеть, произошел ли сигнал тревоги. В примере эскиза этот флаг представляет собой логическую переменную с именем «alarmEventFlag»:
if (alarmEventFlag == true) {
// run the special code
}
В большинстве случаев флаг будет иметь значение false , и цикл будет пропускать специальный код. Как в эскизе установлен флаг? Три шага:
bool alarmEventFlag = false;
void rtcISR() {alarmEventFlag = true;}
attachInterrupt()
предоставляемая Arduino IDE, объединяет все это. Следующий пример сообщает оборудованию Arduino немедленно запускать функцию rtcISR()
всякий раз, когда оно обнаруживает сигнал «ПАДЕНИЕ» на назначенном цифровом выводе.attachInterrupt(digitalPinToInterrupt(dataPin), rtcISR, FALLING);
По глубоким и оккультным причинам всегда используйте специальную функцию digitalPinToInterrupt()
при указании номера контакта для прерывания. Я оставляю это в качестве упражнения, чтобы читатель мог понять, зачем нам нужна эта функция.
Что такое ПАДАЮЩИЙ сигнал? Это означает изменение напряжения с ВЫСОКОГО на НИЗКОЕ, что определяется цифровым выводом Arduino. Откуда происходит изменение напряжения? Он берет свое начало на сигнальном контакте модуля DS3231. Этот вывод помечен как SQW, и большую часть времени он выдает ВЫСОКОЕ напряжение, близкое к уровню питания VCC (т. е. 5 В на Uno). Аварийный сигнал заставляет DS3231 изменить напряжение на выводе SQW на НИЗКОЕ. Arduino воспринимает напряжение, поступающее с вывода SQW, и замечает изменение. Мы говорим Arduino, чтобы он заметил ПАДЕНИЕ, потому что это событие происходит только один раз за сигнал тревоги, тогда как НИЗКИЙ уровень может сохраняться и сбивать с толку Arduino, вызывая множество прерываний.
Какой контакт может обнаружить ПАДЕНИЕ изменения напряжения? Для Unos вы можете выбрать любой из контактов 2 или 3. Для Leonardo это может быть любой из контактов 0, 1 или 7. (Да, я знаю, Leonardo также воспринимает прерывания на контактах 2 и 3. Однако это Выводы I2C Leonardo, что означает, что модуль DS3231 будет использовать их. Я начинаю с вывода 7 для прерываний на Leonardo.) Пример эскиза определяет a. dataPin и инициализирует ее значение 3 для работы на Uno следующим образом:
int dataPin = 3;
Специальный код также позволяет установить новое время будильника, если вы хотите повторить цикл. Начните с расчета нового времени будильника, как описано в шаге 3, и далее следуйте последовательности шагов.
Я ожидаю, что пример эскиза выдаст выходные данные, аналогичные приведенному ниже, при запуске на Arduino Uno, правильно подключенном к модулю DS3231, как я описываю это в этом руководстве. Не удивляйтесь, если время будет отличаться. В любом случае вам следует работать со своим временем.