Привет! Этот проект стал для меня важным шагом в мир : первое устройство с полностью собственной печатной платой, спроектированной с нуля в KiCad. Ранее я экспериментировал с готовыми модулями Arduino и макетными платами, но захотелось перейти к компактному, автономному решению на «голом» микроконтроллере.

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

Получилась миниатюрная игра на реакцию для двух игроков на базе ATmega328P. Устройство питается от одной батарейки CR2032 (3 В), работает на внутреннем RC-генераторе 8 МГц и не требует внешнего кварца или стабилизатора напряжения. Проект доступен на GitHub.

В этой статье я разберу механику игры, выбор компонентов, разработку прошивки (с кодом), прототипирование, проектирование PCB и сборку. Давайте разберёмся по шагам.

Правила игры

Игра предназначена для двух игроков. У каждого — своя кнопка и индикаторный светодиод. Центральный светодиод сигнализирует о старте раунда.

  • Подготовка: Центральный LED мигает три раза коротко — сигнал готовности.

  • Задержка: Случайная пауза от 1 до 5 секунд (генерируется с помощью random() на основе аналогового шума).

  • Старт: Загорается центральный LED — игроки должны как можно быстрее нажать свою кнопку. Первый, кто успел, получает очко; его индикатор загорается на 1 секунду.

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

  • Продолжительность раунда: 5 секунд на реакцию после сигнала.

  • Матч: Игра идёт до 3 очков. Победа отмечается пятью быстрыми миганиями индикатора победителя.

  • Сброс: После матча счёт автоматически сбрасывается, и игра готова к новому раунду.

Механика простая — требует хорошей реакции и внимания. В версии 2.0 (в разработке) добавлены по три LED на игрока для визуализации счёта (загораются последовательно и остаются гореть до конца матча).

Выбор компонентов

Я стремился к минимализму: низкое потребление энергии, компактность и дешевизна. Питание от CR2032 (3 В) идеально подходит — ATmega328P стабильно работает на этой напруге с внутренним тактированием 8 МГц (без внешнего кварца, чтобы сэкономить место и ток).

Компоненты для версии 1.0:

  • ATmega328P — 1 шт.

  • Тактовые кнопки 6×6 мм — 2 шт.

  • Светодиоды 0805 — 3 шт.

  • Резисторы 330 Ом (для ограничения тока LED) — 3 шт.

  • Резистор 10 кОм (pull-up для кнопок или сброса) — 1 шт.

  • Конденсатор 1 мкФ (для фильтрации питания) — 1 шт.

  • Держатель батарейки CR2032 — 1 шт.

  • Миниатюрный выключатель питания — 1 шт. (опционально, для удобства).

Стоимость комплекта — около 350–450 рублей. Для версии 2.0 добавляются дополнительные LED (по 3 на игрока) и резисторы (всего 7 шт. 330 Ом).

В схеме использованы минимальные внешние компоненты: нет регуляторов напряжения, нет внешнего reset'а (используется внутренний pull-up). Кнопки подключены с debounce в коде.

Шаг 1: Разработка прошивки и программирование

Прошивка написана в Arduino IDE на C++ (используются только базовые функции, без библиотек). Это позволило быстро прототипировать логику, а затем скомпилировать в чистый HEX для "голого" AVR.

Ключевые особенности кода:

  • Рандомизация задержки: random(1000, 5000) с сидом от аналогового пина.

  • Debounce кнопок: простая задержка в 20 мс.

  • Анти-чит: счётчик ложных нажатий.

  • Энергосбережение: использование sleep_mode() в будущем (в v1.0 не реализовано полностью).

  • Fuse-биты: Low = 0xE2 (внутренний 8 МГц, без делителя).

Полный код для v1.0 (из GitHub: /src/v1/reaction_game_v1.ino):

// reaction_game_v1.ino

#define LED_CENTER 13  // Центральный  LED (PB5)
#define LED_P1 12      // LED Player 1 (PB4)
#define LED_P2 11      // LED Player 2 (PB3)
#define BTN_P1 2       // Кнопка Player 1 (PD2)
#define BTN_P2 3       // Кнопка Player 2 (PD3)

int scoreP1 = 0;
int scoreP2 = 0;
int falsePressP1 = 0;
int falsePressP2 = 0;
bool blockedP1 = false;
bool blockedP2 = false;

