Предисловие: мир DIY-консолей

Эта статья написана максимально простым языком, чтобы была понятна даже тем, кто не программирует, но увлекается техникой.
Всё началось с покупки DIY-игровой консоли Waveshare GamePi13 за 1500 рублей. Первое же знакомство с предустановленными играми от производителя оказалось шоком: классический Pong едва выдавал 5 кадров в секунду. Такая производительность на современном микроконтроллере — это вызов. Не желая мириться с таким положением дел, я изучил схему устройства, документацию на чип RP2040 и взялся за написание собственного BIOS с нуля. Если вам интересно, как устроены подобные консоли изнутри, как заставить микроконтроллер эффективно запускать программы и как реализовать ключевые подсистемы — добро пожаловать в детали проекта, который завершился написанием плавной 60 FPS «Змейки».

Существует целый андерграундный мир DIY-консолей, ориентированных на гиков и инженеров. В отличие от массовых продуктов, они часто строятся на распространённых и дешёвых микроконтроллерах, что даёт полную свободу для творчества. По характеристикам они обычно близки к классическим портативным консолям вроде GameBoy, а их аудитория — это не только игроки, но и те, кто пишет для них свои небольшие игры и демки. Яркие примеры такого подхода — Playdate и Arduboy, где ограничения железа только подстёгивают разработчиков к оптимизации. В России тоже есть свои проекты, например, MikBoy от «Микрона».

Сам я с детства увлекаюсь embedded-электроникой и микроконтроллерами. Мечта создать собственную консоль и даже запустить её мелкосерийное производство живёт во мне с 14 лет. К 24 годам было сделано несколько прототипов, но все они откладывались. В день рождения один из читателей сделал мне денежный подарок, и я отправился на маркетплейс за интересным железом. Среди прочего мне попалась консоль Waveshare GamePi13, которая и стала героем этой истории.

Отдельное спасибо всем, кто поддерживает такие начинания. Без этой поддержки статья вряд ли бы появилась.

Один из моих ранних прототипов для отладки драйвера дисплея.
Waveshare — известный среди энтузиастов производитель дисплеев и одноплатных компьютеров. Консоль GamePi13 приехала через три недели и оказалась очень компактной, с 1.3-дюймовым экраном и без какого-либо корпуса, что только добавляет ей брутальности.

Консоль и старый Nokia 6230i помещаются на ладони.
Устройство состоит из двух частей: основной платы на RP2040 и подключаемого к ней модуля с геймпадом и дисплеем. Последний изначально создавался для Raspberry Pi, но благодаря совместимому разъёму был адаптирован и для платы от Waveshare.

Аппаратная начинка
Плата RP2040-PiZero, хоть и похожа на Raspberry Pi Pico, имеет важные отличия:
16 МБ SPI-флэш-памяти вместо 2 МБ, что является максимумом для XIP-контроллера RP2040.
Использование менее эффективного стабилизатора напряжения (ULDO), что может ограничивать работу периферии при разряде аккумулятора и требует реализации отсечки по напряжению.
Мощный зарядный контроллер для Li-Ion аккумуляторов с током до 1А.
Богатый набор разъёмов: HDMI, слот для MicroSD (занимает целый SPI0) и два порта USB Type-C.

Модуль с геймпадом не менее интересен. На лицевой стороне — 10 кнопок и 1.3-дюймовый IPS-дисплей с разрешением 240x240 на контроллере ST7789. Для такой диагонали разрешение избыточно: оно расходует оперативную память под фреймбуфер и сильно нагружает шину SPI. Кнопки подключены к отдельным пинам, что занимает много GPIO. С обратной стороны расположены динамик с усилителем, разъём для наушников и обвязка дисплея. Подсветка подключена напрямую, без возможности регулировки яркости.

Схема подключения кнопок оставляет желать лучшего.

Производитель модуля — компания SpotPear.
Собрав устройство и запустив демо-игру «Тетрис», я был разочарован: игра шла с мерцанием и частотой около 1 FPS на микроконтроллере с ядром Cortex-M0+ на 150 МГц! Для сравнения, старые телефоны на менее мощных процессорах справлялись с 3D-играми куда лучше.

И это не сжатая гифка, игра действительно так тормозила.
Изучив официальные примеры кода, я обнаружил, что игры написаны на Python крайне неэффективно, с прямым доступом к железу и без использования DMA. Архитектура кода оставляла желать лучшего, а драйвер дисплея даже не пытался использовать возможности аппаратного ускорения. Звуковая подсистема тоже была реализована с ошибками. Впечатлённый таким подходом, я решил написать свой собственный SDK (или BIOS) для этой консоли, но с совершенно иной философией.

