Как сделать графические аналоговые часы

от автора

Сделать аналоговые часы, которые будут показывать время на цветном графическом TFT-дисплее… Почему бы и да?

Аналоговые часы на базе ATtiny814, отображают время на цветном TFT-дисплее с разрешением 240x240
Аналоговые часы на базе ATtiny814, отображают время на цветном TFT-дисплее с разрешением 240×240

В часах используется кварцевый осциллятор для поддержания точного времени, а также процедуры считывания пикселей с TFT-дисплея, описанные в статье чтение с TFT-дисплея

С помощью приведённой инструкции вы сможете сделать свой проект на макетной плате. В качестве альтернативы можете использовать универсальный интерфейс подключения TFT дисплея.

Как это работает

Без возможности обратного считывания пикселей с дисплея вам пришлось бы перерисовывать весь дисплей каждый раз, когда любая из стрелок совершает движение, а это происходит каждую секунду (если у часов есть секундная стрелка). Чтобы перерисовывать изображение каждую секунду, потребуется быстрый процессор.

Этого можно избежать благодаря возможности считывания цвета каждой стрелки по принципу исключающего “или” с изображения циферблата. В итоге стрелка удаляется с текущего положения, фон под ней возвращается к исходному состоянию, а потом стрелка рисуется на новом месте. И тогда элементы под стрелками (например, цифры часов) не стираются, когда стрелки проходят над ними.

Подходящие дисплеи

Часы предназначены для работы с TFT-дисплеем RGB с разрешением 240×240 или 320×240, доступным на AliExpress. Есть также версия с более низким разрешением, которая будет работать на дисплее 128×128 или 160×128 (см. версию с более низким разрешением).

Подходят следующие дисплеи:

Где брать

Размер

Ширина

Высота

Напряжение

Драйвер

Ссылка

AliExpress

1,54 дюйма

240

240

3,3 В

ST7789

1,54-дюймовый ЖК-дисплей TFT 240×240  *

AliExpress

2,0 дюйма

320

240

3,3 В

ST7789V

2,0-дюймовый ЖК-дисплей TFT 240×320

AliExpress

1,44 дюйма

128

128

3,3 В

ST7735S

1,44-дюймовый TFT-дисплей 128×128 SPI  *

AliExpress

1,8 дюйма

160

128

3,3 В

ST7735

1,8-дюймовый TFT-дисплей 128×160 SPI  *

* Совместим с универсальным интерфейсом подключения дисплеев.

К сожалению, дисплеи Adafruit несовместимы с этим приложением, так как не поддерживают чтение пикселей с дисплея.

Схема

Вот схема, которая по сути является схемой интерфейса подключения дисплея:

Схема графических аналоговых часов на базе ATtiny814
Схема графических аналоговых часов на базе ATtiny814

Программа занимает 5 Кбайт, поэтому вам потребуется устройство ATtiny 1-й серии объёмом не менее 8 Кбайт (начиная с ATtiny814 и до ATtiny3214). Устройства серии 0 не подходят, так как не поддерживают внешний тактовый генератор. Вы можете попробовать устройства 2-й серии, но я их не проверял.

Для генерации прерывания каждую секунду используются часы реального времени ATtiny814, синхронизацией управляет тактовый генератор 32,768 кГц. Я использовал недорогой цилиндрический кварцевый резонатор, который обычно имеет нагрузочную ёмкость 12,5 пФ. Для расчёта ёмкости конденсатора используйте формулу C = 2(CL — CS), где CL — ёмкость нагрузки, а CS — паразитная ёмкость, которая обычно оценивается в 2,5 пФ на печатной плате. Получается, что C=20 пФ.

Если схема построена на макетной плате, вы, вероятно, можете не использовать конденсаторы, так как их роль будет играть сама макетная плата.

Универсальный интерфейс подключения TFT-дисплея

Вы можете запустить программу на  универсальном интерфейсе помощью ATtiny814:

Графические аналоговые часы, работающие на универсальном интерфейсе подключения дисплея
Графические аналоговые часы, работающие на универсальном интерфейсе подключения дисплея

Код

В программу включена библиотека из статьи «Чтение с TFT-дисплея».

Рисуем фон часов

Подпрограмма ClockFace() рисует циферблат и цифры, но без стрелок:

