Pull to refresh
89.51
Cloud4Y
#1 Корпоративный облачный провайдер

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

Reading time7 min
Views7.7K
Original author: David Johnson-Davies

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

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

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

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

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

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

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

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

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

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

Где брать

Размер

Ширина

Высота

Напряжение

Драйвер

Ссылка

AliExpress

1,54 дюйма

240

240

3,3 В

ST7789

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

AliExpress

2,0 дюйма

320

240

3,3 В

ST7789V

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

AliExpress

1,44 дюйма

128

128

3,3 В

ST7735S

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

AliExpress

1,8 дюйма

160

128

3,3 В

ST7735

1,8-дюймовый TFT-дисплей 128x160 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-дисплея 128x128:

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

Скомпилируйте программу с помощью ядра 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)».

Ресурсы

Полный код

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


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

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

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

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

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

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

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

Tags:
Hubs:
Total votes 23: ↑21 and ↓2+19
Comments17

Articles

Information

Website
www.cloud4y.ru
Registered
Founded
2009
Employees
51–100 employees
Location
Россия