Хабр! Добро пожаловать снова. 

Сегодня мы сделаем одно из самых бесполезных устройств из тех, что можно собрать, но как показывает жизнь, лучше сделать что-то, чем не сделать ничего. Тем не менее, в защиту этой бесполезности можно сказать только что-то вроде: много ли интересных дел, которыми мы занимаемся являются хоть сколько бы полезными?

Мы будем делать часы, таймер и игру в одном устройстве.

Готовое устройство и печеньки.
Готовое устройство и печеньки.

UPD #1

Дисклеймер

Внимание!

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

Некоторое вступление

Спустя много лет я решил вернуться снова к написанию статей, с новыми знаниями и силами. Знаете, интернет научил меня всему, что я знаю и даже больше, чем просто всему. Интернет стал не просто учением, в котором тяжело, но и боем, в котором легко. И я благодарен всем, кто так или иначе принял участие в моем обучении, через статьи, описание каких-то технологий, видео на YouTube и просто критику моих работ. Это герои моего времени, только благодаря им я сейчас являюсь неплохим специалистом. Ведь я не учился в этих ваших институтах и образований не получал, да и всего у меня 9 классов. Спасибо тем, кто пишет интернет.

И еще

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

Компоненты

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

Не все компоненты были куплены мой, некоторые лежали без дела, или появились прямо за часы перед разработкой этого устройства :)

  1. Резисторы 150 Ом 0.25 Ватт — 12 шт.

  2. Конденсаторы 50 вольт 10 микрофарад — 4 шт.

  3. Тактовая кнопка  6x6мм — 3 шт.

  4. Светодиод 75x3мм — 1 шт.

  5. Пьезо зуммер  — 1 шт.

  6. Кварцевый резонатор 16 МГц — 1 шт.

  7. Разъём типа гребёнка — 7 шт.

  8. Джампер (перемычка) — 1 шт.

  9. Четырехразрядный семисегментный индикатор (Sm56425bsr3 или аналоги) — 1 шт.

  10. Сдвиговый регистр 74ch595 корпус DIP — 1 шт.

  11. Панель под микросхему 74ch595 корпус DIP (16 ножек) — 1 шт.

  12. Микроконтроллер ATmega328p корпус DIP — 1 шт.

  13. Панель под микросхему ATmega328p корпус DIP (28 ножек) — 1 шт.

  14. Монтажная плата 40x60мм — 2 шт.

  15. Батарейный отсек cr2032 — 2 шт.

  16. Батарейка cr2032 — 2 шт.

  17. Втулка 5x8x0мм  (Не точно) — 4 шт.

  18. Болт 3x6мм (Не точно) — 4 шт.

  19. Шайба 5мм (Не точно)— 4 шт.

  20. Гайка 3мм (Не точно) — 4 шт.

  21. Преобразователь USB-UART CP2102 — 1 шт.

Также, рекомендую при необходимости купить флюс, припой и паяльник.

Я намеренно не указываю марку проводов, которая ва�� подойдет, так как совсем не владею информацией об их параметрах. Могу посоветовать МГТФ, вполне возможно, что очень хорошо подойдут. Если вы знаете, какие провода точно оптимальны, оставьте информацию в комментариях или напишите мне в личные сообщения  @prohetamine.

Сдвиговый регистр 74ch595

Наверное, многим новичкам станет не по себе от понимания принципов работы микросхемы 74ch595, вне этой статьи и пропустить этот этап я просто не хочу. Сейчас я попробую максимально доступно объяснить, как она работает и чем будет полезна в конкретном случае с моим устройством.

Проще говоря, микросхема предназначена для расширения количества цифровых выходов.

Распиновка. Внимание! Рисунок имеет незначительные неточности в маркировке контактов, это сделано для более простого усвоения и понимания работы.

Самые загадочные контакты управления, которые вызывают интерес: 

  • output pin * — контакты вывода

  • DS — (Serial Data Input) контакт, который определяет состояние напряжения на контактах вывода

  • SH — (Shift Register Clock Input) контакт, который записывает состояние которое определенно в DS

  • ST — (Storage Register Clock Input) контакт, который открывает микросхему для записи и закрывает, устанавливая на контакты вывода нужные состояния определенные DS

Уверен, визуальный пример, поможет вам понять происходящее лучше.

Монтажная схема соединений

Если нет, то я оставил и интерактивную версию, кнопочки работают, можно понажимать.

Теперь, когда вы овладели работой с микросхемой, можно приступить к следующему пункту.

Тонкости

Внимание!

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

Когда мы программируем контроллер, очень важно не путать rx и tx, иначе контроллер просто не прошьется.

Это странная шутка, но работает очень просто, каждый разряд имеет 8 сегментов, у каждого разряда есть минус и восемь плюсов, по сути это те же светодиоды, только в общем корпусе.

Каждая микросхема имеет ключ, то есть небольшую метку на корпусе, это признак помогает определить положение установки. 

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

Плюсик: у всех новых электронных компонентов, которые имеют полярность, выглядит как хромоног.

Цветной хромоног:

Батарейный отсек, тоже имеет свою не очевидную полярность..

Монтажная схема соединений

Так выглядит схема нашего устройства:

UPD #2

По требованию комментаторов скоро тут появится принципиальная схема.

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

