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

Не забудьте выбрать плату Arudino NANO в меню Инструменты -> Плата
Возможно понадобится выбрать ATmega328P (Old Bootloader) в меню Инструменты -> Процессор
Скетч установки текущего времени
#include <Wire.h> // Подключаем библиотеку для работы с аппаратной шиной I2C, до подключения библиотеки iarduino_RTC.
#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

Скетч установки адресов
// Подключаем библиотеку для работы с аппаратной шиной I2C, до подключения библиотеки iarduino_I2C_Expander.
#include <Wire.h>
// Подключаем библиотеку для работы с расширителем выводов.
#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' - переход к следующему сегменту. В конце калибровки программа выведет массив, который нужно будет вставить в основной скетч проекта часов.
Скетч калибровки:
// Подключаем библиотеку для работы с аппаратной шиной I2C, до подключения библиотеки iarduino_I2C_Expander.
#include <Wire.h>
// Подключаем библиотеку для работы с модулями расширителей выводов.
#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);
}
Скетч проекта
// Подключаем библиотеку для работы с аппаратной шиной I2C, до подключения библиотеки iarduino_4LED.
#include <Wire.h>
// Подключаем библиотеку для работы с модулями расширителей выводов.
#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);
}
Ссылки
Наборы
Бибилиотеки
3D модели для самостоятельной печати
Инструкция по сборке одной цифры
Комплект для повторения проекта
Что понадобится
Это быстрый ориентир по основным модулям и деталям. Полный список для повторения проекта смотрите в самой статье.
23 позиции
Микросервопривод TianKongRC TS90A
Аналоговый сервопривод с углом поворота от 0 до 180° и крутящим моментом 1,3 кг·см
Открыть в iArduino
Arduino Nano (USB CH340 / С ногами)
Платформа c микроконтроллером ATmega328 на ядре AVR с чипом USB CH340 для разработки электронных устройств на языке C++
Открыть в iArduino
Часы реального времени RTC DS3231 (Trema-модуль)
Электронный энергонезависимый хронометр на чипе DS3231
Открыть в iArduino
Trema Shield NANO Compact
Компактный Shield для Arduino Nano с колодкой G-V-S.
Открыть в iArduino
Piranha Trema (C колодками)
Аналог Arduino Pro Mini c микроконтроллером ATmega328P в форм-факторе Trema с колодками G-V-S для удобного подключения Trema-модулей
Открыть в iArduino
Преобразователь интерфейсов Piranha USB-UART (FT230 / USB Micro)
Конвертер сигналов на чипе FT230 для прошивки и отладки контроллеров без собственного USB
Открыть в iArduino
Расширитель выводов, FLASH-I2C (Trema-модуль)
Trema-модуль Расширитель выводов, FLASH-I2C
Открыть в iArduino
i2C Hub (Trema-модуль)
Будет полезен в проектах с большим количеством устройств с интерфейсом I2C
Открыть в iArduino
Реле (Trema-модуль v2.0)
Позволяет коммутировать цепи как переменного, так и постоянного тока до 10 А.
Открыть в iArduino
Сделай сам #10 - «Механический циферблат» для часов
Сделай сам #10 - «Механический циферблат»
Открыть в iArduino
Сделай сам #11 - «Двоеточие»
Открыть в iArduino
Понижающий DC-DC Преобразователь LM2596
Понижающий регулируемый DC-DC преобразователь (импульсный стабилизатор напряжения ) на LM2596
Открыть в iArduino
Понижающий преобразователь на 5В. 2А.
Понижающий преобразователь на 5В
Открыть в iArduino
Источник питания на 9В. 1А.
Источник питания 9 вольт, 1 ампер, центр положительный, DC jack 5,5*2,5 мм
Открыть в iArduino
Источник питания на 12В. 3А.
Источник питания 12 вольт, 2 ампера, центр положительный, DC jack 5,5*2,1 мм
Открыть в iArduino
Силиконовый провод 20AWG (1 м / Красный)
Многожильный мягкий красный провод сечением 0,5 мм² и длиной 1 м
Открыть в iArduino
Силиконовый провод 20AWG (1 м / Чёрный)
Многожильный мягкий чёрный провод сечением 0,5 мм² и длиной 1 м
Открыть в iArduino
Соединительные силиконовые провода «мама-мама» (20 шт. / 20 см)
Пучок из 20 мягких силиконовых проводов «мама-мама» длиной 20 см для прототипирования электронных устройств
Открыть в iArduino
Шлейф Dupont «Мама – Мама» (4 pin, 20 см)
Шлейф из четырёх проводов длиной 20 см с коннекторами Dupont Female
Открыть в iArduino
Коннектор power jack Мама с клемником
Коннектор power jack
Открыть в iArduino
Батарейный отсек 2×18650
Держатель для двух элементов питания форм-фактора 18650
Открыть в iArduino
Соединительные провода «мама-мама» (40 шт. / 20 см)
Шлейф из 40 проводов «мама-мама» длиной 20 см для прототипирования электронных устройств
Открыть в iArduino
Установщик адресов FLASH-I2C
Позволяет быстро менять адреса модулей Flash-i2c, узнавать количество и адреса i2c модулей на шине.
Открыть в iArduino
