Arduino: библиотеки для работы с RTC DS1307,DS3231

разделы: Arduino , STM32duino , RTC , дата: 18 сентября 2017г.

В завершении прошлой статьи я приводил ссылку для проверки I2C модуля RTC DS3231. Для этого не надо устанавливать никакие библиотеки, достаточно скопировать текст программы в Arduino IDE и кликнуть на загрузку скетча в микроконтроллер. Это одинаково работает как в Arduino IDE, так и в MSP430 Energia и STM32duino.

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

Сам я уже прошел по этому пути, но т.к. написанный код уже не умещался под спойлерами, поэтому пришлось написать полноценную Arduino - библиотеку. В заключение будет несколько примеров с использованием этой библиотеки, с тем, как на мой взгляд нужно правильно работать с DS1307/DS3231.

Но прежде чем "городить огород", предлагаю взглянуть на готовые решения, одобренные "патриархами" arduino.cc, а именно: библиотеки Time, DS1307RTC, а также DS3232RTC которая работает совместно с библиотекой Time.

    Для начала решим, что нам нужно от RTC типа DS1307/DS3231:
  • Автономный отчет времени, т.е. когда микроконтроллер при старте получает текущее время, а затем он уже считает время самостоятельно и не забивает I2C шину трафиком с RTC.
  • Отчет времени по SQW-выводу, когда RTC тактирует счетчик часов микроконтроллера через внешнее прерывание, и микроконтроллер самостоятельно рассчитывает календарные данные и текущее время.
  • Поддержка будильников.
  • Поддержка внесения поправок к ходу часов.
  • Периодическая синхронизация.

Вроде бы немного, и вроде бы несложно.

Весь код я буду тестировать на Arduino Nano, MSP430 Launchpad - Energia и на STM32duino - Blue Pill.

Общая концепция библиотек для работы со временем такая. Имеется базовая библиотека TIME которая ведет через функцию millis() расчет времени при запросе такого через функции библиотеки hour(), minute(), second() и т.д. Библиотека абстрагируется от аппаратной части того или иного хронометра. Она рассчитана на ведение календаря и отчет времени средствами самого микроконтроллера, без подключения RTC. Соответственно библиотеки DS3232RTC и DS1307RTC добавляют функции синхронизации микроконтроллера с RTC.

1) Библиотека Time

Библиотека состоит из файлов: Time.h, Time.cpp, DateStrings.cpp, TimeLib.h.

Time.h имеет очень лаконичное содержание:

#include "TimeLib.h"

DateString.cpp содержит строковые и числовые константы. Т.о. весь функционал библиотеки заключен в Time.cpp и заголовочном TimeLib.h.

TimeLib.h начинается - воодушевляющими словами: "этот грязный кусок кода..."

// This ugly hack allows us to define C++ overloaded functions, when included
// from within an extern "C", as newlib's sys/stat.h does.  Actually it is
// intended to include "time.h" from the C library (on ARM, but AVR does not
// have that file at all).  On Mac and Windows, the compiler will find this
// "Time.h" instead of the C library "time.h", so we may cause other weird
// and unpredictable effects by conflicting with the C library header "time.h",
// but at least this hack lets us define C++ functions as intended.  Hopefully
// nothing too terrible will result from overriding the C library header?!

Алгоритм работы библиотеки прост. Время хранится в глобальной переменной sysTime в Unix - формате, т.е. в количестве секунд с начала 1970г.

static uint32_t sysTime = 0;

При запросе эта переменная индексируется по смещению выдаваемому функцией millis(), и затем разбивается в структуру календаря:

typedef struct  {
  uint8_t Second;
  uint8_t Minute;
  uint8_t Hour;
  uint8_t Wday;   // day of week, sunday is day 1
  uint8_t Day;
  uint8_t Month;
  uint8_t Year;   // offset from 1970; 
}   tmElements_t, TimeElements, *tmElementsPtr_t;

функции типа hour(), minute() и т.д. возвращают поля этой структуры.

В начале работы с библиотекой следует установить текущее время. Для этого есть два варианта функции setTime(). Первая устанавливает текущее время по POSIX-стандарту времени, т.е. по числу секунд с начала 1970-го года:

void setTime(time_t t) {
#ifdef TIME_DRIFT_INFO
 if(sysUnsyncedTime == 0)
   sysUnsyncedTime = t;   // store the time of the first call to set a valid Time   
#endif

  sysTime = (uint32_t)t;
  nextSyncTime = (uint32_t)t + syncInterval;
  Status = timeSet;
  prevMillis = millis();  // restart counting from now (thanks to Korman for this fix)
}

Здесь переменной sysTime присваивается входной параметр. Второй вариант функции устанавливает время по набору параметров: дате, минутам, секундам и т.д.

void setTime(int hr,int min,int sec,int dy, int mnth, int yr){
 // year can be given as full four digit year or two digts (2010 or 10 for 2010);  
 //it is converted to years since 1970
  if( yr > 99)
      yr = yr - 1970;
  else
      yr += 30;
  tm.Year = yr;
  tm.Month = mnth;
  tm.Day = dy;
  tm.Hour = hr;
  tm.Minute = min;
  tm.Second = sec;
  setTime(makeTime(tm));
}