Пока пин кнопки состояние, которого мы читаем не притянут к плюсу или минусу он выдает случайные (101010000101010) результаты и кнопка, не может работать нормально, чтобы «Стабилизировать» состояние кнопки нам нужно притянуть наш пин через резистор к минусу или плюсу (принято к минусу). Тогда при нажатии у нас будет 1 иначе 0. На момент создания устройства и написания статьи, автор не знал, что существует pull-up резистор встроенный в саму ATmega328p. Почитать можно об этом на официальном сайте.

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

Резисторы предназначены для ограничения тока исходящего от ATmega328p, а именно 5 вольт мы ограничиваем до 3-х вольт, так как почти все светодиоды ограничены напряжением в 3 вольта и привыкли работать за еду, более высокое напряжение приведет к деградации, насколько быстрой зависит от тока, хоть у ATmega328p он не большой, примерно 20-40 миллиампер, деградацию и сгорание не будет видно сразу, но оно случится, явно намного раньше положенного.

О нем говорят все, но никто не знает зачем он. На самом деле все просто. Предельно просто. Эта микросхема умножает количество контактов, с условных трех до N. Мой максимум 265+ выводов, но возможно и больше. В этом месте мог бы возникнуть хороший вопрос, по сути ты ведь делаешь из трех контактов четыре, а остальные четыре не используешь(?) На эту тему можно конечно рассуждать, зачем и почему, правильный ответ только один — дать возможность устройству развиваться.

Монтаж компонентов

Внимание!

Контакты компонентов, помеченные красным и черным маркерами, имеют полярность, будьте внимательны при монтаже, придерживайтесь рисунка.

Внимание!

Соблюдайте порядок установки микросхем по ключам.

Устанавливаем панели.

Устанавливаем конденсаторы и кнопки.

Устанавливаем разъемы и пьезо зуммер.

Устанавливаем микросхемы и резисторы.

Устанавливаем индикатор, светодиод и резонатор.

Устанавливаем батарейные отсеки.

Прототип

Теперь, когда у нас есть не просто бесполезная безделушка, но еще и не рабочая, нужно сделать её рабочей, поэтому добавим много магических проводков.

Объединенная схема

Соединим основные линии питания и необходимую обвязку первой платы.

Соединим кнопки, светодиод индикатор прошивки и пьезо зуммер.

Соединим конденсаторы и семисегментный индикатор с сдвиговым регистром 74ch595.

Соединим семисегментный индикатор с микроконтроллером.

В финале первая плата у вас получится такой:

Вторая плата, но тут все совсем просто. Соединим последовательно элементы питания.

Соединим все вместе.

Устройство

Программирование

Подключаем так как на картинке, и можно начинать прошивать микроконтроллер.

Бесспорно, абсолютно, однозначно. Мой код на C++ далек от идеала, но я, как всегда, пытался. Я пишу на JS (ну, вы поняли). И тем не менее, я все равно собой доволен, хотя бы, потому что не притрагиваясь и без того к незнакомому мне языку больше года, мне как-то удалось организовать не только структуру с своими правилами, а также создать богатый функционал: часы, игру и два таймера c разными уровнями точности. Можешь сделать лучше? Есть что дополнить? GitHub

Основной файл проекта, к которому я подключаю все остальные файлы и библиотеку AsyncDelay, с которой управлять синхронным потоком становится проще, чем обычно (имхо). Изначально, в процессе написания кода, я обозначил для себя два компонента - это actionDriver и actionContoller. где первый переводя на JavaScript - тянский (является почти как Event Loop), то есть выполняет стек задач только не событийных, а перманентных, а второй выполняет роль Setter'a.

// Подключаем библиотеку
#include <AsyncDelay.h>

// Назначаем имена прерываний
AsyncDelay delayRenderRowFirst;
AsyncDelay delayRenderRowTwo;
AsyncDelay delayRenderRowThree;
AsyncDelay delayRenderRowFour;
AsyncDelay delayAnimaton;
AsyncDelay delayButtonHandle;

// Определяем пины кнопок
const int BTN_SET_TOP = A5
        , BTN_SET_MIDDLE = A4
        , BTN_SET_BOTTOM = A3;

// Определяем пины сдвигового регистра
const int DS = 11
        , ST_CP = 10
        , SH_CP = 9;

// Определяем пины семисигментного индикатора
const int A = 2
        , B = 4
        , C = 7
        , D = 5
        , E = 1
        , F = 3
        , G = 8
        , DP = 6;

// Определяем пины светодиода и зуммера
const int BLINK = 13;
const int SIGNAL = 12;

// подключаем модули проекта
#include "viewer.h"
#include "animation.h"
#include "time.h"
#include "mtimer.h"
#include "timer.h"
#include "gameUnLocker.h"
#include "button.h"