void setup() {
  pinMode(LED_CENTER, OUTPUT);
  pinMode(LED_P1, OUTPUT);
  pinMode(LED_P2, OUTPUT);
  pinMode(BTN_P1, INPUT_PULLUP);
  pinMode(BTN_P2, INPUT_PULLUP);
  
  randomSeed(analogRead(0));  // Сид для рандома
}

void loop() {
  resetRound();  // Сброс раунда
  
  // Сигнал готовности: 3 мигания
  for (int i = 0; i < 3; i++) {
    digitalWrite(LED_CENTER, HIGH);
    delay(200);
    digitalWrite(LED_CENTER, LOW);
    delay(200);
  }
  
  // Случайная задержка
  delay(random(1000, 5000));
  
  // Старт: зажигаем центр
  digitalWrite(LED_CENTER, HIGH);
  unsigned long startTime = millis();
  
  while (millis() - startTime < 5000) {  // 5 сек на реакцию
    checkFalsePresses();  // Проверка анти-чита
    
    if (!blockedP1 && digitalRead(BTN_P1) == LOW) {
      scoreP1++;
      digitalWrite(LED_P1, HIGH);
      delay(1000);
      digitalWrite(LED_P1, LOW);
      checkWin();
      break;
    }
    
    if (!blockedP2 && digitalRead(BTN_P2) == LOW) {
      scoreP2++;
      digitalWrite(LED_P2, HIGH);
      delay(1000);
      digitalWrite(LED_P2, LOW);
      checkWin();
      break;
    }
    
    delay(20);  // Debounce
  }
  
  digitalWrite(LED_CENTER, LOW);
}

void resetRound() {
  falsePressP1 = 0;
  falsePressP2 = 0;
  blockedP1 = false;
  blockedP2 = false;
}

void checkFalsePresses() {
  if (digitalRead(BTN_P1) == LOW) {
    falsePressP1++;
    if (falsePressP1 > 2) blockedP1 = true;
    delay(20);
  }
  
  if (digitalRead(BTN_P2) == LOW) {
    falsePressP2++;
    if (falsePressP2 > 2) blockedP2 = true;
    delay(20);
  }
}

void checkWin() {
  if (scoreP1 >= 3) {
    for (int i = 0; i < 5; i++) {
      digitalWrite(LED_P1, HIGH);
      delay(100);
      digitalWrite(LED_P1, LOW);
      delay(100);
    }
    scoreP1 = 0;
    scoreP2 = 0;
  } else if (scoreP2 >= 3) {
    for (int i = 0; i < 5; i++) {
      digitalWrite(LED_P2, HIGH);
      delay(100);
      digitalWrite(LED_P2, LOW);
      delay(100);
    }
    scoreP1 = 0;
    scoreP2 = 0;
  }
}

Процесс прошивки:

  1. Скомпилировать в Arduino IDE → получить .hex.

  2. Использовать программатор T48 .

  3. Записать HEX и fuse-биты.

Шаг 2: Прототипирование на макетной плате

С прошитым чипом собрал схему на breadboard: ATmega в панельке, кнопки и LED по пинам, питание от CR2032 или лабораторного БП (3 В).

Тестировал:

  • Стабильность на 3 В (ток в idle ~1 мА).

  • Рандомизацию (без повторяющихся паттернов).

  • Анти-чит и debounce (чтобы избежать ложных срабатываний).

После отладки — переход к PCB.

Шаг 3: Проектирование платы в KiCad

Первый опыт полноценного дизайна в KiCad. Шаги:

  1. Eeschema: Нарисовал схему (простая: MCU + LED + кнопки + пассивка).

  2. Pcbnew: Разводка трасс (2 слоя, минимальные зазоры 0.25 мм). Учитывал компактность.

  3. 3D-viewer: Визуализация для проверки (см. рендер на GitHub: images/v1/3D Model.png).

  4. Генерация Gerber-файлов.

Схема (из GitHub: images/v1/Schematic.png):

Шаг 4: Заказ плат и сборка

Gerber загрузил на PCBWay: 2 слоя, чёрная маска, HASL, 1.6 мм, 5 шт. Стоимость с доставкой ~2160 руб. (пришли через 2,5 месяца).

Пайка: Ручная, SMD .

Первый опыт — не идеально, но плата заработала почти с первого раза.

Итог и планы

Проект дал опыт полного цикла: от идеи до готового устройства. Узнал нюансы AVR, KiCad, пайки SMD. Теперь увереннее в embedded.

В v2.0: Три LED на игрока для счёта, улучшенная шелкография. Есть один нюанс, который я не исправил : неправильное расположение кнопок на PCB — футпринт оказался перевернутым.

