Матричные часы

в 10:37, , рубрики: diy или сделай сам, двоичный код, подарок гику, Часы

Иногда идея в голове зреет годами. Подсознание впитывает интересности, раскладывает по полочкам, копит критическую массу, пока все это наконец-то не выливается во что-нибудь интересное.

Так случилось и в тот раз. В пространственно-временном континууме сошлись красивый, но уже бесполезный «Справочник мини- и микро- ЭВМ», красная светодиодная матрица 5х8 и день рождения друга-программиста.

Как тут не сделать часы? Да и не просто двоичные, а какие-нибудь позаковыристей?

Матричные часы - 1

Цель

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

Железо

Тогда ещё я не имел привычки документировать такие простые схемы (о чем очень жалею), поэтому я просто достал из ящика стола любимую многими tiny2313, разогрел паяльник, и понеслась!

Примерная схема

Матричные часы - 2
Транзисторы, если не ошибаюсь — КТ361 из старых запасов. Выводы матрицы подключены так, чтобы было удобнее паять. Если вам всё-таки непонятно, что здесь происходит — могу только порекомендовать замечательный сайт уважаемого DiHalt: easyelectronics

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

Режимы отображения

Первым делом, естественно, я реализовал двоичное отображение. Вернее, двоично-десятичное (BCD). Так можно несколько быстрее считать информацию с часов, поэтому с таким отображением после небольшой тренировки справится любая домохозяйка, которая привыкла пересчитывать сдачу в магазине (свою любимую гуманитарочку я научил считать на пальцах до 1023, чему она весьма рада).
Матричные часы - 3

Небольшая пояснительная анимация

Матричные часы - 4

Во время посещения музея телекоммуникаций я обратил внимание на то, как в коде морзе легко кодируются цифры. Заменил точки на включенные светодиоды, а тире — на отключенные. Получилось отображение по азбуке Морзе — весьма просто и понятно. Честно скажу — этот режим мой самый любимый! Он все еще заставляет подумать, но плавное перетекание времени с одного разряда в другой просто чарует.
Матричные часы - 5

Небольшая пояснительная анимация и выдержка из азбуки Морзе

Матричные часы - 6

Матричные часы - 7

Ну а потом я серьезно задумался, можно ли с помощью всего 40 светодиодов эмулировать стрелочные часы? Ведь у них есть замечательное преимущество — они понятны всем. Даже краем глаза можно определить примерное время, ну а более пристальное вглядывание позволяет узнать точное время.

Несколько исчерканных листов, куча перепробованного кода, и решение для аналогового отображения нашлось! Не уверен, что домохозяйка с наскоку все поймет, но цель достигнута: после небольшой тренировки беглым взглядом можно определить время с точностью до трёх минут, более пристальным — до 12 секунд.
Матричные часы - 8

Небольшая пояснительная анимация

Матричные часы - 9

Матричные часы - 10
На часах — полночь. Чтобы визуально выделить «часовой» циферблат — текущий час показывается отключенным светодиодом на светящемся круге.

Чтобы смотреть на часы было ещё интереснее, способ отображения времени меняется в полночь (опционально). Время и отображение настраиваются с помощью пары кнопок по простому алгоритму, блок-схема которого прилагается. Правда, владелец часов отметил, что самый простой и удобный способ установки точного времени — это их включение ровно в полночь.

После этого пришла пора долгой и мучительной резки и склейки справочника. Приходилось все делать буквально постранично, следы халтуры сразу же проявлялись в виде волнистости. На это ушла почти целая неделя! Вырез повторял контур матрицы, платы, выступающих деталей, словом был сделан ровно таким, чтобы бережно укутать всю электронику.

Кнопки и алгоритм настройки спрятались на заднем развороте. Наклеечки на кнопки для «слепой» (глядя только на дисплей) настройки добавил уже новый хозяин часов.

Инструкция по использованию

Матричные часы - 11

Конечно, некоторые идеи по улучшению приходят уже после того, как устройство отдано заказчику подарено. Контакт блока питания оказался недостаточно стабильным, было бы здорово закрыть матрицу светофильтром и сделать аварийное питание от съёмной батарейки… Но это в следующий раз, в остальном всё работает отлично.

А вот рассказ о часах нынешнего их хозяина FFormula, записанный по моей просьбе.

Планы на будущее

Появилось место для настенных часов, куплен приёмник DCF77, в качестве резерва предпологается синхронизация по SNTP (с помощью ESP8266), в этом проекте будет учтен опыт прошлого, задумываются новые коварные методы отображения времени… Например, сделать основным режимом UNIX time, что сразу решит проблему отображения даты :)

Осторожно, код!

// 2008.09.02 18:05 -> 2008.09.23 22:39

// Fuse bits: div8 disabled

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>

#define F_CPU 8000000UL  // 8 MHz
#include <util/delay.h>


#define countStart 9 // manually adjusted after one week

#define typeCount 3

volatile unsigned char  sec,
						keys, oldkeys, 
						colSet, type, 
						automatic = 0xff;

volatile unsigned int time;

typedef struct bitfield{
	unsigned char point:1;
	unsigned char settings:1;
	unsigned char blink:1;
}bitfield;

bitfield flag;

volatile char col = 0;

#define C3 PD2
#define C5 PD3
#define C4 PD4
#define C1 PD5
#define C2 PD6

unsigned char c[5] = {
	(1<<C1),
	(1<<C2),
	(1<<C3),
	(1<<C4),
	(1<<C5)
};

#define R2 PB7
#define R4 PB6
#define R1 PB5
#define R3 PB4
#define R7 PB3
#define R5 PB2
#define R8 PB1
#define R6 PB0

unsigned char r[8] = {
	(1<<R1),
	(1<<R2),
	(1<<R3),
	(1<<R4),
	(1<<R5),
	(1<<R6),
	(1<<R7),
	(1<<R8)
};


#define CC ((1<<C1)|(1<<C2)|(1<<C3)|(1<<C4)|(1<<C5))

#define B0 PD0
#define B1 PD1

unsigned char display[5];

unsigned char clock[4];

//****************
// Time calculation and visualisation
//****************

ISR (TIMER0_OVF_vect)
{
	TCNT0 = countStart;
	time ++;
	
	col++;
	if (col == 5)
		col = 0;
	PORTD |= CC;
	PORTD &= ~c[col];
	PORTB = display[col];

	if (time == 2000)
	{
		time = 0;
		if (flag.point) // half second
		{
			flag.point = 0;
			if (colSet>1) sec++;

			if (sec == 60)
			{
				sec = 0;
				clock[0]++;
				if (clock[0] == 10)
				{
					clock[0] = 0;
					clock[1]++;
					if (clock[1] == 6)
					{
						clock[1] = 0;
						clock[2]++;
						if ((clock[2] == 4)&&(clock[3] == 2))
						{
							clock[2] = 0;
							clock[3] = 0;
							if (automatic)
								type++;
						}
						if (clock[2] == 10)
						{
							clock[2] = 0;
							clock[3]++;
						}
					}
				}
			}
		}
		else
		{
			flag.point = 1;
		}
	}
}

//****************
// Init section
//****************

void Init (void)
{
	ACSR = (1<<ACD); //Turn Off ANA_COMP

	//clock[3] = 1;
	//clock[2] = 2;
	//clock[1] = 0;
	//clock[0] = 0;

	//type = 1;

	//flag.settings = 0;
	//flag.blink = 0;
	colSet = 4;
	oldkeys = 0b11;

	//DDRA = 0;
	//PORTA = 0xFF;

	DDRD = ~((1<<0)|(1<<1));
	PORTD = 0b11;
	DDRB = 0xFF;



	TCCR0B = (1<<CS01); // presc. 8
	TCNT0 = countStart; // 256-8 = 250; 8*250/8 000 000 = 0.00025
	TIMSK = (1<<TOIE0); 
	sei();
}

//****************
// Keyboard and time setup
//****************