void setup () {
  // назнчаем интервалы прерываний 
  delayRenderRowFirst.start(1, AsyncDelay::MILLIS);
  delayRenderRowTwo.start(1, AsyncDelay::MILLIS);
  delayRenderRowThree.start(1, AsyncDelay::MILLIS);
  delayRenderRowFour.start(1, AsyncDelay::MILLIS);
  delayButtonHandle.start(1000, AsyncDelay::MILLIS);
  delayAnimaton.start(500, AsyncDelay::MILLIS);
  
  // Устанавливаем пины на выход
  pinMode(A, OUTPUT);
  pinMode(B, OUTPUT);
  pinMode(C, OUTPUT);
  pinMode(D, OUTPUT);
  pinMode(E, OUTPUT);
  pinMode(F, OUTPUT);
  pinMode(G, OUTPUT);
  pinMode(DP, OUTPUT);

  pinMode(BLINK, OUTPUT);
  pinMode(SIGNAL, OUTPUT);
  
  pinMode(DS, OUTPUT);
  pinMode(ST_CP, OUTPUT);
  pinMode(SH_CP, OUTPUT);

  pinMode(BTN_SET_TOP, INPUT);
  pinMode(BTN_SET_MIDDLE, INPUT);
  pinMode(BTN_SET_BOTTOM, INPUT);

  // Сбрасываем все значения на пинах
  digitalWrite(A, 0);
  digitalWrite(B, 0);
  digitalWrite(C, 0);
  digitalWrite(D, 0);
  digitalWrite(E, 0);
  digitalWrite(F, 0);
  digitalWrite(G, 0);
  digitalWrite(DP, 0);

  // Сбрасываем значения на сдвиговом регистре
  digitalWrite(ST_CP, 0);
  for (int i = 0; i < 8; i++) {
    digitalWrite(SH_CP, 0);
    digitalWrite(DS, 1); 
    digitalWrite(SH_CP, 1);
  }
  digitalWrite(ST_CP, 1);
}

// Активируем анимацию
boolean aminationStartActive = true; 

void loop () {  
  // Устанавливаем драйвера в общик поток
  viewDriver(); 
  buttonDriver();
  timeDriver();
  mTimerDriver();
  timerDriver();
  gameUnLockerDriver();
  animationDriver();

  // Останавливаем анимацию и запускам виджет времени
  if (millis() > 3000 && aminationStartActive) {
    aminationStartActive = false;
    timeShow = true;
  }

  // Запускаем анимацию
  if (millis() < 1) {
    animationController(false, "hey ");
  }
}
Управление устройством
// Режим работы
int modeId = -1;
// Перемещение между режимами
int selectId = 0;
// Премещение между символами
int carret = 0;

/*
 * Режимы работ
 * 0 - время
 * 1 - таймер
 * 2 - минутный таймер
 * 3 - игра
 */

#define MODE_NONE -1
#define MODE_TIME 0
#define MODE_TIMER 1
#define MODE_MINUTES_TIMER 2
#define MODE_GAME 3
 
// Состояния кнопок
int clickedFirstButton = 0;
int clickedMiddleButton = 0;
int clickedLastButton = 0;

// Управление состоянием кнопок
void buttonController (int first, int middle, int last) {
  if (first != -1) {
    clickedFirstButton = first;
  }

  if (middle != -1) {
    clickedMiddleButton = middle;
  }

  if (last != -1) {
    clickedLastButton = last;  
  }
}

// Прекращает работу всех виджетов
void mainOffControllers () {
  timeController(false);
  mTimerController(false);
  timerController(false);
  gameUnLockerController(false);
}

// Меню
void menuList () {
  if (selectId == MODE_TIME) {
    viewController(0, String('c'));
    viewController(1, String(' '));
    viewController(2, String(' '));
    viewController(3, String(' ')); 
  }

  if (selectId == MODE_TIMER) {
    viewController(0, String('t'));
    viewController(1, String(' '));
    viewController(2, String(' '));
    viewController(3, String(' ')); 
  }

  if (selectId == MODE_MINUTES_TIMER) {
    viewController(0, String('m'));
    viewController(1, String('t'));
    viewController(2, String(' '));
    viewController(3, String(' ')); 
  }

  if (selectId == MODE_GAME) {
    viewController(0, String('g'));
    viewController(1, String(' '));
    viewController(2, String(' '));
    viewController(3, String(' ')); 
  }
}

// Обработчик первой кнопки
void buttonFristEvent (int clickedFirst, int clickedMiddle, int clickedLast) {
  if (modeId == MODE_NONE) {
    selectId++;
    if (selectId > 3) {
      selectId = MODE_TIME;
    }
    mainOffControllers();
    menuList();
  }

  if (clickedLast == 1 || clickedLast == 2 || clickedLast == 3 || clickedLast == 4) {
    if (modeId == MODE_TIME) {
      for (int i = 0; i < 4; i++) {
        if (carret == i) {
          timeTickerController(i, 1);
          viewController(carret, String(timeTicker[i]));
        }
      }
    }

    if (modeId == MODE_TIMER) {
      for (int i = 0; i < 4; i++) {
        if (carret == i) {
          timerTickerController(i, 1);
          viewController(carret, String(timerTicker[i]));
        }
      } 
    }

    if (modeId == MODE_MINUTES_TIMER) {
      for (int i = 0; i < 4; i++) {
        if (carret == i) {
          mTimerTickerController(i, 1);
          viewController(carret, String(mTimerTicker[i]));
        }
      }     
    }

    if (modeId == MODE_GAME) {
      for (int i = 0; i < 4; i++) {
        if (carret == i) {
          gameUnLockerPlayerController(i, 1);
          viewController(carret, String(gameUnLockerData[i]));
        }
      }     
    }
  }
}