Если статья полезна — лайкните, подпишитесь или оставьте комментарий. Вопросы по коду или схеме — в issues репозитория. Удачи в ваших проектах! ?

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


  1. HardWrMan
    03.02.2026 13:03

    Считывая кнопки по очереди да ещё и через вызов долгой процедуры вы автоматически приоритезируете одну кнопку над другой. Читерство уже встроено в аппарат и одна кнопка статистически будет выигрывать больше другой.


    1. Pelectronik Автор
      03.02.2026 13:03

      Действительно, недоработка есть — тесты с друзьями оказались неполными)) спасибо , скоро все доработаю)


  1. Firelander
    03.02.2026 13:03

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


    1. Pelectronik Автор
      03.02.2026 13:03

      спасибо) скоро все исправлю )


  1. Ivan_shev
    03.02.2026 13:03

    Я тоже такую делал, Начальник попросил сделать такую штуку для нашей игры 100/1, сколхозин на ATtiny13A, сделал простую логику и разместил в светильнике из Fixprice

    Скрытый текст

    Да была такая же проблема что опрос одной кнопки был в приоритете над другой, но логика была на столько простой, что разница в миллисекундах отличить смог бы


  1. juramehanik
    03.02.2026 13:03

    В данной задаче debounce в виде таймера вообще не нужен, нам же надо зафиксировать самое первое нажатие каждой кнопки =)


  1. DungeonLords
    03.02.2026 13:03

    Чтобы сделать потребление ещё меньше предлагаю перейти на Internal 128kHz RC oscillator. Производитель снизиться, но ее все равно должно хватить для задачи...


    1. Pelectronik Автор
      03.02.2026 13:03

      Спасибо! Идея с 128kHz RC отличная, буду пробывать в других проектах)


  1. m039
    03.02.2026 13:03

    Не до конца понятно, что значит "без Arduino", вы же сами пользуетесь Arduino IDE и функциями вида digitalRead, pinMode, digitalWrite, а это Arduino. Аа.. понял, это потому что без отладочной платы Arduino, а на своей.

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

    Еще, если хотите уменьшить потребление, можете уводить МК в сон и пробуждать его по нажатию на кнопке.


    1. Pelectronik Автор
      03.02.2026 13:03

      ATtiny13 правда идеально подошёл бы) но у меня переизбыток Atmega328 и выбор пал именно на него.

      Про сон — полностью согласен, это следующий шаг. В следующих версиях добавлю добавлю пробуждение. Спасибо за подсказки!


  1. dimaviolinist
    03.02.2026 13:03

    1) Сделайте прерывания на кнопки, дребезг даже не нужно ловить в вашем случае. Какое прерывание первым пришло, тот и выиграл. (Если совсем-совсем честно, то сравнивать время прихода прерывания, но это уже почти идеально: после старта игры сохранить timestamp с обоих ПЕРВЫХ по прерыванию нажатий и выдать результат). Если разница была в микросекундах, увы, INT0 имеет преимущество.

    2) Использовать delay в микроконтроллерах хуже, чем GOTO в программировании. Имхо.

    3) Светодиоды красиво было бы сделать RGB (чтобы совсем честно) или ws2812b (в разы проще).

    4) Светодиоды можно fade даже в этом варианте (я не знаю, как это по другому сказать).

    к примеру, после игры 2 секунды пауза, потом светодиод начинает пульсировать (не мигать, fade) и когда либо вспыхнул либо погас (на выбор) начинается игра.

    5) В этом вашем применении спокойно делается так, что спит кристалл примерно 99% времени, даже когда горит или fade светодиод.

    Дальше можно издеваться, взять tp223 вместо кнопок и выяснять расстояние, с которого дозволено подносить палец. :) Потом перепаивать конденсаторы и прочее шулерство.

    P.S. Attimy13 сюда на такой проект даже просится, но по цене прямо всё равно сейчас, RP2040 стоит 1,5 евро нельзя от батарейки, Attimy13 0,5 евро.


    1. Pelectronik Автор
      03.02.2026 13:03

      Большое спасибо за такой детальный разбор! Очень ценю, что потратили время на анализ проекта — все пункты по делу и мотивируют дорабатывать!


  1. Ayawaskay
    03.02.2026 13:03

    Как там сейчас с PCBWay заказывать то кстати?


    1. Pelectronik Автор
      03.02.2026 13:03

      Я заказываю через российских посредников. Доставки очень долгая)