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

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

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

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

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 года.

Комментарии (55)


  1. PoliTeX
    04.12.2021 18:58

    А позвольте нубский вопрос? Почему нельзя упростить эту схему поставив на кнопки один общий резистор. И отдельный - один на общий контакт индикатора?


    1. prohetamine Автор
      04.12.2021 19:40
      +1

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


      1. FGV
        04.12.2021 20:03
        +1

        Потому что кнопки принимают аналоговый сигнал, а не цифровой…

        Аааааааа зачем Вы так сделали? Привешано же по одной кнопке на ногу.

        … если мы будем использовать только один резистор то когда мы зажмем одну кнопку контроллер посчитает…

        Честно говоря непонятно что посчитает контроллер т.к. нет нормальной схемы и как включены переключатели глядя на приведенную "схему" - непонятно.

        P.S. отдельный вопрос возникает по регистру, накой он тут нужен? по хорошему его надо заменить на 4 верхних ключа на pnp транзисторах.


        1. prohetamine Автор
          04.12.2021 20:34

          Зачем я так сделал ? затем что так видел схему, такой опыт имею, если внимательно рассмотрите картинки в статье то найдете распиновку кнопок, изначально они подтянуты к минусу.

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


          1. FGV
            04.12.2021 21:04
            +6

            Зачем я так сделал ? затем что так видел схему…

            Схема тут причем? Судя по Вашим дальнейшим рассуждениям кнопочные ноги через 150 ом подтянуты к земле, кнопка коммутирует на ногу +5В. Вопрос накой задействовать АЦП? Просто состояние регистра pin* считать быстрее и проще.

            … если внимательно рассмотрите картинки в статье …

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

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

            1. Что то слабо верится в то что перечисленный комплект радиодеталей был подрукой;

            2. Токи от индикатора посчитайте, и гляньте сколько 74 серия держит.

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


    1. Kroleg
      04.12.2021 21:35
      +7

      Для чтения кнопок не нужно ничего, кроме ноги контроллера и кнопки. Кнопка подключается одной ногой к земле, другой к пину.

      Пин не нужно читать через analogRead, его нужно читать через digitalRead, предварительно включив присутствующий в атмеге pull-up resistor:

      digitalWrite(PIN_BUTTON, HIGH);
      pinMode(PIN_BUTTON, INPUT);
      // or
      pinMode (PIN_BUTTON, INPUT_PULLUP);

      В результате контроллер будет слегка притягивать через внутренний резистор свой пин к VCC, и при не нажатой кнопке будет читаться 1. А при замыкании пина на землю там будет читаться 0.

      Если кнопку опрашивать слишком часто, можно поймать дребезг контактов, когда в момент нажатия из пина читается последовательно ...111110100110010000... а в момент отпускания - 00001001101111. То есть прежде чем устаканятся в конечном положении контакты несколько микросекунд прыгают и дребезжат.

      Это можно победить или поставив конденсатор параллельно кнопке (номинал подобрать не так просто). Или написав пару строк кода, чтобы после перехода между 0 и 1 в любую сторону игнорировать этот пин в течение нескольких микросекунд.


      1. PoliTeX
        05.12.2021 00:24

        В том и вопрос, зачем усложнять железо, если все решается софтово или встроенными методами? Может есть какие особенности, нюансы или бест практисы?


        1. katzen
          05.12.2021 17:27
          -1

          зачем усложнять железо

          Встроенные pull-up не всегда работают надёжно.

          все решается софтово

          Ах, если бы.


          1. prohetamine Автор
            05.12.2021 17:30

            Да я уже понял, отредактировал в статье.


          1. redsh0927
            05.12.2021 17:48
            +2

            Встроенные pull-up не всегда работают надёжно.
            Ненадёжно — это вообще как? Барахлят что ли…
            Или имеется в виду что 50мка тока подтяжки не хватает чтоб придавить наводки с 10 метров кабеля? Но встроенные подтяжки и не для этого вовсе предназначены.


            1. katzen
              06.12.2021 14:35

              Именно что не хватает для подавления помех.


              1. redsh0927
                06.12.2021 14:50

                В промышленных девайсах почему-то хватает. Померьте ток через кнопочку у монитора и т.п., те же 50-70мка почти везде и будут.


          1. unsignedchar
            05.12.2021 23:36

            Ах, если бы.

            Уж с дребезгом контактов на уровне софта порешать точно можно. Особенно в микрокнопке с мизерным током


            1. katzen
              06.12.2021 14:37

              Каким вообще образом величина тока и софтовый способ решения связаны, в какой вселенной?


              1. unsignedchar
                06.12.2021 18:28
                +1

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


      1. prohetamine Автор
        05.12.2021 01:06

        Благодарю, на днях дополню статью.


      1. redsh0927
        05.12.2021 08:52
        +2

        после перехода между 0 и 1 в любую сторону игнорировать этот пин в течение нескольких микросекунд
        Тактовая кнопка дребезжит несколько миллисекунд, что на 3 порядка больше. Для фильтрации можно взять 20мс, лучше 50.
        И не обязательно фильтровать каждый пин в отдельности, можно просто опрашивать все кнопки не чаще чем раз в 20-50мс. Я тут упоминал, с картинками. Даже если опрос попадёт на нестабильность, мы получим или 0-0-1 или 0-1-1, но всё равно не больше одного фронта.


    1. unsignedchar
      05.12.2021 00:18
      +1

      Так то емнип в нутре этой шайтан микросхемы есть встроенные pull up резисторы. Можно обойтись и без внешних резисторов.


  1. FGV
    04.12.2021 19:03
    +6

    Ни одного кондера по питанию... зато из-за кривого кода аж 4 по 50мкФ привешано на ноги 495 регистра.

    Обычно для динамической индикации сначала гасят все сегмены A-H, потом переключают индикатор, затем включают нужные сегменты A-H.


  1. 13_beta2
    04.12.2021 19:23
    +1

    Наверняка в техническом плане есть неточности, и с уровня ничего в теме не шарящего (читай чайника, то есть меня) осталось много неочевидного. Но статья наверняка может служить хорошим пособием для мотивации начинающих.

    Отдельное спасибо за качественные иллюстрации в одном стиле и по делу.

    P.S. Надеюсь автор не забьёт и обратная связь по орфографии тоже будет вноситься.


    1. redsh0927
      04.12.2021 20:47
      +14

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


      1. prohetamine Автор
        04.12.2021 21:06
        -2

        Я нарисую схему, кто же мог подумать что для начинающих важна именно принципиальная схема, а не наглядность.


        1. redsh0927
          04.12.2021 21:15
          +8

          Принципиальная схема и даёт наглядность. А выискивать одним глазом в мешанине проводов что куда подключено, вторым глазом проверяя функцию ножек микросхем по даташиту нет никакого желания.


          1. prohetamine Автор
            05.12.2021 01:03
            -6

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


            1. unsignedchar
              05.12.2021 09:55

              Может сделаете принципиальную схему тогда ?

              А вот это интересное ;) Хотите сказать, что собирали это без схемы, просто представляя в уме, какой провод куда припаять?


        1. REPISOT
          05.12.2021 11:28
          +2

          Вы в курсе, что семисегментные индикаторы бывают разного типа (общий катод/общий анод) и у разных производителей имеют разную распиновку?
          А ваша фраза

          Sm56425bsr3 или аналоги
          обрекает «начинающих», для которых вы пишете, скачивать даташиты и восстанавливать схему по цветным проводкам на вашей картинке. Что для новичка — непростая задача. А еще нужно догадаться, что это надо делать. (Даташит на этот индикатор еще и не ищется. Где вы его взяли?)


  1. Kroleg
    04.12.2021 20:11
    +1

    Позвольте еще нубский вопрос. Вы подключили ATmega328p по USART. Однако по даташиту поддерживается только проприетарный parallel programming и и serial programming через SPI+reset. Как вам удалось залить на него прошивку через USB-UART?


    1. prohetamine Автор
      04.12.2021 20:42

      Мне ? никак, я прошивал через ESP32 devkit замкнув reset на землю. Но я знаю что на самих arduino mini частенько используется CP2102 для прошивки ATmega328p поэтому я решил что преобразователь USB-UART вполне подойдет.


      1. VT100
        04.12.2021 21:38
        +2

        Т.е. вопрос наличия arduino'вского начального загрузчика не исследован.
        [ИМС прошита перед отправкой пользователю]


      1. Kroleg
        04.12.2021 21:46
        +1

        На ардуинах с завода идет прошитый загрузчик, который умеет обновлять прошивку по USART. Для голой не ардуиновской ATmega328p это не так. Ее можно прошить только по SPI+Reset или через сильно проприетарный Parallel Programming.


        1. redsh0927
          04.12.2021 21:52
          +2

          через сильно проприетарный Parallel Programming
          И что же в нём такого «сильно проприетарного»? Абсолютно типичный протокол программирования ПЗУ. Официально подробно описанный в даташите. Не более проприетарный чем набор регистров/команд любой другой микросхемы. Свой первый мк я прошил 18 лет назад имея только даташит и знание как дёргать ножками лпт порта…


          1. Kroleg
            04.12.2021 23:06

            Как вы одним словом назовете протокол, который не 1284/UART/I2C/SPI/SingleWire/JTAG etc?


            1. redsh0927
              04.12.2021 23:17
              +2

              «Кастомный», вероятно. Сравнение, впрочем, не особо корректное т.к. уарт/и2ц/спай — это не протоколы, а интерфейсы, как впрочем и паралелльная шина, а сам протокол бегает по ним уровнем выше. Так-то протокол которым шьётся ардуйня через уарт — тоже кастомный, как и подавляющее большинство протоколов вообще :)


              1. Kroleg
                05.12.2021 00:29

                С точки зрения OSI - это протоколы.

                OSI protocol Layer 1: physical layer - определяет вольтаж, количество проводов и формы разъемов. Это как раз про 1284/UART/I2C/SPI/SingleWire/JTAG.

                Layer 2: data link layer - это как передавать команды - по каким проводам тактовать, адресовать, как передавать байты и т.д. И это тоже регламентируется 1284/UART/I2C/SPI/SingleWire/JTAG.

                А вот список команд прошивки - это уже Layer 5: session layer для уставновления/завершения сеанса и Layer 6: presentation layer для придания командам смысла. И они действительно свои для каждого семейства контроллеров.

                Таким образом протокол и интерфейс - польностью взаимозаменяемые понятия в этом контексте.

                «Кастомный» - принято. Это действительно лучше чем проприетарный, хотя прямо в описании AVR микроконтроллера написано "In-system programmable using serial/parallel low-voltage proprietary interfaces".

                А вообще, к чему этот терминологический спор?


      1. mkvmaks
        04.12.2021 21:49

        Если голый МК - не заработает, загрузчик нужен, а его туда засунуть можно либо через готовую arduino, либо USBasp программатор. Из коробки не будет.


  1. kibizoidus
    04.12.2021 20:35
    +6

    За modeId == 1 нужно бить по пальцам. Желательно осиновыми розгами, но можно и затрещин навешать. Это не призыв к рукоприкладству, но разве так тяжело учить хорошему сразу? Неужели руки отсохнут сделать

    #define MODE_WITH_DETAILED_DESCRIPTION 1
    if(modeId == MODE_WITH_DETAILED_DESCRIPTION) { ... };


    вот так?

    Любому программисту будет легко понять код с первого взгляда без постоянного скролла по магии чисел и без забивания головы о том, что эта единичка, размазанная ровным слоем по всем if'ам, значит.

    Зачем вы сразу учите людей плохому? Их потом переучивать с этого дерьма годами...


    1. prohetamine Автор
      04.12.2021 20:37
      +1

      Спасибо, будет исправлено.


    1. redsh0927
      04.12.2021 20:55
      +1

      За modeId == 1 нужно бить по пальцам.
      А как же за кодирование восьми битов СТРОКОЙ «01010101»? И намеренье УЧИТЬ этому? Такой жести я давно не видел…


      1. kibizoidus
        04.12.2021 21:11
        +1

        Автор обещал исправиться и это главное.


        1. redsh0927
          04.12.2021 21:16
          +3

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


      1. prohetamine Автор
        04.12.2021 21:13
        -7

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


        1. redsh0927
          04.12.2021 21:19
          +3

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


        1. kibizoidus
          05.12.2021 05:42
          +4

          Аргумент "сначала добейся" - моветон в приличном обществе инженеров. Вы, батенька, сначала сделайте так, чтоб комар носа не подточил со всех сторон, а уж после бравируйте призывами "ты сначала собери мое устройство". Здесь многие могут собрать устройства в разы сложнее, с модульностями и куртизанками.


  1. unsignedchar
    04.12.2021 21:13
    +1

    Я так и не понял что за игра там запрограммирована.

    Да, я думаю, это не для начинающих. Начинающий не поймет, ни как заливать прошивку в эту хтоническую машину (ни слова про fuse-биты, это круто же), ни что не так с точностью часов, с питанием, с кодом на JavaScript++..


  1. VT100
    04.12.2021 21:40
    +2

    Буквы придумали не для меня и вообще не для детей которые хотят заниматься электроникой.

    "Знание некоторых принципов — способно возместить незнание некоторых фактов".
    Держите честно заработанный статейкой минус.


    1. prohetamine Автор
      05.12.2021 00:03

      Чукча, однако, не читатель..


  1. REPISOT
    04.12.2021 22:43
    +15

    Резисторы предназначены для ограничения тока исходящего от ATmega328p, а именно 5 вольт мы ограничиваем до 3-х вольт

    Все. Дальше я не смог.


  1. redsh0927
    05.12.2021 00:06
    +9

    image
    Обратили внимание тут, кстати :)


  1. Indemsys
    05.12.2021 01:10
    +2

    Если использовать пин 13 регистра сдвига, то можно было бы отказаться от конденсаторов.
    А если поставить еще один регистр сдвига, то можно было бы еще сэкономить ног у микроконтроллера.

    Сами мы используем микросхему AS1115-BSST для таких целей.
    Тогда вообще только два пина от микроконтроллера требуется.


    1. prohetamine Автор
      05.12.2021 01:17

      Спасибо, буду знать)


  1. tehnolog
    05.12.2021 10:19

    А какой программой вы рисовали? Симпатичные картинки получились.


    1. prohetamine Автор
      05.12.2021 16:58

      Figma и Google SketchUp


  1. sargon5000
    05.12.2021 11:09
    +6

    Тяжело всё это читать. Натужный юмор, пафосный стиль, отсутствие половины запятых. Почему автор пишет Ватт с заглавной буквы, хотя омы и фарады с маленькой? Почему он миллиметры пишет латиницей, а прочие единицы измерений кириллицей? Что это за светодиод такой длиной 75 мм? Где он нашел кварц на 16 миллигерц, и главное – зачем? Зачем слова "четырех разрядный" и "семисегментный" он разорвал пополам?


  1. Polaris99
    06.12.2021 15:08
    +2

    Собрание заблуждений и откровенной неграмотности, преподносящееся как учебное руководство - и неплохой позитивный рейтинг с благодарными читателями в комментах. Во что превратился хабр? Одни трехвольтные светодиоды чего стоят!


  1. isb97
    07.12.2021 13:21

    Статья очень понравилась) Возник вопрос, а что за ПО использовали для визуализации и сборки компонентов?