// Обработчик второй кнопки
void buttonMiddleEvent (int clickedFirst, int clickedMiddle, int clickedLast) {
  if (modeId == MODE_NONE) {
    selectId--;
    if (selectId < 0) {
      selectId = MODE_MINUTES_TIMER;
    }
    mainOffControllers();
    menuList();
  }

  if (clickedLast == 1 || clickedLast == 2 || clickedLast == 3 || clickedLast == 4) {
    if (modeId == MODE_TIME) {
      for (int i = 0; i < 4; i++) {
        if (carret == i) {
          timeTickerController(i, -1);
          viewController(carret, String(timeTicker[i]));
        }
      }
    }

    if (modeId == MODE_TIMER) {
      for (int i = 0; i < 4; i++) {
        if (carret == i) {
          timerTickerController(i, -1);
          viewController(carret, String(timerTicker[i]));
        }
      } 
    }

    if (modeId == MODE_MINUTES_TIMER) {
      for (int i = 0; i < 4; i++) {
        if (carret == i) {
          mTimerTickerController(i, -1);
          viewController(carret, String(mTimerTicker[i]));
        }
      }     
    }

    if (modeId == MODE_GAME) {
      for (int i = 0; i < 4; i++) {
        if (carret == i) {
          gameUnLockerPlayerController(i, -1);
          viewController(carret, String(gameUnLockerData[i]));
        }
      }     
    }
  }
}

// Обработчик третьей кнопки
void buttonLastEvent (int clickedFirst, int clickedMiddle, int clickedLast) {
  if (modeId == MODE_NONE) {
    modeId = selectId;
    buttonController(0, 0, 0);
  
    if (modeId == MODE_TIME) {
      timeController(true);
    }

    if (modeId == MODE_TIMER) {
      timerController(true);
    }

    if (modeId == MODE_MINUTES_TIMER) {
      mTimerController(true);
    }

    if (modeId == MODE_GAME) {
      gameUnLockerController(true);
    }
    return; 
  }

  if (clickedLast == 1 && modeId == MODE_TIME) {
    mainOffControllers();
    for (int i = 0; i < 4; i++) {
      viewController(i, String(timeTicker[i]));
    }
    viewController(carret, String('_'));  
    animationController(false, "edit"); 
    return;
  }

  if (modeId == MODE_TIME) {
    for (int i = 0; i < 4; i++) {
      viewController(i, String(timeTicker[i]));   
    } 
  
    if (carret < 3) {
      carret++;
      viewController(carret, String('_'));  
      return;
    } else {
      carret = 0;
      modeId = MODE_NONE;
      timeController(true);
      buttonController(0, 0, 0);
    }
  }

  if (clickedLast == 1 && modeId == MODE_TIMER) {
    mainOffControllers();
    for (int i = 0; i < 4; i++) {
      viewController(i, String(timerTicker[i]));
    }
    viewController(carret, String('_'));  
    animationController(false, "edit"); 
    return;
  }

  if (modeId == MODE_TIMER) {
    for (int i = 0; i < 4; i++) {
      viewController(i, String(timerTicker[i]));   
    } 
  
    if (carret < 3) {
      carret++;
      viewController(carret, String('_'));  
      return;
    } else {
      carret = 0;
      modeId = MODE_NONE;
      timerController(true);
      buttonController(0, 0, 0);
    }
  }

  if (clickedLast == 1 && modeId == MODE_MINUTES_TIMER) {
    mainOffControllers();
    for (int i = 0; i < 4; i++) {
      viewController(i, String(mTimerTicker[i]));
    }
    viewController(carret, String('_'));  
    animationController(false, "edit"); 
    return;
  }

  if (modeId == MODE_MINUTES_TIMER) {
    for (int i = 0; i < 4; i++) {
      viewController(i, String(mTimerTicker[i]));   
    } 
  
    if (carret < 3) {
      carret++;
      viewController(carret, String('_'));  
      return;
    } else {
      carret = 0;
      modeId = MODE_NONE;
      mTimerController(true);
      buttonController(0, 0, 0);
    }
  }

  if (clickedLast == 1 && modeId == MODE_GAME) {
    mainOffControllers();
    for (int i = 0; i < 4; i++) {
      viewController(i, String(gameUnLockerData[i]));
    }
    viewController(carret, String('_'));  
    animationController(false, "play"); 
    return;
  }

  if (modeId == MODE_GAME) {
    for (int i = 0; i < 4; i++) {
      viewController(i, String(gameUnLockerData[i]));   
    } 
  
    if (carret < 3) {
      carret++;
      viewController(carret, String('_'));  
      return;
    } else {
      carret = 0;
      modeId = MODE_NONE;
      gameUnLockerController(true);
      buttonController(0, 0, 0);
    }
  }
}

// Отслеживает состояние кнопок
boolean buttonDriverFlag = false;

