Часы реального времени
В данной статье рассматривается пример создания часов рального времени. На индикатор будет выводиться точное время, а двоеточие на нем будет моргать раз в секунду. Точное время будет автоматически устанавливаться во вермя компиляции прошивки.
Описание компонентов
В нашем проекте мы используем:
Часы реального времени
Мы используем модуль с часами реального времени от Seeed Studio. Они построены на базе микросхемы DS1307 от Maxim Integrated. Из элементов обвязки она требует три резистора, часовой кварц и батарейку, которые уже имеются на данном модуле. Модуль обладает следующими свойствами:
- Подсчет времени (секунды, минуты, часы), даты (год, месяц, число), дня недели
- Двухпроводной интерфейс I²C
Суть часов реального времени в том, что при наличии батарейки, они могут идти даже если основное устройство обесточено. Мы с такими часами сталкиваемся постоянно в ноутбуках или цифровых фотоаппаратах. Если достать из этих устройств аккумулятор, а через некоторое время вернуть их обратно, то время не сбросится. В этом заслуга часов реального времени, Real Time Clock (RTC).
Все необходимые библиотеки можно скачать с официального сайта.
Индикатор
Мы используем четырёхразрядный индикатор от Seeed Studio. Основное в индикаторе — микросхема TM1637, представляющая собой драйвер для отдельных 7-сегментных разрядов. В данном модуле используется 4 разряда. Модуль обладает следующими свойствами:
- 8 градаций яркости
- Двухпроводной интерфейс работы (CLK, DIO)
Данный модуль мы используем для показа времени: часов и минут. Удобство модуля в том, что подключается он всего по двум проводам и не требует программной реализации динамической индикации, поскольку все уже реализовано внутри модуля.
Динамическая индикация — это процесс, при котором индикаторы в нашем модуле загораются последовательно. Но мерцания мы не видим, поскольку человеческой глаз обладает большой инертностью. Данный метод позволяет очень хорошо экономить количество соединений между индикаторами и контроллером:
- Статическая индикация: 4 цифры × 7 сегментов = 28 соединений.
- Динамическая индикация: 7 сегментов + 4 общих анода или катода = 11 соединений.
- Микросхема TM1637: 2 соединения.
Выгода очевидна.
Библиотека для данного модуля также может быть скачана с сайта производителя.
Подключение
Модуль часов реального времени необходимо подключить к выводам SCL/SDA, относящимся к шине I²C. Также необходимо подключить линии питания (Vcc) и земли (GND).
Линии SDA/SCL имеют собственные отдельные пины на Arduino, однако внутри они так или иначе подключены к пинам общего назначения. Если рассмотреть Arduino Uno, линии SDA соответствует пин A4, а SCL — A5.
В комплекте с модулем поставляется шлейф с мама-контактами, которые удобнее подключать к Troyka Shield. Однако отдельные пины SDA и SCL на ней не выведены, поэтому мы осуществили подключение прямо через пины A5 и A4.
В плане подключения индикатора — все гораздо проще. Выводы CLK и DIO можно подключить к любым цифровым выводам. В данном случае используются 12-й и 11-й выводы соответственно.
Написание прошивки
Функция setup
должна инициализировать часы реального времени и индикатор, а также записывать время компиляции во внутреннюю память часов реального времени. Все действие, а точнее, чтение времени из RTC и вывод его на индикатор, будет производиться в функции loop
.
Код для этого выглядит следующим образом:
- rtc.ino
#include <Wire.h> #include <EEPROM.h> #include "TM1637.h" #include "DS1307.h" //Массив, содержащий время компиляции char compileTime[] = __TIME__; //Номера пинов Arduino, к которым подключается индикатор #define DISPLAY_CLK_PIN 12 #define DISPLAY_DIO_PIN 13 //Для работы с микросхемой часов и индикатором мы используем библиотеки //Классы TM1637 и DS1307 объявлены именно в них TM1637 display(DISPLAY_CLK_PIN, DISPLAY_DIO_PIN); DS1307 clock; void setup() { //Включаем и настраиваем индикатор display.set(); display.init(); //Запускаем часы реального времени clock.begin(); //Получаем число из строки, зная номер первого символа byte hour = getInt(compileTime, 0); byte minute = getInt(compileTime, 3); byte second = getInt(compileTime, 6); //Готовим для записи в RTC часы, минуты, секунды clock.fillByHMS(hour, minute, second); //Записываем эти данные во внутреннюю память часов. //С этого момента они начинают считать нужное для нас время clock.setTime(); } void loop() { //Значения для отображения на каждом из 4 разрядов int8_t timeDisp[4]; //Запрашиваем время с часов clock.getTime(); //Получаем десятки часов с помощью целочисленного деления timeDisp[0] = clock.hour / 10; //Получаем единицы часов с помощью остатка от деления timeDisp[1] = clock.hour % 10; //Проделываем то же самое с минутами timeDisp[2] = clock.minute / 10; timeDisp[3] = clock.minute % 10; //... а затем выводим его на экран display.display(timeDisp); //у нас нет отдельных разрядов для секунд, поэтому //будем включать и выключать двоеточие каждую секунду display.point(clock.second % 2 ? POINT_ON : POINT_OFF); } //Содержимое функции объяснено ниже char getInt(const char* string, int startIndex) { return int(string[startIndex] - '0') * 10 + int(string[startIndex+1]) - '0'; }
Теперь загружаем этот код в среду разработки, компилируем и заливаем. Смотрим на дисплей — бинго! Время на дисплее — время компиляции.
Объяснение функции getInt
Для начала необходимо понять, откуда же в массиве compileTime
появляется время.
Оно появляется в этой строчке:
unsigned char compileTime[] = __TIME__;
Компилятор вместо __TIME__
подставляет строку, содержащую время компиляции в виде __TIME__ = "hh:mm:ss"
, где hh - часы, mm - минуты, ss - секунды.
Вернемся к коду, который необходимо объяснить:
char getInt(const char* string, int startIndex) { return int(string[startIndex] - '0') * 10 + int(string[startIndex+1]) - '0'; }
В массиве string
, передаваемом в качестве параметра в функцию getInt
, мы получаем символ с индексом startIndex
и следующий за ним, чтобы в итоге получить двухзначное целое число. Однако, изначально это не число, а пара символов. Чтобы получить число по символу, нам необходимо вычесть из этого символа символ нуля ('0
'): ведь в таблице ASCII все символы цифр идут одна за другой, начиная с символа нуля. Поэтому код int(string[startIndex]) - '0')
, дословно, делает следующее: «Берем символ номер startIndex
, вычитаем из него символ нуля и переводим в целочисленный тип».
Проблемы
Да, этот код рабочий, и часы будут идти. Однако, если отключить питание, а через несколько минут включить, то после включения время время вновь станет тем, которое было при компиляции.
Это происходит потому что после включения питания, вновь исполняется код, находящийся в функции setup
. А он записывает в часы реального времени старое значение времени.
Чтобы этого избежать, нам необходимо еще чуть-чуть модифицировать код. Каждый раз в функции setup
будет происходить подсчет «хэша» времени компиляции — будет рассчитываться количество секунд, прошедшее с 00:00:00 до времени компиляции. И этот хэш будет сравниваться с хэшем в EEPROM. Напомним EEPROM — память, которая не обнуляется при отключении питания.
Если значения посчитанного и сохранённого ранее хэша совпадают, то это значит, что перезаписывать время в модуль часов нет необходимости: это уже было сделано. А вот если эта проверка не проходит, то происходит перезапись времени в RTC.
Для записи/чтения числа типа unsigned int
в/из EEPROM написаны две дополнительные функции EEPROMWriteInt
и EEPROMReadInt
. Они добавлены потому что функции EEPROM.read
и EEPROM.write
могуть читать и писать только данные типа char
.
- rtc-eeprom.ino
#include <Wire.h> #include <EEPROM.h> #include "TM1637.h" #include "DS1307.h" //Массив, содержащий время компиляции char compileTime[] = __TIME__; //Номера пинов Arduino, к которым подключается индикатор #define DISPLAY_CLK_PIN 12 #define DISPLAY_DIO_PIN 13 //Для работы с микросхемой часов и индикатором мы используем библиотеки TM1637 display(DISPLAY_CLK_PIN, DISPLAY_DIO_PIN); DS1307 clock; void setup() { //Включаем и настраиваем индикатор display.set(); display.init(); //Запускаем часы реального времени clock.begin(); //Получаем число из строки, зная номер первого символа byte hour = getInt(compileTime, 0); byte minute = getInt(compileTime, 3); byte second = getInt(compileTime, 6); //Импровизированный хэш времени //Содержит в себе количество секунд с начала дня unsigned int hash = hour * 60 * 60 + minute * 60 + second; //Проверяем несовпадение нового хэша с хэшем в EEPROM if (EEPROMReadInt(0) != hash) { //Сохраняем новый хэш EEPROMWriteInt(0, hash); //Готовим для записи в RTC часы, минуты, секунды clock.fillByHMS(hour, minute, second); //Записываем эти данные во внутреннюю память часов. //С этого момента они начинают считать нужное для нас время clock.setTime(); } } void loop() { //Значения для отображения на каждом из 4 разрядов int8_t timeDisp[4]; //Запрашиваем время с часов clock.getTime(); //Получаем десятки часов с помощью целочисленного деления timeDisp[0] = clock.hour / 10; //Получаем единицы часов с помощью остатка от деления timeDisp[1] = clock.hour % 10; //Проделываем то же самое с минутами timeDisp[2] = clock.minute / 10; timeDisp[3] = clock.minute % 10; //... а затем выводим его на экран display.display(timeDisp); //у нас нет отдельных разрядов для секунд, поэтому //будем включать и выключать двоеточие каждую секунду display.point(clock.second % 2 ? POINT_ON : POINT_OFF); } char getInt(const char* string, int startIndex) { return int(string[startIndex] - '0') * 10 + int(string[startIndex+1]) - '0'; } //Запись двухбайтового числа в память void EEPROMWriteInt(int address, int value) { EEPROM.write(address, lowByte(value)); EEPROM.write(address + 1, highByte(value)); } //Чтение числа из памяти unsigned int EEPROMReadInt(int address) { byte lowByte = EEPROM.read(address); byte highByte = EEPROM.read(address + 1); return (highByte << 8) | lowByte; }
Заключение
В данной статье был показан пример работы с микросхемой часов реального времени RTC DS1307 и микросхемой-драйвером индикатора TM1637, также мы научились получать дату и время на этапе компиляции. Теперь, если выставить нужное время на часах, а потом отключить питание хоть на несколько часов, то после включения время вновь будет точным. Проверено!