Как использовать RTC (часы реального времени) с Arduino и LCD
В данной статье мы рассмотрим, как сделать точные часы на базе Arduino или AVR-микроконтроллера микросхемы часов реального времени DS1307. Время будет выводиться на LCD дисплей.
Что необходимо
- компьютер с установленной Arduino IDE;
- плата Arduino;
- микросхема DS1307 или модуль RTC на ее основе;
- перемычки;
- макетная плата;
- комплектующие из списка элементов.
Вы можете заменить плату Arduino на контроллер Atmel, но убедитесь, что у него достаточно входных и выходных выводов и есть аппаратная реализация интерфейса I2C. Я использую ATMega168A-PU. Если вы будете использовать отдельный микроконтроллер, то вам понадобится программатор, например, AVR MKII ISP.
Предполагается, что читатель знаком с макетированием, программированием в Arduino IDE и имеет некоторые знания языка программирования C. Обе программы, приведенные ниже, не нуждаются в дополнительном разъяснении.
Введение
Как микроконтроллеры отслеживают время и дату? Обычный микроконтроллер обладает функцией таймера, который стартует от нуля при подаче напряжения питания, а затем начинает считать. В мире Arduino мы можем использовать функцию millis()
, чтобы узнать, сколько прошло миллисекунд с того времени, когда было подано напряжение питания. Когда вы снимете и снова подадите питания, она начнет отсчет с самого начала. Это не очень удобно, когда дело доходит до работы с часами и датами.
Вот здесь и будет удобно использование микросхемы RTC (Real Time Clock, часов реального времени). Эта микросхема с батарейкой 3В или каким-либо другим источником питания следит за временем и датой. Часы/календарь обеспечивают информацию о секундах, минутах, часах, дне недели, дате, месяце и годе. Микросхема корректно работает с месяцами продолжительностью 30/31 день и с високосными годами. Связь осуществляется через шину I2C (шина I2C в данной статье не обсуждается).
Если напряжение на главной шине питания Vcc падает ниже напряжения на батарее Vbat, RTC автоматически переключается в режим низкого энергопотребления от резервной батареи. Резервная батарея – это обычно миниатюрная батарея (в виде «монетки», «таблетки») напряжением 3 вольта, подключенная между выводом 3 и корпусом. Таким образом, микросхема по-прежнему будет следить за временем и датой, и когда на основную схему будет подано питание, микроконтроллер получит текущие время и дату.
В этом проекте мы будем использовать DS1307. У этой микросхемы вывод 7 является выводом SQW/OUT (выходом прямоугольных импульсов). Вы можете использовать этот вывод для мигания светодиодом и оповещения микроконтроллера о необходимости фиксации времени. Мы будем делать и то, и другое. Ниже приведено объяснение работы с выводом SQW/OUT.
Для управления работой вывода SQW/OUT используется регистр управления DS1307.
Бит 7 | Бит 6 | Бит 5 | Бит 4 | Бит 3 | Бит 2 | Бит 1 | Бит 0 |
---|---|---|---|---|---|---|---|
OUT | 0 | 0 | SQWE | 0 | 0 | RS1 | RS0 |
- Бит 7: управление выходом (OUT)
- Этот бит управляет выходным уровнем вывода SQW/OUT, когда выход прямоугольных импульсов выключен. Если SQWE = 0, логический уровень на выводе SQW/OUT равен 1, если OUT = 1, и 0, если OUT = 0. Первоначально обычно этот бит равен 0.
- Бит 4: включение прямоугольных импульсов (SQWE)
- Этот бит, когда установлен в логическую 1, включает выходной генератор. Частота прямоугольных импульсов зависит от значений битов RS0 и RS1. Когда частота прямоугольных импульсов настроена на значение 1 Гц, часовые регистры обновляются во время спада прямоугольного импульса. Первоначально обычно этот бит равен 0.
- Биты 1 и 0: выбор частоты (RS[1:0])
- Эти биты управляют частотой выходных прямоугольных импульсов, когда выход прямоугольных импульсов включен. Следующая таблица перечисляет частоты прямоугольных импульсов, которые могут быть выбраны с помощью данных битов. Первоначально обычно эти биты равны 1.
RS1 | RS0 | Частота импульсов и уровень на выходе SQW/OUT | SQWE | OUT |
---|---|---|---|---|
0 | 0 | 1 Гц | 1 | x |
0 | 1 | 4,096 кГц | 1 | x |
1 | 0 | 8,192 кГц | 1 | x |
1 | 1 | 32,768 кГц | 1 | x |
x | x | 0 | 0 | 0 |
x | x | 1 | 0 | 1 |
Данная таблица поможет вам с частотой:
Частота импульсов | Бит 7 | Бит 6 | Бит 5 | Бит 4 | Бит 3 | Бит 2 | Бит 1 | Бит 0 |
---|---|---|---|---|---|---|---|---|
1 Гц | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
4,096 кГц | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
8,192 кГц | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 |
32,768 кГц | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 |
Если вы подключили светодиод и резистор к выводу 7 и хотите, чтобы светодиод мигал с частотой 1 Гц, то должны записать в регистр управления значение 0b00010000. Если вам нужны импульсы 4,096 кГц, то вы должны записать 0b000100001. В этом случае, чтобы увидеть импульсы вам понадобится осциллограф, так как светодиод будет мигать так быстро, что будет казаться, что он светится постоянно. Мы будем использовать импульсы с частотой 1 Гц.
Аппаратная часть
Ниже показана структурная схема того, что нам необходимо.
Мы нужны:
- разъем ISP (In System Programming, внутрисхемное программирование) для прошивки микроконтроллера;
- кнопки для установки времени и даты;
- микроконтроллер для связи с RTC через шину I2C;
- дисплей для отображения даты и времени.
Принципиальная схема:
Перечень элементов
Ниже приведен скриншот из Eagle:
Программное обеспечение
В этом руководстве мы будем использовать два различных скетча: один, который записывает время и дату в RTC, и один, который считывает время и дату из RTC. Мы сделали так потому, что так вы сможете получить более полное представление о том, что происходит. Мы будем использовать одну и ту же схему для обеих программ.
Сперва мы запишем время и дату в RTC, что аналогично установке времени на часах.
Мы используем две кнопки. Одну для увеличения часов, минут, даты, месяца, года и дня недели, а вторую для выбора между ними. Приложение не считывает состояния каких-либо критически важных датчиков, поэтому мы будем использовать прерывания для проверки, нажата ли кнопка, и обработки дребезга контактов.
Следующий код устанавливает значения и записывает их в RTC:
// Включение заголовочных файлов
#include <Wire.h>
#include <LiquidCrystal.h>
// Определение выводов LCD
#define RS 9
#define E 10
#define D4 8
#define D5 7
#define D6 6
#define D7 5
LiquidCrystal lcd(RS, E, D4, D5, D6, D7);
// Прерывание 0 – это вывод 4 микроконтроллера (цифровой вывод 2 Arduino)
int btnSet = 0;
// Прерывание 1 – это вывод 5 микроконтроллера (цифровой вывод 3 Arduino)
int btnSel = 1;
// Флаги прерываний
volatile int togBtnSet = false;
volatile int togBtnSel = false;
volatile int counterVal = 0;
// Переменные для отслеживания, где в "меню" мы находимся
volatile int menuCounter = 0;
// Массив значений
volatile int menuValues[6]; // 0=часы, 1=минуты, 2=день месяца, 3=месяц, 4=год, 5=день недели
// Заголовки меню
char* menuTitles[6] =
{
"Set hour. ",
"Set minute. ",
"Set date. ",
"Set month. ",
"Set year. ",
"Set day (1=mon)."
};
// Массив дней недели
char* days[] = { "NA", "Mon", "Tue", "Wed", "Thu", "Fre", "Sat", "Sun" };
void setup() {
// Объявление прерываний, выполнение функций increaseValue/nextItem
// по переднему фронту на btnXXX
attachInterrupt(btnSet, increaseValue, RISING);
attachInterrupt(btnSel, nextItem, RISING);
Wire.begin();
lcd.begin(16,2);
showWelcome();
}
// Функция прерывания
void increaseValue()
{
// Переменные
static unsigned long lastInterruptTime = 0;
// Создание метки времени
unsigned long interruptTime = millis();
// Если timestamp - lastInterruptTime больше, чем 200
if (interruptTime - lastInterruptTime > 200)
{
togBtnSet = true;
// Увеличить counterVal на 1
counterVal++;
}
// Установка lastInterruptTime равным метке времени
// так мы знаем, что прошли дальше
lastInterruptTime = interruptTime;
}
// Функция прерывания для следующего пункта меню
void nextItem()
{
static unsigned long lastInterruptTime = 0;
unsigned long interruptTime = millis();
if (interruptTime - lastInterruptTime > 200)
{
togBtnSel = true;
// Увеличить счетчик меню, так мы переходим к следующему пункту меню
menuCounter++;
if (menuCounter > 6)
menuCounter = 0;
// Поместить counterVal в элемент массива счетчиков меню
menuValues[menuCounter] = counterVal;
// Сбросить counterVal, сейчас мы начинаем с 0 для следующего пункта меню
counterVal = 0;
}
lastInterruptTime = interruptTime;
}
// Функция преобразования десятичных чисел в двоично-десятичный код
byte decToBCD(byte val)
{
return ((val/10*16) + (val%10));
}
// Функция проверки, была ли нажата кнопки листания меню,
// и обновления заголовка на дисплее.
void checkCurrentMenuItem()
{
if (togBtnSel)
{
togBtnSel = false;
lcd.setCursor(0,0);
lcd.print(menuTitles[menuCounter]);
}
}
// Функция проверки, была ли нажата кнопка увеличения значения,
// и обновления переменной в соответствующем элементе массива,
// плюс вывод нового значения на дисплей.
void checkAndUpdateValue()
{
// Проверить, если прерывание сработало = кнопка нажата
if (togBtnSet)
{
// Обновить значение элемента массива с counterVal
menuValues[menuCounter] = counterVal;
// Сбросить флаг прерывания
togBtnSet = false;
lcd.setCursor(7,1);
// Напечатать новое значение
lcd.print(menuValues[menuCounter]);
lcd.print(" ");
}
}
// Короткое приветственное сообщение, теперь мы знаем, что всё нормально
void showWelcome()
{
lcd.setCursor(2,0);
lcd.print("Hello world.");
lcd.setCursor(3,1);
lcd.print("I'm alive.");
delay(500);
lcd.clear();
}
// Запись данных в RTC
void writeRTC()
{
Wire.beginTransmission(0x68);
Wire.write(0); // начальный адрес
Wire.write(0x00); // секунды
Wire.write(decToBCD(menuValues[1])); // преобразовать минуты в BCD-код и записать
Wire.write(decToBCD(menuValues[0])); // преобразовать часы в BCD-код и записать
Wire.write(decToBCD(menuValues[5])); // преобразовать день недели в BCD-код и записать
Wire.write(decToBCD(menuValues[2])); // преобразовать день месяца в BCD-код и записать
Wire.write(decToBCD(menuValues[3])); // преобразовать месяц в BCD-код и записать
Wire.write(decToBCD(menuValues[4])); // преобразовать год в BCD-код и записать
Wire.write(0b00010000); // включить прямоугольные импульсы 1 Гц на выводе 7
Wire.endTransmission(); // закрыть передачу
}
// Показать время
// Чтобы посмотреть, что RTC работает, вам необходимо посмотреть другую программу
void showTime()
{
lcd.setCursor(0,0);
lcd.print(" ");
lcd.print(menuValues[0]); lcd.print(":"); // часы
lcd.print(menuValues[1]); lcd.print(":"); lcd.print("00 "); // минуты
lcd.setCursor(3,1);
lcd.print(days[menuValues[5]]); lcd.print(" "); // день недели
lcd.print(menuValues[2]); lcd.print("."); // дата
lcd.print(menuValues[3]); lcd.print("."); // месяц
lcd.print(menuValues[4]); lcd.print(" "); // год
// вызов функции writeRTC
writeRTC();
}
void loop()
{
if (menuCounter < 6)
{
checkCurrentMenuItem();
checkAndUpdateValue();
}
else
{
showTime();
}
}
Эта программа начинается с короткого приветственного сообщения. Это сообщение говорит нам, что подано питание, LCD работает, и что программа запустилась. Так как скетч служит лишь для того, чтобы показать, как записать данные из Arduino в RTC DS1307, то в нем отсутствует вспомогательный функционал (проверка, попадают ли значения в допустимые диапазоны; зацикливание при нажимании на кнопку увеличения значения, то есть сброс на 0, когда значение, например, минут превысит 60, и т.д.)
// Включение заголовочных файлов
#include <Wire.h>
#include <LiquidCrystal.h>
// Определение выводов LCD
#define RS 9
#define E 10
#define D4 8
#define D5 7
#define D6 6
#define D7 5
LiquidCrystal lcd(RS, E, D4, D5, D6, D7);
// Вывод, который будет принимать импульсы от RTC
volatile int clockPin = 0;
// Переменные времени и даты
byte second;
byte minute;
byte hour;
byte day;
byte date;
byte month;
byte year;
// Массив дней недели
char* days[] = { "NA", "Mon", "Tue", "Wed", "Thu", "Fre", "Sat", "Sun" };
// Функция, которая выполняется только при запуске
void setup() {
pinMode(clockPin, INPUT); pinMode(clockPin, LOW);
Wire.begin();
lcd.begin(16,2);
showWelcome();
}
// Короткое приветственное сообщение, теперь мы знаем, что всё нормально
void showWelcome()
{
lcd.setCursor(2,0);
lcd.print("Hello world.");
lcd.setCursor(3,1);
lcd.print("I'm alive.");
delay(500);
lcd.clear();
}
byte bcdToDec(byte val)
{
return ((val/16*10) + (val%16));
}
// Это выполняется постоянно
void loop()
{
// Если уровень на выводе clockPin высокий
if (digitalRead(clockPin))
{
// Начать передачу I2C, адрес 0x68
Wire.beginTransmission(0x68);
// Начать с адреса 0
Wire.write(0);
// Закрыть передачу
Wire.endTransmission();
// Начать чтение 7 двоичных данных от 0x68
Wire.requestFrom(0x68, 7);
second = bcdToDec(Wire.read());
minute = bcdToDec(Wire.read());
hour = bcdToDec(Wire.read());
day = bcdToDec(Wire.read());
date = bcdToDec(Wire.read());
month = bcdToDec(Wire.read());
year = bcdToDec(Wire.read());
// Форматирование и отображение времени
lcd.setCursor(4,0);
if (hour < 10) lcd.print("0");
lcd.print(hour); lcd.print(":");
if (minute < 10) lcd.print("0");
lcd.print(minute); lcd.print(":");
if (second < 10) lcd.print("0");
lcd.print(second);
lcd.setCursor(2,1);
// Форматирование и отображение даты
lcd.print(days[day]); lcd.print(" ");
if (date < 10) lcd.print("0");
lcd.print(date); lcd.print(".");
if (month < 10) lcd.print("0");
lcd.print(month); lcd.print(".");
lcd.print(year);
}
}
Заключение
В данной статье мы рассмотрели микросхему DS1307 от Maxim Integrated и написали две демонстрационные программы: одну для установки времени и даты и вторую для чтения времени и даты. Для проверки нажатия кнопок мы использовали прерывания, в которых также избавлялись от влияния дребезга контактов.