Здесь входные параметры переписываются в структуру календаря, а функция makeTime() переводит эту структуру в POSIX формат.

В библиотеке Time время индексируется следующим образом. При запросе любого параметра календаря, вызываются следующие функции:

int hour() { // the hour now 
  return hour(now());
}

int hour(time_t t) { // the hour for the given time
  refreshCache(t);
  return tm.Hour;
}

int hourFormat12() { // the hour now in 12 hour format
  return hourFormat12(now());
}

int minute() {
  return minute(now());
}

int minute(time_t t) { // the minute for the given time
  refreshCache(t);
  return tm.Minute;
}

int second() {
  return second(now());
}

int second(time_t t) {  // the second for the given time
  refreshCache(t);
  return tm.Second;
}

int day(){
  return(day(now()));
}

int day(time_t t) { // the day for the given time (0-6)
  refreshCache(t);
  return tm.Day;
}

int weekday() {   // Sunday is day 1
  return  weekday(now());
}

int weekday(time_t t) {
  refreshCache(t);
  return tm.Wday;
}

int month(){
  return month(now());
}

int month(time_t t) {  // the month for the given time
  refreshCache(t);
  return tm.Month;
}

int year() {  // as in Processing, the full four digit year: (2009, 2010 etc) 
  return year(now());
}

