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

UPD #1
Дисклеймер
Внимание!
Эта статья имеет много профессиональных неточностей, по мнению уважаемых экспертов по всем вопросам и по совместительству комментаторов, также не является руководством к действию, может привести к производственным травмам и нарушениям психики. Автор призывает, не использовать данное руководство для обучения и лишь демонстрирует хаотично накопленные знания и реализует их в горделивой позе больного шизофренией с бредовыми идеями величия. Никогда не верьте мне! Я украл ваши технологии!
Некоторое вступление
Спустя много лет я решил вернуться снова к написанию статей, с новыми знаниями и силами. Знаете, интернет научил меня всему, что я знаю и даже больше, чем просто всему. Интернет стал не просто учением, в котором тяжело, но и боем, в котором легко. И я благодарен всем, кто так или иначе принял участие в моем обучении, через статьи, описание каких-то технологий, видео на YouTube и просто критику моих работ. Это герои моего времени, только благодаря им я сейчас являюсь неплохим специалистом. Ведь я не учился в этих ваших институтах и образований не получал, да и всего у меня 9 классов. Спасибо тем, кто пишет интернет.
И еще
В детстве, когда я только начинал гуглить какие-то схемы, я любил статьи с картинками, больше всего мне нравилось, как нагляден процесс сборки, как процесс обучения реализован через картинки. Буквы придумали не для меня и вообще не для детей, которые хотят заниматься электроникой. Поэтому я приложил грандиозное количество усилий, чтобы эта статья могла стать для кого-то первой ступенью. Я знаю, как сложно сделать первый шаг. Мое соприкосновение с контроллером случилось только в 2016 году, хотя я был знаком с ними и заочно, задолго до 2016 года.
Компоненты
Приступим. Первое, что нужно для разработки любого устройства – это, подготовить все необходимые радиокомпоненты и крепеж или хотя бы основные.

Не все компоненты были куплены мой, некоторые лежали без дела, или появились прямо за часы перед разработкой этого устройства :)
Резисторы 150 Ом 0.25 Ватт — 12 шт.
Конденсаторы 50 вольт 10 микрофарад — 4 шт.
Тактовая кнопка 6x6мм — 3 шт.
Светодиод 75x3мм — 1 шт.
Пьезо зуммер — 1 шт.
Кварцевый резонатор 16 МГц — 1 шт.
Разъём типа гребёнка — 7 шт.
Джампер (перемычка) — 1 шт.
Четырехразрядный семисегментный индикатор (Sm56425bsr3 или аналоги) — 1 шт.
Сдвиговый регистр 74ch595 корпус DIP — 1 шт.
Панель под микросхему 74ch595 корпус DIP (16 ножек) — 1 шт.
Микроконтроллер ATmega328p корпус DIP — 1 шт.
Панель под микросхему ATmega328p корпус DIP (28 ножек) — 1 шт.
Монтажная плата 40x60мм — 2 шт.
Батарейный отсек cr2032 — 2 шт.
Батарейка cr2032 — 2 шт.
Втулка 5x8x0мм (Не точно) — 4 шт.
Болт 3x6мм (Не точно) — 4 шт.
Шайба 5мм (Не точно)— 4 шт.
Гайка 3мм (Не точно) — 4 шт.
Преобразователь 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 года.
