Что бы ни делал начинающий электронщик,
у него получаются либо часы, либо метеостанция
Народная мудрость

Если кто помнит, в 1970-80-е годы в советских учреждениях (на вокзалах, заводах, в школах, институтах и министерствах, а также просто на улице) висели такие круглые часы, они еще назывались «вторичными». При этом где-то размещались «первичные», подававшие раз в минуту импульс 24 вольта на все остальные, которые одновременно (с таким характерным клацанием) сдвигали стрелки ровно на одну минуту. Об этой часовой системе в подробностях рассказано вот в этой публикации.

Варианты исполнения первичных (вверху) и вторичных часов (внизу)
Варианты исполнения первичных (вверху) и вторичных часов (внизу)

Характерно, что в основе там нет никакой электроники, только электромеханика (известные еще со времен Фарадея шаговые двигатели) и электромеханические контакты. Система была дешева, проста и фактически не требовала обслуживания, кроме начальной установки после подключения к электрической сети и периодической коррекции хода первичных часов. Поэтому у меня вызвал искреннее изумление факт, что современные аналоги могут стоить десятки тысяч рублей и занимать место целого сервера в стандартной стойке. Для домашних применений или для установки в небольших офисах это все дорого и избыточно.  Мы попробуем сделать попроще и заодно избавиться от проводных соединений, которые, конечно, повышают надежность, но сильно усложняют монтаж системы.

Современные первичные часы
Современные первичные часы

Постановка задачи

Важное преимущество современных часовых систем (пример первичных часов показан на фото выше)  — у них предусмотрены встроенные системы автоматической синхронизации, причем сразу несколько: по атомным часам в интернете (наподобие синхронизации времени операционных систем на ПК), по спутниковой навигации, по сигналам радиостанций типа DCF-77 и так далее. Кроме того, современные системы работают со вторичными часами по универсальному протоколу NTP, что позволяет объединять их с системами видеонаблюдения, сигнализации и пр. Так что цена их в значительной степени оправдана.

Мы без всего этого можем обойтись. В моем деревенском доме пять экземпляров часов (некоторые объединены, да-да, с метеостанцией), и меня в этом деле в первую очередь заинтересовало отсутствие необходимости постоянно заботиться о точности хода множества часовых терминалов — подкорректировал, если надо, ход первичных часов, и все остальные сразу тоже начали идти правильно.

Добавим еще к этому, что вторичный терминал можно повесить где угодно — хороший приемопередатчик, микроконтроллер и LED-индикаторы с обвязкой будут работать и на двадцатиградусном морозе, а вот RTC-микросхема c кварцем в холодное время года может «глючить» и сбиваться, особенно при работе от резервной батарейки во время длительных выключений электричества. По моему опыту, с почти стопроцентной вероятностью это происходит с китайскими версиями DS3231 — для этого не требуются даже морозы. Причем заодно они у меня сажали резервную батарейку, которая по идее должна служить не менее десяти лет.

Итак, формулируем задачу: первичные часы на простейшем Arduino с микросхемой DS1307, с подключенным радиомодулем для передачи часовых данных раз в минуту. Вторичные часы с крупными LED-индикаторами, управляемыми, как описано в этой статье, и отдельным OLED-дисплеем для индикации даты и дня недели. Конечно, вторичные часы можно упростить, пожертвовав размером индикаторов или избавившись от дисплея с календарем, а первичные, наоборот, усложнить, приставив простенький серверок на каком-нибудь ESP32 для автоматической синхронизации с атомными часами по NTP-протоколу. Но здесь мы не будем растекаться мыслью по древу.

Первичные часы

Первичные часы представляют собой контроллер с подключенным к нему часовым модулем с резервной батарейкой (как правило, CR2032), обычно на основе RTC-микросхем DS1307 или DS3231. Вариант с традиционной DS1307 (не теряющей актуальность уже более четверти века) хорош тем, что позволяет отдельно подобрать часовой кварц. Опыт показал, что достаточно стабильными «часовыми» кварцевыми резонаторами 32768 Гц являются те, в спецификации которых указана величина точности настройки (frequency tolerance) при 25°С не хуже, чем 10-15 ppm (что обеспечивает точность хода часов около ±1 сек в месяц). Отметим, что все «часовые» кварцы имеют температурную зависимость с максимумом около 25°, в обе стороны от этой точки резонансная частота уменьшается, потому при отклонении температуры от комнатной все часы будут отставать.

