Создание портативной игровой консоли с нуля: от железа до кода



Многие из нас обожают видеоигры. Я уже не раз рассказывал о ремонте и моддинге различных консолей — от китайских аналогов до фирменных PSP и PS Vita. Но меня влечёт не только восстановление старых устройств, но и страсть к созданию чего-то принципиально нового своими руками. Я увлекаюсь программированием игр и графики, и недавно у меня родилась идея: разработать собственную портативную консоль для игры в тетрис полностью с нуля. Это включает в себя проектирование схемы, разработку дизайна платы, написание прошивки и, конечно, самой игры. Что же получается, когда программист, живущий электроникой, решает создать собственное устройство? Давайте разбираться!

❯ Предыстория и мотивация


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



Собрать функциональный гаджет за одну-две тысячи рублей теперь вполне реально. Энтузиасты создают самые разные устройства, и игровые приставки — одно из самых популярных направлений. Для новичков в мире электроники сборка консоли на базе Raspberry Pi с установленной оболочкой RetroPie уже кажется вершиной мастерства.


Однако существует особая категория разработчиков, к которой я и себя причисляю — мы стремимся делать всё с чистого листа! Мне нравится реализовывать проекты на собственных движках и подходах, и в электронике я придерживаюсь того же принципа: нельзя просто взять готовые решения, нужно самому разобраться в сути проблемы. За моими плечами уже есть несколько интересных демонстрационных проектов. Например, игра "Taz Rally Cup", которую я написал с нуля за неделю в 2022 году (рендеринг, звук, ввод, редактор уровней — всё моё):


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

❯ Выбор компонентов и архитектура


Как уже упоминалось, сегодня существует огромный выбор железа для создания собственных устройств — от мощных микроконтроллеров и одноплатных компьютеров, сравнимых по производительности с телефонами середины 2000-х, до разнообразнейшей периферии. Однако проектировать будущую консоль нужно, отталкиваясь от конкретных требований.

Технические характеристики моего устройства получились следующими:

  1. Процессор: двухъядерный микроконтроллер ARM RP2040 на архитектуре Cortex-M3 с тактовой частотой 133 МГц. Чип установлен на плате Raspberry Pi Pico.

  2. Оперативная память: 260 КБ встроенной SRAM. Немного, но при грамотном управлении ресурсами — достаточно.

  3. ПЗУ: 2 МБ внешней SPI Flash-памяти, также распаянной на плате.

  4. Экран: 1,8-дюймовая TFT-матрица с разрешением 128x160 пикселей. Выбор такого разрешения обусловлен производительностью — более высокое разрешение процессор мог бы не потянуть.

  5. Управление: 6 кнопок (4 на направления и 2 для действий). В будущем количество можно увеличить.

  6. Звук: динамик. Пока не определился с управлением — возможно, будет использован ШИМ процессора, а может, внешний ЦАП с интерфейсом I2S.

  7. Питание: аккумулятор BL-4C на 3,7 В (такие ставили в старые Nokia и современные кнопочные телефоны). Ёмкости в 800 мА·ч должно хватить на 4-5 часов игры. Зарядку обеспечивает модуль на базе TP4056.


Неплохо для самоделки, правда? Эти характеристики примерно соответствуют сотовым телефонам 2004-2006 годов выпуска, таким как Nokia 6600 или Sony Ericsson K510i. Основное отличие — в объёме оперативной памяти (в тех телефонах было 2-4 МБ) и в периферийных модулях, например, контроллере дисплея.


На картинке — Motorola E398, телефон 2004 года, и он здесь не случайно. :)

Важное замечание по дисплеям: у этих 1,8-дюймовых матриц часто встречается дефект "синения". Это не брак железа, а особенность конструкции. Контроллер дисплея сильно греется (хотя токоограничивающий резистор на месте), что негативно влияет на клей, и матрица начинает отслаиваться от подсветки, вызывая синий оттенок. Лечится это аккуратной проклейкой суперклеем.



Я выбрал Raspberry Pi Pico, потому что информации о них пока не так много, характеристики хорошие, и в рунете с ними ещё мало кто делал что-то серьёзное. К тому же у них очень удобный и простой SDK, практически bare-metal. ESP32, например, работает на FreeRTOS и имеет кучу библиотек, а здесь API более простой и понятный.



