Электроника всем начинающим +17



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

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

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

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

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
    /#23785483

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

    • prohetamine
      /#23785623 / +1

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

      • FGV
        /#23785675 / +1

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

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

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

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

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

        • prohetamine
          /#23785781

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

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

          • FGV
            /#23785891 / +6

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

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

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

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

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

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

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

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

    • Kroleg
      /#23786017 / +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 в любую сторону игнорировать этот пин в течение нескольких микросекунд.

      • PoliTeX
        /#23786465

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

        • katzen
          /#23788249 / -1

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

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

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

          Ах, если бы.

          • prohetamine
            /#23788253

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

          • redsh0927
            /#23788289 / +2

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

            • katzen
              /#23790775

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

              • redsh0927
                /#23790817

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

          • unsignedchar
            /#23789077

            Ах, если бы.

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

            • katzen
              /#23790777

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

              • unsignedchar
                /#23791687 / +1

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

      • prohetamine
        /#23786555

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

      • redsh0927
        /#23787059 / +2

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

    • unsignedchar
      /#23786453 / +1

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

  2. FGV
    /#23785503 / +6

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

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

  3. 13_beta2
    /#23785571 / +1

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

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

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

    • redsh0927
      /#23785821 / +14

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

      • prohetamine
        /#23785907 / -2

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

        • redsh0927
          /#23785943 / +8

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

          • prohetamine
            /#23786545 / -6

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

            • unsignedchar
              /#23787123

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

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

        • REPISOT
          /#23787327 / +2

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

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

  4. Kroleg
    /#23785701 / +1

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

    • prohetamine
      /#23785807

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

      • VT100
        /#23786027 / +2

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

      • Kroleg
        /#23786047 / +1

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

        • redsh0927
          /#23786063 / +2

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

          • Kroleg
            /#23786267

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

            • redsh0927
              /#23786303 / +2

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

              • Kroleg
                /#23786481

                С точки зрения 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".

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

      • mkvmaks
        /#23786053

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

  5. kibizoidus
    /#23785783 / +6

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

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


    вот так?

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

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

    • redsh0927
      /#23785853 / +1

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

      • kibizoidus
        /#23785927 / +1

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

        • redsh0927
          /#23785957 / +3

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

      • prohetamine
        /#23785937 / -7

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

        • redsh0927
          /#23785969 / +3

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

        • kibizoidus
          /#23786907 / +4

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

  6. unsignedchar
    /#23785935 / +1

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

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

  7. VT100
    /#23786035 / +2

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

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

  8. REPISOT
    /#23786203 / +15

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

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

  9. redsh0927
    /#23786427 / +9

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

  10. Indemsys
    /#23786567 / +2

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

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

  11. tehnolog
    /#23787159

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

  12. sargon5000
    /#23787287 / +6

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

  13. Polaris99
    /#23790883 / +2

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

  14. isb97
    /#23794411

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