суббота, 19 сентября 2020 г.

DS3231 - будильник для Ардуино


В данной публикации я расскажу об использовании RTC модуля на базе DS3231 для вывода Ардуино (или отдельного микроконтроллера) из режима энергосбережения. Можно сказать, будем делать будильник для Ардуино.

Микросхема DS3231 - это высокоточные часы реального времени с I2C интерфейсом, встроенным кварцевым генератором и температурной компенсацией. Как и другие подобные микросхемы DS3231 имеет два входа питания: основной и резервный для подключения батарейки или суперконденсатора. Рекомендуемое напряжение питания 2.3В .. 5.5В. Точность хода часов составляет ±3.5ppm при работе в температурном диапазоне от -40°C до +85°C, что обеспечивает отклонение не более, чем на 2 минуты за год - это, действительно, хороший показатель. Кроме учета времени в данной микросхеме реализованы два будильника и программируемый генератор прямоугольных импульсов. Ссылка на даташит: https://datasheets.maximintegrated.com/en/ds/DS3231.pdf


Почему именно DS3231?

У тех же Maxim/Dallas Semiconductor номенклатура RTC устройств насчитывает десятки микросхем. Среди них немало достойных моделей. И при желании их, конечно, можно купить, например, в ЧИП и ДИП. Но всё же большинство покупаемых деталей - это Аликспресс, а что он предлагает нам на запрос RTC? - Убогий DS1302, DS1307 и DS3231, что-то другое встречается значительно реже. Из указанных трёх микросхем будильник есть только в DS3231. Таким образом, DS3231 - это наиболее доступная микросхема (или модуль) RTC с функцией будильника и высокой точностью.


Подключение DS3231 к Ардуино

Общение с DS3231 осуществляется по линиям I2C. Схема подключения RTC модуля на DS3231 к Ардуино Уно приведена ниже.


Подключение DS3231 к Ардуино для работы с будильником

Если бы нас интересовало только текущее время, то достаточно было бы подключить VCC, GND, SCL и SDA модуля к соответствующим выводам Ардуино. В данном же случае нам нужен ещё вывод INT/SQW. На нём будет генерироваться сигнал запроса прерывания для пробуждения микроконтроллера. Поэтому проще всего подключить его к входу внешнего прерывания, для УНО это D2 и D3. Хотя можно использовать и другие выводы, разрешив на них PCINT.

Кстати, на моём модуле не было вывода INT/SQW. При этом в штыревом разъеме один из выводов был не задействован. Я подпаял его к третьей ножке микросхемы DS3231 (это и есть INT/SQW) и получил нужный мне вывод на штыревом разъеме модуля.


Добавление недостающего вывода INT/SQW на модуле DS3231


Библиотека для DS3231

Я использую библиотеку https://github.com/jarzebski/Arduino-DS3231, она поддерживает все функции DS3231 и не требует установки других библиотек. Предлагаю и вам скачать данную библиотеку и добавить её в среду разработки Ардуино. В ней есть несколько полезных примеров. Для сейчас интересует пример DS3231_intalarm, показывающий, как установить будильник и генерировать им внешнее прерывание. Правда в примере раскрыты не все опции библиотеки, поэтому я немного подправил его и привел ниже для рассмотрения. Залейте его в Ардуино и откройте монитор порта. Модуль DS3231 должен быть подключен к Ардуино по приведённой выше схеме.

#include <Wire.h>
#include <DS3231.h>

DS3231 clock;
volatile boolean isAlarm = false;

void alarmFunction() {
  isAlarm = true;
}