int year(time_t t) { // the year for the given time
  refreshCache(t);
  return tmYearToCalendar(tm.Year);

Все они вызывают функцию now():

time_t now() {
    // calculate number of seconds passed since last call to now()
  while (millis() - prevMillis >= 1000) {
        // millis() and prevMillis are both unsigned ints thus the subtraction will always be the absolute value of the difference
    sysTime++;
    prevMillis += 1000;
#ifdef TIME_DRIFT_INFO
    sysUnsyncedTime++; // this can be compared to the synced time to measure long term drift     
#endif
  }
  if (nextSyncTime <= sysTime) {
    if (getTimePtr != 0) {
      time_t t = getTimePtr();
      if (t != 0) {
        setTime(t);
      } else {
        nextSyncTime = sysTime + syncInterval;
        Status = (Status == timeNotSet) ?  timeNotSet : timeNeedsSync;
      }
    }
  }
  return (time_t)sysTime;
}

Здесь интересный прием. В начале функции вычисляется не разность в секундах с последней индексации счетчика sysTime, вместо этого, в цикле while, к переменной sysTime прибавляется по единице пока, разница с текущим временем не станет меньше секунды.

Далее вызывается функция refreshCashe()

void refreshCache(time_t t) {
  if (t != cacheTime) {
    breakTime(t, tm);
    cacheTime = t;
  }
}

... которая в свою очередь вызывает breakTime(). Эта функция разбивает POSIX формат даты в общеупотребительный формат.

void breakTime(time_t timeInput, tmElements_t &tm){
// break the given time_t into time components
// this is a more compact version of the C library localtime function
// note that year is offset from 1970 !!!

  uint8_t year;
  uint8_t month, monthLength;
  uint32_t time;
  unsigned long days;

  time = (uint32_t)timeInput;
  tm.Second = time % 60;
  time /= 60; // now it is minutes
  tm.Minute = time % 60;
  time /= 60; // now it is hours
  tm.Hour = time % 24;
  time /= 24; // now it is days
  tm.Wday = ((time + 4) % 7) + 1;  // Sunday is day 1 

  year = 0;
  days = 0;
  while((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= time) {
    year++;
  }
  tm.Year = year; // year is offset from 1970 

  days -= LEAP_YEAR(year) ? 366 : 365;
  time  -= days; // now it is days in this year, starting at 0

  days=0;
  month=0;
  monthLength=0;
  for (month=0; month<12; month++) {
    if (month==1) { // february
      if (LEAP_YEAR(year)) {
        monthLength=29;
      } else {
        monthLength=28;
      }
    } else {
      monthLength = monthDays[month];
    }

    if (time >= monthLength) {
      time -= monthLength;
    } else {
        break;
    }
  }
  tm.Month = month + 1;  // jan is month 1  
  tm.Day = time + 1;     // day of month
}

И вот эта функция вызывается при запросе любого параметра даты. Самый интересный здесь момент - вычисление дня дня недели.

  tm.Wday = ((time + 4) % 7) + 1;  // Sunday is day 1 

Это самый простой алгоритм вычисления для недели который я встречал.

Ок, теперь установим библиотеку, и из примеров загрузим скетч TimeSerial:

Как видно, после компиляции размер прошивки для MSP430G2553 составил 4496 байт. В этом примере мне понравился способ установки даты, который позволяет это сделать с точностью до секунды. Для этого после загрузки прошивки в микроконтроллер следует открыть окно терминала последовательного порта, и в консоли ввести следующую команду:

  TZ_adjust=3; echo T$(($(date +%s)+60*60*$TZ_adjust)) > /dev/ttyACM0

Таким способом можно установить дату и в Windows, если воспользоваться для этого CYGWIN. Там имена последовательных портов пишутся как /dev/ttySx, и среди них нужно найти имя своей железки.

Если все получится удачно, то в терминале пойдет такой лог:

Посылать команду с датой, в случае с Energia можно и открытым терминалом последовательного порта. Для Arduino Nano и STM32duino это окно должно быть закрыто, иначе выдаст ошибку: "устройство или ресурс занято".

Т.е. надо сначала послать дату, а потом открыть окно монитора порта. Правда в случае с Arduino Nano имеется проблема. При открытии окна монитора порта, микроконтроллер будет перезагружаться. Что бы это предотвратить, можно поставить электролитический конденсатор на 10мкФ. Плюс ставится на RESET, минус на землю.

Зато в случае с STM32duino таких проблем нет:

При компиляции одного и того же скетча, размер прошивки для MSP430G2553 составил 4496 байт, для ATMega328 - 5254 байт, для stm32f103c8t6 - аж 19308 байт.

2) Библиотека DS1307RTC

Это очень простая библиотека которая реализует программный интерфейс для работы с RTC DS1307. Объявление класса выглядит так:

// library interface description
class DS1307RTC
{
  // user-accessible "public" interface
  public:
    DS1307RTC();
    static time_t get();
    static bool set(time_t t);
    static bool read(tmElements_t &tm);
    static bool write(tmElements_t &tm);
    static bool chipPresent() { return exists; }
    static unsigned char isRunning();
    static void setCalibration(char calValue);
    static char getCalibration();

  private:
    static bool exists;
    static uint8_t dec2bcd(uint8_t num);
    static uint8_t bcd2dec(uint8_t num);
};

Метод get() читает RTC и возвращает полученную дату в POSIX формате:

time_t DS1307RTC::get()   // Aquire data from buffer and convert to time_t
{
  tmElements_t tm;
  if (read(tm) == false) return 0;
  return(makeTime(tm));
}

set() - записывает дату в RTC принимая в качестве параметра опять же дату в POSIX формате:

bool DS1307RTC::set(time_t t)
{
  tmElements_t tm;
  breakTime(t, tm);
  return write(tm);
}

Далее, read() и write() реализуют чтение и запись в RTС. я думаю, что их можно было бы сделать приватными, раз есть get() и set(). Булева переменная exist устанавливается в случае успешного выполнения write() или read(), т.е. это можно воспринимать как флаг наличия чипа на шине. Функции setCalibration() и getCalibration() записывают и читают Control-регистр.

    Немого критики:
  • Нет функции записи в SRAM;
  • Нет записи отдельных битов в control-регистр, т.е. без перезаписи остальных значений.
  • Установку рабочей частоты SQW пина хотелось бы иметь в более удобном виде нежели число типа char.

Ок, теперь практика. Библиотека включает в себя два тестовых скетча: SetTime и ReadTest. Загрузим и скомпилируем первый скетч:

Этот скетч не компилируется в Energia, компилятор выдает что-то вроде: "sscanf() not declared function". STM32duino этот скетч скомпилировался в ~50Kb(!) файл прошивки, работоспособность проверять не стал, меня это в любом случае не устраивает. А вот для Arduino Nano, этот пример работает вполне сносно.

Пример устанавливает в RTC дату компиляции прошивки, и больше он ничего не делает, там пустой главный цикл. И хотя мне способ установки даты через последовательный порт понравился больше, зато здесь не надо парится с конденсатором ;)

Можно немного модифицировать пример, что бы получить более осмысленный лог:

#include <Wire.h>
#include <TimeLib.h>
#include <DS1307RTC.h>

const char *monthName[12] = {
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};

tmElements_t tm;

void setup() {
  bool parse=false;
  bool config=false;

  // get the date and time the compiler was run
  if (getDate(__DATE__) && getTime(__TIME__)) {
    parse = true;
    // and configure the RTC with this info
    if (RTC.write(tm)) {
      config = true;
      setTime(tm.Hour, tm.Minute, tm.Second, tm.Day, tm.Month, tm.Year-30);
    }
  }

  Serial.begin(9600);
  while (!Serial) ; // wait for Arduino Serial Monitor
  delay(200);
  if (parse && config) {
    Serial.print("DS1307 configured Time=");
    Serial.print(__TIME__);
    Serial.print(", Date=");
    Serial.println(__DATE__);
  } else if (parse) {
    Serial.println("DS1307 Communication Error :-{");
    Serial.println("Please check your circuitry");
  } else {
    Serial.print("Could not parse info from the compiler, Time=\"");
    Serial.print(__TIME__);
    Serial.print("\", Date=\"");
    Serial.print(__DATE__);
    Serial.println("\"");
  }
  delay(1000);
}