Я давно развел и заказал себе платки модуля DS1307 на основе схемы, рекомендуемой в даташите DS1307, отобрав для нее кварцы, и после этого с наслаждением повыкидывал поделки с микросхемой DS3231. Модуль подключается к интерфейсу I2C (в контроллерах Atmel он, как известно, носит название TWI).

В качестве дистанционного приемопередатчика я выбрал HC-12, работающий через порт UART. HC-12 хорош тем, что «из коробки» готов к работе на стандартной скорости обмена 9600 бит/с, а частота 433 МГц слабо задерживается обычными строительными материалами (подробности см. тут). Сами часы соб��раются на отдельной «голой» микросхеме (см. далее), но отладку мы будем производить на Arduino-модулях. На них основной (аппаратный) UART контроллера уже подключен к адаптеру USB-UART, и при попытке подключить НС-12 к выводам D0 (Rx) и D1 (Tx), какое-нибудь из устройств будет выведено из строя: либо USB-UART адаптер модуля Arduino (скорее всего), либо приемопередатчик HC-12. Поэтому последний мы будем подключать к программному UART (см. статью по ссылке выше).

Первичные часы с передатчиком HC-12
Первичные часы с передатчиком HC-12

На рисунке выше показана cхема простейших первичных часов (без индикации) с RTC-модулем DS1307 и приемопередатчиком HC-12. Схема составлена на основе стандартного модуля Arduino (Uno, Nano, Mini), подключение вместо него «голого» контроллера показано на рисунке ниже. В качестве контроллера (без изменения текста программы) годится любой из серии ATmega48/88/168/328, и в данном случае даже указанный на схеме архаичный ATmega8. Отметим, что частоту кварца необязательно выбирать именно 16 МГц, для наших целей 4-8 МГц вполне достаточно. Программу после проверки на Uno следует откомпилировать для выбранного контроллера с выбранной частотой.

Подключение «голого» контроллера вместо Arduino
Подключение «голого» контроллера вместо Arduino

Вывод SQW/RES часового модуля необходимо настроить на период 1 сек, тогда по внешнему прерыванию INT0 (в Arduino оно выведено на вывод D2) контроллер будет отсчитывать секунды. Поэтому если будете выбирать RTC-модуль из имеющихся в продаже, обратите внимание на наличие внешнего вывода SQW (RES). В принципе и вывод прерывания INT0, и линии I2C-интерфейса SDA/SCL следует подтянуть к питанию резисторами в несколько килоом (см. подключение «голого» контроллера на предыдущем рисунке), но в готовых модулях часов такие резисторы обычно уже установлены. Потому мы ограничимся тем, что в программе (см. далее) на всякий случай подтянем к питанию вход прерывания INT0 (D2) программно.

Разумеется, всю конструкцию первичных часов можно оставить собранной на основе модуля Arduino, и не заморачиваться с «голым» контроллером — все равно энергосбережением мы не занимаемся. В этом случае не нужен отдельный стабилизатор питания 5 В (на схеме выше это LP2950), а процедура настройки RTC из следующего раздела упрощается.

Начальная настройка часового модуля

Перед установкой часового модуля в готовую схему необходимо произвести один важный подготовительный этап: часы необходимо установить на правильное время-дату. Кроме того, они должны выдавать импульсы на выводе SQW (RST) с нужной частотой 1 Гц. Часы DS1307 должны иметь такую настройку вывода SQW по умолчанию, но другие разновидности RTC (в т.ч. DS3231) совсем нет, потому ради общности ее также следует выполнить при настройке. Та же самая операция начальной установки времени послужит в дальнейшем для коррекции хода.

Кстати, новые или полностью сброшенные (например, при смене батарейки) часы DS1307 от корректно настроенных отличить программным путем несложно: часы, которые ни разу не настраивались, выдают значения даты и времени вне допустимых диапазонов значений: например, одно и то же число 168 для всех регистров.

Можно включить соответствующие команды настройки в основную программу первичных часов, но я привык делать это отдельной программой. Для этого часовой модуль должен быть подключен к модулю Arduino (а не к «голому» контроллеру без преобразователя USB-UART). В него загружается специальная установочная программа. Текст такой программы clock1307_set приведен далее (полный текст с комментариями и инструкцией см. архив по ссылке в конце статьи):