Закупаем всё необходимое и приступаем к созданию!

❯ Работа с графикой и дисплеем


Первым делом нужно подключить дисплей и организовать вывод изображения. Я использовал интерфейс SPI — с ним очень просто работать. Настраиваем пины (gpio_set_function), конфигурируем SPI-контроллер — и можно отправлять данные.


SPI в RP2040 может работать на частоте до ~60 МГц — это приличная скорость даже для быстрой графики. На самом деле SPI часто предпочтительнее параллельного интерфейса 8080 в микроконтроллерах: он занимает меньше выводов и, что важнее, позволяет задействовать DMA (прямой доступ к памяти)!

В таких проектах всегда стоит предусмотреть возможность смены дисплея, а в идеале — научить систему работать с несколькими контроллерами. Дисплеи одинаковой диагонали могут использовать разные чипы. В моём случае это ST7735. Для разрешений 240x320 часто встречаются ILI9341 или ST7789. Команды инициализации дисплея можно частично позаимствовать — система команд относительно стандартизирована, различия обычно лишь в настройках мощности, гамма-коррекции и т.д. Часто производитель сам приводит последовательность инициализации в документации.



После инициализации пробуем что-нибудь вывести. Всё работает. Важные нюансы: вывод CS (Chip Select) у ST7735 нужно подтянуть к земле, его нельзя оставлять в воздухе (вряд ли у вас на одной шине будет несколько экранов). Вывод RESET должен быть в логической единице (1), иначе дисплей будет в перманентном сбросе.



Для статичной или полустатичной графики можно обойтись встроенными командами дисплея — например, есть удобные функции заливки прямоугольников. Но для динамичных игр этого мало. Мы выделяем память под фреймбуфер (буфер кадра) и настраиваем DMA-канал, чтобы разгрузить процессор при передаче данных на экран.



Процессор готовит изображение в памяти: рисует спрайты, обрабатывает прозрачность. На него ложится основная нагрузка, но мы можем помочь, поручив передачу готового кадра на экран модулю DMA. DMA (Direct Memory Access) — это устройство внутри микроконтроллера, которое позволяет настроить параметры передачи данных, а затем автоматически скопировать их из памяти или в память без участия ядра.

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

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



Также важно выбрать цветовой формат. Я выбрал 16-битный RGB565 (5 бит на красный, 6 на зелёный, 5 на синий). Это экономичный формат, который выглядит лучше, чем графика с палитрой, и не съедает много памяти. Для реализации прозрачности (альфа-канала) используется техника цветового ключа (chroma key), аналогичная хромакею. В качестве "прозрачного" цвета был выбран ярко-розовый (255, 0, 255).



Общая производительность графического рендерера получилась удовлетворительной: он легко справляется с отрисовкой сотни-другой спрайтов в зависимости от их размера. Для такого разрешения экрана и будущих игр — более чем достаточно!

❯ Реализация системы ввода (геймпада)


Теперь нужно обеспечить управление устройством. Для этого я собрал геймпад на макетной плате.



Все кнопки подключены к общему проводу (земле), а второй контакт каждой кнопки — к своему пину GPIO. Я выбрал предпоследние пины, так как на них ничего важного не висит. Текущая конфигурация занимает 6 пинов. На фото это выглядит как прототип, но для теста сойдёт.



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

Существует и другой способ реализации геймпада, который раньше часто использовали в бюджетных устройствах: все кнопки подключаются к одной аналоговой линии через резисторы разного номинала. АЦП микроконтроллера считывает результирующее напряжение и определяет, какая кнопка нажата. Однако у такого метода есть серьёзный недостаток — невозможность обработки одновременного нажатия некоторых комбинаций клавиш (например, вверх+вправо).

❯ Написание первой игры


