Сделать аналоговые часы, которые будут показывать время на цветном графическом 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 и ВКонтакте.
Комментарии (12)
PR200SD
20.10.2022 12:42+1Графика слабовата, хотя для Attiny наверное и ничего. Вот тут: https://habr.com/ru/post/548598/ есть про круглый дисплей и часы в том числе.
OldFashionedEngineer
20.10.2022 17:29+2Что не делай на микроконтроллере, все равно получатся часы
OldFashionedEngineer
21.10.2022 16:34+1Перевод получился по принципу "чтобы что-то было". Если хочется детально разобраться с материалом, надо идти по ссылкам на другие оригинальные статьи автора. Они на английском языке. Если я могу свободно читать на английском, зачем тогда мне здесь читать кусочек на русском?
Моё мнение, если делаете перевод, то нужно переработать материал так, чтобы его было достаточно для понимания.
Radisto
Возможно, точнее было бы назвать их часами с аналоговой индикацией. Сами часы ведь не аналоговые. А еще точнее, с имитацией аналоговой индикации