Общие сведения
Электромеханические часы — часы собранные на основе сервоприводов, Arduino Nano и Trema-модуля часов реального времени. Сервоприводы потребляют электроэнергию только во время переключения, поэтому мы сделаем часы на аккумуляторах.
Нам понадобится
Модули
- 1x Arduino Nano
- 1x Nano Shield
- 8x Trema-модуль Расширитель выводов, FLASH-I2C
- 5x i2c hub
- 1x Trema-модуль Реле
- 1x Модуль часов реального времени
- 4x Набор: Сделай сам #10 - «Механический циферблат»
- 1x Набор: Сделай сам #11 - «Двоеточие»
- 2x Понижающий DC-DC преобразователь LM2596
- 1x Батарейный отсек
- 2x Аккумулятор 18650
- Провода мама-мама
Библиотеки
Сборка механики часов
Соедините корпуса цифр чёрными винтами 10мм, которые идут в комплекте с каждой цифрой.
Подробнее про сборку одной цифры
Установка часов реального времени
Подключение
Для удобства подключения мы воспользуемся Trema Shield Nano Compact

Не забудьте выбрать плату Arudino NANO
в меню Инструменты -> Плата
Возможно понадобится выбрать ATmega328P (Old Bootloader)
в меню Инструменты -> Процессор
Скетч установки текущего времени
#include <iarduino_RTC.h> // Подключаем библиотеку iarduino_RTC для работы с модулями реального времени. // iarduino_RTC time(RTC_DS1302, 2, 3, 4); // Объявляем объект time для работы с RTC модулем на базе чипа DS1302, указывая выводы Arduino подключённые к выводам модуля RST, CLK, DAT. // iarduino_RTC time(RTC_DS1307); // Объявляем объект time для работы с RTC модулем на базе чипа DS1307, используется аппаратная шина I2C. iarduino_RTC time(RTC_DS3231); // Объявляем объект time для работы с RTC модулем на базе чипа DS3231, используется аппаратная шина I2C. // // Определяем системное время: // Время загрузки скетча. const char* strM="JanFebMarAprMayJunJulAugSepOctNovDec"; // Определяем массив всех вариантов текстового представления текущего месяца. const char* sysT=__TIME__; // Получаем время компиляции скетча в формате "SS:MM:HH". const char* sysD=__DATE__; // Получаем дату компиляции скетча в формате "MMM:DD:YYYY", где МММ - текстовое представление текущего месяца, например: Jul. // Парсим полученные значения в массив: // Определяем массив «i» из 6 элементов типа int, содержащий следующие значения: секунды, минуты, часы, день, месяц и год компиляции скетча. const int i[6] {(sysT[6]-48)*10+(sysT[7]-48), (sysT[3]-48)*10+(sysT[4]-48), (sysT[0]-48)*10+(sysT[1]-48), (sysD[4]-48)*10+(sysD[5]-48), ((int)memmem(strM,36,sysD,3)+3-(int)&strM[0])/3, (sysD[9]-48)*10+(sysD[10]-48)}; // void setup(){ // delay(300); // Ждем готовности модуля отвечать на запросы. Serial.begin(9600); // Инициируем передачу данных в монитор последовательного порта на скорости 9600 бод. time.begin(); // Инициируем работу с модулем. time.settime(i[0],i[1],i[2],i[3],i[4],i[5]); // Устанавливаем время в модуль: i[0] сек, i[1] мин, i[2] час, i[3] день, i[4] месяц, i[5] год, без указания дня недели. } // void loop(){ // if(millis()%1000==0){ // Если прошла 1 секунда. Serial.println(time.gettime("d-m-Y, H:i:s, D")); // Выводим время. delay(1); // Приостанавливаем скетч на 1 мс, чтоб не выводить время несколько раз за 1мс. } // } //
Установка адресов расширителей выводов
Адреса расширителей выводов можно установить двумя способами, в зависимости от ситуации:
1. Используя установщик адресов I2C
Об установке адресов при помощи установщика адресов можно узнать на странице установщика адресов
2. Используя Nano Shield Compact и Arduino Nano
Подключим модули расширителей по одному и установим адреса с 9 до 16 в десятеричной системе счисления. Для удобства подключения мы воспользуемся Trema Shield Nano Compact

