![image](https://habrastorage.org/webt/ki/pp/rj/kipprje_0ufsk-ylj6fyck6tsnm.jpeg)
Я, как и многие мои читатели, очень люблю игры. Уже довольно обширное число моих статей было посвящено ремонту и моддингу самых разных игровых консолей — как китайских «нонеймов», так и брендовых PSP и PS Vita! Однако, меня тянет к железу не только желание отремонтировать и поставить в строй «устаревшие» девайсы, но и мания делать и созидать что-то своё! А ещё я очень люблю программировать игры и графику сам. Недавно я загорелся идеей разработать с нуля свой портативный «тетрис»: от схемы и разводки платы, до написания прошивки и игр под нее. Что получается, когда программист, который поставил электронику практически во главе своей жизни, пытается сделать свое устройство? Читайте в статье!
❯ Как я к этому вообще пришел?
Проекты разработки самодельных игровых приставок стали очень популярны к нашему времени. Если раньше embedded-разработка была достаточно дорогой и доступной лишь для избранных, то сейчас на рынке можно найти все что хочешь — и мощные микроконтроллеры с кучей периферии за 300 рублей, и готовые дисплейные модули по 250 рублей, и макетные платы с удобными dupont коннекторами за весьма скромные деньги.
![image](https://habrastorage.org/getpro/habr/post_images/5b2/06b/406/5b206b4066965145d2c06e22f2fe8d28.jpg)
Собрать свой гаджет в пределах одной-двух тысяч рублей стало вполне реальным. Люди собирают себе самые разные устройства, а игровые приставки — одна из самых популярных тем. Однако, для многих людей, которые только начинают знакомится с миром embedded-электроники, собрать консоль в своем корпусе с Raspberry Pi на борту и RetroPie в качестве оболочки — за счастье.
![image](https://habrastorage.org/getpro/habr/post_images/25a/c82/f2f/25ac82f2fa05d9aed9fe9ac00269e49e.jpg)
Однако есть определенная категория электронщиков, к которой отношусь и я — нам нужно делать всё с нуля! Свои проекты я стараюсь реализовывать на самопальных фреймворках/движках, точно также я мыслю и в подходе электроники — ну не могу я использовать чужие решения и стараюсь разобраться в вопросе сам. За моей спиной есть весьма интересные демки. Например, это моя игрушка с незамысловатым названием «ралли-кубок ТАЗов», которую я написал за неделю с нуля (рендерер, звук, ввод, редактор уровней — все свое) в 2022 году:
![image](https://habrastorage.org/getpro/habr/post_images/d02/63a/280/d0263a28024cafce8af0b62b9e4c6025.png)
Вот так, с любовью программировать игры, я и пришел к мысли сделать свою консоль, так как вижу её именно я. Только без чужих библиотек и наработок, но не прям уж bare metal. Сел я и начал думать, на чём же мы будем строить наш игровой девайс!
❯ Из чего будем делать?
Как я уже говорил выше, в наше время выбор железа для создания своих девайсов большой — тут и мощные микроконтроллеры/одноплатники, по производительности сравнимые с телефонами 2005-2006 годов, и различная периферия — аж глаза разбегаются. Однако проектировать будущую консоль нужно исходя из некоторых требований.
Характеристики моего девайса следующие:
- Процессор: двухядерный ARM микроконтроллер RP2040 на частоте 133мгц, построенный на архитектуре Cortex-M3. Сам процессор распаян на плате Raspberry Pi Pico.
- ОЗУ: 260 килобайт SRAM, встроена в процессор. Немного, но если грамотно распоряжаться ресурсами — то хватит.
- ПЗУ: 2Мб SPI Flash-памяти, также распаяны на плате.
- Дисплей: 1.8" TFT-матрица с разрешением 128x160. Выбор разрешения обусловлен производительностью будущей консоли — процессор банально не сможет заполнять матрицу с относительно высоким разрешением.
- Ввод: 6 кнопок, 4 из которых — направление, 2 — действий. В будущем могут добавиться еще несколько.
- Звук: динамик. Пока не знаю, с чего рулить будем — возможно, возьмем «железный» ШИМ-контроллер процессора, а возможно прикрутим внешний ЦАП с i2s.
- Питание: 3.7в аккумулятор BL-4C. Да, да, тот самый с Nokia и современных кнопочников! Аккумулятора, емкостью в 800мАч должно хватать хотя-бы на 4-5 часов игры. При этом зарядка АКБ обеспечивается модулем TP4056.
Весьма неплохо для самоделки, согласны? Как я уже говорил раннее, эти характеристики примерно соответствуют мобильным телефонам 2004-2006 годов — Nokia 6600, Sony Ericsson K510i, Samsung D800. Отличие лишь в ОЗУ (в телефонах её 2-4 мегабайта) и периферийных модулях типа контроллера дисплея.
![image](https://habrastorage.org/getpro/habr/post_images/93a/80a/f89/93a80af898fbfc29c69d0da9770dcdf6.jpg)
На фото E398 — мобилка 2004 года выпуска, но она здесь не просто так. :)
Важную пометку нужно сделать касательно дисплеев: эти 1.8" матрицы бывают приходят с «синевой» — это не железная проблема и не совсем брак. Сам контроллер в дисплея в них сильно греется (хотя токоограничивающий резистор стоит) и негативно влияет на клей, из-за чего матрицы отклеивается от подсветки и слои поляризации начинают «синить» картинку. Лечится проклееванием подложки матрицы суперклеем.
![image](https://habrastorage.org/getpro/habr/post_images/c45/aaa/a8a/c45aaaa8a205456dc986cb2773e992c5.jpg)
RPi Pico я решил выбрать, поскольку информации про них достаточно мало, характеристики хорошие и пока что никто особо ничего на них не делал, тем более в рунете. А ещё у них очень удобное и простое SDK, практически bare-metal. ESP32, например, работает на FreeRTOS и имеет кучу библиотек, здесь же API простое и понятное.
![image](https://habrastorage.org/getpro/habr/post_images/450/dac/bdf/450dacbdf25a80114ea3ddf037ee811c.jpg)
Закупаем все необходимое и начинаем творить!
❯ Графика
В первую очередь нам нужно подключить дисплей и что-нибудь на него вывести. Заодно и SPI погоняем на незнакомом чипсете, благо работа с ним очень простая — задаем конфигурацию пинам (gpio_set_function), настраиваем SPI-контроллер и можно посылать данные.
![image](https://habrastorage.org/getpro/habr/post_images/fec/b94/495/fecb94495bc586989ccf9907c4e87ebe.png)
SPI у RP2040 работает на частоте вплоть до ~60мгц — это достойная скорость передачи, в том числе и для быстрого вывода графики. На самом деле, SPI даже предпочтительнее чем параллельный 8080-интерфейс для использования в микроконтроллерах: дело не только в количестве занимаемых пинов, но и в возможности использования DMA!
В подобных проектах всегда нужно делать так, чтобы дисплей можно было при необходимости поменять, а желательно вообще научить работать его с несколькими контроллерами: разные дисплеи одной диагонали могут использовать разные контроллеры. В моём случае, это ST7735. Для разрешений 240x320 используются ILI9325, ILI9341, ST7789. Команды инициализации дисплея честно позаимствованы, но именно в этом нет ничего зазорного — сама система команд относительно стандартизирована, отличается лишь первичная настройка питания, гамма-коррекции и т. д — часто init sequence вставляет сам производитель в даташит.
![image](https://habrastorage.org/getpro/habr/post_images/914/928/ede/914928ededa14a07d583bf16f1ef4910.png)
После инициализации дисплея пробуем что-нибудь вывести. Да, все работает без проблем. Пару важных нюансов: ST7735 требует посаженный на землю CS, в воздухе его оставлять нельзя, как некоторые ILI (вы ведь навряд ли будете вешать несколько устройств на одну шину с дисплеем, когда есть вторая?) и логическое состояние 1 на пине RESET (в воздухе и «на земле» он будет висеть в постоянном ресете).
![image](https://habrastorage.org/getpro/habr/post_images/89e/06b/5fc/89e06b5fc3a25edb92b0a41fd746877c.jpg)
Для полустатичной графики, можно обойтись лишь командами дисплея — например, тут есть удобные функции для заливки прямоугольников (setArea и пишем цвет без остановки) или скроллинга. Сделано это для более слабых микроконтроллеров. Нам они не подойдут — выделяем память под фреймбуфер/бэкбуфер и настраиваем канал DMA для разгрузки процессора в процессе передачи данных:
![image](https://habrastorage.org/getpro/habr/post_images/cd3/5e4/7ea/cd35e47eab4d799d88652dc09494536e.png)
Саму картинку подготавливает процессор: именно он рисует картинки и он же делает их прозрачными. На него ложится основная работа, однако мы можем ему помочь разгрузиться, если отдадим передачу уже подготовленного кадра на дисплей на DMA (Direct Memory Access) — устройство в микроконтроллере, которое позволяет процессору настроить параметры передачи данных, а DMA их будет сам копировать из памяти или в память. Таким образом, можно реализовать асинхронное копирование нескольких блоков ОЗУ, или, как в моем случае — передачу буфера кадра на дисплей, пока процессор готовит следующий. Чем больше разрешение — тем больше эффекта от DMA!
![image](https://habrastorage.org/getpro/habr/post_images/9c7/3cd/76e/9c73cd76e8159b5fd63d94daf33488e9.jpg)
Кроме того, важно выбрать формат цвета для нашего дисплея: я выбрал 2-х байтный RGB565 (5 бит красный, 6 бит зеленый, 5 бит синий). Это экономичный формат который выглядит красивее палитровой графики и кушает не так уж и много драгоценной памяти. Кроме того, на данный момент мы умеем отрисовывать изображения произвольных размеров с прозрачностью — вместо альфа-канала здесь используется так называемый colorkey — концепция, очень близкая к хромакею, только она берет в качестве трафарета конкретный цвет. В нашем случае это «255 0 255» (ярко розовый).
![image](https://habrastorage.org/getpro/habr/post_images/091/fa0/f8d/091fa0f8d7a460808d799943e1638cb4.jpg)
Общая производительность рендерера порадовала: он легко осилит около сотни-двух различных спрайтов с адекватной производительностью, в зависимости от их размера. Но для такого разрешения экрана и будущих игр — это неплохой результат!
❯ Ввод
Теперь нам нужно как-то управлять нашим девайсом. Для этого пора сделать реализовать геймпад: в рамках этой статьи, я собрал его на макетке.
![image](https://habrastorage.org/getpro/habr/post_images/db1/a8c/98d/db1a8c98dcf8cd8eafaa23ce5ec37804.jpg)
Кидаем общий минус на все кнопки, а второй вывод размыкателя кидаем на соответствующие пины GPIO. Я выбрал предпоследние т. к. на них ничего важного не висит, текущая конфигурация занимает 6 пинов. На фото выглядит не очень красиво — на то она и макетка.
![image](https://habrastorage.org/getpro/habr/post_images/210/50d/1d7/21050d1d76ca9b91479612820e79bcae.jpg)
Переходим к реализации драйвера. Игры могут слушать события кнопок из специальной структуры — CInput, где на каждую кнопку выделено по одному полю. В будущем конфигурация геймпада может поменяться — например, я захочу добавить аналоговый стик.
![image](https://habrastorage.org/getpro/habr/post_images/799/a51/a15/799a51a15f3ad3c3c3e11710c80f94c4.png)
Есть ещё способ реализации больших клавиатур и геймпадов: когда все кнопки вешаются на пару линий, где на выходе каждой кнопки есть резистор определенного номинала. ЦАП микроконтроллера считывает это значение (допустим — 1024 это вверх, а 2048 — вниз) и таким образом определяет текущую нажатую кнопку. Таким раньше любили промышлять китайцы, из-за чего нельзя было нажать одновременно вверх и вправо, или вниз и влево и т. п.
![image](https://habrastorage.org/getpro/habr/post_images/569/5b0/2d6/5695b02d6f732b1f5a95dda86508f17d.jpg)
❯ Пишем игру
Теперь у нас есть минимально-необходимая основа для написания игры. Первой игрой для своей консоли я решил написать классический шутер в космосе — летаем на кораблике и сбиваем врагов, попутно уворачиваясь от их пулек. Заодно проверим консоль на стабильность.
Писать я её решил в классическом C-стиле, как и принято в embedded-мире: без std и тем более stl, без ООП и виртуальных методов, аллокаций по минимуму. В общем, примерно как писали игры под GBA! В первую очередь, подготавливаем спрайты нашей игры, прямо в пейнте, а затем конвертируем их в представление обычного массива байтов в виде header-файла. На первых порах это удобнее, чем делать свой ассет-пул:
![image](https://habrastorage.org/getpro/habr/post_images/fe3/6e0/6b9/fe36e06b992ad61a2f3a1aa8be3e32c5.png)
Архитектуру я организовал в виде нескольких подфункций, каждая из которых занимается своим стейтом (world/menu) и своими объектами (playerUpdate) и их отдельные версии для отрисовки. Сами игровые объекты я описал в виде структур, а центральным объектом сделал CWorld.
![image](https://habrastorage.org/getpro/habr/post_images/e6d/530/44b/e6d53044baaf319e5817c56e70d7e0b6.png)
Время я решил описывать в тиках, а не миллисекундах, как я обычно это делаю на ПК — у консоли железо одно и там следить за этим нужно меньше.
![image](https://habrastorage.org/getpro/habr/post_images/0eb/239/9a8/0eb2399a88f4432f96d57de055b5a635.png)
Единственные аллокации, что я использовал — это для пулов с пулями, и с врагами. Оба пула четко ограничены — до 8 врагов на экране, и до 16 пулек — вполне хватает. Динамические аллокации помогли мне найти серьезную ошибку в коде — в один из моментов игра просто валилась с Out Of Memory. После того, как я немного поменял условия и делал аллокейты тех же самых объектов каждый кадр — игра переставала крашится. Причина оказалась простая — невнимательность (вместо >= было >), по итогу при отрисовке спрайтов за пределами экрана, программа сама начинала портить вунтренние структуры аллокатара и самой игры (проявлялось в глюках и телепортациях). После фикса, все заработало как нужно. :)
![image](https://habrastorage.org/getpro/habr/post_images/e0c/135/7d7/e0c1357d7da7aa4d96db108280a965c4.png)
Ну и для основной части геймплея с выстрелами и столкновениями, я предусмотрел несколько функций, которые спавнят игровые объекты и сами управляют пулом. Противники обновляются как обычно, для коллизий используется AABB (axis aligned bounding box, ну или его 2D-подмножество в виде rect vs rect).
![image](https://habrastorage.org/getpro/habr/post_images/397/8f0/cce/3978f0cce35a40e309a5301f44c36d59.png)
По итогу, у нас получилось простенькая, но рабочая игрушка, которая без проблем работала почти все время, что я писал этот материал, а значит устройство работает стабильно. И я очень горд, что у меня получилось сделать рабочий прототип своего собственного гаджета!
![image](https://habrastorage.org/getpro/habr/post_images/65b/d6e/583/65bd6e5832a60e17e9ce1e923bbd2990.jpg)
Ниже выкладываю принципиальную схему устройства, она очень простая, поэтому смысла делить ее на несколько листов нет. Разводить учился, читая сервис-мануалы и схемы :)
![image](https://habrastorage.org/getpro/habr/post_images/d05/5ce/643/d055ce6433e71c3d9cc800ea80329a0f.png)
❯ Заключение
Полная цена сборки прототипа составила:
- Raspberry Pi Pico — 557 рублей (но я брал на Яндекс Маркете, на «алике» дешевле — около 300 рублей).
- Дисплей — 380 рублей, заказывал на «алике».
- Макетка — 80 рублей, в местном радиомагазине.
- Кнопки. По 5 или 10 рублей штучка, пусть будет 60 рублей.
По итогу, прототип мне обошелся в 1077 рублей. Бюджетненько, да, с учетом того, что можно сделать еще дешевле? Я тут так подумал, у меня есть желание развивать и поддерживать консоль в будущем и под консоль уже можно делать что-то своё… может, если вам будет интересно, делать их на заказ? Соберу вам по себестоимости (до 1.000 рублей) + доставка, если хочется попрограммировать под что-то маленькое, но самому паять не хочется. Мне было бы очень приятно. Пишите в личку или комменты, если вас заинтересовало бы такое! :)
![image](https://habrastorage.org/getpro/habr/post_images/5a3/084/3c4/5a30843c4d1fe4edb5aa29e9ce2706ac.jpg)
Весь процесс разработки этого девайса занял у меня всего несколько дней. Я и до этого понимал концепцию работы 2D-графики на видеокартах прошлого века, поэтому ничего особо нового я для себя не открыл. Однако, я попробовал свои силы в разработке игровых девайсов, которые могут приносить удовольствие — как ментальное от самого процесса сборки и программирования, так и физическое от осознания того, что игра на нем работает. :)
Однако, это далеко не конец проекта! У нас ещё много работы: нужно развести и протравить полноценную плату, реализовать звук и API для сторонних игр, придумать корпус и распечатать его 3D-принтере. Кстати, я ведь обещал что скоро будут и другие интересные проекты с 3D-принтером: как минимум, мы доделаем предыдущий проект игровой консоли из планшета с нерабочим тачскрином и RPi Pico.
Возможно, захочется почитать и это:
- ➤ Nokia N9 — уникальный Linux-смартфон, опередивший своё время на много лет вперед
- ➤ Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера
- ➤ Мой опыт первого принятого коммита в open source
- ➤ Болгарский «Правец-16» (ЕС-1839). Вылазка в консольный интернет в 2023 году на PC/XT 35-летней давности
- ➤ От «из мха и шишек» до продукта | Постройка электросерфа и гидрофойла
![](https://habrastorage.org/r/w1560/webt/b5/pj/of/b5pjofdoxth14ro-rjsrn7sbmiy.png)
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Есть смысл в этом проекте?
68.06% Да, конечно есть! Прямо таки Pico-8, но нативная и в железе :)49
13.89% Нет. Их тысячи уже, и пофиг что базируются друг на друге — результат один!10
18.06% Тебе нужна изюминка, как у Playdate. Например, выведи гребенку под расширения и присоедини к шине SPI!13
Проголосовали 72 пользователя. Воздержались 12 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Если бы я представил частично готовое устройство (с корпусом, АКБ и полноценной платой) — заказали бы его по себестоимости?
53.03% Ну да. Почему-б нет, возможно и попробовал бы под неё что-то написать.35
46.97% Неа. Не вижу смысла.31
Проголосовали 66 пользователей. Воздержались 12 пользователей.