КОРЗИНА
магазина
8 (499) 500-14-56 | ПН. - ПТ. 12:00-18:00
ЛЕСНОРЯДСКИЙ ПЕРЕУЛОК, 18С2, БЦ "ДМ-ПРЕСС"

Электромеханические часы

Общие сведения

Электромеханические часы — часы собранные на основе сервоприводов, Arduino Nano и Trema-модуля часов реального времени. Сервоприводы потребляют электроэнергию только во время переключения, поэтому мы сделаем часы на аккумуляторах.

Нам понадобится

Комплект с питанием от сети:

Это оптимальный вариант питания для данных часов.

Комплект с питанием от аккумуляторов:

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

Библиотеки

Сборка механики часов

Соедините корпуса цифр чёрными винтами 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 модели для самостоятельной печати

Инструкция по сборке одной цифры




Обсуждение

Гарантии и возврат Используя сайт Вы соглашаетесь с условями