void loop() {
    // digital clock display of the time
  Serial.print(hour());
  printDigits(minute());
  printDigits(second());
  Serial.print(" ");
  Serial.print(day());
  Serial.print(" ");
  Serial.print(month());
  Serial.print(" ");
  Serial.print(year());
  Serial.println();
}

void printDigits(int digits){
  // utility function for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if(digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

bool getTime(const char *str)
{
  int Hour, Min, Sec;

  if (sscanf(str, "%d:%d:%d", &Hour, &Min, &Sec) != 3) return false;
  tm.Hour = Hour;
  tm.Minute = Min;
  tm.Second = Sec;
  return true;
}

bool getDate(const char *str)
{
  char Month[12];
  int Day, Year;
  uint8_t monthIndex;

  if (sscanf(str, "%s %d %d", Month, &Day, &Year) != 3) return false;
  for (monthIndex = 0; monthIndex < 12; monthIndex++) {
    if (strcmp(Month, monthName[monthIndex]) == 0) break;
  }
  if (monthIndex >= 12) return false;
  tm.Day = Day;
  tm.Month = monthIndex + 1;
  tm.Year = CalendarYrToTm(Year);
  return true;
}

Результат работы:

Здесь я только добавил команду передачи записываемого в RTC времени в Time библиотеку:

setTime(tm.Hour, tm.Minute, tm.Second, tm.Day, tm.Month, tm.Year-30);

И вместо пустого главного цикла добавил вывод текущего времени из предыдущего примера библиотеки Time:

void loop() {
    // digital clock display of the time
  Serial.print(hour());
  printDigits(minute());
  printDigits(second());
  Serial.print(" ");
  Serial.print(day());
  Serial.print(" ");
  Serial.print(month());
  Serial.print(" ");
  Serial.print(year());
  Serial.println();
}

void printDigits(int digits){
  // utility function for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if(digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

Второй пример - ReadTest. Здесь производится простое чтение RTC раз в секунду и вывод результатов через UART:

3) Часы реального времени повышенной точности с алгоритмом термокомпенсации DS3231

Эта штука несколько сложнее, но при этом интереснее, чем DS1307. Посмотрим на карту регистров:

Здесь есть два регистра температурного датчика, регистр поправок, управляющий и флаговый регистры. По управлению он не совместим DS1307. Здесь нет CH-бита, вместо OUT/SQW пина имеется INT/SQW и 32kHz пины. С DS1307 совместим только календарь.

На github.com поиском по "ds3231" можно найти библиотеку DS3232RTC которая автором предлагается как замена DS1307RTC. Так же как и DS1307RTC, библиотека работает в паре c библиотекой Time. Несмотря на то, что библиотека называется DS3232RTC, она без проблем работает с чипом DS3231, т.к. от DS3232 он отличается лишь отсутствием SRAM.

Кроме официального руководства на микросхему DS3231, про конфигурацию регистров на русском можно почитать, например, здесь: DS3231: высокоточная микросхема RTC.

Описание класса библиотеки DS3232RTC выглядит так.

class DS3232RTC
{
    public:
        DS3232RTC();
        static time_t get(void);    //must be static to work with setSyncProvider() in the Time library
        byte set(time_t t);
        static byte read(tmElements_t &tm);
        byte write(tmElements_t &tm);
        byte writeRTC(byte addr, byte *values, byte nBytes);
        byte writeRTC(byte addr, byte value);
        byte readRTC(byte addr, byte *values, byte nBytes);
        byte readRTC(byte addr);
        void setAlarm(ALARM_TYPES_t alarmType, byte seconds, byte minutes, byte hours, byte daydate);
        void setAlarm(ALARM_TYPES_t alarmType, byte minutes, byte hours, byte daydate);
        void alarmInterrupt(byte alarmNumber, bool alarmEnabled);
        bool alarm(byte alarmNumber);
        void squareWave(SQWAVE_FREQS_t freq);
        bool oscStopped(bool clearOSF = true);  //defaults to clear the OSF bit if argument not supplied
        int temperature(void);
        static byte errCode;

    private:
        uint8_t dec2bcd(uint8_t n);
        static uint8_t bcd2dec(uint8_t n);
};

Примеры идущие вместе с библиотекой - так себе, а две так еще и требуют установки сторонних библиотек. В качестве отправной точки будем использовать пример TimeRTC. В функции void digitalClockDisplay() Последнюю строку Serial.println() заменим на:

    Serial.print(" temp: "); 
    Serial.println((float)RTC.temperature()/4.f);

Ок, добились вывода температуры.

Температура в DS3231 хранится в двух регистрах. Целая часть хранится в 0x11, она может иметь знак. Дробная часть хранится в 0x12 в двух старших битах. Т.е. дробная часть может принимать значения: ноль, четверть, половина, три четверти. Младшие биты не "плывут", тенденцию к понижению или повышению показывают стабильно. Замерять по этому термодатчику комнатную температуру невозможно по той причине, что он замеряет температуру чипа а не воздуха. При подключении к питанию, минут за пятнадцать температура чипа поднимается на пару градусов. Т.е. замерять по нему комнатную температуру, это все равно что измерять ее по температуре чипсета материнской платы.

Также в RTC DS3231 имеется два будильника. Первый можно устанавливать с точностью до одной секунды, второй - с точностью до минуты. Бит DY/DT указывает, будут ли регистры 0x0A или 0x0D хранить день недели или число месяца. Биты A1M1,A1M2 и т.д. конфигурируют режим срабатывания: при совпадении минут; при совпадении часа и минут; при совпадении дня, часа, и минут; и т.д. Таблица конфигурации дана в руководстве на чип DS3231:

Состояние битов A1F и A2F флагового регистра 0x0F отображает состояние будильника. В эти биты можно записать только нули, попытка записи единицы ни к чему не приведет. Единица в A1F и A2F записывается только аппаратно.

Попробуем "завести" второй будильник на две минуты. Для этого приведем функцию главного цикла loop() к такому виду:

void loop(void)
{
    digitalClockDisplay();

    static int i=0;
    if (++i == 5)
      RTC.setAlarm(ALM2_MATCH_MINUTES, ((minute()+2)%60), hour(), day());
          Serial.print("status = "); Serial.println(RTC.readRTC(RTC_STATUS),BIN);

    if (RTC.alarm(ALARM_2))
      Serial.println("Alarm 2 is ON");
    else
      Serial.println("Alarm 2 is OFF");

    delay(5000);
}

Компилируем и загружаем в микроконтроллер, после чего ждем срабатывания таймера:

В данном случае, метод alarm() при считывании положительного сигнала срабатывания сразу же сбрасывает флаг обратно в ноль. А так, обычно его надо сбрасывать вручную.

При распечатке флагового регистра, было видно, что были установлены биты 3 и 7. Чтобы понять, за что они отвечают, посмотрим на карту флагового регистра:

Третий бит включает ножку 32kHz, которая генерирует меандр с частотой 32kHz. Седьмой бит - Oscillator Stop Flag (OSF), бит сброса часов, т.е. когда пропадает питание и батарейка и часы сбрасываются. Этот бит устанавливается при включении RTC, может использоваться как флаг для синхронизации/установки времени RTC.

Будильники DS3231 можно использовать для пробуждения из спящего режима микроконтроллера. Приведём скетч к такому виду:

/*
 * TimeRTC.pde
 * Example code illustrating Time library with Real Time Clock.
 * This example is identical to the example provided with the Time Library,
 * only the #include statement has been changed to include the DS3232RTC library.
 */

#include <DS3232RTC.h>    //http://github.com/JChristensen/DS3232RTC
#include <Time.h>         //http://www.arduino.cc/playground/Code/Time  
#include <Wire.h>         //http://arduino.cc/en/Reference/Wire (included with Arduino IDE)
#include <avr/sleep.h>

void setup(void)
{
    Serial.begin(9600);
    setSyncProvider(RTC.get);   // the function to get the time from the RTC
    if(timeStatus() != timeSet)
        Serial.println("Unable to sync with the RTC");
    else
        Serial.println("RTC has set the system time");

    pinMode(2, INPUT_PULLUP);
    attachInterrupt(0, wakeup, FALLING);

}

void loop(void)
{
    static int i=0;

    digitalClockDisplay();

    Serial.print("status = "); Serial.println(RTC.readRTC(RTC_STATUS),BIN);
    if (++i==5)
    {
        Serial.println();
        Serial.println("Set alarm_2 and sleep...");
        delay(1000);
        RTC.squareWave(SQWAVE_NONE);
        RTC.setAlarm(ALM2_MATCH_MINUTES, ((minute()+2)%60), hour(), day());
        set_sleep_mode(SLEEP_MODE_PWR_DOWN);
        cli();
        sleep_enable();
        sleep_bod_disable();
        sei();
        sleep_cpu();
        delay(1000);
        setSyncProvider(RTC.get);
        digitalClockDisplay();
    }

    if (i==8) {
        Serial.println();
        Serial.println("Set alarm_1 and sleep...");
        delay(1000);
        RTC.squareWave(SQWAVE_NONE);
        RTC.setAlarm(ALM1_MATCH_MINUTES, second(), ((minute()+2)%60), hour(), day());
        set_sleep_mode(SLEEP_MODE_PWR_DOWN);
        cli();
        sleep_enable();
        sleep_bod_disable();
        sei();
        sleep_cpu();
        delay(1000);
        setSyncProvider(RTC.get);
        digitalClockDisplay();

    }

    if (RTC.alarm(ALARM_2))
      Serial.println("Alarm 2 is ON");
    else
      Serial.println("Alarm 2 is OFF");

    if (RTC.alarm(ALARM_1))
      Serial.println("Alarm 1 is ON");
    else
      Serial.println("Alarm 1 is OFF");

    delay(5000);
}

void wakeup() {
  Serial.println("WAKEUP!");
}

void digitalClockDisplay(void)
{
    // digital clock display of the time
    Serial.print(hour());
    printDigits(minute());
    printDigits(second());
    Serial.print(' ');
    Serial.print(day());
    Serial.print(' ');
    Serial.print(month());
    Serial.print(' ');
    Serial.print(year());
    Serial.print(" temp: ");
    Serial.println((float)RTC.temperature()/4.f);

}

void printDigits(int digits)
{
    // utility function for digital clock display: prints preceding colon and leading 0
    Serial.print(':');
    if(digits < 10)
        Serial.print('0');
    Serial.print(digits);
}

Вывод INT/SQW DS3231 нужно будет соеденить с Digital Pin 2 Arduino. В результате имеем такой лог:

Скетч последовательно устанавливает таймер на две минуты, сначала через второй будильник, а потом через первый. После установки будильника, микроконтроллер отправляется в спящий режим, и пробуждение происходит от внешнего прерывания которое вызывает RTC через INT/SQW пин.

В обычном состоянии вывод INT/SQW подтянут к питанию, точнее говоря он в состоянии open-drain т.е. висит в воздухе, но в готовых модулях он должен быть подтянут к питанию. При срабатывании будильника, INT/SQW подтягивается к земле, вызывая внешнее прерывание, и выводя микроконтроллер из режима "сна".

Как должно быть видно по логу, второй будильник сработал не точно через две минуты, а при 00-секундах. На логе видно, что первые секунды, что показывает Arduino после пробуждения, равняются единице, т.е. величине задержки delay(1000) разделяющих команды sleep_cpu() и digitalClockDisplay(). Затем используется первый будильник который можно "завести" уже с точностью от секунды, и он будит Arduino ровно через две минуты. Опять же две секунды разницы, что видны на логе, равны сумме двух задержек delay(1000);

4) Библиотека DS3231SQW

