- Суть проекта,
- Составляющие и комплектующие,
- Немного о работе с двуосным шаговиком,
- Дизайнерские трудности
- Вывод,
- FAQ.
Суть проекта
Идея не нова, но давно зрело желание ее воплотить собственными силами. Эффектное, редкое и, к тому же, весьма интересное с технической, программной и дизайнерской точки зрения устройство прямо таки бросило нам вызов и он был принят.Итак, большие цифровые часы состоят из множества маленьких аналоговых, то есть стрелочных. Например, такой - пожалуй, самый крупный из подобных - образец выставлялся на выставке дизайна в Дубаи в 2013 году. Панно составлено аж из 288 часов. Строго говоря, маленькие часы не являются часами, так как обе стрелки имеют абсолютно одинаковую форму, а значит, отличить минутную стрелку от часовой нельзя. Пытаясь подобрать более точное определение, выразимся так: механический пиксель часового типа. Но сам принцип построения изображения, а особенно процесс перехода одной картинки в другую, выглядит очень впечатляюще и необычно. Разумеется, при помощи часовых пикселей можно формировать и другие фигуры, а также несложную анимацию, но часы - это классика, некоторый практический смысл, да и одно другому не мешает.
Прикинув объем работ и материалов, мы решили, что для начала не станем замахиваться на Дубайские масштабы, попробуем сделать что-то поменьше. Ну как поменьше, из минимального количества пикселей, которыми можно изобразить уверенно читаемые цифры. Таким количеством является 6 пикселей на одну цифру. Итого потребуется минимум 26 аналоговых часов для создания одних цифровых. Даже в таком размере, конечный результат обещает получиться шикарным, свежим и интересным.
Составляющие и комплектующие
Очевидно, что самым необычным компонентом часов из часов должен стать механизм нашего механического пикселя. Это должен быть точный мотор, точнее два мотора с соосными валами. Простые моторы, сервоприводы и обычные шаговики на эту роль не годились, во всяком случае без сложных доработок и дополнительных редукторов, рычагов, датчиков и так далее. Изобретение собственной механики слишком усложнило бы и так непростой проект, непозволительно увеличило расходы средств и времени, поставив под угрозу его осуществление. Очень кстати на просторах Интернета нашелся готовый к применению двуосный шаговый двигатель, практически то, что нам и нужно. Да, он небольшой, всего 6х3 см, не очень мощный, что ограничивает размер стрелок, а значит и всех часов. Но, по нашим подсчетам, один аналоговый пиксель без перегрузок на двигатель может составить порядка 120 см в диаметре, а то и больше, что для нашего проекта подходит почти идеально. Подобрать остальные комплектующие уже не составило труда. Время хранится и отсчитывается в модуле ЧРВ на базе DS3231 с батарейкой на борту для полной энергонезависимости. На роль вторичных часов взяли простой и надежный индикатор на TM1637. Для непрофессиональных часовщиков поясним. Первичные часы - это именно то, ради чего существует и на что работает весь механизм часов, то есть именно то, что видит сторонний наблюдатель. Вторичные часы нужны для установки времени, регулировок и прочей обратной связи часов с техником-настройщиком. Их сторонний наблюдатель не видит, но их существование важно и как минимум здорово упрощает и ускоряет процесс обслуживания.В качестве мозга часов берем Ардуино Нано, она компактна, неприхотлива и достаточно мощна для этой цели. Пара кнопок, блок питания, пригоршня радиодеталей, корпус для защиты от внешней среды и готово.
Если про то, как работать с ЧРВ, индикатором и тем более кнопками даже начинающего DIY-мастера много вопросов не возникнет, то вот про оживление необычного шаговика, да ещё и двойного, можно рассказать немного подробнее.
Необходимы модули для Arduino?
Купить модули Ардуино можно в нашем магазине 3DIY онлайн!
Немного о работе с двуосным шаговиком
Мотор состоит из двух принципиально одинаковых независимых шаговых двигателей с двумя электромагнитными катушками каждый. Один крутит внутренний длинный вал, другой внешний короткий.Шаговики расположены в одном корпусе и развернуты относительно друг друга на 180 градусов. Подавая на катушки напряжение в определенной последовательности и полярности, можно добиться вращения валов в ту или другую сторону. Интервал смены питания влияет на скорость вращения. Максимальная частота, согласно даташиту, составляет 500Гц. Один импульс проворачивает вал на 0.33 градуса, значит в теории максимальная скорость шаговика может достигать 165 градусов в секунду, запомним это. Однако лучше выжимать из него все соки и крутить чуть медленнее, это положительно скажется на точности работы и времени эксплуатации.
Последовательность сигналов хорошо описана в даташите и даже изображена в виде графика. На первый взгляд ничего непонятно, но если разобраться, не так уж сложно. Схема делится на так называемые Атомарные, то есть минимально возможные шаги (Partial Step), три подряд превращаются в Полный шаг (Full Step), шесть Атомарных, или два Полных в один Цикл (One Cycle). Конец Цикла означает, что мы вернулись к тому же состоянию на магнитах, с которого начали. Прогоняя Цикл в одну сторону, вращаем вал влево, прогоняя в обратную, вращаем вправо. Мы можем остановиться и/или сменить направление в любой момент, на любом шаге. Однако, лучше прогонять Циклы полностью, так проще не запутаться, легче запоминать состояние на магнитах и меньше вероятность сбиться. Один Цикл проворачивает вал на 2 градуса и этой точности более чем достаточно для нашей задачи.
Техническое отступление специально для перфекционистов. Если например в других проектах потребуется еще более мелкое изменение угла вала, даже меньше одного Атомарного шага (0.33 градуса), можно воспользоваться системой микрошагов, когда каждый Атомарный шаг разбивается еще на 4 (или более) с разным уровнем напряжения. То есть цифровых сигналов 0В - +5В будет недостаточно, потребуются аналоговые. По 4 на один вал, 8 на один пиксель. Это на порядок сложнее и потребуется очень серьезная причина для необходимости осуществления подобного управления. Вернемся к реальности и немного сосредоточимся, чтобы понять принцип работы описанный далее. Катушка UL, согласно схеме показанной выше, запитана от контактов 1 и 2, а катушка UR от 4 и 3. Возьмем первый Атомарный шаг, согласно схеме это +5В на UL и на UR тоже, то есть +5 на контактах 1 и 4, и 0 (GND) на контактах 2 и 3. Именно при таком питании катушки будут приведены в должное состояние первого Атомарного шага. Даем паузу не менее 2 мс, ибо чтим даташит и помним про максимальную частоту в 500 Гц. Шаг второй. На катушке UR все без изменений, то есть контакты 3 и 4 не трогаем, а вот на UL видим общий ноль, значит контакты 1 и 2 следует привести в одинаковое состояние, или оба в плюс или оба в ноль. Чтобы не было разницы с графиком и путаницы, будем приводить оба в ноль. Готово. Пауза. Шаг третий. Теперь приводим в ноль оба контакта катушки UR, а на UL по графику видим -5В. Это значит, что следует подать питание с обратной полярностью: на контакт 2 подаем +5В, а на контакт 3 - GND. Пауза. И по той же логической схеме переключаем магниты по остальным трем шагам. Вот и все, Цикл пройден, вал провернулся на 0.33 градуса! Остается повторить столько раз, сколько нужно для заданного угла поворота. Напомню, что для вращения вала в другую сторону следует всего лишь прогнать цикл в обратном направлении, то есть от шага 6 до 1. Вот так все просто, оказывается.
Давайте создадим простой программно-аппаратный драйвер для одного шаговика при помощи одной платы Ардуино, благо двигатель наш потребляет всего 20 mA на контакт, а значит не перегрузит возможности пинов без дополнительных устройств. Но сперва нарисуем в любом 3D редакторе и напечатаем на любом 3D принтере подходящие по размеру стрелки, чтобы было хорошо видно, как вращаются валы. Подключаем контакты шаговика к любым восьми пинам. Например со второго по девятый включительно. Пин 2 к контакту 1 мотора А, пин 3 к контакту 2 и так далее, начиная с пина 6 подключаемся к контакту 1 мотора В и до конца.
Дело за малым - написать программу, которая будет подавать на магниты импульсы в правильной последовательности. Чуть усложним и напишем программу для программы, по которой стрелки будут циклически вращаться на заранее заданные нами углы в заранее заданные нами стороны. Углы и стороны зададим абсолютно хаотично и разные для разных стрелок, а значит одна из них иногда будет дожидаться вторую. Все это должно быть предусмотрено в программе.
byte pinA[4] = {2, 3, 4, 5}; // шаговик А byte pinB[4] = {6, 7, 8, 9}; // шаговик B byte bitMapL[2][6] = { // сигналы на магниты A по шагам 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0 }; byte bitMapR[2][6] = { // сигналы на магниты В по шагам 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0 }; byte progA[2][5] = { 180, 90, 45, 45, 90, // углы, на которые следует циклично вращать стрелку A 0, 1, 0, 0, 1 // направления для этих углов }; byte progB[2][5] = { 180, 90, 90, 45, 90, // углы, на которые следует циклично вращать стрелку B 0, 0, 0, 1, 0 // направления для этих углов }; int stepA = 0; // градусов для А int stepB = 0; // градусов для В byte revA = 0; // направление А byte revB = 0; // направление В byte ok = 1; // флаг "выполнено", устанавливается драйвером void setup() { for (byte i = 0; i < 4; i++) { pinMode(pinA[i], OUTPUT); pinMode(pinB[i], OUTPUT); } } void loop() { static byte pr = 0; driver(); // драйвер if (ok) { // если заданные углы на валах еще не достигнуты (на обоих), шагаем дальше stepA = progA[0][pr]; stepB = progB[0][pr]; revA = progA[1][pr]; revB = progB[1][pr]; } else { pr = pr == 4 ? 0 : pr + 1; // если дошагали, переключаемся на новые цели и шагаем к ним ok = 0; } } void driver() { // драйвер, функция установки сигналов на магниты static unsigned long timer; static byte st = 0; // часть шага if (!stepA && !stepB) { // если все сделано устанавливаем флаг в единицу ok = 1; return; } if (timer > millis()) return; byte stA = revA ? st : 5 - st; byte stB = revB ? 5 - st : st; digitalWrite(pinA[0], bitMapL[0][stA]); digitalWrite(pinA[1], bitMapL[1][stA]); digitalWrite(pinA[2], bitMapR[0][stA]); digitalWrite(pinA[3], bitMapR[1][stA]); digitalWrite(pinB[0], bitMapL[0][stB]); digitalWrite(pinB[1], bitMapL[1][stB]); digitalWrite(pinB[2], bitMapR[0][stB]); digitalWrite(pinB[3], bitMapR[1][stB]); if (st == 5) { st = 0; stepA = !stepA ? 0 : stepA - 1; stepB = !stepB ? 0 : stepB - 1; } else { st++; } timer = millis() + 5; // пауза 5 мс }
Запускаем и, если все правильно собрали, видим что-то подобное. Стрелки вращаются плавно, тихо, со скоростью примерно 64 градуса в секунду. Почему именно с такой скоростью и как ее изменить? Ответ предлагаю найти самостоятельно. Тонкая подсказка: все дело в паузе.Если сделать одну стрелку короче, хотя бы временно, и добавить циферблат, то получатся вполне себе настоящие аналоговые часы, готовые показать любое время в любой момент. Например, в разных часовых поясах.
Казалось бы, что еще нужно? Нужна точка отсчета. Сейчас стрелки вращаются на заданные углы, физически начиная с того места, на котором стояли, когда запустили программу. Они могли быть направлены вверх, вниз, в любую сторону, причем не обязательно в одну. Датчика положения у стрелок нет. Но для того, чтобы всегда устанавливать стрелки в нужное положение, мы должны обязательно знать в каком положении они находятся при запуске программы.
Тут два варианта. Первый очевидный и простой: перед запуском всегда вручную выкручиваем их в начальное положение. А если стрелки за стеклом? Еще и на улице? Выкрутить один раз, а потом выставлять в начальное перед выключением. А если сбой в питании? А хаотичные импульсы при включении? А если стрелка зацепится за что-нибудь хоть разок? Вариант второй: при запуске крутить их в определенном направлении пока гарантированно не упрутся в ограничители, это и будет начальным положением. К слову, ограничители в этих шаговиках уже имеются, именно для этой цели. Второй вариант единственно верный, только так можно всегда быть уверенным, что стрелки не разъедутся в неизвестные стороны. Для проекта с часами будем пользоваться этим методом, назовем его “выравнивание”.
Теперь все? Нет, не все. Дело в том, что для одного лишь шаговика, то есть пикселя, требуется аж 8 пинов контроллера. В проекте участвует 24 пикселя, умножаем, получаем цифру 192. Напомню, что у нас управляет часами Нано, у которой пинов и трех десятков не наберется, это без учета того, что нужно и кнопки, и индикатор куда-то подключать. Вспоминаем классический способ размножить цифровые пины - конечно же использовать выходные сдвиговые регистры 74HC595. Три пина передают данные на 8 пинов регистра. Регистры собираются в цепочку почти неограниченной длины, таким образом из трех управляющих пинов Ардуино мы можем получить сколько угодно пинов на регистрах. Нам надо вполне определенное количество 192/8 = 24 регистра, что и так понятно, один регистр на один пиксель, так уж удачно совпало, что и у того, и у другого по 8 ног.
Для компактности, упрощения сборки и минимизации ошибок, тратим некоторое количество времени на создание печатной платы для одного пикселя. Немного переделываем программу, внеся изменения в драйвер. Вместо прямой установки импульсов на пины, отправляем команды с этими импульсами на регистр. Ну и крутим быстрее раза в два, для проверки возможностей. Попутно выясняется, что при подаче питания регистры “трепещут”, то есть долю секунды выдают на шаговики хаотичные сигналы. Вместе с ними “трепещут” и стрелки, немного асимметрично смещаясь в непредсказуемую сторону. Данный факт дополняет важности к процедуре выравнивания.
Приближаясь к финалу, для дальнейшей сборки прототипа берем первый попавшийся под руку, но подходящий по размерам кусок фанеры или картона и размещаем на нем шесть пикселей для первой цифры. Соединяем их проводами в “паровозик” в правильном порядке.
Множим программу на шесть шаговиков, добавляем предварительное выравнивание, прописываем положения каждой стрелки каждого пикселя для каждой из 10 цифр и не только. Любуемся.
Обратите внимание на процедуру выравнивания. Левые пиксели поднимаются влево, правые вправо. Именно там, вне рабочего поля, находятся ограничители. Запомните этот важный момент.
Таким образом, одну цифру или, выражаясь технически грамотно, разряд часов мы изготовили и запустили. Можно считать, что осталась совсем чепуха, собрать еще три разряда, поставить все четыре в ряд и вот они - полноразмерные часы. Окончательный дизайн имеет возможность отличаться, в зависимости от гениальности исполнителя или заказчика. Можно разместить пиксели на одном поле, как в Дубайском примере, можно их пристроить на четырех отдельных полях, по одному на разряд, а можно и каждый пиксель заключить в свой квадратный, круглый или многогранный корпус, стилизованный, например, под стрелочные часы. У кого на что хватит фантазии. А вот начинка для этого уже готова.
Вот тут надо картинку готовых часов с несколькими комбинациями цифр. Видео лучше разместить в конце следующей главы.
Если потребуются комплектующие для 3d принтера
Купить запчасти для 3д принтера можно у нас в магазине 3DIY
Дизайнерские трудности
В процессе созидания мы внезапно столкнулись с небольшой проблемой, на удивление не связанной с механикой или с электроникой, и даже не с программированием. Еще на этапе моделирования выяснилось, что далеко не все цифры можно красиво и читаемо изобразить при помощи шести стрелочных пикселей. Цифры: 0, 2, 3, 5, 6, 8 и 9 трудностей не вызвали, а вот при формировании 4, 7 и, особенно, 1 появились “лишние элементы”, которые портят симметрию, ухудшают читаемость и внешний вид вообще.Примечательно, что на тех немногих картинках, которые мы нашли в Интернете, именно этих трудных цифр никогда не было видно. Как выкручивались, авторы доподлинно неизвестно. Единственный раз в кадр попала четверка, где линий пиксель был просто стыдливо повернут в дальнюю сторону от самой цифры.
Нам показалось, что такой компромисс для нас неприемлем и мы решили поискать собственное решение. Тут очень кстати вспомнились ограничители, по которым выравниваются стрелки. Как уже говорилось, они расположены вне рабочего поля, а значит, эти места никак не участвуют при формировании цифр. Стало быть, можно их использовать для сокрытия “лишних” стрелок, установив в этих местах крышки с тем же цветом или фрагментом рисунка, что и на фоне.
Пробуем с единицей. Да, хвостики на осях торчат, от этого никуда не деться, но цифра стала намного ровнее и понятнее, чем раньше.
Четверка и семерка, даже на ранних этапах проектирования, тоже стали выглядеть куда более правильно. Хвостики их тоже не сильно испортили.
В итоге мы пришли к горизонтально расположенным крышкам, комбинированным с ограничителями. Варианты, понятное дело, отличаются для левых и правых пикселей, то есть стрелочные часики уже не все абсолютно одинаковы, а разбиты на две группы, что немного усложняет изготовление и монтаж, но это того стоит. Таким образом, преодолев все приключения и победив все трудности, мы пришли к тому, к чему стремились - изготовили собственный первый образец редчайших в мире “часов из часов”, который реально работает. Спасибо всем, кто принимал участие и просто болел за успех этого проекта. Ниже вы можете посмотреть полный скетч, при необходимости его доработать.
// 3DIY (3d-diy.ru) // управление // 1 кнопка >1 сек - парковка, >5 сек выравнивание // 2 кнопка >1 сек - установка часов: 1 кнопка смена цифры, 2 кнопка подтверждение, последняя цифра - запись с 00 сек #include <TM1637Display.h> #include <Wire.h> #include "ds3231.h" // первый #define DATA_PIN_1 2//0 #define LATCH_PIN_1 4//1 #define CLOCK_PIN_1 3//2 // второй #define DATA_PIN_2 5//0 #define LATCH_PIN_2 7//1 #define CLOCK_PIN_2 6//2 #define BUT1 A0 #define BUT2 A1 #define Msteppers 12 #define CLK 9//A4 #define DIO 8//A5 byte cd = 0; TM1637Display display(CLK, DIO); struct ts t; #define itSinh 3600 volatile unsigned long sSinh = 0; struct its { uint8_t sec; uint8_t min; uint8_t hour; uint8_t wday; }; struct its it; #define timeSens 100 #define BLD 3000 #define incr 5 byte flag_B1D; byte flag_B1U; byte flag_B1LD; byte flag_B2D; byte flag_B2U; byte flag_B2LD; byte flag_B2SU; byte maxDig[4] = {2, 9, 5, 9}; const byte DP[] = { 0b00000000, 0b01011110, 0b01110011, 0b00000000, }; byte drive[6] = { 0b00001001, 0b00001000, 0b00000010, 0b00000110, 0b00000100, 0b00000001 }; /*byte drive[6] = { 0b00000001, 0b00000100, 0b00000110, 0b00000010, 0b00001000, 0b00001001 };*/ struct stepper { byte stepA = 0; byte stepB = 0; byte revA = 0; byte revB = 0; byte ok = 0; byte absGrA = 90; byte absGrB = 90; byte setGrA = 90; byte setGrB = 90; byte comb = 0x00; byte oldComb; byte oldRevB; byte oldRevA; byte st = 0; void setGr(byte A, byte B) { setGrB = B; setGrA = A; //if (!ok) return; byte a; if ((absGrA > setGrA)) { a = (absGrA - setGrA); if (a > 90) { stepA = 180 - a; revA = 0; } else { stepA = a; revA = 1; } } else if (setGrA > absGrA) { a = (setGrA - absGrA); if (a > 90) { stepA = 180 - a; revA = 1; } else { stepA = a; revA = 0; } } if ((absGrB > setGrB)) { a = (absGrB - setGrB); if (a > 90) { stepB = 180 - a; revB = 0; } else { stepB = a; revB = 1; } } else if (setGrB > absGrB) { a = (setGrB - absGrB); if (a > 90) { stepB = 180 - a; revB = 1; } else { stepB = a; revB = 0; } } if (oldRevB != revB) stepB++; // if (oldRevA != revA) stepA++; absGrA = setGrA; absGrB = setGrB; oldRevB = revB; oldRevA = revA; ok = 0; } void go() { comb = 0x00; // static byte st = 0; if (ok) { comb = oldComb; return; } if (!stepA && !stepB) { st = 0; ok = 1; comb = oldComb; return; } byte stA = revA ? st : 5 - st; byte stB = revB ? 5 - st : st; if (!stepA) stA = 5; if (!stepB) stB = 5; comb = drive[stB] | (drive[stA] << 4); oldComb = comb; if (st == 5) { st = 0; stepA = !stepA ? 0 : stepA - 1; stepB = !stepB ? 0 : stepB - 1; } else { st++; } } }; stepper STEP_1[Msteppers]; stepper STEP_2[Msteppers]; #define maxPoz 11 unsigned long pozS[12] = {135135, 135135, 135135, 45045, 45045, 45045, 135135, 135135, 135135, 45045, 45045, 45045}; unsigned int BITime_1[12] = {45, 30, 12, 27, 59, 15, 22, 33, 87, 42, 74}; unsigned int BITime_2[12] = {01, 23, 34, 56, 78, 90, 12, 11, 68, 23, 00}; unsigned long poz[maxPoz][6] = { 135090, 90000, 135, 45, 90000, 45090, 45045, 45045, 45045, 0, 90, 67090, 135135, 135090, 135, 45045, 45, 45090, 135135, 135135, 135135, 45, 90, 45090, 90090, 135, 45045, 0, 90, 90090, 135090, 135000, 135135, 45, 45090, 45045, 135090, 90, 135, 45, 45090, 45045, 135135, 45045, 45045, 0, 90, 45090, 135112, 157090, 135, 45, 22090, 45067, 90135, 135, 135135, 45, 90, 45090, 90090, 90090, 90090, 90090, 90090, 90090, }; byte rej = 0; byte Scalibr[12] = {30, 30, 30, 150, 150, 150, 30, 30, 30, 150, 150, 150}; byte Rcalibr[12] = {44, 44, 45, 135, 135, 136, 44, 44, 45, 135, 135, 136}; byte Sstop; void setup() { DDRD = 0b11111111; delay(500); pinMode(BUT1, INPUT); pinMode(BUT2, INPUT); Wire.begin(); DS3231_init(DS3231_INTCN); cli(); TCCR1A = 0; OCR1A = 0x3D08; TCCR1B = 0b00001101; TIMSK1 = 0b00000010; sei(); //Serial.begin(9600); //Serial.println("S"); display.setBrightness(7); display.setSegments(DP); delay(1000); readT(); flags_null(); } void loop() { calibr(); goGr(); driver(); edit(); sensitive(); vClock(); if (sSinh > itSinh) readT(); park(); level(); } void level() { static long timer; static byte rej_1 = 0; if (flag_B2U && flag_B2D) { // Serial.println("R"); rej = 5; flags_null(); } if (rej != 5) return; if (rej_1 == 0) { for (byte i = 0; i < Msteppers; i++) { STEP_1[i].stepA = 180; STEP_1[i].stepB = 180; STEP_2[i].stepA = 180; STEP_2[i].stepB = 180; STEP_1[i].ok = 0; STEP_2[i].ok = 0; if (i == 0 or i == 1 or i == 2 or i == 6 or i == 7 or i == 8) { STEP_1[i].revA = 1; STEP_1[i].revB = 1; STEP_2[i].revA = 1; STEP_2[i].revB = 1; } else { STEP_1[i].revA = 0; STEP_1[i].revB = 0; STEP_2[i].revA = 0; STEP_2[i].revB = 0; } } timer = millis() + 15000; rej_1 = 1; // Serial.println("B"); } if (rej_1 == 1) { if (timer < millis()) { // Serial.println("-"); rej_1 = 0; for (byte i = 0; i < Msteppers; i++) { STEP_1[i].ok = 1; STEP_2[i].ok = 1; } cd = 1; rej = 0; } } } void park() { static byte rej_1 = 10; if (flag_B2LD) { rej = 3; flags_null(); } if (rej != 3)return; if (rej_1 == 10) { if (Mstop() == Msteppers * 2) { delay(100); rej_1 = 0; } } if (rej_1 == 0) { for (byte i = 0; i < Msteppers; i++) { byte a = pozS[i] / 1000; byte b = pozS[i] % 1000; STEP_1[i].setGr(a, b); STEP_2[i].setGr(a, b); } byte data[4]; data[0] = 0b00000000; data[1] = display.encodeDigit(0); data[2] = display.encodeDigit(0xf); data[3] = display.encodeDigit(0xf); display.setSegments(data); delay(200); rej_1 = 1; } else if (rej_1 == 1) { if (Mstop() == Msteppers * 2) { delay(100); rej_1 = 2; } } else if (rej_1 == 2) { for (byte i = 0; i < Msteppers; i++) { STEP_1[i].setGr(90, 90); STEP_2[i].setGr(90, 90); } rej_1 = 3; } else if (rej_1 == 3) { if (Mstop() == Msteppers * 2) { while (1); } } } void edit() { static byte dig = 0; static unsigned long timer; static byte mig = 0; static byte old2; static byte td[4]; static byte te[4]; static byte flag_2; if (flag_B1LD) { td[3] = it.min % 10; td[2] = it.min / 10; td[1] = it.hour % 10; td[0] = it.hour / 10; old2 = td[1]; flags_null(); dig = 0; rej = 2; } if (rej != 2) return; if (timer > millis()) return; for (byte i = 0; i < 4; i++) { te[i] = display.encodeDigit(td[i]); if (!mig && i == dig) te[i] = 0 & 0x0f; // display.encodeDigit(16); } display.setSegments(te); mig = !mig; if (flag_B2D) { td[dig] = td[dig] >= maxDig[dig] ? 0 : td[dig] + 1; // Serial.println(td[dig]); if (dig == 0 && td[0] == 2) { maxDig[1] = 3; td[1] = td[1] % 3; flag_2 = 1; } else { maxDig[1] = 9; if (flag_2) { td[1] = old2; flag_2 = 0; } } flags_null(); } if (flag_B1D) { dig++; flags_null(); if (dig > 3) { t.min = td[2] * 10 + td[3]; t.hour = td[0] * 10 + td[1]; t.sec = 0; DS3231_set(t); readT(); dispTime(); rej = 1; } } timer = millis() + 50; // flags_null(); // rej = 0; } void sensitive () { static unsigned long timerS; static byte fb1[2]; static byte fb2[2]; static byte ldB1; static byte ldB2; if (timerS > millis()) return; fb1[1] = fb1[0]; fb1[0] = digitalRead(BUT1); fb2[1] = fb2[0]; fb2[0] = digitalRead(BUT2); if (fb1[0] && !fb1[1]) flag_B1U = 1; if (!fb1[0] && fb1[1]) flag_B1D = 1; if (fb2[0] && !fb2[1]) flag_B2U = 1; if (!fb2[0] && fb2[1]) flag_B2D = 1; if (!fb1[0] && !fb1[1]) { ldB1++; if (ldB1 > (BLD / timeSens)) flag_B1LD = 1; } else { ldB1 = 0; } if (!fb2[0] && !fb2[1]) { ldB2++; if (ldB2 > (BLD / timeSens)) flag_B2LD = 1; } else { ldB2 = 0; // flag_B2SU = 1; } timerS = millis() + timeSens; } void flags_null() { flag_B1D = 0; flag_B1U = 0; flag_B1LD = 0; flag_B2D = 0; flag_B2U = 0; flag_B2LD = 0; flag_B2SU = 0; // rej = 1; } void vClock() { static byte oldMin; if (rej == 2 or rej == 0 or rej == 5) return; if (it.min == oldMin) return; dispTime(); // goGr(); rej = 1; oldMin = it.min; } void dispTime() { int timer = it.hour * 100 + it.min; display.showNumberDec(timer, true); Serial.println(timer); } void readT() { DS3231_get(&t); cli(); it.sec = t.sec; it.min = t.min; it.hour = t.hour; it.wday = t.wday; TCNT1 = 0; sei(); sSinh = 0; } void calibr() { static unsigned long timer; static byte a = 0; static byte rej_1 = 0; // if (timer < millis()) rej = 0; if (rej != 0) return; if (cd)rej_1 = 1; if (rej_1 == 0) { Serial.println("C"); for (byte i = 0; i < Msteppers; i++) { STEP_1[i].setGr(Scalibr[i], Scalibr[i]); STEP_2[i].setGr(Scalibr[i], Scalibr[i]); } rej_1 = 1; } else if (rej_1 == 1) { if (Mstop() == Msteppers * 2) { delay(200); for (byte i = 0; i < Msteppers; i++) { STEP_1[i].absGrA = Rcalibr[i]; STEP_1[i].absGrB = Rcalibr[i];// + (i < 3 ? -1 : +1); STEP_1[i].setGr(90, 90); STEP_2[i].absGrA = Rcalibr[i]; STEP_2[i].absGrB = Rcalibr[i];// + (i < 3 ? -1 : +1); STEP_2[i].setGr(90, 90); } cd = 0; rej_1 = 2; } } else if (rej_1 == 2) { if (Mstop() == Msteppers * 2) { delay(1000); rej_1 = 0; rej = 1; flags_null(); } } // timer = millis()+20000; } void goGr() { static byte rej_1 = 0; if (rej != 1) return; static byte pr = 0; // static unsigned long timer; byte a, b; // if (timer > millis()) return; if (rej_1 == 0) { for (byte i = 0; i < Msteppers; i++) { a = pozS[i] / 1000; b = pozS[i] % 1000; STEP_1[i].setGr(a, b); STEP_2[i].setGr(a, b); } rej_1 = 1; } else if (rej_1 == 1) { if (Mstop() == Msteppers * 2) rej_1 = 2; } else if (rej_1 == 2) { for (byte i = 0; i < Msteppers; i++) { if (i < 6) { a = poz[it.hour / 10][i] / 1000; b = poz[it.hour / 10][i] % 1000; } else { a = poz[it.hour % 10 - 1][i] / 1000; b = poz[it.hour % 10 - 1][i] % 1000; } STEP_1[i].setGr(a, b); if (i < 6) { a = poz[it.min / 10][i] / 1000; b = poz[it.min / 10][i] % 1000; } else { a = poz[it.min % 10 - 1][i] / 1000; b = poz[it.min % 10 - 1][i] % 1000; } STEP_2[i].setGr(a, b); } pr = pr == maxPoz ? 0 : pr + 1; rej_1 = 0; rej = 4; //Serial.println(pr); // timer = millis() + 6000; } } void driver() { static byte st = 0; static unsigned long timer; if ((timer + 5) > millis()) return; // PORTB = 1; byte sti = 0; if (st == 0) { for (byte i = 0; i < Msteppers; i++) { STEP_1[i].go(); if (!STEP_1[i].ok) sti = 1; } if (sti) { PDdigWL(LATCH_PIN_1); for (byte i = 0; i < Msteppers; i++) { writeByteP_1(STEP_1[i].comb); } PDdigWH(LATCH_PIN_1); } } else { for (byte i = 0; i < Msteppers; i++) { STEP_2[i].go(); if (!STEP_2[i].ok) sti = 1; } if (sti) { PDdigWL(LATCH_PIN_2); for (byte i = 0; i < Msteppers; i++) { writeByteP_2(STEP_2[i].comb); } PDdigWH(LATCH_PIN_2); } } st = !st; delayMicroseconds(100); timer = millis(); // PORTB = 0; } byte Mstop() { byte a = 0; for (byte i = 0; i < Msteppers; i++) { a = a + STEP_1[i].ok; } for (byte i = 0; i < Msteppers; i++) { a = a + STEP_2[i].ok; } return a; } inline void writeByteP_1(byte byteW) { for (int i = 0; i <= 7; i++) { if (bitRead(byteW, i)) { PDdigWH(DATA_PIN_1); } else { PDdigWL(DATA_PIN_1); } PDdigWH(CLOCK_PIN_1); PDdigWL(CLOCK_PIN_1); } } inline void writeByteP_2(byte byteW) { for (int i = 0; i <= 7; i++) { if (bitRead(byteW, i)) { PDdigWH(DATA_PIN_2); } else { PDdigWL(DATA_PIN_2); } // delayMicroseconds(20); PDdigWH(CLOCK_PIN_2); PDdigWL(CLOCK_PIN_2); } } inline void PDdigWH(byte NB) { PORTD |= 1 << NB; } inline void PDdigWL(byte NB) { PORTD &= ~(1 << NB); } ISR(TIMER1_COMPA_vect) { sSinh++; it.sec++; if (it.sec < 60) return; it.sec = 0; it.min++; if (it.min < 60) return; it.min = 0; it.hour++; if (it.hour < 24) return; it.hour = 0; it.wday++; if (it.wday < 8) return; it.wday = 0; }
Вывод
Несмотря на то, что изготовленные нами часы имеют минимально возможное количество элементов, знания, умения и опыт, приобретенные в процессе работы позволят - при желании и должной финансовой поддержке - изготовить подобную конструкцию практически любого размера и сложности. Наш принцип одновременного управления множеством шаговиков доказал свою эффективность. Возможно, не все решения, изобретенные и примененные в этом проекте идеальны, но они работают и могут быть усовершенствованы в дальнейших разработках.FAQ
Для вывода команд на сдвиговые регистры тратится время. Если регистров много, не будет ли это проблемой, ограничивающей скорость движения стрелок?На вывод команд для всех задействованных в проекте 24 шаговиков уходит менее одной миллисекунды. Минимальная задержка между шагами 2 миллисекунды, то есть максимальную скорость можно развить на большем (в два или три раза) количестве пикселей. Если же в проекте количество шаговиков перевалит за сотню, ничто не помешает разделить задачу между несколькими согласованными контроллерами.
Не возникает ли проблема со скачками напряжения, питающего шаговики, от их одновременного включения?
Да, если блок питания недостаточно мощный, перепады могут просадить напряжение, что приведет к пропускам шагов и даже сбоям в работе контроллера. Следует применять блок питания с двух-трехкратным запасом и включать в схему элементы, сглаживающие пульсации, например простейшую RC-цепь из конденсатора и резистора. В идеале лучше вообще гальванически разделить питание контроллера и шаговиков, и на моторы подавать 5.5-6В, что допустимо и для регистров, и для самих шаговиков. Это повысит их мощность и надежность работы, а контроллер избавится от перепадов.
При формировании цифры разные стрелки проходят разный путь. Как они синхронизируются, чтобы не начать следующее движение раньше законченного предыдущего?
Этот механизм показан даже на простом примере в данной статье. Функция “драйвер” поднимает флаг, когда обе стрелки заняли заданное положение и не раньше. При управлении несколькими шаговиками работает несколько “драйверов” и каждый понимает свой флаг. Когда все флаги подняты, картина считается завершенной. В случае с часами, этот механизм не так важен, так как все стрелки займут свое положение гарантированно раньше, чем пройдет минута и настанет момент снова двигаться.
Как происходит выравнивание? На какой угол вращаются стрелки, чтобы точно упереться в ограничитель, ведь заранее неизвестно, где они были до включения?
Следует избегать внезапного отключения, для чего рекомендуется использовать схему с переключением на аварийные аккумуляторы с мониторингом оставшегося заряда, а при плановой остановке или при разрядке аккумуляторов ниже опасного значения стрелки выводятся в парковочное положение, то есть вниз. В таком случае провернуть до ограничителя потребуется немногим более чем на 90 градусов, например, 100-120. Однако, если потребуется притянуть стрелки из любого положения, в программу добавляется специальная функция, вызываемая нажатием кнопки, которая повернет их на все 360 градусов.
Не вредно ли для моторов упираться в ограничители?
Сломаться от этого они не должны, потому что магниты не очень мощные, а нейлоновые шестерни в редукторах достаточно прочные. Но все же лучше не злоупотреблять этой функцией.
Можно ли выводить на часы другую информацию, кроме времени?
Можно любую, которую смогут изобразить стрелки. Температуру, атмосферное давление и любую фигуру или узор. Можно даже сделать анимацию, например, движение волны, синхронное вращение и так далее, на что хватит воображения и способностей к программированию. Часы выглядят наиболее эффектно и завораживающе именно в процессе движения стрелок.