// Ловит нажатия кнопок
void buttonDriver () {
  if (delayButtonHandle.isExpired()) {
    if (analogRead(BTN_SET_TOP) >= 128) {
      if (!buttonDriverFlag) {
        tone(SIGNAL, 1700, 50);
        clickedFirstButton++;
        buttonFristEvent(clickedFirstButton, clickedMiddleButton, clickedLastButton);
      }
      buttonDriverFlag = true;
      return;
    }

    if (analogRead(BTN_SET_MIDDLE) >= 128) {
      if (!buttonDriverFlag) {
        tone(SIGNAL, 1700, 50);
        clickedMiddleButton++;
        buttonMiddleEvent(clickedFirstButton, clickedMiddleButton, clickedLastButton);
      }
      buttonDriverFlag = true;
      return;
    }
    
    if (analogRead(BTN_SET_BOTTOM) >= 128) {
      if (!buttonDriverFlag) {
        tone(SIGNAL, 1700, 50);
        clickedLastButton++;
        buttonLastEvent(clickedFirstButton, clickedMiddleButton, clickedLastButton);
      }
      buttonDriverFlag = true;
      return;
    }

    buttonDriverFlag = false;
  }
}
Отрисовка на семисигментном индикаторе
/*
 *                     A                
 *              @@@@@@@@@@@@@@@@@      
 *             @@@@@@@@@@@@@@@@@      
 *         @@                    @@   
 *        @@@                   @@@   
 *        @@@                   @@@   
 *       @@@                    @@@   
 *       @@@ F                 @@@ B   
 *       @@@                   @@@    
 *      @@@                    @@     
 *      @@@                   @@@     
 *      @@          G         @@@     
 *         @@@@@@@@@@@@@@@@@@         
 *         @@@@@@@@@@@@@@@@@          
 *     @@                    @@@      
 *    @@@                   @@@       
 *    @@@                   @@@       
 *    @@                    @@@       
 *   @@@ E                 @@@ C       
 *   @@@                   @@@        
 *   @@                    @@@        
 *  @@@                   @@@         
 *  @@@         D         @@@         
 *     @@@@@@@@@@@@@@@@@@      @@     
 *     @@@@@@@@@@@@@@@@@       @@ DP
 * 
 * 
 */

// Состояние
int valueRenderRow[4][8] = {
  { 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0 }
};

// Состояние двоеточия 
boolean dotShow = false;

// Отрисовывет отдельный символ
void view (int* symbol, int offset) {
  digitalWrite(A, 0);
  digitalWrite(B, 0);
  digitalWrite(C, 0);
  digitalWrite(D, 0);
  digitalWrite(E, 0);
  digitalWrite(F, 0);
  digitalWrite(G, 0);
  digitalWrite(DP, 0); 
  
  digitalWrite(ST_CP, 0);
  for (int i = 0; i < 8; i++) {
    digitalWrite(SH_CP, 0);
    digitalWrite(DS, 1); 
    digitalWrite(SH_CP, 1);
  }
  digitalWrite(ST_CP, 1);

  delayMicroseconds(1000);

  for (int r = 0; r < 10; r++) {
    digitalWrite(A, symbol[0]);
    digitalWrite(B, symbol[1]);
    digitalWrite(C, symbol[2]);
    digitalWrite(D, symbol[3]);
    digitalWrite(E, symbol[4]);
    digitalWrite(F, symbol[5]);
    digitalWrite(G, symbol[6]);
    digitalWrite(DP, symbol[7]); 
  
    if (dotShow && offset == 4) {
      digitalWrite(DP, 1); 
    } else {
      digitalWrite(DP, 0); 
    }
    
    digitalWrite(ST_CP, 0);
    for (int i = 0; i < 8; i++) {
      digitalWrite(SH_CP, 0);
      digitalWrite(DS, i != offset); 
      digitalWrite(SH_CP, 1);
    }
    digitalWrite(ST_CP, 1); 
  }
  
  delayMicroseconds(1000);
}

// Отрисовывет символы
void viewDriver () {
  int OFFSET_0 = 6;
  int OFFSET_1 = 4;
  int OFFSET_2 = 3;
  int OFFSET_3 = 5;

  if (delayRenderRowFirst.isExpired()) {
    view(valueRenderRow[0], OFFSET_0);
  }

  if (delayRenderRowTwo.isExpired()) {
    view(valueRenderRow[1], OFFSET_1);
  }

  if (delayRenderRowThree.isExpired()) {
    view(valueRenderRow[2], OFFSET_2);
  }

  if (delayRenderRowFour.isExpired()) {
    view(valueRenderRow[3], OFFSET_3);
  }
}