Пришло время доставать рояль из кустов) В книге Юрий Ревич "Практическое программирование микроконтроллеров Atmel AVR на языке ассемблера", работа с DS1307 описывается так: при включении микроконтроллера, он считывает с RTC текущую дату, и в дальнейшем счет времени ведет самостоятельно через внешнее прерывание срабатывающиее от SQW вывода RTC, с частотой 1Гц, т.е. один раз в секунду.

Если подумать, такой режим требует минимум ресурсов от микроконтроллера. Не нужен отдельный таймер для отчета времени, при этом отчет идет с точностью самого RTC. Соответственно, не нужны периодические синхронизации. В самом прерывании выполняется лишь прибавление единицы.

В выше описанных библиотеках, нет режима тактирования от SQW, точнее, сам пин на работу включить можно, в DS3231 он по умолчанию включен, но библиотека Time не умеет работать в таком режиме, она считает время только по собственному таймеру в функции millis(). Поэтому пришлось потратить немного времени и написать свою библиотеку.

Написанная библиотека "сырая" и не оттестированная должным образом. Однако включенные примеры, компилируются и работают по крайней мере а Arduino Nano, и значит режим работы от SQW я смогу показать наглядно на практике.

Библиотека заменяет функционал всех трех вышеописанных библиотек, и состоит из двух классов DS1307 и DS3231. Первый класс базовый, он работает с DS1307, ведет календарь, переключает

    режимы работы часов:
  • только через запросы RTC;
  • через синхронизацию по SQW;
  • самостоятельный подсчет времени с периодической синхронизацией от RTC.
