Сделать аналоговые часы, которые будут показывать время на цветном графическом TFT-дисплее… Почему бы и да?
В часах используется кварцевый осциллятор для поддержания точного времени, а также процедуры считывания пикселей с TFT-дисплея, описанные в статье чтение с TFT-дисплея.
С помощью приведённой инструкции вы сможете сделать свой проект на макетной плате. В качестве альтернативы можете использовать универсальный интерфейс подключения TFT дисплея.
Как это работает
Без возможности обратного считывания пикселей с дисплея вам пришлось бы перерисовывать весь дисплей каждый раз, когда любая из стрелок совершает движение, а это происходит каждую секунду (если у часов есть секундная стрелка). Чтобы перерисовывать изображение каждую секунду, потребуется быстрый процессор.
Этого можно избежать путём считывания цвета каждого пикселя стрелки по принципу исключающего “или” с изображением циферблата. В итоге стрелка удаляется с текущего положения, фон под ней возвращается к исходному состоянию, а потом стрелка рисуется на новом месте. И тогда элементы под стрелками (например, цифры часов) не стираются, когда стрелки проходят над ними.
Подходящие дисплеи
Часы предназначены для работы с TFT-дисплеем RGB с разрешением 240x240 или 320x240, доступным на AliExpress. Есть также версия с более низким разрешением, которая будет работать на дисплее 128x128 или 160x128 (см. версию с более низким разрешением).
Подходят следующие дисплеи:
Где брать | Размер | Ширина | Высота | Напряжение | Драйвер | Ссылка |
AliExpress | 1,54 дюйма | 240 | 240 | 3,3 В | ST7789 | |
AliExpress | 2,0 дюйма | 320 | 240 | 3,3 В | ST7789V | |
AliExpress | 1,44 дюйма | 128 | 128 | 3,3 В | ST7735S | |
AliExpress | 1,8 дюйма | 160 | 128 | 3,3 В | ST7735 |
* Совместим с универсальным интерфейсом подключения дисплеев.
К сожалению, дисплеи Adafruit несовместимы с этим приложением, так как не поддерживают чтение пикселей с дисплея.
Схема
Вот схема, которая по сути является схемой интерфейса подключения дисплея:
Программа занимает 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 и ВКонтакте.