Скетч установки адресов
// Подключаем библиотеку для работы с расширителем выводов. #include <iarduino_I2C_Expander.h> // Создаём объект iarduino_I2C_Expander gpio; // Функция очистки буфера последовательного порта void DiscardSerial() { while(Serial.available()) Serial.read(); } void setup() { // Инициируем последовательный порт. Serial.begin(9600); while(!Serial){;} delay(500); // Проверяем наличие модуля if (!gpio.begin()) { Serial.println("Модуль не найден, проверьте подключение."); return; } // Устанавливаем адрес while(true) { Serial.print("Текущий адрес в десятеричной системе: "); Serial.println(gpio.getAddress()); Serial.println( "Введите новый адрес и нажмите " "<Enter> или \"Отправить\"" ); DiscardSerial(); while(!Serial.available()); String newAddress = Serial.readStringUntil('\n'); newAddress.trim(); uint8_t newAddr = newAddress.toInt(); if (gpio.changeAddress(newAddr)) { Serial.println("Адрес изменён."); } else { Serial.println( "Адрес не изменён, " "проверьте подключение " "и нажмите \"RES\"" ); break; } } } void loop() { delay(1); }
Подключение
Для удобства подключения мы воспользуемся Trema-модулями i2c hub и Trema-модуль Расширитель выводов, FLASH-I2C.
В момент включения модулей, подключённых к реле, происходит скачок потребления тока и падение напряжения (микроконтроллер может перезагружаться от этого), поэтому мы подключили питание модулей цифр к отдельному преобразователю. Так же можно решить эту задачу резервуарным конденсатором и/или подобранным последовательным резистором на линии питания цифр (например: 1Ω, 2W).
Перед подключением DC-DC преобразователей необходимо установить напряжение в 5 вольт на их выходах при помощи подстроечного резистора.
Подключаем Arduino Nano, часы реального времени и i2c hub'ы

Подключаем сервоприводы (на примере 2-й цифры)