От базового класса наследуется класс DS3231 к которому добавляются дополнительные методы для работы с будильниками, запрос температуры чипа и т.д.

Кроме того, что библиотека "сырая", в ней отсутствуют некоторые функции ввиду того, что для меня они бесполезны. Это поддержка "американского" формата времени: am/pm, и запись/чтение SRAM DS1307.

В библиотеке не используется POSIX формат времени, дату (год:месяц:день_недели:число) я храню в самом календаре и в отдельном суточном счетчике я храню количество секунд прошедших с начала суток. Если количество секунд превышает 86400(количество секунд в сутках), то вызывается метод add_day(), который индексирует календарь на один день. Такой подход позволяет избежать расчета {год:месяц:день_недели:число}, каждый раз, когда потребуется узнать час или минуту.

Календарь хранится не в структуре, а в простом массиве из семи байт. Так проще в цикле читать его из RTC. Для доступа к полям календаря служит нумерованный список:

enum FIELD { YEAR=6, MONTH=5, DATE=4, DAY=3, HOUR=2, MINUTE=1, SECOND=0};

Запрос минут, дня и т.д. выполняется одной функцией:

uint8_t DS1307::get(FIELD value) {
    if ((millis()-last_update) > 1000)
        request_date();

    return cal[value];
}

Будильники хранятся в структуре с битовыми полями:

struct ALARM {
    unsigned second_a1 :7;
    unsigned a1m1 : 1;
    unsigned minute_a1 :7;
    unsigned a1m2 : 1;
    unsigned hour_a1 :7;
    unsigned a1m3 : 1;
    unsigned day_date_a1 :6;
    unsigned dydt_a1: 1;
    unsigned a1m4 : 1;

