Предисловие
Будучи поклонником «старой» школы шутеров с одной стороны и embedded-разработчиком с другой, я всегда испытывал интерес, как и почему авторам той эпохи удавалось воплощать новый жанр, требующий совершенно новых подходов на весьма «скромном» железе. И я решил попробовать запустить нечто подобное используя решения на основе современных МК — тут и bare-metal и «скромные» ресурсы и довольно мощный инструмент отладки (stm32, имхо). И так, мой выбор пал на плату разработчика stm32f769i discovery.
Примечания
На текущий момент сборка возможна только из среды Keil MDK (загрузчик, игры) или же с помощью arm-gcc + make (только загрузчик). На данный момент доступны порты для — Quake I (+mods), Doom (+mods), Duke Nukem (+mods), Hexen, Heretic. С учетом всех модификаций, список может быть значительно расширен.
Начнем
В настоящей статье я постараюсь коротко изложить основные идеи и принципы их реализации на пути к созданию игровой консоли в частности для stm32f769i discovery. Так же, я постараюсь избегать развернутых технических подробностей, я скорее преследую цель познакомить читателя с еще одним вариантом использования современных МК. Под «игровой консолью» — я подразумеваю самостоятельное устройство с возможностью запуска «пользовательских» приложений без обновления основного ПО.
Архитектура
Так как конечная реализация требует возможности независимого запуска различных игр без обновления прошивки МК, возникла необходимость в некотором варианте «загрузчика», итак:
- Загрузчик; работа с памятью — «установка» приложения (игры), запуск.
- Драйвер; Обслуживание HAL уровня системы и сопутствующие функции, предача API в приложение.
- Приложение; Конечная программа, не возвращает управление в загрузчик.
1. Загрузчик
В основе лежит модель IAP (In-Application-Programming) — драйвера на примере от ST-microelectronics.
Особенность этого подхода в том, что нет необходимости менять когфигурацию загрузки МК,
Все «тело» загрузчика находится в основной памяти, и это в свою очередь позволяет использовать stm32f769i discovery «из коробки».
Основной функционал этого уровня — чтение .bin файла, запись его содержимого в память МК и передача управления. На этом этапе ключевой момент — прочитать адрес точки входа и адрес указателя стека, второе не требуется — т.к. приложение не возвращает управления —
стэк является общим и в перезаписи указателя нет необходимости. Вызов так же может быть выполнен через указатель на функцию. Таким образом, в результате будет получен «гибридный» загрузчик — его «драйверная» часть продолжает обслуживать приложение, в то время как ресурсы самого загрузчика выгружены.
2. Драйвер
Драйвер выполнен в виде «обертки» над уровнем HAL, предоставляя доступ к необходимым ресурсам — файловая система, дисплей\монитор, звук, игровой контроллер\сенсор дисплея. Для дальнейшего использования, API драйвера передается в виде структуры указателей через «общую память» — участок памяти зарезервированный как для загрузчика так и для ответной стороны. Подобная манипуляция требует затрат памяти, и возможно лучшим решением было бы использовать SWI (soft-interrupt, svc call), но для этого в свою очередь необходимо иметь возможноть смены контекста — т.к. не все вызовы могут быть обработанны в прерывании. Так же «общая» память используется для передачи пользовательских аргументов (например через консоль), обязательное условие — добавить атрибут no-init для этого участка, это позволит избежать его перезаписи runtime-с библиотекой на момент инициализации пользовательского приложения.
3. Приложение
Как следствие — единственное что необходимо знать на момент сборки приложения — это архитектуру ядра процессора, никаких зависимостей из HAL нет, так же отсутствует таблица векторов прерываний, — все прерывания обрабатываются загрузчиком. Приложение в результате использует гораздо меньше места в памяти программ — благодаря тому что часть функционала «зашита» вместе с загрузчиком\драйвером, что позволяет установить его в область SRAM data (встроенной оперативной памяти). Это в свою очередь позволяет значительно сократить количество циклов записи Flash памяти и так же ускорить процесс отладки и выполнения в целом. Из минусов же — на момент отладки, вызов приложения возможно произвести только извне, например командой из консоли (COM порт сверх ST-Link, VCOM), для этого используется очень упрощенный вариант командной строки.
Ресурсы:
Загрузчик, hal, Драйвер
Особенности разработки
Память
Первое с чем пришлось столкнуться — это проблема распределения ресурса внешней памяти (SDRAM). Связанно это с тем, что некоторые игры требуют бОльшего объема памяти для .bss секции (duke nukem ~5.5mbytes). Разместить такой объем возможно лишь в sdram, но т.к. эта же память используется загрузчиком для хранения временных данных — изображения, звук, содержимое файлов и т.д.., необходимых только до запуска приложения, — было решено разделить управление этой частью памяти — с каждой стороны есть свой malloc/free. После запуска — драйвер использует указатели на функции malloc\free, которые при необходимости, передаются как параметр в функцию вызова. То есть, после запуска игры — драйвер не может напрямую выполнить аллокацию из sdram. Интересный факт о D-Cache и I-Cache — в силу особенностей обращения с внешней памятью — перед запуском необходимо выключать обе линии, т.к. sdram повторно инициализируется, все бы хорошо, но есть одно «но» — необходимо всегда инвалидировать кэш, иначе, по умолчанию он сохраняет валидное состояние всех линий, в то время как они были перезаписанны в промежутке когда кэш был выключен.
Еще одна особенность — все данные загрузчика помещаются в DTCM секцию, это позволяет не задействовать кэш при обращении к памяти (то же самое позволяет выполнить MPU) и как следствие — решаются проблемы когерентности при работе с DMA;
В связке — CPU -> D-Cache -> Memory < — DMA
Графика
В большей части — это несколько функций масштабирования изображения (2х2, 3х3),
функция инициализации и загрузка палитры. Ключевым моментом является правильное указание атрибутов памяти кадра (выполняется посредством конфигурации MPU) — для загрузчика это будет «Write-back, no write allocate», для устранения эффекта мерцания, т.к. используется single-buffer режим, приложение же использует — «Write through, no write allocate», чем достигается наибольший показатель FPS (Doom ~28-40).
Все существующие порты игр оперируют 8-битной графикой, но так же есть возможность переключится в 16-битный true-color режим (требуется модификация со стороны игры).
Есть возможность выполнить масштабирование 8-битного изображения с помощью DMA2D, но такой подход не оправдал себя — это обходится в ~1000 прерываний, необходимых для обработки изображения с конечным разрешением в 640х480 пикселей, так же порождает массу артефактов в играх — отдельные изображения (спрайты, полигоны) будут отрисованы не полностью, т.к. в этом случае весь процесс рендеринга в игре будет происходить параллельно с зарисовкой экрана.
Звук
Эта часть выполнена в виде простого программного 16-битного, 16-ти канального микшера на основе примеров от ST-microelectronics, так же используется I2S контроллер. На данный момент нет возможности конвертировть аудио форматы между собой — эта часть реализуется на уровне игры в зависимости от требований. Duke Nukem, на мой взгляд, имеет самый богатый набор утилит для работы со звуком, в т.ч. и реверберация.
Ввод
Драйвер джойстика выполнен так же с использованием usb-hid класса (фактически, геймпад будет определен как компьютерная мышь..). Сенсор дисплея — аналогично геймпаду, использует тот же канал для передачи событий, при этом исключительно неудобная вещь.
Игры
Doom
За основу был взят порт stm32doom,
добавлена поддержка звука, пропатчен последними изменениями chocolate doom, добавлены некоторые исправления за авторством Killough, из prBoom. Игра позволяет использовать все доступные для chocolate doom модификации и карты, в т.ч. добавлены декорации и звук из 3DO и PS1 версий игры. Добавлена оптимизация графики — разрешение отрисовки текстур зависит от расстояния, решение так себе, на разных локациях — выигрыш +3-7 кадров в сек. возможен. В последней версии так же добавлена поддержка «прозрачных» спрайтов — все выполнено на основе сгенерированной таблицы комбинаций элементов палитры — нечто подобное используется в Quake II и играх на основе Build движка. Игра в т.ч. модификации могут быть полностью пройдены.
Ресурсы:
stm32doom, 3DO doom, chocolate doom, Текущая версия
Duke Nukem
За основу взят порт chocolate duke. С исходным кодом игры разобраться полностью не было времени, так что все осталось «как есть», исправлены лишь незначительные дефекты. Порт так же позволяет запускать официальные и не совсем модификации — Atomic edition, Nuclear winter, etc…
Примечание — на данный момент ни один из эпизодов игры не может быть полностью пройден в силу существующих дефектов.
Ресурсы:
chocolate duke, Текущая версия
Quake I
К сожалению ссылка на оригинальный репозиторий не сохранилась и найти его я не могу,
порт был выполнен под названием sdl quake. Из особенностей стоит отметить наличие клиент-серверной архитектуры, очень «прожорливый» стэк (~700kb) из-за которого первое время возникло много интересных ситуаций (armcc не очень-то и следит за его использованием), повсеместные проблемы с выравниванием — возможно это касается только armcc компилятора, но практически везде, где есть обращение к элементу структуры размером больше одного байта — нужно использовать функцию-обертку для побайтного чтения\записи иначе — hard-fault exception. Игра довольно неплохо «идет», при среднем fps ~15. Так же могут быть пройдены несколько эпизодов, более-менее комфортно только на первом уровне сложности :)
Ресурсы:
Текущая версия
Hexen, Heretic
В большей части наследуют движок Doom, так что с точки зрения портирования — они практически идентичны (имхо). В hexen добавлена возможность начать игру на выбранной локации, игры не могут быть пройдены полностью.
Ресурсы:
hexen stm32, heretic stm32
Результат
Doom, Duke Nukem, Quake
Спасибо за внимание.
ArtemkaXD
Ничего не понял, но очень интересно!