// Контроллер управляющий символами
void viewController (int offset, String symbol) {
  String viewSymbol = "00000000";

  if (symbol[0] == '0') { viewSymbol = "11111100"; }
  if (symbol[0] == '1') { viewSymbol = "01100000"; }
  if (symbol[0] == '2') { viewSymbol = "11011010"; }
  if (symbol[0] == '3') { viewSymbol = "11110010"; }
  if (symbol[0] == '4') { viewSymbol = "01100110"; }
  if (symbol[0] == '5') { viewSymbol = "10110110"; }
  if (symbol[0] == '6') { viewSymbol = "10111110"; }
  if (symbol[0] == '7') { viewSymbol = "11100000"; }
  if (symbol[0] == '8') { viewSymbol = "11111110"; }
  if (symbol[0] == '9') { viewSymbol = "11110110"; }
  if (symbol[0] == ' ') { viewSymbol = "00000000"; }
  if (symbol[0] == '_') { viewSymbol = "00010000"; }
  if (symbol[0] == 'a') { viewSymbol = "11101110"; }
  if (symbol[0] == 'b') { viewSymbol = "00111110"; }
  if (symbol[0] == 'c') { viewSymbol = "00011010"; }
  if (symbol[0] == 'd') { viewSymbol = "01111010"; }
  if (symbol[0] == 't') { viewSymbol = "00011110"; }
  if (symbol[0] == 'e') { viewSymbol = "10011110"; }
  if (symbol[0] == 'f') { viewSymbol = "10001110"; }
  if (symbol[0] == 'g') { viewSymbol = "11110110"; }
  if (symbol[0] == 'h') { viewSymbol = "00101110"; }
  if (symbol[0] == 'i') { viewSymbol = "00001100"; }
  if (symbol[0] == 'j') { viewSymbol = "01111000"; }
  if (symbol[0] == 'k') { viewSymbol = "01101110"; }
  if (symbol[0] == 'l') { viewSymbol = "00011100"; }
  if (symbol[0] == 'm') { viewSymbol = "00101010"; }
  if (symbol[0] == 'n') { viewSymbol = "00101010"; }
  if (symbol[0] == 'o') { viewSymbol = "00111010"; }
  if (symbol[0] == 'p') { viewSymbol = "11001110"; }
  if (symbol[0] == 'q') { viewSymbol = "11100110"; }
  if (symbol[0] == 'r') { viewSymbol = "11001100"; }
  if (symbol[0] == 's') { viewSymbol = "10110110"; }
  if (symbol[0] == 't') { viewSymbol = "00011110"; }
  if (symbol[0] == 'u') { viewSymbol = "00111000"; }
  if (symbol[0] == 'v') { viewSymbol = "01111100"; }
  if (symbol[0] == 'w') { viewSymbol = "00111000"; }
  if (symbol[0] == 'x') { viewSymbol = "01101110"; }
  if (symbol[0] == 'x') { viewSymbol = "01101110"; }
  if (symbol[0] == 'y') { viewSymbol = "01110110"; }
  if (symbol[0] == 'z') { viewSymbol = "11011010"; }
 
  for (int i = 0; i < 8; i++) {
    valueRenderRow[offset][i] = String(viewSymbol[i]).toInt();
  }
}

// Контроллер отвечающий за двоеточие
void viewControllerDot (boolean isShow) {
  dotShow = isShow;
}
Анимации переходов
// Состояние переходов анимации
float animationTicker = -1;

// Звук в анимации
int animationSound = true;

// Сообщение анимации
String animationMessage = "";

// Последнее отрисовоное состояние, так как я не понял как 
// работают коллбеки и есть ли они вообще, я придумал свой способ 
// тут я храню то что было отрисованно в основном стейте чтобы показать его после анимации 
int animationSaveValueRow[4][8] = {
  { 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 0, 0, 0, 0, 0, 0 }
};

// Сохраняем основное состояние
void animationSaveState () {
  for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 8; x++) {
      animationSaveValueRow[y][x] = valueRenderRow[y][x];
    }  
  }
}

// Восстанавливаем состояние
void animationPushState () {
  for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 8; x++) {
      valueRenderRow[y][x] = animationSaveValueRow[y][x];
    }  
  }
}

// Отрисовывем анимацию
void animationDriver () {
  if (delayAnimaton.isExpired()) {
    if (animationTicker != -1) {
      animationTicker += 0.2;      
    }

    if (animationTicker > 32 || (animationMessage.length() == 0 && animationTicker > 16)) {
      animationPushState();
      animationTicker = -1;
      return;
    }

    if (animationTicker == -1) {
      return;
    }

    viewController(0, String(' ')); 
    viewController(1, String(' ')); 
    viewController(2, String(' ')); 
    viewController(3, String(' ')); 

    if (animationSound) {
      if (int(animationTicker) > 23) {
        tone(SIGNAL, (100 * (int(animationTicker) + 1) + 500), 50);
      } else {
        if (int(animationTicker) % 23) {
          tone(SIGNAL, (20 * (int(animationTicker) + 1) + 500), 50);
        } else {
          noTone(SIGNAL); 
        }
      }
    }

    if (animationMessage.length() != 0 && int(animationTicker) > 8 && int(animationTicker) < 24) {
      for (int x = 0; x < 4; x++) { 
        viewController(x, String(animationMessage[x]));
      }
      return;
    } else {
      if (int(animationTicker) % 8 < 4) {
        viewController(int(animationTicker) % 4, String('0')); 
        return;
      } else {
        viewController(int(animationTicker) % 4, String(' '));
        return;
      } 
    }
  }
}

// Контроллируем состояние анимации
void animationController (boolean isSound, String message) {  
  animationSaveState();
  animationMessage = message;
  animationSound = isSound;
  animationTicker = 0;
}

Виджеты

Время
// Изначательное время
int timeTicker[4] = { 1,2,4,8 };
// Счетчик секунд
int timeSecond = 0;

// Последнее время внутреннего счетчика микроконтроллера
uint32_t timeDelta;

// Флаг включающий и отключающий виджет 
boolean timeShow = false;

// Управление виджетом
void timeController (boolean isShow) {
  timeShow = isShow;
  viewControllerDot(isShow);
}