Программа установки часов
//программа clock1307_set 
#include <RTClib.h> //библиотека для часов
#define DP 13  // пин D13, мигалка часов 
//RTC_DS1307 RTC; //запускаем библиотеку для работы с DS1307 
//RTC_DS3231 RTC; //запускаем библиотеку для работы с DS3231 
DateTime clock;
char daysOfTheWeek[7][12] = {"Voskresenie", "Ponedelnik", "Vtornik", "Sreda", "Chetverg", "Pyatnica", "Subbota"};

void setup() { 
  Serial.begin(9600); // Serial-порт для контроля  
  if (! RTC.begin()) { 
    Serial.println("Couldn't find RTC");
    while (1);  } 
  pinMode(DP,OUTPUT);
//RTC.adjust(DateTime(__DATE__, __TIME__)); //установка времени
//установка выхода SQW 1 Гц для DS3107:
//  RTC.writeSqwPinMode(SquareWave1HZ); //установка выхода 1 Гц SQW для DS3132:
//  RTC.writeSqwPinMode(DS3231_SquareWave1Hz);   attachInterrupt(0, impuls, FALLING);   //Прерывание на D2
}

void impuls() //переключение LED раз в секунду
{  if (digitalRead(DP)==LOW) digitalWrite(DP,HIGH);  
  else digitalWrite(DP,LOW);
}
void loop() { //контрольный вывод в Serial каждые 5 сек
  delay(5000);
  uint8_t dayw;
  DateTime clock = RTC.now();
    Serial.print(clock.day(), DEC);
    Serial.print('.');
    Serial.print(clock.month(), DEC);
    Serial.print('.');
    Serial.print(clock.year(), DEC);
    Serial.print(" (");
  //день недели словами:
    Serial.print(daysOfTheWeek[clock.dayOfTheWeek()]);
  //день недели номер:    dayw=clock.dayOfTheWeek();     Serial.print(dayw, DEC);    Serial.print(") ");    Serial.print(clock.hour(), DEC);    Serial.print(':');    Serial.print(clock.minute(), DEC);
    Serial.print(':');
    Serial.print(clock.second(), DEC);
    Serial.println();
}

В программе clock1307_set используется «часовая» библиотека RTClib, обладающая нужными функциями: в частности она «умеет» извлекать часовые данные (дату и время) из компьютера во время компиляции, и переносить их в RTC-микросхему без муторных операций подгонки значений и форматов. Но такое удобство оборачивается необходимостью нестандартного обращения при загрузке.

Краткая инструкция приведена в начале полного текста программы clock1307_set (в архиве): следует раскомментировать строки, относящиеся к вашей модели часов (DS1307 или DS3231), и загрузить программу в контроллер, управляющий часовым модулем. Самое главное — после этой операции не перезапускать контроллер: дата-время считываются из компьютера один раз в процессе компиляции, и повторные перезагрузки контроллера просто будут тупо загружать в часовой модуль одни и те же данные. Монитор порта для контроля введенных значений поэтому следует запустить перед загрузкой. Сразу после загрузки, убедившись, что все работает правильно, следует обратно перезаписать в контроллер рабочую программу (или любую другую), в которой начальных установок времени и даты не имеется.

Программа первичных часов

Программа Peredatchik_1307 (см.  архив по адресу в конце статьи) реализует простейший вариант первичных часов: она только читает данные из модуля и раз минуту посылает их через приемопередатчик HC-12. Приемопередатчик подключен к цифровым пинам 8 (Rx) и 9 (Tx), на которых установлен программный UART под именем Serialpr. Чтение часов производится с помощью той же библиотеки RTClib, данные в байтовом формате сохраняются в массиве из 11 членов, первые три из которых представляют идентификатор (сигнатуру) передатчика (ASCII-коды заглавных букв «RTC»). После идентификатора размещаются данные даты-времени в последовательности 7 байт: Year (последние две цифры года), Month, DayWeek (т.е. номер дня недели), Day, Hour, Min, sek. Замыкается массив ASCII-кодом символа «;», означающем конец передачи.

Уникальный идентификатор данных и символ конца передачи нам пригодятся на приемной стороне: в эфире могут одновременно вещать несколько передатчиков, и идентификатор необходим для выделения нужной нам передачи. Символ конца передачи позволит дополнительно контролировать длину принятой последовательности, не допуская вмешательства посторонних передатчиков в середине приема массива.