void ClockFace () {   int x0 = 120, y0 = 120, radius = 120;   MoveTo(x0, y0); fore = BLUE; DrawCircle(radius);   radius = radius - 2; fore = DARKBLUE; FillCircle(radius);   int x = 0, y = 118<<sca;   for (int i=0; i<60; i++) {     // Hours and hour marks     if (i%5 == 0) {       fore = YELLOW;       MoveTo(x0+(x>>sca), y0+(y>>sca));       DrawTo(x0 + ((x*15)>>(sca+4)), y0 + ((y*15)>>(sca+4)));       scale = 2;       MoveTo(x0 + ((x>>sca)*13/16) - 3*(1+(i==0))*2, y0 + ((y>>sca)*13/16) - 8);       fore = GREEN; back = DARKBLUE;       if (i==0) PlotInt(12); else PlotInt(i/5);       scale = 1;     }     for (int i=2;i--;) { x = x + (y*top)/bot; y = y - (x*top)/bot; }   } }

Здесь применяется алгоритм окружности Минского для вычисления 60 точек по окружности без использования функций с плавающей запятой или триггеров. Значение top / bot или 10/191 является близким приближением к (2*π)/120, где 2*π — количество радиан в окружности. 120 — это количество делений окружности, где два деления соответствуют одной секунде. Значение sca представляет собой коэффициент масштабирования, выбранный таким образом, чтобы 2sca*118*top соответствовали целому числу.

Настройка часов реального времени

Для актуализации времени используется периферийное устройство RTC в ATtiny814, которое тактируется от внешнего тактового генератора 32,768 кГц таким образом, чтобы каждую секунду происходило прерывание.

Подпрограмма  RTCSetup()  настраивает кварцевый генератор, а затем указывает его в качестве источника тактового сигнала для периферийного устройства часов реального времени:

void RTCSetup () {   uint8_t temp;   // Initialize 32.768kHz Oscillator:    // Disable oscillator:   temp = CLKCTRL.XOSC32KCTRLA & ~CLKCTRL_ENABLE_bm;    // Enable writing to protected register   CPU_CCP = CCP_IOREG_gc;   CLKCTRL.XOSC32KCTRLA = temp;    while (CLKCTRL.MCLKSTATUS & CLKCTRL_XOSC32KS_bm);   // Wait until XOSC32KS is 0   temp = CLKCTRL.XOSC32KCTRLA & ~CLKCTRL_SEL_bm;      // Use External Crystal      // Enable writing to protected register   CPU_CCP = CCP_IOREG_gc;   CLKCTRL.XOSC32KCTRLA = temp;   temp = CLKCTRL.XOSC32KCTRLA | CLKCTRL_ENABLE_bm;    // Enable oscillator      // Enable writing to protected register   CPU_CCP = CCP_IOREG_gc;   CLKCTRL.XOSC32KCTRLA = temp;      // Initialize RTC   while (RTC.STATUS > 0);                             // Wait until synchronized    // 32.768kHz External Crystal Oscillator (XOSC32K)   RTC.CLKSEL = RTC_CLKSEL_TOSC32K_gc;      // RTC Clock Cycles 32768, enabled ie 1Hz interrupt   RTC.PITCTRLA = RTC_PERIOD_CYC32768_gc;    RTC.PITINTCTRL = RTC_PI_bm;                         // Periodic Interrupt: enabled }  // Interrupt Service Routine called every second ISR(RTC_PIT_vect) {   RTC.PITINTFLAGS = RTC_PI_bm;                        // Clear interrupt flag   NextSecond(BOTH); }

По сути, это та же процедура, которую я использовал в более ранних часах на базе чипов ATtiny 1-й серии, таких как  Mega Tiny Time Watch.

Движение стрелок

Процедура обслуживания прерывания вызывается каждую секунду:

ISR(RTC_PIT_vect) {   RTC.PITINTFLAGS = RTC_PI_bm;                        // Clear interrupt flag   NextSecond(BOTH); }

Код просто вызывает NextSecond() для движения стрелок, если это необходимо:

void NextSecond (int draw) {   int x0 = 120, y0 = 120;   // Positions of hands   static int secx = 0, secy = 118<<sca;   static int minx = 0, miny = 118<<sca;   static int hrx = 0, hry = 86<<sca;   // Seconds and minutes   static uint8_t secs = 0, mins = 0;      // Advance second hand   fore = White;   if (draw & UNDRAW) { MoveTo(x0, y0); DrawTo(x0+(secx>>sca), y0+(secy>>sca)); }   for (int i=2;i--;) { secx = secx + (secy*top)/bot; secy = secy - (secx*top)/bot; }   if (secs == 59) { secx = 0, secy = 118<<sca; }  // Realign   if (draw & DRAW) { MoveTo(x0, y0); DrawTo(x0+(secx>>sca), y0+(secy>>sca)); }      // Advance hour hand every 12 mins   if (secs == 59 && mins%12 == 0) {     fore = RED;     DrawHand(x0, y0, hrx>>sca, hry>>sca);     for (int i=2;i--;) { hrx = hrx + (hry*top)/bot; hry = hry - (hrx*top)/bot; }   } else if (secs == 0 && mins%12 == 0) {     fore = RED;     DrawHand(x0, y0, hrx>>sca, hry>>sca);   }       // Advance minute hand every 60 secs   if (secs == 59) {     fore = PINK;     if (draw & UNDRAW) DrawHand(x0, y0, minx>>sca, miny>>sca);     for (int i=2;i--;) {minx = minx + (miny*top)/bot; miny = miny - (minx*top)/bot; }   } else if (secs == 0) {     fore = PINK;     if (mins == 0) minx = 0, miny = 118<<sca;  // Realign     if (draw & DRAW) DrawHand(x0, y0, minx>>sca, miny>>sca);     mins = (mins + 1)%60;   }   secs = (secs + 1)%60; }

В каждом случае стрелка рисуется дважды по схеме исключающего “или”: один раз в предыдущей позиции, чтобы удалить старое изображение, и один раз в новой позиции, чтобы нарисовать новое изображение.

Секундная стрелка рисуется в виде линии и продвигается каждую секунду.

Минутная стрелка продвигается вперед на одно деление каждые 60 секунд. Из-за времени, затраченного на его отрисовку, я стираю старую версию и рисую новую при последовательных вызовах NextSecond() .

Часовая стрелка продвигается вперед на одну секунду каждые 12 минут.

Рисуем часовую и минутную стрелки

Как часовая, так и минутная стрелки рисуются с помощью  DrawHand()  как закрашенные ромбовидные четырехугольники с использованием процедур из раздела Рисование закрашенных четырехугольников и треугольников :

void DrawHand(int x0, int y0, int x, int y) {    int v = x/2, u = y/2, w = v/5, t = u/5;    FillQuad(x0, y0, x0+v-t, x0+u+w, x0+x, x0+y, x0+v+t, x0+u-w); }

Поскольку мне нужно построить только заполненный четырёхугольник, я немного упростил процедуру, чтобы сделать её немного быстрее.

Установка времени

Чтобы задать время на часах, вы можете вызвать SetTime() с соответствующими значениями часов и минут:

void SetTime (int hour, int minute) {   uint32_t secs = (uint32_t)(hour * 60 + minute) * 60;   for (uint32_t i=0; i<secs; i++) NextSecond(NONE); }

Параметр draw для NextSecond() указывает, следует ли рисовать или удалять стрелки при каждом вызове, а SetTime() вызывает NextSecond(NONE) для смещения положения стрелок без фактического их построения, что намного быстрее.

Как только правильное время установлено, вызывается EnableClock() , чтобы включить односекундное прерывание:

void EnableClock () {   RTC.PITCTRLA = RTC.PITCTRLA | RTC_PITEN_bm;  }

Вы должны указать время начала, прежде чем запускать программу:

const int Hour = 12, Minute = 34;          // E.g. 12:34

Версия с более низким разрешением

Я также хочу поделиться версией часов с более низким разрешением и параметрами, адаптированными для цветного TFT-дисплея 128×128:

Компиляция программы

Скомпилируйте программу с помощью ядра Spence Konde megaTiny на GitHub. Выберите параметр  ATtiny3224/1624/1614/1604/824/814/804/424/414/404/241/204 под  заголовком  megaTinyCore в меню Board. Убедитесь, что последующие параметры установлены следующим образом (игнорируйте любые другие параметры):

Chip: «ATtiny814» (или соответствующий)

Clock: «20 MHz internal»

Затем загрузите программу с помощью программатора UPDI. Рекомендуемый вариант — использовать плату USB to Serial 3,3 В, например, базовую плату SparkFun FTDI, подключённую к резистору 4,7 кОм следующим образом:

Установите для  параметра Programmer  значение  «SerialUPDI with 4.7k resistor or diode (230400 baud)».

Ресурсы

Полный код

А вот версия для дисплея 128×128 (или 160×128):  Графические аналоговые часы 128×128 .


Что ещё интересного есть в блоге Cloud4Y

→ Как открыть сейф с помощью ручки

→ OpenCat — создай своего робокотика

→ Как распечатать цветной механический телевизор на 3D-принтере

→ WD-40: средство, которое может почти всё

→ Подержите моё пиво, или как я сделал RGBeeb, перенеся BBC Micro в современный корпус

Подписывайтесь на наш Telegram-канал, чтобы не пропустить очередную статью. Пишем только по делу. А ещё напоминаем про второй сезон нашего сериала ITить-колотить. Его можно посмотреть на YouTube и ВКонтакте.


ссылка на оригинал статьи https://habr.com/ru/company/cloud4y/blog/694332/