Калибровка сервоприводов
Для калибровки сервоприводов можно воспользоваться следующим скетчем.
После загрузки скетча, откройте монитор последовательного порта и следуйте инструкциям, которые будут туда выводиться. Вкратце: введённое число - новый угол сервопривода сегмента в положении ВКЛ., символ 'n' - переход к следующему сегменту. В конце калибровки программа выведет массив, который нужно будет вставить в основной скетч проекта часов.
Скетч калибровки:
// Подключаем библиотеку для работы с модулями расширителей выводов. #include <iarduino_I2C_Expander.h> // Подключаем библиотеку механических часов #include <MechaClock.h> #define REL_PIN 3 // Создаём массив объектов библиотеки расширителя выводов, указывая адреса // (не забудьте устонавить адреса расширителей) iarduino_I2C_Expander gpio[8]{9, 10, 11, 12, 13, 14, 15, 16}; /* Сегменты _a f|_|b e|g|c d^ */ /* Калибровка сервоприводов */ //!!! Сегменты E, B и G работают в обратном направлении !!! // Положение ВКЛ. сегментов uint8_t ON[DIGITS][SEGMENTS] { {// ЦИФРА 1 (крайняя левая) // g f e d c b a 90, 10, 90, 10, 10, 90, 10 }, {// ЦИФРА 2 // g f e d c b a 90, 10, 90, 10, 10, 90, 10 }, {// ЦИФРА 3 // g f e d c b a 90, 10, 90, 10, 10, 90, 10 }, {// ЦИФРА 4 (крайняя правая) // g f e d c b a 90, 10, 90, 10, 10, 90, 10 } }; // Положение ВЫКЛ. сегментов uint8_t OFF[DIGITS][SEGMENTS] { { // g f e d c b a 10, 90, 10, 90, 90, 10, 90 }, { // g f e d c b a 10, 90, 10, 90, 90, 10, 90 }, { // g f e d c b a 10, 90, 10, 90, 90, 10, 90 }, { // g f e d c b a 10, 90, 10, 90, 90, 10, 90 } }; Digit myDigits[DIGITS]; // Функция очистки буфера последовательного порта void discardSerial() { while(Serial.available()) Serial.read(); } void setup() { pinMode(REL_PIN, OUTPUT); digitalWrite(REL_PIN, HIGH); Serial.begin(9600); Serial.println(F("Скетч калибровки сервоприводов механических часов.")); Serial.println(); Serial.println(F("Адреса расширителей выводов в десятеричной системе:")); bool flag = false; for (auto& i:gpio) { if(i.begin()) { Serial.print(i.getAddress()); Serial.print("\t"); } else { Serial.print("failed"); Serial.print("\t"); flag = true; } } Serial.println(); if (flag) { Serial.println( F("Один из расширителей не обнаружен." " Проверьте подключение и адрес " "модуля и запустите скетч заново.") ); goto exit; } for (size_t i = 0; i < DIGITS; i++) { myDigits[i] = Digit(gpio, i); } for (auto& i:myDigits) { i.set('8'); } Serial.println( F("Все сервоприводы сегментов установлены в положение\r\n" "ВКЛ. Если вы уже выполняли это шаг, пропустите его\r\n" ", если нет, отключите питание и установите качалки\r\n" "сервоприводов и сегменты цифр. Качалки должны\r\n" "\"смотреть\" насколько это возможно вверх от \r\n" "оснований (цифры показывают восьмёрки)") ); delay(1000); Serial.println(); Serial.println(F("Карта сегментов:")); Serial.println(); Serial.println(F(" A")); Serial.println(F(" #####")); Serial.println(F(" # #")); Serial.println(F(" F # # B")); Serial.println(F(" # G #")); Serial.println(F(" #####")); Serial.println(F(" # #")); Serial.println(F(" E # # C")); Serial.println(F(" # D #")); Serial.println(F(" #####")); Serial.println(); Serial.println( F("Для продолжения введите любой символ в поле ввода" " и нажмите \"Отправить\" или <Enter>.") ); while(!Serial.available()); Serial.println(F("Начинаем процесс калибровки. Для выхода из калибровки" " введите \"exit\" и нажмите <Enter> или \"Отправить\"")); Serial.println(F("Для дострочного вывода углов сервоприводов и выхода" " введите \"printout\" и нажмите <Enter> или \"Отправить\"")); delay(2000); uint8_t dig = 0; uint8_t seg = SEGMENTS; char current_seg = 'A'; do { Serial.println("Калибруем сегмент" + String(current_seg) + " " + String(dig+1) + "-й цифры"); uint8_t on_deg = 0; if (current_seg == 'B' || current_seg == 'E' || current_seg == 'G') Serial.println(F("Угол сервопривода этого сегмента должен" "быть ближе к 90 градусам в положении ВКЛ.")); else Serial.println(F("Угол сервопривода этого сегмента должен" "быть ближе к 0 градусам в положении ВКЛ.")); Serial.print(F("текущее значение: ")); Serial.println(ON[dig][seg-1]); Serial.println( F("Введите новое значение... (если калибровка" " текущего сегмента закончена, введите \"n\"" " и нажмите <Enter> или \"Отправить\")") ); // Опустошаем буфер последовательного порта discardSerial(); // Ждём ввода пользователя while(!Serial.available()); // Читаем введённые данные String s = Serial.readStringUntil('\n'); // Удаляем непечатные символы s.trim(); // Выходим из калибровки по желанию if (s == "exit") goto exit; // Переходим к следующему сегменту else if (s == "n") { seg--; current_seg++; } else if (s == "printout") goto printout; // Записываем новые показания в массив else { on_deg = uint8_t(s.toInt()); ON[dig][seg-1] = on_deg; myDigits[dig].set('8'); } if (seg == 0) { dig++; seg = SEGMENTS; current_seg = 'A'; } } while(dig != DIGITS); printout: Serial.println(); Serial.println(F("Калибровка завершена.")); Serial.println(); Serial.println(F("Замените следующие данные в основном скетче:")); Serial.println(); Serial.println(F("uint8_t ON[DIGITS][SEGMENTS] {")); Serial.println(F("\t{ ЦИФРА 1 (крайняя левая)")); int k = 1; bool first_dig = true; for (auto& i:ON) { if (!first_dig) { k++; Serial.println(); Serial.println("\t},"); Serial.print("\t{// ЦИФРА "); Serial.println(k); } first_dig = false; Serial.println(F("\t//\tg f e d c b a")); Serial.print("\t\t"); bool first_seg = true; for (auto& j:i) { if (!first_seg) Serial.print(", "); first_seg = false; Serial.print(j); } } Serial.println(); Serial.println("\t}"); Serial.println("};"); exit: Serial.println(); Serial.println(F("Выходим из калибровки.")); // "Отпускаем" все серво всех цифр for (auto& i:myDigits) i.release(); } void loop() { delay(1000); }
Скетч проекта
// Подключаем библиотеку для работы с модулями расширителей выводов. #include <iarduino_I2C_Expander.h> // Подключаем библиотеку для работы с модулями реального времени. #include <iarduino_RTC.h> // Подключаем файл с объектом механических часов. #include <MechaClock.h> // Вывод реле или силового ключа #define REL_PIN 3 // Создаём массив объектов библиотеки расширителя выводов, указывая адреса iarduino_I2C_Expander gpio[8]{9, 10, 11, 12, 13, 14, 15, 16}; // Объявляем объект time для работы с RTC модулем на базе чипа DS3231 iarduino_RTC time(RTC_DS3231); /* Калибровка сервоприводов */ // Положение ВКЛ. сегментов, этот массив необходимо заменить // массивом, который вывел скетч калибровки uint8_t ON[DIGITS][SEGMENTS] { {// ЦИФРА 1 (крайняя левая) // g f e d c b a 90, 10, 90, 10, 10, 90, 10 }, {// ЦИФРА 2 // g f e d c b a 90, 10, 90, 10, 10, 90, 10 }, {// ЦИФРА 3 // g f e d c b a 90, 10, 90, 10, 10, 90, 10 }, {// ЦИФРА 4 // g f e d c b a 90, 10, 90, 10, 10, 90, 10 } }; // Положение ВЫКЛ. сегментов uint8_t OFF[DIGITS][SEGMENTS] { { // g f e d c b a 10, 90, 10, 90, 90, 10, 90 }, { // g f e d c b a 10, 90, 10, 90, 90, 10, 90 }, { // g f e d c b a 10, 90, 10, 90, 90, 10, 90 }, { // g f e d c b a 10, 90, 10, 90, 90, 10, 90 } }; // Создаём объект механических часов, и передаём указатель на массив // объектов расширителей выводов MechaClock myClock(gpio); constexpr unsigned long timer = 60000; constexpr uint8_t NONE = 0; static unsigned long currMillis = 0; // Функция включения реле или силового ключа void activate() { // Переводим вывод к которому подключено реле в режим "выход" pinMode(REL_PIN, OUTPUT); // Устанавливаем высокий логический уровень (включаем реле) digitalWrite(REL_PIN, HIGH); delay(300); } // Функция выключения реле void deactivate() { delay(300); // Переводим вывод реле в режим "вход" (выключаем реле) pinMode(REL_PIN, INPUT); } // Функция ожидания переключения минуты void waitaminute() { // Бесконечный цикл while(true) { // Узнаём текущее время time.gettime(); // Выводим время на табло myClock.print(time.Hours, time.minutes, TIME); // Если минута только что переключилась if (time.seconds == 0) { // Записываем millis переключения currMillis = millis(); // Возвращаемся в вызывающую функцию return; } delay(100); } } // Функция вывода номера не найденного расширителя выводов на встроенный светодиод. // Количество миганий перед паузой - номер расширителя в массиве (не адрес!) void troubleBlink(uint8_t err) { while(true) { for (uint8_t i = 0; i < err; i++) { digitalWrite(LED_BUILTIN, HIGH); delay(300); digitalWrite(LED_BUILTIN, LOW); delay(300); } delay(1000); } } void setup() { pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, LOW); activate(); // Проходим по всем расширителям выводов uint8_t j = 0; for (auto& i:gpio) { j++; // Инициируем текущий расширитель if(!i.begin()) { // Если инициализация не прошла - мигаем светодиодом deactivate(); troubleBlink(j); } } // Инициируем модуль часов реального времени time.begin(); time.blinktime(NONE); // Даём модулю время на инициализацию delay(300); // Вызываем функцию ожидания перехода минуты waitaminute(); // Выключаем реле deactivate(); } void loop() { // Если прошла минута... if (abs(millis() - currMillis) > timer) { currMillis = millis(); // Включаем питание расширителей и сервоприводов activate(); // Инициируем расширители for (auto& i:gpio) i.begin(); // Инициируем модуль часов реального времени //time.begin(); // Даём модулям время на инициализацию delay(300); // Узнаём текущее время time.gettime(); // Выводим текущее время myClock.print(time.Hours, time.minutes, TIME); // Выключаем питание расширителей и сервоприводов deactivate(); } delay(100); }
Обсуждение