Программа не очень большая по объему, и здесь я ее привожу полностью:

Программа первичных часов
//Программа Peredatchik_1307
#define RX 8 // * Определяем вывод RX (TX на модуле)
#define TX 9 // * Определяем вывод TX (RX на модуле)
#define DP 13  // пин D13, мигалка часов 

#include <RTClib.h> //библиотека для часов

#include <SoftwareSerial.h> //библиотека программного последовательного порта
SoftwareSerial Serialpr(RX,TX);           

#define DP 13  // пин D13, мигалка для контроля 

RTC_DS1307 RTC; //запускаем библиотеку для работы с DS1307 

DateTime clock;

volatile boolean pr_sek=true; //разрешение передачи
volatile byte sek=0;
volatile int Year=0;
volatile byte Month=0;
volatile byte DayWeek=0;
volatile byte Day=0;
volatile byte Hour=0;
volatile byte Min=0;
//Year, Month, DayWeek, Day, Hour, Min, sek
volatile byte aRTC[11] = {'R','T','C',0,0,0,0,0,0,0,';'}; 

void setup() {
  Serialpr.begin(9600);
  if (! RTC.begin()) { //зависаем если часы не работают
    while (1);
  }
  pinMode(DP,OUTPUT);
  attachInterrupt(0, impuls, FALLING);   //Прерывание на D2
}

void impuls()
{
if (digitalRead(DP)==LOW) digitalWrite(DP,HIGH);
else digitalWrite(DP,LOW);
   sek++;
   if (sek>59) sek=0;
   if (sek>2) pr_sek=true; //разрешаем передачу
}

void loop() {
if ((sek==0) && (pr_sek==true))
{
  delay(5000);
    DateTime clock = RTC.now();

    Year=clock.year();
    aRTC[3]=(Year-2000);
    Month=clock.month();
    aRTC[4]=Month;
    DayWeek=clock.dayOfTheWeek();
    aRTC[5]=DayWeek;
    Day=clock.day();
    aRTC[6]=Day;
    Hour=clock.hour();
    aRTC[7]=Hour;
    Min=clock.minute();
    aRTC[8]=Min;
    sek=clock.second(); //синхронизируем секунды
    aRTC[9]=sek;

       for (byte i = 0; i < 11; i++) { 
          Serialpr.write(aRTC[i]); //передаем массив 
       }
    pr_sek=false;  //запрещаем передачу во избежание повторов  
} //end if sek=0     

} //end loop

Мигающий светодиод на выводе D13 сигнализирует о готовности программы; в модулях Arduino он встроен в плату, при использовании «голого» контролера устанавливается отдельно (Led1 на схеме выше). Программу можно дополнить контрольно-отладочными посылками данных через аппаратный Serial — чтобы убедиться, что мы считываем и посылаем правильные данные. Так как здесь мы никуда не торопимся, отладочный обмен ничему не помешает.

Вторичные часы

Со вторичными часами пришлось повозиться. Общая схема, как уже говорилось, напрашивается: основа в виде крупных LED-индикаторов, управляемых согласно упоминавшейся статье и отдельного OLED-дисплея для индикации даты и дня недели. Если задать в качестве исходного массив из 7 значений даты-времени (см. структуру массива aRTC[] в программе первичных часов выше), то все работает безупречно.

Но работает только при условии, что массив с данными либо жестко задан в качестве глобальной переменной, либо читается откуда-то (например, из RTC-микросхемы) в строго определенный момент времени, синхронизированный с остальной деятельностью программы по выводу результатов. Можете попробовать сами, например, объединить процедуры индикации из программы Indikacia_328_I2C (в конце статьи) с процедурами чтения данных из часового модуля (см. программу Peredatchik_1307 выше) так, чтобы чтение производилось раз в минуту (например, в момент обнуления значения секунд). Вы получите отличные локальные часы, которые будут легко, практически незаметно подмигивать раз в минуту в момент обновления часовых данных.

Собака зарыта в том, что если попробовать напрямую подставить вместо принудительного чтения микросхемы по I2C прием данных по радиоканалу, который неизбежно происходит в случайный момент времени (и может быть осложнен еще и вмешательством других передатчиков), то такая система проработает один-два цикла обновления, потом неизбежно начнет глючить, выдавая вместо значений даты-времени всякую потустороннюю чепуху. Все дело в том, что программа динамической индикации основательно нагружает контроллер: обновление данных на дисплее происходит с частотой 244 Гц и притормозить этот процесс нельзя (см. указанную по ссылке выше статью про управление LED-индикаторами).