    unsigned minute_a2 :7;
    unsigned a2m2 : 1;
    unsigned hour_a2 :7;
    unsigned a2m3 : 1;
    unsigned day_date_a2 :6;
    unsigned dydt_a2: 1;
    unsigned a2m4 : 1;

    unsigned control :8;
    unsigned status : 8;
    unsigned aging : 8;
};

Чтение в такую структуру производится довольно просто:

void DS3231::update_status_control() {
    Wire.beginTransmission(DS1307_I2C_ADDRESS);
    Wire.write(DS1307_CONTROL_ADDRESS);
    Wire.endTransmission();

    char Buffer[10];
    char * ptr= (char*)Buffer;
    Wire.requestFrom(DS1307_I2C_ADDRESS,10);
    for(char i=0; i<10; i++)
        Buffer[i]=Wire.read();

    alarm=*(ALARM*)ptr;
}

Описания классов выглядят так. Базовый класс DS1307:

class DS1307 {
protected:
    FREQ frequency;
    bool sqw;
    uint16_t freq32;
    uint8_t control_address;
    int adjust;
    bool auto_update;
    uint8_t cal[7];
    unsigned long last_update;
    unsigned long last_millis;
    unsigned long last_date;
    unsigned long update_period;
    uint8_t dec_bcd(uint8_t dec) { return (dec/10*16) + (dec%10);};
    uint8_t bcd_dec(uint8_t bcd) { return (bcd-6*(bcd>>4));};
    void set_address(const uint8_t, const uint8_t);
    void calendar_update();
    void request_date();
    void add_day();
    uint8_t get_week_day(uint8_t, uint8_t, uint8_t);
public:
    DS1307 ();
    DS1307 operator++(int);  //posfix
    void set_control(FREQ);
    void set_date(const uint8_t, const uint8_t, const uint8_t, const uint8_t, const uint8_t, const uint8_t, const uint8_t);
    void set_date(long);
    void set_self_update(STATUS);
    void set_adjust(int value) { adjust=value;};
    void set_rtc_adjust(int adjust);
    void stop_clock();
    void force_update();
    uint8_t get(FIELD);
    void print_calendar();
};

Класс DS3231:

class DS3231: public DS1307 {
private:
    ALARM alarm;
    char temp_MSB;
    char temp_LSB;
    uint8_t control;
    void update_status_control();
    void print_bcd(uint8_t);
public:
    DS3231();
    char get_temperature();
    char* get_temperature_fraction();
    //
    void set(uint8_t);
    bool is_alarm(ALARM_NUMBER);
    void set_sqw(const uint8_t);
    void force_update();
    // control
    uint8_t get_control();
    void preset_control(uint8_t);
    void set_control(uint8_t);
    void reset_control(uint8_t);
    // status
    uint8_t get_status();
    void set_status(uint8_t);
    void reset_status(uint8_t);
    // aging
    uint8_t get_aging();
    void set_aging(uint8_t);
    // print
    void print_calendar();
    void print_alarm_1();
    void print_alarm_2();
    // alarm
    void set_alarm_a1(ALARM_A1_TYPE, uint8_t,uint8_t, uint8_t,uint8_t);
    void set_alarm_a2(ALARM_A2_TYPE, uint8_t, uint8_t,uint8_t);
    void disable_alarm(ALARM_NUMBER);
};

Для настройки библиотеки на тот или иной режим работы, важно соблюсти правильную последовательность действий. Предлагаю рассмотреть этот процесс на примерах. Скачать библиотеку можно здесь. Для начала, из примеров загрузим скетч "SetSerialTime":

Скетч устанавливает текущее время аналогично тому, как это делал пример TimeSerial из библиотеки Time, только в этом случае, принимаемое время записывается сразу в RTC. Скетч без проблем работает с (MSP430 Launchpad|STM32 BluePill) + DS3231. Для Arduino Nano, повторюсь, во избежании перезагрузки во время открытия UART-сессии, нужно ставить конденсатор на Reset. Это работает, я проверял. Как результат:

Для настройки библиотеки, в setup() используются две команды:

  // RTC Setup ////////
  rtc.force_update();
  rtc.set_self_update(DISABLE);

Первая команда - rtc.force_update(); синхронизирует время микроконтроллера с RTC. Вторая команда - rtc.set_self_update(DISABLE), отключает автоматический отчет времени средствами микроконтроллера. Т.е. время каждый раз запрашивается у RTC, конечно при условии, что с момента предыдущего запроса прошло более одной секунды.

Следующий пример может показаться примитивным. В данном случае, в начале работы программы считывается дата с RTC, после чего отчет времени производится уже на стороне микроконтроллера. Настройка библиотеки от предыдущего примера отличается лишь параметром ENABLE в методе set_self_update(). Проверяется работа просто. После запуска отключатся RTC от микроконтроллера, и если в терминале продолжается отчет времени, заначит все в порядке.

Скетч работает нормально как на Arduino, так и на MSP430 Launchpad и STM32duino_BluePill.