// Управление состоянием виджета
void timeTickerController (int offset, int n) {
  if (offset == 0) {
    timeTicker[0] += n;
    if (timeTicker[0] > 2) {
      timeTicker[0] = 0;
    }
    if (timeTicker[0] < 0) {
      timeTicker[0] = 2;
    }
  }

  if (offset == 1) {
    timeTicker[1] += n;

    if (timeTicker[0] < 2) {
      if (timeTicker[1] < 0) {
        timeTicker[1] = 9;
      }
      if (timeTicker[1] > 9) {
        timeTicker[1] = 0;
      }
    } else {
      if (timeTicker[1] < 0) {
        timeTicker[1] = 3;
      }
      if (timeTicker[1] > 3) {
        timeTicker[1] = 0;
      }
    }
  }

  if (offset == 2) {
    timeTicker[2] += n;
    if (timeTicker[2] > 5) {
      timeTicker[2] = 0;
    }
    if (timeTicker[2] < 0) {
      timeTicker[2] = 5;
    }
  }

  if (offset == 3) {
    timeTicker[3] += n;
    if (timeTicker[3] > 9) {
      timeTicker[3] = 0;
    }
    if (timeTicker[3] < 0) {
      timeTicker[3] = 9;
    }
  }
}

// Отвечает за ход времени
void timeDriver () {
  if (timeShow && millis() - timeDelta >= 1000) {   
    timeDelta = millis();              

    timeSecond++;
    viewControllerDot(timeSecond % 2 == 0);
    if (timeSecond > 59) {
      timeSecond = 0;
      timeTicker[3]++;
      if (timeTicker[3] > 9) {
        timeTicker[3] = 0;
        timeTicker[2]++;
        if (timeTicker[2] > 5) {
          timeTicker[2] = 0;
          timeTicker[1]++;
          if ((timeTicker[0] < 2 && timeTicker[1] > 9) || (timeTicker[0] == 2 && timeTicker[1] > 3)) {
            timeTicker[1] = 0;
            timeTicker[0]++;
            if (timeTicker[0] > 2) {
              timeTicker[0] = 0;
            }
          }  
        } 
      }
    }
    
    viewController(0, String(timeTicker[0])); 
    viewController(1, String(timeTicker[1])); 
    viewController(2, String(timeTicker[2])); 
    viewController(3, String(timeTicker[3])); 
  }
}
Таймер
// Изначальное время
int timerTicker[4] = { 0, 0, 3, 0 };
// Счетчик секунд
int timerSecond = 59;

// Последнее время внутреннего счетчика микроконтроллера
uint32_t timerDelta;

// Флаг включающий и отключающий виджет
boolean timerShow = false;

// Управление виджетом
void timerController (boolean isShow) {
  timerShow = isShow;
  viewControllerDot(isShow);
}

// Управление состоянием виджета
void timerTickerController (int offset, int n) {
  if (offset == 0) {
    timerTicker[0] += n;
    if (timerTicker[0] > 2) {
      timerTicker[0] = 0;
    }
    if (timerTicker[0] < 0) {
      timerTicker[0] = 2;
    }
  }

  if (offset == 1) {
    timerTicker[1] += n;

    if (timerTicker[0] < 2) {
      if (timerTicker[1] < 0) {
        timerTicker[1] = 9;
      }
      if (timerTicker[1] > 9) {
        timerTicker[1] = 0;
      }
    } else {
      if (timerTicker[1] < 0) {
        timerTicker[1] = 3;
      }
      if (timerTicker[1] > 3) {
        timerTicker[1] = 0;
      }
    }
  }

  if (offset == 2) {
    timerTicker[2] += n;
    if (timerTicker[2] > 5) {
      timerTicker[2] = 0;
    }
    if (timerTicker[2] < 0) {
      timerTicker[2] = 5;
    }
  }

  if (offset == 3) {
    timerTicker[3] += n;
    if (timerTicker[3] > 9) {
      timerTicker[3] = 0;
    }
    if (timerTicker[3] < 0) {
      timerTicker[3] = 9;
    }
  }
}

// Отвечает за обратный отсчет
void timerDriver () {
  if (timerShow && millis() - timerDelta >= 1000) {   
    timerDelta = millis();              

    timerSecond--;
    viewControllerDot(timerSecond % 2 == 0);
    if (timerSecond < 0) {
      timerSecond = 60;
      timerTicker[3]--;
      if (timerTicker[3] < 0) {
        timerTicker[3] = 9;
        timerTicker[2]--;
        if (timerTicker[2] < 0) {
          timerTicker[2] = 5;
          timerTicker[1]--;
          if (timerTicker[1] < 0) {
            timerTicker[1] = 9;
            timerTicker[0]--;
            if (timerTicker[0] < 0) {
              timerTicker[0] = 0;
            } 
          }
        }
      }
    }
        
    viewController(0, String(timerTicker[0])); 
    viewController(1, String(timerTicker[1])); 
    viewController(2, String(timerTicker[2])); 
    viewController(3, String(timerTicker[3]));

    if (
      timerTicker[0] == 0 && 
      timerTicker[1] == 0 && 
      timerTicker[2] == 0 && 
      timerTicker[3] == 0
    ) {
      timerController(false);
      viewControllerDot(false);
      animationController(true, "end");
    } 
  }
}
Минутный таймер
// Изначальное время
int mTimerTicker[4] = { 0, 0, 0, 5 };