void setup() {
  Serial.begin(9600);

  // Инициализация RTC, сброс будильников
  clock.begin();
  clock.enableOutput(false); // INT/SQW используем для генерации прерываний
  clock.armAlarm1(false);    // Запрещаем прерывания от будильников
  clock.armAlarm2(false);
  clock.clearAlarm1();       // Сбрасываем флаги будильников
  clock.clearAlarm2();

  // Выставляем время
  clock.setDateTime(__DATE__, __TIME__); // Используем время компиляции
  //clock.setDateTime(2020, 9, 11, 12, 0, 0); // Выставляем время вручную: 11.09.2020 12:00:00

  // Выставляем будильник 1
  //clock.setAlarm1(0, 0, 0, 0, DS3231_EVERY_SECOND);   // Сигнал каждую секунду
  clock.setAlarm1(0, 0, 0, 20, DS3231_MATCH_S);         // Сигнал в 20 секунд каждой минуты
  //clock.setAlarm1(0, 0, 30, 20, DS3231_MATCH_M_S);    // Сигнал в 30 минут и 20 секунд каждого часа
  //clock.setAlarm1(0, 12, 30, 20, DS3231_MATCH_H_M_S); // Сигнал ежедневно в 12:30:20
  //clock.setAlarm1(10, 12, 30, 20, DS3231_MATCH_DT_H_M_S); // Сигнал 10 числа в 12:30:20
  //clock.setAlarm1(4, 12, 30, 20, DS3231_MATCH_DY_H_M_S);  // Сигнал по четвергам в 12:30:20

  // Выставляем будильник 2
  clock.setAlarm2(0, 0, 0, DS3231_EVERY_MINUTE);        // Сигнал каждую минуту
  //clock.setAlarm2(0, 0, 30, DS3231_MATCH_M);          // Сигнал в 30 минут каждого часа
  //clock.setAlarm2(0, 12, 30, DS3231_MATCH_H_M);       // Сигнал каждый день в 12:30:00
  //clock.setAlarm2(10, 12, 30, DS3231_MATCH_DT_H_M);   // Сигнал 10 числа в 12:30:00
  //clock.setAlarm2(10, 12, 30, DS3231_MATCH_DT_H_M);   // Сигнал 10 числа в 12:30:00
  //clock.setAlarm2(4, 12, 30, DS3231_MATCH_DY_H_M);    // Сигнал по четвергам в 12:30:00

  // Разрешаем внешнее прерывание по сигналу будильника
  pinMode(2, INPUT_PULLUP); // Вход нужно подтянуть к питанию
  attachInterrupt(0, alarmFunction, FALLING); // Разрешаем прерывание
}

void loop() {
  RTCDateTime dt = clock.getDateTime(); // Получаем текущее время
  Serial.println(clock.dateFormat("d-m-Y H:i:s - l", dt)); // Выводим его в Serial
  if (isAlarm) { // Сработал будильник
    isAlarm = false;
    // Если задействованы оба будильника, то нужна проверка, какой из них сработал
    if (clock.isAlarm1()) { // Сработал первый будильник
      clock.clearAlarm1();
      Serial.println("*** Alarm 1 ***");
    }
    if (clock.isAlarm2()) { // Сработал второй будильник
      clock.clearAlarm2();
      Serial.println("*** Alarm 2 ***");
    }
  }
  delay(1000);
}


Перед настройкой будильников необходимо запретить их работу и сбросить флаги. Это делается в начале функции setup. Далее выставляется время, это может быть сделано с использованием времени компиляции скетча или вручную. Далее настраиваем будильники. В DS3231 их два, первый может быть настроен с точностью до секунд, второй до минут. В скетче я привел все возможные варианты их установки. После этого остаётся разрешить внешнее прерывание и привязать для него обработчик. В данном скетче обработчик устанавливает переменную isAlarm, что будет говорить о срабатывании будильника.

Функция loop проверяет значение переменной isAlarm и, если она установлена, определяет какой из будильников сработал. В результате мы видим в мониторе порта сообщения "Alarm 1" и "Alarm 2":


Пример вывода при пробуждении Ардуино

Генерировать прерывания DS3231 может не только с помощью будильников, но и программируемым генератором прямоугольных импульсов. Генератор может быть настроен на частоту 1Гц, 4.096кГц, 8.192кГц и 32.768кГц. Поскольку в DS3231 применён термокомпенсированный генератор, то и частота импульсов будет достаточно стабильна. Выводятся они на всё тот же INT/SQW. В папке с примерами есть скетч DS3231_sqw_32khz, показывающий, как настраивать DS3231 на генерацию нужной частоты.

Разобравшись с использованием часов DS3231 для генерации запроса прерывания, переходим к нашей главной задаче - пробуждению Ардуино.

Использование DS3231 для пробуждения Ардуино

Итак, наша задача - перевести Ардуино в режим энергосбережения и периодически просыпаться по будильнику для выполнения каких-то действий. Попробуем, например, мигать встроенным светодиодом при пробуждении Ардуино. Практически всё, что нам для этого нужно, уже есть в приведённом выше скетче. Остаётся лишь добавить переход в режим энергосбережения:


#include <avr/sleep.h>
#include <Wire.h>
#include <DS3231.h>