Теперь у нас есть минимальная база для создания игры. В качестве первой игры для консоли я решил написать классический космический шутер: мы управляем кораблём, сбиваем врагов и уворачиваемся от их выстрелов. Заодно проверим стабильность работы системы.
Я писал игру в классическом стиле на C, как это принято во встраиваемых системах: без стандартной библиотеки (std) и STL, без ООП и виртуальных методов, с минимальным динамическим выделением памяти. Во многом это подход, похожий на разработку для Game Boy Advance. Первым делом я подготовил спрайты для игры в графическом редакторе, а затем сконвертировал их в байтовые массивы, сохранённые в виде заголовочных файлов. На начальном этапе это удобнее, чем создание сложной системы загрузки ресурсов.



Я организовал архитектуру вокруг нескольких функций, каждая из которых отвечает за своё состояние (игровой мир, меню) и свои объекты (например, playerUpdate). Сами игровые объекты описаны структурами, а центральным объектом стала структура CWorld.


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



Единственное, где я использовал выделение памяти — это пулы для пуль и врагов. Оба пула имеют жёсткие ограничения: не более 8 врагов и 16 пуль на экране одновременно — этого вполне достаточно. Динамическое выделение помогло мне найти серьёзную ошибку: в какой-то момент игра вылетала с Out Of Memory. Оказалось, причина в невнимательности (ошибка в условии, вместо '>=' было '>'), из-за чего при отрисовке спрайтов за пределами экрана программа начинала портить внутренние структуры аллокатора. После исправления всё заработало стабильно. :)


Для основной механики (выстрелы, столкновения) я написал функции, которые создают игровые объекты и управляют пулами. Враги обновляются по стандартному алгоритму, а для проверки столкновений используется метод AABB (Axis-Aligned Bounding Box) — проверка пересечения прямоугольников.



В итоге получилась простая, но полностью рабочая игра, которая стабильно работала всё время, пока я писал эту статью. Я очень горжусь тем, что мне удалось создать рабочий прототип собственного игрового устройства!



Ниже я привожу принципиальную схему устройства. Она достаточно проста, поэтому нет смысла разбивать её на несколько листов. Учился разводить платы, читая сервис-мануалы и изучая чужие схемы :)

❯ Итоги и планы на будущее


Общая стоимость сборки прототипа составила:

  • Raspberry Pi Pico — 557 рублей (я брал на Яндекс.Маркете, на Алиэкспресс можно найти дешевле, около 300 рублей).

  • Дисплей — 380 рублей, заказал на "Алиэкспрессе".

  • Макетная плата — 80 рублей в местном радиомагазине.

  • Кнопки — около 60 рублей (мелочь по 5-10 рублей за штуку).


Итого прототип обошёлся мне в 1077 рублей. Вполне бюджетно! Думаю, можно сделать ещё дешевле. У меня есть желание развивать и поддерживать эту консоль в будущем. Возможно, если будет интерес, сделать небольшую партию для энтузиастов? Соберу по себестоимости (до 1000 руб) + доставка, если вам хочется программировать на чём-то компактном, но не хочется паять самим. Буду рад! Пишите в личные сообщения или комментарии, если интересно! :)



Весь процесс разработки устройства занял у меня всего несколько дней. Ранее я уже разбирался с принципами 2D-графики на старом железе, поэтому ничего кардинально нового для себя не открыл. Однако я получил бесценный опыт в проектировании игровых устройств, которые могут приносить радость — как от процесса создания, так и от осознания, что игра на них работает. :)

Но это далеко не конец проекта! Впереди ещё много работы: нужно развести и изготовить полноценную печатную плату, реализовать звуковую подсистему и API для сторонних разработчиков, придумать и напечатать на 3D-принтере корпус. Кстати, скоро будут и другие интересные проекты с 3D-печатью: как минимум, мы закончим предыдущий проект по превращению планшета с неработающим тачскрином в консоль на базе RPi Pico.

Пост подготовлен при поддержке TimeWeb Cloud. Подписывайтесь на меня и @Timeweb.Cloud, чтобы не пропускать новые статьи каждую неделю!

[mine]ГаджетыGamedevРазработкаСделай самRaspberry piСамодельное железо2DSИгровые приставкиВидеоSilentLongpostНеделя писателя на Пикабу 17 Support Emotions

Больше интересных статей здесь: Гаджеты.

Источник статьи: Сам себе игровая консоль: как я сделал свой «тетрис» с нуля. Что происходит, когда программист встречается с железом?.