Следующие примеры будут уже с использованием SQW вывода RTC. В DS1307 и DS3231 тактирование SQW устанавливается по разному, поэтому для примеры с тактированием через SQW пин для них пришлось разделить.

Сначала загрузим пример для DS1307 и Arduino - DS1307_SQW_Clock. Здесь для настройки библиотеки используются уже знакомые первые две команды:

 // RTC Setup ////////
  rtc.force_update();
  rtc.set_self_update(ENABLE);
  rtc.set_control(FREQ_1HZ);

Третья команда включает тактирование от SQW, а само тактирование происходит в обработчике внешнего прерывания чрез перегруженный оператор постфикс инкремента ++.

Здесь есть подводный камень, не зная который, можно взорвать моск ;) Если НЕ соединять SQW пин с Arduino и запустить прошивку на выполнение, она все-равно будет тикать как-будто все OK. Может показаться, что программа не в порядке, контакт SQW не подключен же... Но в setup() мы задали автоматический ход часов без участия RTC, а режим тактирования через SQW задается в перегруженном операторе rtc++. И если прерывание не работает, то и режим тактирования через SQW не задается. Пока не знаю как это сделать лучше, поживем - увидим. Зато, если SQW пин изначально подключен, и в процессе работы отключить его, результат будет вполне ожидаем - счет времени останавливается:

Красным выделен интервал, для когда SQW пин был отключен от Arduino.

С DS3231 дело обстоит таким образом. Пин INT/SQW работает в двух режимах, он может использоваться для тактирования как SQW и он может использоваться для будильников. Кроме того, частота SQW задается в статусном регистре, а не в control-регистре как в DS1307.

Загружаем пример DS3231_SQW_Clock, и в setup() видим такой код:

  // RTC Setup ////////
  rtc.force_update();
  rtc.set_self_update(ENABLE);
  rtc.set_sqw(DS3231_1HZ); 
  rtc.reset_control(DS3231_INTCH);

В DS3231 указание частоты SQW вынесено в отдельный метод: set_sqw(). Кроме того, здесь еще сбрасывается флаг INTCH в control-регистре для перевода INT/SQW пина в SQW режим.

Результат работы:

В операциях с status, control регистрами в библиотеке кроется еще один подводный камень. При чтении регистра, библиотека выдает значение сохраненное во время последнего обновления области регистров DS3231. А обновляются эти значения при записи туда. Т.е. при записи в регистр, сначала записывается требуемое значение, а потом, для контроля они считываются. Делалось это во избежании черезмерного доступа к RTC. Т.е. все те значения status и control регистров, что видны в логе, это распечатки последних сохраненных значений. В простейшем случае, при доступе только одного микроконтроллера к RTC и в случае одно экземпляра класса DS3231, это должно работать без проблем.

Скетч работает как на Arduino так и на MSP430 Launchpad:

На STM32duino работает как-то странно, но вроде работает:

Осталось два примера с будильниками. В RTC DS3231 пин INT/SQW с обычном режиме может служить для тактирования часов микроконтроллера. При необходимости использовать будильники, нужно выставить флаг INTСH в control-регистре. Тактирование остановится и микроконтроллер можно будет послать с спящий режим. При выходе из спящего режима, следует сбросить INTCH, после чего часы снова начнут тактироваться.

Первый пример, проверяет работу режимов энергосбережения. Загрузим из примеров скетч Wakeup_Alarm_A2, скомпилируем и загрузим в микроконтроллер:

В Arduino пример работает вроде как корректно.

В MSP430 launchpad тоже все Ок.

А вот c BluePill получается загвоздочка. В спящий режим микроконтроллер входит, и даже просыпается, по крайней мере, прерывание отрабатывается, но микроконтроллер так и не выходит из прерывания. Боюсь, что пока моего опыта по STM32 не хватает чтобы это корректно поправить без SPL и HAL.

Когда работу режимов энергосбережения и выходов их них проверили, можно загрузить в редактор последний пример: SQW_Alaram_A1. Здесь счет времени ведется уже через SQW пин, при установке будильника тактирование с SQW выхода снимается, а сам микроконтроллер погружается в спящий режим. При пробуждении от внешнего прерывания, SQW вывод вновь настраивается на тактирование хода часов. Для Arduino выполнение скетча выглядит так:

А вот в MSP430 в этот раз не сложилось, не выходит из спящего режима:

Подводя итоги, скажу, что остался не рассмотренным aging регистр. Он позволяет вносить поправку к внутреннему таймеру. Минимальная поправка за сутки при этом составит 86400/32768 ~2.5 сек. Возможно я чего-то недопонимаю, но для меня это кажется как-то многовоа-то, для хронометра у которого гарантированный дрифт в течении года, составляет не более одной минуты, при комнатной температуре, и четыре минуты, при перепадах от -40°C до +40°C. Также остался не понятым мной загадочный алгоритм термокомпенсации TCXO, запустить который можно установив бит CONV в control-регистре... но все остальное кажется разобрали.

поделиться: