В этой статье описываются простые часы на базе процессора ATtiny85, в которых для отображения времени в 12-ти часовом формате используютя два светодиодных матричных дисплея с интерфейсом I2C.

Каждую минуту цифры случайным образом "растворяются", и дисплей становится пустым, а затем таким же образом появляются цифры обновленных часов и минут, создавая впечатление, что время распределяется между двумя экранами.

Вступление

На двух светодиодных дисплеях с матрицей 8x8 можно разместить четыре цифры для отображения времени, используя символы размером 3x8, и даже останется место для двоеточия между часами и минутами.

В данном устройстве используются два матричных дисплея Keyestudio на основе драйвера HT16K33, который управляет матрицами методом мультиплексирования и имеет интерфейс I2C, что позволяет общаться с ним при помощи всего двух линий ввода/вывода.

Три перемычки на плате позволяют выбрать один из восьми адресов I2C для каждого дисплея, что позволяет одновременно управлять восемью дисплеями. Можно выбрать адрес от 0x70 (112) до 0x77 (119), а адрес по умолчанию - 0x70 или 112.

Схема

Часы на ATtiny85 и светодиодной матрице с драйвером HT16K33 - схема

Кроме микроконтроллера в схеме используется кварцевый резонатор частотой 8 МГц. Кнопка, подключенная к выводу PB1, позволяет установить время. На платах индикаторов уже имеются подтягивающие резисторы на SCL и SDA, поэтому устанавливать их в цепь не нужно.

Сборка

Схема собрана на макетной плате, индикаторы установлены вертикально в передней части платы. Вот вид макета:

Часы питаются от перезаряжаемой батареи Li-Po 3,7 В.

Программа

Сначала определяем наборы символов для цифр размером 3x8, от 0 до 9:

char CharMap[10][3] = {
{ 0x7F, 0x41, 0x7F }, // 0
{ 0x00, 0x20, 0x7F }, // 1
{ 0x4F, 0x49, 0x79 }, // 2
{ 0x49, 0x49, 0x7F }, // 3
{ 0x78, 0x08, 0x7F }, // 4
{ 0x79, 0x49, 0x4F }, // 5
{ 0x7F, 0x49, 0x4F }, // 6
{ 0x40, 0x40, 0x7F }, // 7
{ 0x7F, 0x49, 0x7F }, // 8
{ 0x79, 0x49, 0x7F }, // 9
};

Переменные adr1 и adr2 используются для указания адресов двух дисплеев:

const int addr1 = 117;
const int addr2 = 116;

Подпрограммы setupDisplay() и setBrightness() настраивают драйвер дисплея HT16K33 и устанавливают яркость от 0 до 15:

void setupDisplay (int addr) {
  Wire.beginTransmission(addr);
  Wire.write(0x21);
  Wire.endTransmission();
  Wire.beginTransmission(addr);
  Wire.write(0x81);
  Wire.endTransmission();
}

void setBrightness (int addr, int bri) {
  Wire.beginTransmission(addr);
  Wire.write(0xe0 + bri);
  Wire.endTransmission();
}

Подпрограмма putColumn() записывает битовый шаблон для одного столбца (от 0 до 15) для соответствующего дисплея:

void putColumn (int col, int bits) {
  int addr;
  if (col < 8) addr = addr1; else addr=addr2;
  Wire.beginTransmission(addr);
  Wire.write((col & 0x07)<<1);
  Wire.write(bits>>1 | bits<<7);
  Wire.endTransmission();
}

Выражение:

(bits >> 1)|(bits << 7)

исправляет момент, что по какой-то причине строки этих дисплеев нумеруются в последовательности 7, 0, 1, 2, 3, 4, 5, 6 снизу вверх.

Подпрограмма copyDigit() копирует три столбца для определения символа указанной цифры в три последовательных элемента в массиве columns[], начиная со столбца col:

void copyDigit (int digit, int col, int columns[]) {
  for (int i=0; i < 3; i++) columns[col+i] = CharMap[digit][i];
}

Наконец, showTime() отображает время в часах и минутах:

void showTime (int hours, int mins, int halfseconds) {
  int cols[16];                         // Массив столбцов матрицы
  // Десятки часов
  if (hours/10 != 0) {
    cols[0] = CharMap[1][1]; cols[1] = CharMap[1][2];
  } else {
    cols[0] = 0; cols[1] = 0;
  }
  // Единицы часов
  cols[2] = 0;
  copyDigit(hours % 10, 3, cols);
  // Двоеточие
  cols[6] = 0;
  if (halfseconds & 1) cols[7] = 0x02; else cols[7] = 0x10;
  // Десятки минут
  cols[8] = 0;
  copyDigit(mins / 10, 9, cols);
  // Единицы минут
  cols[12] = 0;
  copyDigit(mins % 10, 13, cols);
  if (halfseconds == 119 && !ButtonDown()) {
    FadeOut(cols);
  } else if (halfseconds == 0 && !ButtonDown()) {
    FadeIn(cols);
  } else {
    // Обычный режим
    for (int col=0; col<16; col++) {
      putColumn(col, cols[col]);
    }
  }
}

Эффект растворения

Эффект растворения включается при каждом изменении минут:

 

Это реализуется с помощью следующих двух процедур:

Подпрограмма FadeOut() гасит пиксели на предыдущем экране:

void FadeOut(int cols[]) {
  for (int i=0; i<127; i++) {
    int r = pseudoRandom();
    int col = r & 0xF;
    int bit = r>>4;
    cols[col] = cols[col] & ~(1<<bit);
    putColumn(col, cols[col]);
    delay(3); // Max. 3
  }
}

а затем процедура FadeIn() зажигает пиксели на новом экране:

void FadeIn(int cols[]) {
  int newcols[16];
  for (int i=0; i<16; i++) newcols[i] = 0;
  for (int i=0; i<127; i++) {
    int r = pseudoRandom();
    int col = r & 0xF;
    int bit = r>>4;
    newcols[col] = newcols[col] | (cols[col] & 1<<bit);
    putColumn(col, newcols[col]);
    delay(3); // Max. 3
  }
}

Значение бита вычисляется с помощью генератора псевдослучайных чисел, который генерирует числа от 1 до 127 в псевдослучайной последовательности. Каждое число используется для выбора одной из точек, чтобы создать видимость случайной последовательности изменений:

int pseudoRandom() {
  static int r = 1;
  int l = r & 1;
  r = r>>1;
  if (l == 1) r = r ^ 0x69;
  return r;
}

Кнопка установки времени

Кнопка "Установить время" позволяет установить правильное время. При удержании кнопки происходит увеличение значения часов с интервалом два раза в секунду. Отпустив и нажав кнопку еще раз, происходит увеличение минут также с интервалом два раза в секунду.

Кнопка подключается к выводу PB1, у которого настроена подтяжка к плюсу питания. Состояние кнопки обрабатывается функцией ButtonDown(), которая возвращает true, если кнопка нажата:

boolean ButtonDown() {
  return (digitalRead(1) == 0); // Если кнопка нажата, то истина
}

Процедура updateTime() вызывается дважды в секунду и обновляет текущее время, также в зависимости от состояния кнопки "Установить время" обновляются часы или минуты:

void updateTime() {
  int minutes, hours, halfseconds;
  halfseconds = Time % 120;
  minutes = (Time / 120) % 60;
  hours = (Time / 7200) % 12;
  if (ButtonDown()) {
    if (ButtonState == 1 || ButtonState == 3) {
        ButtonState = (ButtonState + 1) % 4;
    } else if (ButtonState == 2) {                 // Подготовка часов
      hours = (hours + 1) % 12;
    } else {                                       // Подготовка минут
      minutes = (minutes + 1) % 60;
    }
    Time = (unsigned long)hours * 7200 + minutes * 120 + 1;
  } else {                                         // Кнопка не нажата
     if (ButtonState == 0 || ButtonState == 2) {
        ButtonState = (ButtonState + 1) % 4;
     }
     Time = (Time + 1) % 172800;                   // Повтор через 24 часа
  }
  showTime(hours+1, minutes, halfseconds);
}

Чтобы эффект "растворения" не раздрожал во время устанавки времени, он отключается, если кнопка нажата.

Основной цикл просто вызывает функцию updateTime() каждые 500 мс:

void loop() {
  while (millis() - Start < 500);
  Start = Start + 500;
  updateTime();
} 

Компилирование программы

Программа скомпилирована с помощью Spence Konde's ATTiny Core, для этого выберите вариант ATtiny25/45/85 под заголовком ATTinyCore в меню плат. Затем убедитесь, что следующие параметры установлены следующим образом (игнорируйте любые другие параметры):

Chip: "ATtiny85"
Clock: "8 MHz (external)"
B.O.D Level: "B.O.D. Disabled"

Выберите записать загрузчик чтобы установить биты конфигурации для использования с внешним кристаллом. Затем загрузите программу с помощью ISP программатора (внутрисистемное программирование).

Дальнейшие предложения

Данные дисплеи очень яркие, и вы можете затемнить их после наступления темноты для экономии энергии. Для этого достаточно добавить функцию, которая регулирует яркость, вызывая setBrightness() в зависимости от времени суток, используя глобальную переменную Time.

Дисплеи поддерживают восемь разных адресов I2C, поэтому к данной схеме можно полключить до восьми дисплеев. Например, вы можете расширить этот проект, чтобы отображать время в четырех разных точках мира.

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

char CharMap[10][3] = {
{ 0x3E, 0x41, 0x3E }, // 0
{ 0x00, 0x20, 0x7F }, // 1
{ 0x27, 0x49, 0x31 }, // 2
{ 0x2A, 0x49, 0x36 }, // 3
{ 0x18, 0x28, 0x7F }, // 4
{ 0x7A, 0x49, 0x4E }, // 5
{ 0x3E, 0x49, 0x26 }, // 6
{ 0x47, 0x48, 0x70 }, // 7
{ 0x36, 0x49, 0x36 }, // 8
{ 0x32, 0x49, 0x3E }, // 9
};


Файлы к статье "Часы на ATtiny85 и светодиодной матрице с драйвером HT16K33"
Описание:

Исходный код(СИ)

Размер файла: 1.67 KB Количество загрузок: 170 Скачать