// Последнее время внутреннего счетчика микроконтроллера
uint32_t mTimerDelta;

// Флаг включающий и отключающий виджет
boolean mTimerShow = false;

// Управление виджетом
void mTimerController (boolean isShow) {
  mTimerShow = isShow;
  viewControllerDot(isShow);
}

// Управление состоянием виджета
void mTimerTickerController (int offset, int n) {
  if (offset == 0) {
    mTimerTicker[0] += n;
    if (mTimerTicker[0] > 5) {
      mTimerTicker[0] = 0;
    }
    if (mTimerTicker[0] < 0) {
      mTimerTicker[0] = 5;
    }
  }

  if (offset == 1) {
    mTimerTicker[1] += n;
    if (mTimerTicker[1] > 9) {
      mTimerTicker[1] = 0;
    }
    if (mTimerTicker[1] < 0) {
      mTimerTicker[1] = 9;
    }
  }

  if (offset == 2) {
    mTimerTicker[2] += n;
    if (mTimerTicker[2] > 5) {
      mTimerTicker[2] = 0;
    }
    if (mTimerTicker[2] < 0) {
      mTimerTicker[2] = 5;
    }
  }

  if (offset == 3) {
    mTimerTicker[3] += n;
    if (mTimerTicker[3] > 9) {
      mTimerTicker[3] = 0;
    }
    if (mTimerTicker[3] < 0) {
      mTimerTicker[3] = 9;
    }
  }
}

// Отвечает за обратный отсчет
void mTimerDriver () {
  if (mTimerShow && millis() - mTimerDelta >= 1000) {   
    mTimerDelta = millis();              

    mTimerTicker[3]--;
    viewControllerDot(mTimerTicker[3] % 2 == 0);
    if (mTimerTicker[3] < 0) {
      mTimerTicker[3] = 9;
      mTimerTicker[2]--;
      if (mTimerTicker[2] < 0) {
        mTimerTicker[2] = 5;
        mTimerTicker[1]--;
        if (mTimerTicker[1] < 0) {
          mTimerTicker[1] = 9;
          mTimerTicker[0]--;
          if (mTimerTicker[0] < 0) {
            mTimerTicker[0] = 5;
          }
        }  
      }
    }
        
    viewController(0, String(mTimerTicker[0])); 
    viewController(1, String(mTimerTicker[1])); 
    viewController(2, String(mTimerTicker[2])); 
    viewController(3, String(mTimerTicker[3]));

    if (
      mTimerTicker[0] == 0 && 
      mTimerTicker[1] == 0 && 
      mTimerTicker[2] == 0 && 
      mTimerTicker[3] == 0
    ) {
      mTimerController(false);
      viewControllerDot(false);
      animationController(true, "end");
    } 
  }
}
Игра
// Изначальное состояние
int gameUnLockerData[4] = { 0,0,0,0 };

// Состояние измененное случайным образом
int gameUnLockerHiddenData[4] = { 
  random(0, 9),
  random(0, 9),
  random(0, 9),
  random(0, 9)
};

// Флаг включающий и отключающий виджет
boolean gameUnLockerShow = false;

// Управление виджетом
void gameUnLockerController (boolean isShow) {
  gameUnLockerShow = isShow; 
}

// Управление состояние виджета
void gameUnLockerPlayerController (int offset, int n) {
  if (offset == 0) {
    gameUnLockerData[0] += n;
    if (gameUnLockerData[0] > 9) {
      gameUnLockerData[0] = 0;
    }
    if (gameUnLockerData[0] < 0) {
      gameUnLockerData[0] = 9;
    }
  }

  if (offset == 1) {
    gameUnLockerData[1] += n;
    if (gameUnLockerData[1] > 9) {
      gameUnLockerData[1] = 0;
    }
    if (gameUnLockerData[1] < 0) {
      gameUnLockerData[1] = 9;
    }  
  }

  if (offset == 2) {
    gameUnLockerData[2] += n;
    if (gameUnLockerData[2] > 9) {
      gameUnLockerData[2] = 0;
    }
    if (gameUnLockerData[2] < 0) {
      gameUnLockerData[2] = 9;
    }   
  }

  if (offset == 3) {
    gameUnLockerData[3] += n;
    if (gameUnLockerData[3] > 9) {
      gameUnLockerData[3] = 0;
    }
    if (gameUnLockerData[3] < 0) {
      gameUnLockerData[3] = 9;
    }    
  }
}

// Обработка состояния виджета
void gameUnLockerDriver () {
  if (gameUnLockerShow) {
    if (
      gameUnLockerData[0] == gameUnLockerHiddenData[0] &&
      gameUnLockerData[1] == gameUnLockerHiddenData[1] &&
      gameUnLockerData[2] == gameUnLockerHiddenData[2] &&
      gameUnLockerData[3] == gameUnLockerHiddenData[3] 
    ) {
      viewController(0, String('g'));
      viewController(1, String('o'));
      viewController(2, String('o'));
      viewController(3, String('d')); 
    } else {
      viewController(0, String('b'));
      viewController(1, String('a'));
      viewController(2, String('d'));
      viewController(3, String(' '));   
    } 
  }
}

Демонстрационная версия

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

Также, пользуясь случаем, передаю привет своему другу Каро, в гараже которого, было собранно это устройство осенью 2019 года.