DS3231 clock;

void alarmFunction() {
  // Обработчик ничего не делает
}

void setup() {
  // Инициализация RTC, сброс будильников
  clock.begin();
  clock.enableOutput(false); // INT/SQW используем для генерации прерываний
  clock.armAlarm1(false);    // Запрещаем прерывания от будильников
  clock.armAlarm2(false);
  clock.clearAlarm1();       // Сбрасываем флаги будильников
  clock.clearAlarm2();

  // Выставляем будильник 1
  clock.setAlarm1(0, 0, 0, 10, DS3231_MATCH_S); // Сигнал в 10 секунд каждой минуты

  // Разрешаем внешнее прерывание по сигналу будильника
  pinMode(2, INPUT_PULLUP); // Вход нужно подтянуть к питанию
  attachInterrupt(0, alarmFunction, FALLING); // Разрешаем прерывание

  pinMode(13, OUTPUT); // Светодиод на 13 выводе
  ADCSRA &= ~(1 << ADEN); // Отключаем АЦП
}

void loop() {
  //Переходим в режим энергосбережения
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  noInterrupts();
  sleep_enable();
  MCUCR = bit (BODS) | bit (BODSE);
  MCUCR = bit (BODS);
  interrupts();
  sleep_cpu();

  // Проснулись
  sleep_disable();
  // Проверка флага будильника может оказаться полезной
  // на случай пробуждения по другой причине
  if (clock.isAlarm1()) {
    // Будильник 1, действительно, сработал
    clock.clearAlarm1(); // Сбрасываем его флаг
    // Включаем светодиод на 2 секунды
    digitalWrite(13, HIGH);
    delay(2000);
    digitalWrite(13, LOW);
    // Можно спать дальше
  }
}


В данном примере будильник 1 устанавливается на срабатывание в 10 секунд каждой минуты, после чего Ардуино переводится в режим энергосбережения. Доступные для Ардуино (ATmega328P) режимы энергосбережения, а также события, пробуждающие микроконтроллер, я приводил в статье Режимы энергосбережения Ардуино. Проснувшись, не лишним будет проверить флаг будильника, чтобы убедиться, что именно он нас разбудил. Если флаг будильника 1 установлен, то можем выполнять наш код, в данном случае - включение и выключение светодиода.

Будильник не обязательно должен настраиваться единожды, как в приведённых выше скетчах. Его можно менять хоть при каждом срабатывании. Так можно добиться пробуждения Ардуино через определённый интервал времени. Например, чтобы будильник срабатывал каждые 10 минут, его нужно выставлять при каждом пробуждении на текущее время + 10 минут. Это потребует минимальных изменений приведённого выше скетча:

    ...
    // Будильник 1, действительно, сработал
    clock.clearAlarm1(); // Сбрасываем его флаг
    RTCDateTime dt = clock.getDateTime(); // Получаем текущее время
    clock.setAlarm1(0, 0, (dt.minute + 10) % 60, dt.second, DS3231_MATCH_M_S); // Сигнал через 10 минут
    ...

Полный пример такого скетча вы можете скачать по ссылке


Энергопотребление DS3231

Если мы хотим использовать DS3231 для вывода микроконтроллера из режима энергосбережения, то неплохо бы разобраться с потреблением и самих часов. Обратимся для этого к даташиту. В нём нас интересуют две таблицы Electrical Characteristics. Первая для случая, когда источником питания служит вход VCC, вторая - питание от VBAT, то есть от батарейки. Небольшая выжимка из этих таблиц приведена ниже:

Параметр Обозначение Условия

Максимальное

значение

Питание от Vcc
Active Supply Current  ICCA FSCL = 400МГц VCC = 3.63В 200мкА
VCC = 5.5В 300мкА
Standby Supply Current  ICCS

I2C неактивна,

Вывод 32кГц разрешён,

Меандр (SQW) запрещён

VCC = 3.63В 110мкА
VCC = 5.5В 170мкА
Temperature Conversion Current  ICCSCONV

I2C неактивна,

Вывод 32кГц разрешён,

Меандр (SQW) запрещён

VCC = 3.63В 575мкА
VCC = 5.5В 650мкА
Питание от VBAT
Active Battery Current  IBATA  

FSCL = 400МГц,

Меандр (SQW) запрещён

VBAT = 3.63В 70мкА
VBAT = 5.5В  150мкА
Timekeeping Battery Current   IBATT I2C неактивна,