void CheckKeyboard (void)
{
	keys = PIND&0b11;
	if (keys != 0b11)
	{
		_delay_ms(5);
		if (((PIND&0b11) == keys)&&(keys!=0b11))
		{
			oldkeys = keys;
		}
	}
	else if (oldkeys != 0b11)
	{
		if (oldkeys == 0b10)
		{
			if ((flag.settings == 0)&&(flag.blink == 0))
			{
				flag.blink = 1;
			}
			else if (flag.blink)
			{
				flag.blink = 0;
				flag.settings = 1;
				colSet = 0;
			}
			else if (flag.settings)
			{
				if (colSet == 4)
				{
					colSet = 0;
				}
				else if (colSet == 2)
				{
					colSet=4;
					flag.settings = 0;
				}
				else
				{
					colSet++;
				}
			}
		}
		else if (oldkeys == 0b01)
		{
			if (flag.blink)
			{
				flag.blink = 0;
				automatic = ~automatic;
			}
			else if (flag.settings)
			{
				clock[colSet]++;
				if (colSet < 2)
					sec = 0;
				if ((colSet==0)&&(clock[0]==10))
				{
					clock[0] = 0;
				}
				else if ((colSet==1)&&(clock[1]==6))
				{
					clock[1] = 0;
				}
				else if ((colSet==2)&&(clock[2]==10))
				{
					clock[2]=0;
					clock[3]++;
				}
				else if (((colSet==2)&&(clock[3]==2))&&(clock[2]==4))
				{
					clock[2]=0;
					clock[3]=0;
				}
			}
			else
			{
				type ++;
			}
		}
		oldkeys = 0b11;
	}
}


//****************
// Service functions
//****************

unsigned char bcd (unsigned char a)
{
	return (((a / 10) << 4)|(a%10));
}

unsigned char transform (unsigned char a)
{
	unsigned char temp, i;
	temp = 0;
	for (i = 0; i<8; i++)
		if (a & (1<<i))
			temp |= r[i];
	return ~temp;
}

//****************
// BCD Visualisation
//****************

void DigitalCode(void)
{
	display[4] = transform(bcd(sec));

	if ((colSet == 0)&&(flag.point))
	{
			display [3] = 0xf0;
	}
	else if ((colSet == 1)&&(flag.point))
	{
			display [3] = 0x0f;
	}
	else if ((flag.point)&&(flag.blink)) display[3] = 0xFF;
	else display[3] = 0;

	display[2] = transform((clock[1]<<4)|(clock[0]));

	if ((colSet == 2)&&(flag.point))
		display[1] = 0xFF;
	else if ((flag.point)&&(flag.blink)) display[1] = 0xFF;
	else display[1] = 0;

	display[0] = transform((clock[3]<<4)|(clock[2]));
}

//****************
// Analog clocks
//****************