Архитектура собственного BIOS
При проектировании я поставил перед собой чёткие цели:
Абстракция: BIOS должен скрывать от игры детали реализации железа. Игра должна работать с понятными подсистемами: графика, ввод, звук, хранилище. Это также упрощает портирование на другие платформы, включая симулятор на ПК.
Производительность: Это главный приоритет. Позорно, когда простейшие игры тормозят на мощном микроконтроллере. Поэтому для разработки игр выбран язык C++ (фактически «C с классами»), а не скриптовые языки.
Портативность: BIOS должен легко переноситься между разными платами и даже аппаратными платформами, что может пригодиться для будущих проектов.
Было решено писать на C++ с использованием интерфейсов и виртуальных методов (VMT). Это улучшает структуру и читаемость, избавляя от ручного составления таблиц системных вызовов. Главный объект системы — CSystem, который предоставляет доступ ко всем подсистемам и информации о платформе. Конкретная реализация для каждой платы («порт») заполняет структуру с указателями на реализации сервисов, что напоминает machine-файлы в Linux.
Такая архитектура проста и понятна. Давайте рассмотрим реализацию ключевых модулей.
Графическая подсистема
Графика разделена на два модуля: драйвер дисплея и модуль для рисования на поверхностях (по аналогии с DirectDraw). Драйвер инициализирует контроллер, выделяет память под фреймбуфер и обновляет изображение. Поскольку используются стандартные экраны с интерфейсом MIPI DBI, часть кода удалось унифицировать.
Возникает вопрос: зачем выделять целых 115 КБ под фреймбуфер, если можно рисовать прямо в память дисплея? Ответ — производительность. При отрисовке множества спрайтов скорость будет падать, а использование DMA станет невозможным. Фреймбуфер же позволяет готовить целый кадр в памяти, а затем быстро отправлять его на дисплей через DMA, что критично для плавной анимации.

DMA-контроллер настраивается для передачи данных по SPI. Пока идёт отправка текущего кадра, система может готовить следующий, что минимизирует простои.
Для отрисовки изображений используется формат RGB565. Вместо дорогого альфа-блендинга применяется техника Color Key (как хромакей): один заранее определённый цвет (например, ярко-розовый) считается прозрачным.

Принцип работы Color Key.
Текст рисуется с помощью bitmap-шрифтов формата 8x8, где каждый бит соответствует пикселю. Такие шрифты занимают мало места, быстро рисуются и легко масштабируются.

Качество изображения на дисплее отличное.
Подсистема ввода
Все кнопки подключены к отдельным GPIO, что, с одной стороны, просто, а с другой — расходует много пинов. Драйвер ввода позволяет получить состояние кнопки, значение «оси» (например, для джойстика) и проверить, нажата ли хоть какая-то кнопка.
Для портативности между разными платами кнопки маппятся на GPIO через таблицу трансляции. Состояние кнопки — это не просто «нажата/отпущена». Реализована обработка дребезга контактов с задержкой, а также состояние «только что отпущена» для удобства создания меню.
В результате получается удобная и отзывчивая система ввода, которая скрывает от игры всю низкоуровневую работу с GPIO.

Результат работы графической подсистемы и ввода.
Загрузка и запуск программ
Это одна из самых интересных частей проекта. Как запустить нативную программу, скомпилированную отдельно, на микроконтроллере? Каждая программа состоит из секций: код (.text), инициализированные данные (.data) и неинициализированные данные (.bss).
Чтобы не реализовывать сложный динамический линкер, было решено выделить под игру фиксированный блок оперативной памяти размером 128 КБ. Скрипт линкера был модифицирован так, чтобы программа загружалась по строго определённому адресу в SRAM.

Каждая игра представляет собой класс, реализующий интерфейс IApplication. Для этого нужна runtime-поддержка: аллокатор памяти и функция для создания экземпляра приложения. Поэтому каждая программа экспортирует специальный заголовок с указателем на эту функцию-создатель.
Таким образом, для запуска игры достаточно загрузить её бинарный файл по нужному адресу, найти заголовок и вызвать функцию создания экземпляра. Всё просто и элегантно.
Но «моргалка» — это скучно. В качестве демонстрации возможностей системы была написана классическая «Змейка», которая работает на стабильных 60 кадрах в секунду.

Заключение и планы на будущее
Таким образом, шаг за шагом, я реализую свою давнюю мечту — создаю платформу для «андерграундной» консоли. Конечно, впереди ещё много работы перед тем, как разводить собственную плату, но начало положено. Для консоли GamePi13 моя реализация BIOS, безусловно, предлагает куда больше возможностей и производительности, чем заводское ПО.
Я понимаю, что мой подход, с использованием ООП и виртуальных методов в embedded-среде, может вызвать споры среди purist'ов. Приглашаю всех заинтересованных обсудить в комментариях потенциальные узкие места, оптимизации и архитектурные решения.
Если вам интересна тематика ремонта, моддинга и программирования для старых и новых гаджетов — добро пожаловать в мой Telegram-канал и на YouTube. Ваша поддержка и донаты (в том числе в виде интересного железа) помогают таким проектам и статьям появляться.
Всего голосов: Всего голосов: Всего голосов:
Подготовлено при поддержке@Timeweb.Cloud
Больше интересных статей здесь: Гаджеты.
Источник статьи: Я купил игровую консоль и написал для неё BIOS.