Из этой ситуации есть несколько выходов: например, можно усложнить (и удорожить) аппаратную часть, организовав статическую индикацию (см. об этом в указанной статье), или просто применить более быстрый и производительный 32-разрядный контроллер. Если сверху наворотить еще и какую-нибудь RTOS, разделив программу на изолированные потоки, то все должно нормально заработать, но … это уже не будет простейшее решение, доступное любому начинающему без изучения этих ваших, извините за выражение, фреймворков и тулчейнов.

Потому я пошел другим путем, просто разделив функции приема данных и индикации на два контроллера. Это не так усложняет аппаратную часть, как многоразрядная статическая индикация, и к тому же облегчает процесс отладки. В качестве контроллеров здесь по-прежнему можно применить стандартный Arduino (Uno, Nano, Mini) или любой из серии ATmega48/88/168/328 (старинный ATmega8 потребует некоторых переделок в программах, на которых мы не будем останавливаться).

Приемник данных и модуль индикации размещаются в одном корпусе вторичных часов. Обмен данными между ними следует организовать с помощью какого-либо интерфейса межконтроллерного обмена. В процессе поиска наиболее удобного способа, кстати, набрел на интересный самопальный вариант связи по одному проводу от известного техноблогера AlexGyver. При всех расхваленных автором достоинствах, недостаток этого способа понятен — в таких случаях нет уверенности, что все пойдет по плану и не придется утонуть в НИиОКР, выясняя что и где именно пошло не так.

Как показали многочисленные эксперименты, самым удобным для этой цели оказывается I2C (TWI). Он сочетает минимальное  число проводов (всего два, не считая общего провода) с возможностью работать в режиме «запрос-ответ» на аппаратном уровне, что позволяет минимальными средствами синхронизировать обновление данных с отображением на дисплеях.

Приемник данных

Почему-то во многих примерах межконтроллерного обмена по I2C режим «запрос-ответ» организуется универсальным способом с помощью посылки определенной команды (см. у того же AlexGyver). Этого совершенно лишнее усложнение и замедление процедуры: в I2C ведомый обязан иметь определенный адрес, так что послать запрос «не туда» просто невозможно.

Пример правильного обмена данными по I2C с запросом от мастера без посылки дополнительных команд можно найти в этой публикации. На его основе и была разработана простая схема промежуточного приемника данных, приведенная на рисунке ниже.

Приемник данных вторичных часов
Приемник данных вторичных часов

Как и в предыдущем случае, модуль стандартного Arduino, выводы которого обозначены на рисунке, может быть заменен на «голый» контроллер из серии ATmega8/48/88/168/328 (см. предыдущий рисунок). Не забудьте только окончательный вариант программы после тестирования на Uno скомпилировать под выбранный контроллер с выбранной частотой). Стандартная библиотека Wire, обслуживающая аппаратный интерфейс TWI/I2C, устанавливает по линиям SDA/SCL внутренние подтягивающие резисторы, поэтому показанные на схеме выше резисторы 5,1 кОм устанавливать необязательно, это сделано скорее «для порядка» (на расстоянии как минимум до 1 метра обмен и так происходит без сбоев).

Программа промежуточного приемника Priemnik_HC-12_WDT принимает данные через приемопередатчик HC-12 в случайный момент времени, складирует их в массив в памяти, а затем по запросу через TWI/I2C выдает в основной контроллер. При этом по запросу выдаются только чистые 7 байт данных даты-времени, без каких-то служебных дополнений — здесь в обмен никто вмешаться не может, и дополнять передачу идентификатором нет нужды.

Программа приемника вторичных часов
//Программа Priemnik_HC-12_WDT
#define ledPin 13 //вывод светодиода (PB5 вывод 19 ATmega)
#define RX_HC  3  // вывод 5 ATmega вывод RX (TX на модуле)
#define TX_HC  4  // вывод 6 ATmega вывод TX (RX на модуле)

#include  <avr/wdt.h>
#include <Wire.h>
#include <TimerOne.h>
#include <SoftwareSerial.h> // Библиотека программного Serial
SoftwareSerial SerialHC(RX_HC,TX_HC); //Программный Serial

