- Суть проекта,
- Составляющие и комплектующие,
- Немного о работе с двуосным шаговиком,
- Дизайнерские трудности
- Вывод,
- FAQ.
Суть проекта
Идея не нова, но давно зрело желание ее воплотить собственными силами. Эффектное, редкое и, к тому же, весьма интересное с технической, программной и дизайнерской точки зрения устройство прямо таки бросило нам вызов и он был принят.Итак, большие цифровые часы состоят из множества маленьких аналоговых, то есть стрелочных. Например, такой - пожалуй, самый крупный из подобных - образец выставлялся на выставке дизайна в Дубаи в 2013 году. Панно составлено аж из 288 часов.
Прикинув объем работ и материалов, мы решили, что для начала не станем замахиваться на Дубайские масштабы, попробуем сделать что-то поменьше. Ну как поменьше, из минимального количества пикселей, которыми можно изобразить уверенно читаемые цифры. Таким количеством является 6 пикселей на одну цифру. Итого потребуется минимум 26 аналоговых часов для создания одних цифровых.
Составляющие и комплектующие
Очевидно, что самым необычным компонентом часов из часов должен стать механизм нашего механического пикселя. Это должен быть точный мотор, точнее два мотора с соосными валами. Простые моторы, сервоприводы и обычные шаговики на эту роль не годились, во всяком случае без сложных доработок и дополнительных редукторов, рычагов, датчиков и так далее. Изобретение собственной механики слишком усложнило бы и так непростой проект, непозволительно увеличило расходы средств и времени, поставив под угрозу его осуществление. Очень кстати на просторах Интернета нашелся готовый к применению двуосный шаговый двигатель, практически то, что нам и нужно.В качестве мозга часов берем Ардуино Нано, она компактна, неприхотлива и достаточно мощна для этой цели. Пара кнопок, блок питания, пригоршня радиодеталей, корпус для защиты от внешней среды и готово.
Необходимы модули для Arduino?
Купить модули Ардуино можно в нашем магазине 3DIY онлайн!
Немного о работе с двуосным шаговиком
Мотор состоит из двух принципиально одинаковых независимых шаговых двигателей с двумя электромагнитными катушками каждый. Один крутит внутренний длинный вал, другой внешний короткий.
Последовательность сигналов хорошо описана в даташите и даже изображена в виде графика.
Техническое отступление специально для перфекционистов. Если например в других проектах потребуется еще более мелкое изменение угла вала, даже меньше одного Атомарного шага (0.33 градуса), можно воспользоваться системой микрошагов, когда каждый Атомарный шаг разбивается еще на 4 (или более) с разным уровнем напряжения. То есть цифровых сигналов 0В - +5В будет недостаточно, потребуются аналоговые. По 4 на один вал, 8 на один пиксель. Это на порядок сложнее и потребуется очень серьезная причина для необходимости осуществления подобного управления.
Давайте создадим простой программно-аппаратный драйвер для одного шаговика при помощи одной платы Ардуино, благо двигатель наш потребляет всего 20 mA на контакт, а значит не перегрузит возможности пинов без дополнительных устройств. Но сперва нарисуем в любом 3D редакторе и напечатаем на любом 3D принтере подходящие по размеру стрелки, чтобы было хорошо видно, как вращаются валы.
Дело за малым - написать программу, которая будет подавать на магниты импульсы в правильной последовательности. Чуть усложним и напишем программу для программы, по которой стрелки будут циклически вращаться на заранее заданные нами углы в заранее заданные нами стороны. Углы и стороны зададим абсолютно хаотично и разные для разных стрелок, а значит одна из них иногда будет дожидаться вторую. Все это должно быть предусмотрено в программе.
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 мс }
Запускаем и, если все правильно собрали, видим что-то подобное. Если сделать одну стрелку короче, хотя бы временно, и добавить циферблат, то получатся вполне себе настоящие аналоговые часы, готовые показать любое время в любой момент. Например, в разных часовых поясах.
Казалось бы, что еще нужно? Нужна точка отсчета. Сейчас стрелки вращаются на заданные углы, физически начиная с того места, на котором стояли, когда запустили программу. Они могли быть направлены вверх, вниз, в любую сторону, причем не обязательно в одну. Датчика положения у стрелок нет. Но для того, чтобы всегда устанавливать стрелки в нужное положение, мы должны обязательно знать в каком положении они находятся при запуске программы.
Тут два варианта. Первый очевидный и простой: перед запуском всегда вручную выкручиваем их в начальное положение. А если стрелки за стеклом? Еще и на улице? Выкрутить один раз, а потом выставлять в начальное перед выключением. А если сбой в питании? А хаотичные импульсы при включении? А если стрелка зацепится за что-нибудь хоть разок? Вариант второй: при запуске крутить их в определенном направлении пока гарантированно не упрутся в ограничители, это и будет начальным положением. К слову, ограничители в этих шаговиках уже имеются, именно для этой цели. Второй вариант единственно верный, только так можно всегда быть уверенным, что стрелки не разъедутся в неизвестные стороны. Для проекта с часами будем пользоваться этим методом, назовем его “выравнивание”.
Теперь все? Нет, не все. Дело в том, что для одного лишь шаговика, то есть пикселя, требуется аж 8 пинов контроллера. В проекте участвует 24 пикселя, умножаем, получаем цифру 192. Напомню, что у нас управляет часами Нано, у которой пинов и трех десятков не наберется, это без учета того, что нужно и кнопки, и индикатор куда-то подключать. Вспоминаем классический способ размножить цифровые пины - конечно же использовать выходные сдвиговые регистры 74HC595. Три пина передают данные на 8 пинов регистра. Регистры собираются в цепочку почти неограниченной длины, таким образом из трех управляющих пинов Ардуино мы можем получить сколько угодно пинов на регистрах. Нам надо вполне определенное количество 192/8 = 24 регистра, что и так понятно, один регистр на один пиксель, так уж удачно совпало, что и у того, и у другого по 8 ног.
Для компактности, упрощения сборки и минимизации ошибок, тратим некоторое количество времени на создание печатной платы для одного пикселя.
Приближаясь к финалу, для дальнейшей сборки прототипа берем первый попавшийся под руку, но подходящий по размерам кусок фанеры или картона и размещаем на нем шесть пикселей для первой цифры. Соединяем их проводами в “паровозик” в правильном порядке.
Обратите внимание на процедуру выравнивания. Левые пиксели поднимаются влево, правые вправо. Именно там, вне рабочего поля, находятся ограничители. Запомните этот важный момент.
Таким образом, одну цифру или, выражаясь технически грамотно, разряд часов мы изготовили и запустили. Можно считать, что осталась совсем чепуха, собрать еще три разряда, поставить все четыре в ряд и вот они - полноразмерные часы. Окончательный дизайн имеет возможность отличаться, в зависимости от гениальности исполнителя или заказчика. Можно разместить пиксели на одном поле, как в Дубайском примере, можно их пристроить на четырех отдельных полях, по одному на разряд, а можно и каждый пиксель заключить в свой квадратный, круглый или многогранный корпус, стилизованный, например, под стрелочные часы. У кого на что хватит фантазии. А вот начинка для этого уже готова.
Вот тут надо картинку готовых часов с несколькими комбинациями цифр. Видео лучше разместить в конце следующей главы.
Если потребуются комплектующие для 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 градусов.
Не вредно ли для моторов упираться в ограничители?
Сломаться от этого они не должны, потому что магниты не очень мощные, а нейлоновые шестерни в редукторах достаточно прочные. Но все же лучше не злоупотреблять этой функцией.
Можно ли выводить на часы другую информацию, кроме времени?
Можно любую, которую смогут изобразить стрелки. Температуру, атмосферное давление и любую фигуру или узор. Можно даже сделать анимацию, например, движение волны, синхронное вращение и так далее, на что хватит воображения и способностей к программированию. Часы выглядят наиболее эффектно и завораживающе именно в процессе движения стрелок.