unsigned char hours [12][3] PROGMEM =
{
	{(1<<6)|(1<<5)|(0<<4)|(1<<3)|(1<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2)},

	{(1<<6)|(1<<5)|(1<<4)|(0<<3)|(1<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2)},

	{(1<<6)|(1<<5)|(1<<4)|(1<<3)|(0<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2)},

	{(1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(0<<2),
	 (1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2)},

	{(1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (1<<6)|(1<<5)|(1<<4)|(1<<3)|(0<<2)},

	{(1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (1<<6)|(1<<5)|(1<<4)|(0<<3)|(1<<2)},

	{(1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (1<<6)|(1<<5)|(0<<4)|(1<<3)|(1<<2)},

	{(1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (1<<6)|(0<<5)|(1<<4)|(1<<3)|(1<<2)},

	{(1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (0<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2)},

	{(1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2), 
	 (0<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2)},

	{(0<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2)},

	{(1<<6)|(0<<5)|(1<<4)|(1<<3)|(1<<2), 
	 (1<<6)|(0<<5)|(0<<4)|(0<<3)|(1<<2),
	 (1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2)}
};

unsigned char mins [20][2] =
{
	{0, (1<<4)},
	{0, (1<<3)},
	{0, (1<<2)},
	{0, (1<<1)},
	{1, (1<<1)},
	{2, (1<<1)},
	{3, (1<<1)},
	{4, (1<<1)},
	{4, (1<<1)},
	{4, (1<<3)},
	{4, (1<<4)},
	{4, (1<<5)},
	{4, (1<<6)},
	{4, (1<<7)},
	{3, (1<<7)},
	{2, (1<<7)},
	{1, (1<<7)},
	{0, (1<<7)},
	{0, (1<<6)},
	{0, (1<<5)}

};

unsigned char mmins[3] =
{
	0, (1<<4), (1<<5)|(1<<3)
};

unsigned char anal (unsigned char line)
{
	unsigned char res, min, mmin, hr;
	res = 0;
	if (((sec / 12) == (4 - line))&&flag.point)
		res = 1;

	if (flag.settings) res = 0;

	if (flag.point)
		if (flag.blink)
			res = 1;
		else if ((colSet == 0)&&(line > 1))
			res = 1;
		else if ((colSet == 1)&&(line % 4))
			res = 1;
		else if ((colSet == 2)&&(line <3))
			res = 1;

	min = (clock[1]*10+clock[0]);
	mmin = min % 3;
	min = min / 3;
	hr = (clock[3]*10+clock[2]) % 12;
	
	if (mins[min][0] == line)
		res |= mins[min][1];

	switch (line)
	{
		case 1:
		{
			res |= pgm_read_byte(&(hours[hr][0]));
			break;
		}
		case 2:
		{
			res |= mmins[mmin];
			res |= pgm_read_byte(&(hours[hr][1]));
			break;
		}
		case 3:
		{
			res |= pgm_read_byte(&(hours[hr][2]));
			break;
		}
	}
	return transform(res);
}


void AnalogCode(void)
{
	display[0] = anal(0);
	display[1] = anal(1);
	display[2] = anal(2);
	display[3] = anal(3);
	display[4] = anal(4);
}

//****************
// Morse Visualisation
//****************

unsigned char morseArray[5][10] PROGMEM= {
	{0, 0, 0, 0, 0, 1, 1, 1, 1, 1},
	{0, 0, 0, 0, 1, 1, 1, 1, 1, 0},
	{0, 0, 0, 1, 1, 1, 1, 1, 0, 0},
	{0, 0, 1, 1, 1, 1, 1, 0, 0, 0},
	{0, 1, 1, 1, 1, 1, 0, 0, 0, 0}
};

unsigned char morse(unsigned char line)
{
	unsigned char res, temp, temp1;
	temp = bcd(sec);
	temp1 = ((temp & 0xf0)>>4);
	temp &= 0x0f;
	res = (pgm_read_byte(&(morseArray[line][temp]))<<0)|(pgm_read_byte(&(morseArray[line][temp1]))<<1);
	res |= (pgm_read_byte(&(morseArray[line][clock[0]]))<<3)|(pgm_read_byte(&(morseArray[line][clock[1]]))<<4);
	res |= (pgm_read_byte(&(morseArray[line][clock[2]]))<<6)|(pgm_read_byte(&(morseArray[line][clock[3]]))<<7);
	if (flag.point)
		if (flag.blink)
			res |= (1<<2)|(1<<5);
		else if (colSet == 2)
			res |= (1<<5);
		else if (((colSet == 0)&&(line>1))||((colSet == 1)&&(line<3)))
				res |= (1<<2);
	return transform(res);
}

void MorseCode(void)
{
	display[0] = morse(0);
	display[1] = morse(1);
	display[2] = morse(2);
	display[3] = morse(3);
	display[4] = morse(4);
}

//****************
// Type selection
//****************

void PrintPrepare (void)
{
	// Prepare display[];
	
	if (type == typeCount)
		type = 0;
	switch (type)
	{
		case 1:
		{
			DigitalCode();
			break;
		}

		case 2:
		{
			AnalogCode();
			break;
		}

		case 0:
		{
			MorseCode();
			break;
		}
	}
	if ((flag.blink)&&automatic)
		if (flag.point)
			display[0] &= ~((1<<R8)|(1<<R7));
		else
			display[0] |= ((1<<R8)|(1<<R7));
}

void main(void)
{
	Init();
	for (;;) 
	{
		CheckKeyboard();
		PrintPrepare();
	}
}

Автор: maksfff

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js