Вывод 32кГц разрешён,

Меандр (SQW) запрещён

VBAT = 3.63В 3мкА 
VBAT = 5.5В  3.5мкА
Temperature Conversion Current  IBATTC I2C неактивна,

Меандр (SQW) запрещён

VBAT = 3.63В 575мкА
VBAT = 5.5В 650мкА


Active Supply Current - это среднее значение тока, потребляемого микросхемой DS3231 при работающей шине I2C (причем на максимальной частоте - 400кГц), с учётом тока измерения температуры.

Standby Supply Current отражает среднее потребление DS3231 при неактивной шине I2C, разрешённом выводе частоты 32кГц и с запрещённым меандром SQW. Данное значение также включает в себя ток при измерении температуры.

Temperature Conversion Current - ток, потребляемый микросхемой в момент измерения температуры. Измерения выполняются при подаче питания и далее каждые 64 секунды для подстройки частоты генератора. Время измерения температуры (параметр TCONV - Temperature Conversion Time) не превышает 200мс.

Active Battery Current - среднее значение тока, потребляемого DS3231 при питании от VBAT, работающей шине I2C, с учетом тока при измерении температуры.

Timekeeping Battery Current - среднее значение тока, потребляемого DS3231 при питании от VBAT, при неактивной шине I2C, с учетом тока при измерении температуры.

Переключение источника питания с VCC на VBAT происходит при выполнении двух условий: VCC < VPF и VCC < VBAT. VPF - это Power-Fail Voltage, его значение составляет 2.45В .. 2.70В. Переключение на VCC происходит, когда напряжение на данном входе становится больше VBAT.

Из таблицы видно, что потребление DS3231 не превышает 300мкА при работающей шине I2C и VCC = 5.5В. В целом же потребление значительно ниже. Для его оценки я подключил модуль DS3231 к микроконтроллеру ATmega328P без обвязки. Тактирование от внутреннего генератора 8МГц. Питание - литиевый аккумулятор 18650 с напряжением 4.1В. В режиме POWER_DOWN с отключённым АЦП и BOD потребление микроконтроллера и часов составило 93мкА:



Таким образом, DS3231 - это не просто точные часы, это полезный инструмент для многих проектов, требующих минимизации энергопотребления.

13 комментариев:

  1. Интересно, какой тип памяти используется чипом для записи в будильники. Если это флеш-память (если таковая реализована), то при постоянном изменении настроек будильника она будет изнашиваться.

    ОтветитьУдалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить
  3. Этот комментарий был удален автором.

    ОтветитьУдалить
    Ответы
    1. Подтяжка в скетче:
      pinMode(2, INPUT_PULLUP);

      Удалить
    2. Можно ещё вопрос, у меня плата как на первом рисунке, там подтяжка разведена на плате. В этом случае pinMode(2, INPUT_PULLUP); лучше убрать?

      Удалить
    3. У меня версия платы с выводом прерывания (есть ножка SQW).
      Я убрал подтяжку на плате - приподнял ножки резисторной сборки. Подтяжка на плате прожорливая очень.
      Но подтяжки одна другой не мешают, можно оставить и INPUT_PULLUP.

      Удалить
    4. Да, подтяжки друг другу не мешают. INPUT_PULLUP можно оставить.

      Удалить
    5. Просто недавно случай у меня был, заменил один датчик на другой. Старый был голый и подтяжку резистором делал. А новый был уже с резистором на плате. Получилось, что 2 резистора на подтяжке. И они сожрали батарею буквально за сутки, хотя обычно дней на 5 хватало (это без спящего режима). Поэтому и спросил, у меня система на батарейке работает и хочу добиться максимального времени работы. У меня есть оба варианта модулей как у вас на фото. Экономичнее будет второй вариант, тот что с ионистором?

      Удалить
  4. "разрешённом выводе частоты 32кГц и с запрещённым меандром SQW" - это как? И как же она выводится?
    93 мка - это много. Должно быть в районе 10. И лучше измерять по отдельности, для точной картины.

    ОтветитьУдалить
  5. Доброго времени. Не пойму, почему не работает вывод строки:
    Serial.println(clock.dateFormat("d-m-Y H:i:s - l", dt)); // Выводим его в Serial
    Также попробовал другие примеры из библиотеки - в текстовом формате выводить не хочет, помогите разобраться почему.

    ОтветитьУдалить