Иногда идея в голове зреет годами. Подсознание впитывает интересности, раскладывает по полочкам, копит критическую массу, пока все это наконец-то не выливается во что-нибудь интересное.
Так случилось и в тот раз. В пространственно-временном континууме сошлись красивый, но уже бесполезный «Справочник мини- и микро- ЭВМ», красная светодиодная матрица 5х8 и день рождения друга-программиста.
Как тут не сделать часы? Да и не просто двоичные, а какие-нибудь позаковыристей?
Цель
Главной целью было не просто показать время, а показать его так, чтобы пришлось решить небольшую головоломку, развлечься между погружениями в код. и посмотреть время на компьютере и продолжить программировать.
Железо
Тогда ещё я не имел привычки документировать такие простые схемы (о чем очень жалею), поэтому я просто достал из ящика стола любимую многими tiny2313, разогрел паяльник, и понеслась!

Транзисторы, если не ошибаюсь — КТ361 из старых запасов. Выводы матрицы подключены так, чтобы было удобнее паять. Если вам всё-таки непонятно, что здесь происходит — могу только порекомендовать замечательный сайт уважаемого DiHalt: easyelectronics
В качестве времязадающей цепи я использовал внутренний RC генератор частоты, чего, конечно же, делать нельзя, но прошлый опыт создания комнатных часов показал, что калибровка по отклонению за неделю позволяет достигнуть точности в пять минут за год. Хотя, может быть, мне просто повезло.
Режимы отображения
Первым делом, естественно, я реализовал двоичное отображение. Вернее, двоично-десятичное (BCD). Так можно несколько быстрее считать информацию с часов, поэтому с таким отображением после небольшой тренировки справится любая домохозяйка, которая привыкла пересчитывать сдачу в магазине (свою любимую гуманитарочку я научил считать на пальцах до 1023, чему она весьма рада).

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

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

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

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