//Year, Month, DayWeek, Day, Hour, Min, Sek
volatile byte arr[11] = {'R','T','C',0xFF,0,0,0xFF,0,0,0,';'}; 
volatile byte temp_arr[11]; // 'R','T','C',0,0,0,0,0,0,0,';' 
volatile byte i=0;

volatile uint16_t t_time = 0; //время приема
volatile byte count_sek=0; //счетчик секунд

volatile byte Flag=0;//признак первого ожидания

void setup() {
  pinMode(ledPin,OUTPUT);
  SerialHC.begin(9600);
 Wire.begin(0x08);                // join i2c bus with address 8 
 Wire.onRequest(requestEvent); // register request event 
  Timer1.initialize(1000000); //set timer of 1000000 microseconds
  Timer1.attachInterrupt( timerIsr ); //attach the service routine 
  wdt_enable(WDTO_4S); //4 c
  wdt_reset();
}//end setup

// function that executes whenever data is requested from master
void requestEvent() {
if (Flag==1){
     Wire.write(arr[3]);  //0 send Year tempr on request 
     Wire.write(arr[4]);  //1 send Month tempr on request 
     Wire.write(arr[5]);  //2 send Dayweek on request 
     Wire.write(arr[6]);  //3 send Day tempr on request 
     Wire.write(arr[7]);  //4 send Hour tempr on request 
     Wire.write(arr[8]);  //5 send Minuts on request
     arr[9]=count_sek; //синхронизация секунд в момент запроса
     Wire.write(arr[9]);  //6 send Sek on request 
        Flag=0;
   } //if Flag
}

void timerIsr() { //прерыв 1 сек
  t_time++;
    Flag = 1;  
  if (t_time>65) { //более минуты не обновлялось, связь прервалась
    t_time=0;
    for (byte i=0;i<11;i++) arr[i]=0;
    arr[3]=0xFF;}
  count_sek++; //независимый счетчик секунд
  if (count_sek>59) count_sek=0;
} //end timer 1 sek

void loop() {
  wdt_reset();
  byte bb=0;
    i=0;
    if (SerialHC.available() > 0) {
        // Пришла! Считываем её и анализируем
       while (bb!=';'){
       bb = SerialHC.read(); //массив побайтно
       temp_arr[i]=bb;
    delay(1);
    i++; 
   }
    if (temp_arr[10]==';') {
      bb=0;
      if ((temp_arr[0] == 'R') && (temp_arr[1] == 'T') && (temp_arr[2] == 'C')) 
      //если первые три символа = "RTC"
      //тот самый датчик
      {
        digitalWrite(ledPin,HIGH); 
        for (byte i=0;i<11;i++) {arr[i]=temp_arr[i];}
        for (byte i=0;i<11;i++) temp_arr[i]=0;
        count_sek=arr[9]; //синхронизация секунд в момент приема
        t_time=0; 
        delay(100);
        digitalWrite(ledPin,LOW); 
      } //end signature RTC
    } //конец if bb
  } //конец Serialpr.available
  delay(1000);
}//end loop

При приходе данных из приемопередатчика HC-12 на программный UART они сначала записываются во временный массив temp_arr[]. После окончания приема проверяется последний символ (11-й принятый символ должен быть равен ASCII-коду нашего концевого символа «;») и затем идентификатор массива «RTC». Если все совпадает, то временный массив переписывается в основной буферный массив arr[], сохраняемый в памяти, а временный обнуляется, чтобы вновь принятые данные не перемешивались со старыми. Из основного м��ссива данные по запросу через I2C передаются в контроллер индикации. Тактируется программа ежесекундными прерываниями через Timer1 (для этого в данном случае используется библиотека TimerOne). Программа предохраняется от зависания с помощью Watchdog-таймера (встроенная библиотека avr/wdt.h) — крайне редкий случай, но не исключенный, если программа запутается в данных при случайном пересечении приема массива с какими-то сторонними передачами. Светодиод на выводе D13 в данном случае сигнализирует о факте приема «правильных» данных; как мы говорили, в модулях Arduino он встроен в плату, при использовании «голого» контролера его необходимо установить.

