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



Я, как и многие мои читатели, очень люблю игры. Ремонту и моддингу самых разных игровых приставок посвящено уже достаточно большое количество моих статей - как китайских "нонеймов", так и фирменных PSP и PS Vita! Однако в железе меня привлекает не только желание ремонтировать и пускать в эксплуатацию «устаревшие» устройства, но и мания делать и создавать что-то свое! Я также люблю программировать игры и графику. Недавно у меня появилась идея разработать собственный портативный тетрис с нуля: от схемы и дизайна платы, до написания для него прошивки и игр. Что происходит, когда программист, посвятивший свою жизнь электронике, пытается сделать собственное устройство? Прочитать статью!

❯ Как я к этому вообще пришел?


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



собрать гаджет в пределах одной-двух тысяч рублей стало вполне реально. Люди собирают для себя всевозможные устройства, и игровые приставки — одна из самых популярных тем. Но для многих людей, только начинающих знакомство с миром встраиваемой электроники, сборка консоли в собственном корпусе с Raspberry Pi на борту и RetroPie в качестве оболочки — это блаженство.


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


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

❯ Из чего будем делать?


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

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

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

  2. Оперативная память: 260 килобайт SRAM, встроенная в процессор. Немного, но если правильно распоряжаться своими ресурсами, то достаточно.

  3. ПЗУ: 2Мб SPI Flash, также распаяно на плате.

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

  5. Вход: 6 кнопок, 4 из которых направление, 2 действия. В будущем может быть добавлено больше.

  6. Звук: динамик. Пока не знаю, от чего будем управлять - может, возьмем "железный" ШИМ-контроллер процессора, а может, прицепим внешний ЦАП с i2s.

  7. Питание: батарея 3,7 В BL-4C. Да-да, тот, что с Нокией и современными кнопочками! Аккумулятора емкостью 800 мАч должно хватить как минимум на 4-5 часов игр. При этом зарядку аккумулятора обеспечивает модуль TP4056.


Неплохо для хозяев поля, согласитесь? Как я уже говорил ранее, эти характеристики примерно соответствуют сотовым телефонам 2004-2006 годов выпуска - Nokia 6600, Sony Ericsson K510i, Samsung D800. Разница только в оперативной памяти (в телефонах это 2-4 мегабайта) и периферийных модулях типа контроллера дисплея.


На картинке Е398 — мобильник 2004 года, но он тут неспроста. :)

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



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



Мы покупаем все необходимое и начинаем делать!

❯ Графика


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


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

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



После инициализации экрана пытаемся что-то отобразить. Да все работает без проблем. Пара важных нюансов: ST7735 требует заземленного КС, его нельзя оставлять в воздухе, как некоторые ИЛИ (ведь вы вряд ли повесите несколько устройств на одну шину с одним экраном, когда есть второй ?) и логическое состояние 1 на выводе RESET (в воздухе и "на земле" будет висеть в перманентном сбросе).



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



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

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

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



Также важно выбрать цветовой формат для нашего дисплея: я выбрал 2-байтовый RGB565 (5-битный красный, 6-битный зеленый, 5-битный синий). Это экономичный формат, который выглядит лучше, чем графика с палитрой, и не занимает так много ценной памяти. Кроме того, в настоящее время мы можем рисовать изображения произвольных размеров с прозрачностью — вместо альфа-канала здесь используется так называемый цветовой ключ — понятие, очень близкое к хромакею, только в качестве трафарета он берет определенный цвет. В нашем случае это «255 0 255» (светло-розовый).



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

❯ Ввод


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



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



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

Есть еще один способ реализации больших клавиатур и геймпадов: когда все кнопки повешены на пару линий, где на выходе каждой кнопки присутствует сопротивление определенной величины. ЦАП микроконтроллера считывает это значение (скажем, 1024 вверх, а 2048 вниз) и, таким образом, определяет, какая кнопка нажата. Раньше так делали китайцы, из-за этого нельзя было одновременно нажать вверх и вправо, или вниз и влево и т.д.

❯ Пишем игру


Теперь у нас есть минимально необходимая база для написания игры. С первой игрой для своей консоли я решил написать классический шутер в космосе — мы летим на лодке и сбиваем врагов, по пути избегая их пуль. Заодно проверим консоль на стабильность.
Я решил написать его в классическом стиле C, как принято в мире встраиваемых систем: без std и тем более stl, без ООП и виртуальных методов, аллокации по минимуму. Вообще о том, как писали игры для GBA! Первым делом подготавливаем спрайты для нашей игры, прямо в краске, а потом конвертируем их в представление обычного байтового массива в виде заголовочного файла. Изначально это удобнее, чем создание собственного пула активов:



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


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



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


Ну и для основной части геймплея с выстрелами и столкновениями я предусмотрел несколько функций, создающих игровые объекты и управляющих самим пулом. Оппоненты обновляются как обычно, для столкновений используется AABB (осевой ограничивающий прямоугольник или его 2D-подмножество в виде rect vs rect).



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



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

❯ Заключение


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

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

  • Дисплей - 380 рублей, заказал на "алике».

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

  • Кнопки. За 5 или 10 рублей мелочь, пусть будет 60 рублей.


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



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

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

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

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

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

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