Глобальная переменная t_time подсчитывает число секундных тиков таймера; при приеме «правильного» массива t_time обнуляется. Если в прерывании обнаруживается, что t_time более 65 секунд не обнулялась, то в буферный массив записываются все нули, а на позицию года arr[3] число 0xFF, что для контроллера индикации будет означать обрыв связи с первичными часами.

Особое значение тут имеет процедура синхронизации секунд, отсчитываемых прерыванием таймера в приемном контроллере, с секундами из часов реального времени. Мы включаем приемник в случайный момент времени, и при отсутствии синхронизации отсчет времени в приемной части может разбежаться с показаниями RTC-модуля на время до минуты. Для этого имеется отдельный счетчик секунд count_sek, и синхронизация проводится при каждом приеме «правильного» массива (значение принятых секунд загружается в переменную count_sek). При каждом запросе со стороны контроллера индикации текущее значение этой переменной загружается в соответствующий секундам элемент буферного массива arr[9], таким образом передаваемое число секунд оказывается синхронизированным с принятым из первичных часов.

Модуль индикации

Индикация в данной конструкции вынесена на отдельную плату, на которой размещены четыре цифры LED-индикаторов с парой разделяющих светодиодов и ниже двухстрочный OLED-дисплей конфигурации 1602 для демонстрации даты и времени (см. три рисунка ниже).

Подключения контроллера индикации на примере платы стандартного Arduino
Подключения контроллера индикации на примере платы стандартного Arduino
Плата индикации: LED-индикаторы часов-минут и разделительное двоеточие
Плата индикации: LED-индикаторы часов-минут и разделительное двоеточие
Плата индикации: OLED-дисплей 1602 даты и дня недели
Плата индикации: OLED-дисплей 1602 даты и дня недели

Индикаторы, если они размером в дюйм (как показанные на схеме SA10-21) должны иметь питание не менее 6-7 вольт (подробности см. указанную ранее статью). В данном случае они питаются от внешнего стабилизатора +9 В. Поскольку два диода двоеточия подключены к тому же питанию, управлять ими непосредственно от вывода 5-вольтового контроллера нельзя, и они подключены к ключевому МОП-транзистору. Сопротивление токозадающего резистора Rext для индикаторов обычной яркости (например, желтых SA10-21YWA) — около 1 кОм; для повышенной яркости (SA10-21SYKWA) его лучше увеличить до 2-2,4 кОм. Диоды следует подобрать по длине волны под индикаторы; к сожалению, того же нельзя сделать для OLED-дисплея и его цвет обязательно будет отличаться (что отчасти можно скорректировать наложением цветной пленки от театрального фильтра, как упоминалось в статье по ссылке).

Для этих схем нужны два питания: +5 В (стабилизированное) на контроллер и +9 В (можно нестабилизированое) для индикаторов. В случае платы Arduino Uno или Nano ставить отдельный стабилизатор на 5,0 В необязательно: можно просто подключить внешний адаптер +9 В к выводу Vin, а пятивольтовое питание на микросхемы обвязки индикаторов и двухстрочный дисплей раздать c выхода +5V. В случае «голого» котроллера отдельный стабилизатор ставить придется, причем в данной схеме вполне можно обойтись упомянутым 100-миллиамперным LP2950 (OLED-дисплей потребляет не более 30-40 мА).

Внешний вид платы индикации при снятом затемняющем фильтре
Внешний вид платы индикации при снятом затемняющем фильтре

Программа индикации

Программу индикации Indikacia_328_I2C полностью можно найти в архиве по ссылке в конце статьи. Программа рассчитана на контроллер ATmega328P с тактовой частотой 16 МГц. Здесь мы приведем только самое основное, требующее комментариев.

Тактирование программы производится ежесекундными прерываниями таймера1. Настройка Timer1 в функции setup():

//инициализация Timer1 1 sek (ATmega328P, 16 МГц!!!)
    TCCR1A = 0;   // обнулить регистры на всякий случай
    TCCR1B = 0;
    OCR1A = 62499; // регистр совпадения А 
    TCCR1B |= (1 << WGM12);  // включить CTC режим 
    TCCR1B |= (1 << CS12); // коэффициент деления 256
    TIMSK1 |= (1 << OCIE1A);  // прерывание по совпадению timer1

Процедура ежесекундного прерывания по таймеру 1:

ISR(TIMER1_COMPA_vect) //1 Гц
{
  if (digitalRead(DP)==LOW) digitalWrite(DP,HIGH);
  else digitalWrite(DP,LOW);  //мигалка двоеточия часов
  if (digitalRead(LED)==LOW) digitalWrite(LED,HIGH);
  else digitalWrite(LED,LOW);  //мигалка встроенного светодиода
    count_sec++;  //независимый отсчет секунд
    flag_date=true;  //разрешение на запрос данных
   if (count_sec==60) count_sec=0;
}

Мигание встроенного светодиода здесь функционально не требуется (вывод 13 на схеме специально показан никуда не подключенным, хотя в модулях Arduino светодиод наличествует), он только сигнализирует о работоспособности программы после ее загрузки в контроллер.

Динамическая индикация, как описано в неоднократно упоминавшейся статье про LED-индикаторы, тактируется прерываниями 2-го таймера с частотой 244 Гц. Промежуточная инстанция в виде отдельного приемника позволяет синхронизировать запрос на получение данных и их прием с логикой работы динамической индикации: каждую минуту (например, когда секунды равны нулю) временно запрещаются прерывания таймера 2, проделываются все необходимые процедуры запроса, приема и обновления данных, и прерывания разрешаются вновь. Визуально это проявится в коротком мигании дисплея в момент смены минут.

Настройка прерываний Timer2 со скоростью 244 Гц в функции Setup():

// инициализация Timer2 244 Гц
    TCCR2A = 0;
    TCCR2B = 0;
    OCR2A = 64; // установка регистра совпадения 4 мс (244 Гц)
    TCCR2A = (1 << WGM21);  // CTC режим
    Timer2start (); //запуск Timer2

Функции Timer2start () и Timer2stop() нам понадобятся отдельно:

void Timer2stop() 
{ //стоп Timer2   
    bitClear(TIMSK2, OCIE2A);//запрещаем прерывания  по совпадению
 }

void Timer2start ()
{ //пуск Timer2 с делителем 1024
    TCCR2B = (1 << CS22) | (1 << CS21) | (1 << CS20); 
    bitSet(TIMSK2, OCIE2A);//разрешаем прерывания по совпадению
}

Процедура прерывания Timer2 для динамической индикации также подробно описана в вышеуказанной статье. Здесь она только дополнена выводом даты и дня недели на двухстрочный OLED-индикатор с помощью русифицированной библиотеки LiquidCrystal:

#include <LiquidCrystalRus_OLED.h>

Библиотека описана в давней статье по этому адресу, там же имеется ссылка на скачивание.

Остановимся теперь на алгоритме запроса и получения данных даты-времени. Для этого в начале программы объявляется библиотека Wire

#include <Wire.h> //библиотека для TWI

 В процедуре setup() для инициализации ведущего требуется строка:

 Wire.begin(); // join I2C bus as Master

 В основном цикле loop() в момент равенства секунд нулю делаем запрос, принимаем данные и реагируем на их содержимое (если на месте года стоит 0xFF, выводим прочерки). Манипуляции с флагом разрешения flag_date нужны во избежание повторных запросов, пока значение секунд равно нулю.

  if ((count_sec==1)&&(flag_date==true)){
    Timer2stop();
    delay(100);
    Wire.requestFrom(0x08,7);    // request 7 bytes from slave 0x08
    byte i=0;
    while(Wire.available()) {        // read response from slave 0x08
    byte bb = Wire.read();
    arr_t[i]=bb;
    i++;
  }
  if (arr_t[0]!=0xFF){
     Year = uint16_t(arr_t[0])+2000;
     Month = arr_t[1]; //month
     Day = arr_t[3]; //day
     DayWeek = arr_t[2]; //day week;
     ShowData();
     count_sec= arr_t[6]; //синхронизация секунд
  } else {
   OLED1.clear();
   OLED1.setCursor(0, 0);//начало верхней строки
   OLED1.print("- - - -   ");//прочерки
   OLED1.setCursor(0, 1);//начало нижней строки
   OLED1.print("- - - -   ");//прочерки
  }
    flag_date=false;
    Timer2start ();
  }//end if sek=1

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

В основном цикле, кроме того, каждую секунду обновляются значения разрядов индикаторов часов-минут (см. текст программы в архиве). Если принят пустой массив, то вместо нулей а разрядах выводятся прочерки. Поскольку процедура короткая во времени, она не влияет на процесс динамической индикации, и нет нужды ее привязывать к более длинным промежуткам времени.

 Архив с программами