Перевод статьи 在Arduino Nano ESP32 上玩《毁灭战士》 — K_b0KBsM, рубрика «柴火创客空间» на 极术社区 (aijishu.com).

Мейкер Naveen из Йокогамы собрал на Arduino Nano ESP32 карманную игровую консоль, на которой запускается классический DOOM: средняя частота кадров — 34 FPS при разрешении 320×240. Устройство получилось лёгким, компактным и дружелюбным к пользователю. Сегодня разбираем этот проект по шагам — от выбора железа до сборки прошивки и корпуса.

Вся конструкция строится вокруг платы Arduino Nano ESP32 на базе ESP32-S3, дисплея Adafruit 2,8" TFT Touch Shield, аналогового джойстика M5Stack и пары кнопок Grove. Программная основа — открытая прошивка Retro-Go и порт DOOM под неё, а собирается всё это в ESP-IDF.

Материалы#

Аппаратная часть:

  • Arduino Nano ESP32 ×1
  • Adafruit 2,8" TFT Touch Shield ×1
  • M5Stack Joystick Unit (MEGA328P, I2C/Grove-разъём)
  • Seeed Studio Grove — Dual Button (двойная кнопка)
  • ArduEZ ONE Stackable Breadboard (стекаемая макетная плата)
  • Seeed Studio Grove — кабель «папа-джамперы → Grove 4 pin»

Программная часть:

  • Espressif ESP-IDF

DOOM, WAD-файлы и микроконтроллеры#

«DOOM» — классический шутер от первого лица, разработанный и изданный id Software в 1993 году. Действие разворачивается в научно-фантастическом будущем: игрок управляет космическим десантником, которому предстоит прорываться сквозь полчища демонов.

DOOM написан в основном на C, отдельные компоненты — на ассемблере. Разработчики работали на компьютерах NeXT под управлением NeXTSTEP. Уровни и графические данные хранятся отдельно от движка в WAD-файлах (от «Where’s All the Data?»), благодаря чему дизайн игры можно менять, не трогая код движка.

Исходному DOOM требовались как минимум 4 МБ оперативной памяти и 20 МБ на диске — это заметно больше, чем типичные ресурсы микроконтроллера. Чтобы перенести игру на МК, приходится прибегать к сжатию и оптимизации, чтобы уместить данные и код во флеш и RAM, а также адаптировать движок под другие платформы и архитектуры — например, чипы ESP32 или RP2040. Такие изменения могут внести в поведение и производительность игры ошибки и отличия, способные повлиять на впечатления от игры.

Недавно энтузиасты приложили немало усилий, чтобы сделать игру совместимой с малопотребляющими микроконтроллерами. Для этого проекта автор выбрал Retro-Go — открытую прошивку, позволяющую играть в ретро-игры на микроконтроллерах семейства ESP32. Официально она не поддерживает Arduino Nano ESP32, но её гибкость и настраиваемость позволяют перенести прошивку и на другие платы на базе ESP32.

Интерфейс эмулятора Retro-Go на дисплее консоли

Retro-Go — открытая прошивка для ретро-гейминга на ESP32, поверх которой и работает порт DOOM.

Шаг 1. Железо#

Главная цель проекта — показать, что запуск DOOM на микроконтроллере с ограниченными ресурсами не только возможен, но и работает с играбельной производительностью. Классический DOOM переносится на Arduino Nano ESP32, а дисплей Adafruit 2,8" TFT Touch Shield, джойстик M5Stack Joystick Unit и двойная кнопка Seeed Grove превращают связку в ручную игровую консоль.

Компоненты проекта: Arduino Nano ESP32, TFT-дисплей Adafruit, джойстик M5Stack и кнопки Grove

Все компоненты проекта: Arduino Nano ESP32, дисплей Adafruit 2,8 дюйма TFT Touch Shield, джойстик M5Stack и кнопки Grove.

Дисплейный шилд Adafruit подключается к Arduino Nano ESP32 по SPI через джамперы и стекаемую макетную плату.

TFT-шилд, подключённый к плате джамперами

TFT-шилд Adafruit соединяется с платой джамперами через стекаемую макетку.

Сама Arduino Nano ESP32 размещена с обратной стороны макетного шилда, сверху стопки плат.

Arduino Nano ESP32, закреплённая с обратной стороны стопки плат

Arduino Nano ESP32 закреплена с обратной стороны макетного шилда.

Джойстик и кнопки подключаются через I2C и GPIO при помощи пап-джамперов и переходных кабелей Grove 4 pin. Схема соединений приведена в разделе о корпусе.

Шаг 2. Настройка окружения разработки#

Прошивку автор компилировал на macOS, но на других операционных системах процесс выглядит похожим образом. Сначала нужно скачать и установить среду разработки ESP-IDF (если вы раньше не работали с ESP-IDF, у нас есть вводное руководство по VS Code):

$ mkdir ~/Nano_ESP32_DOOM
$ cd ~/Nano_ESP32_DOOM
$ git clone -b release/v4.4 https://github.com/espressif/esp-idf
$ cd esp-idf
$ ./install.sh
$ source export.sh

Теперь клонируем репозиторий Retro-Go:

$ cd ~/Nano_ESP32_DOOM
$ git clone https://github.com/ducalex/retro-go
$ cd retro-go

Шаг 3. Конфигурация#

Кодовая база Retro-Go хорошо конфигурируется: для нескольких плат на ESP32 уже есть готовые профили. Для Arduino Nano ESP32 конфигурацию нужно добавить и доработать. В файл baseS3.sdkconfig добавьте строки:

CONFIG_ESPTOOLPY_FLASHMODE="qio"
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y

Конфиг components/retro-go/targets/esplay-s3/config.h из проекта можно переиспользовать для Arduino Nano ESP32, ведь она построена на том же ESP32-S3. В нём нужно указать выводы GPIO, I2C и SPI для всех компонентов — кнопок, джойстика, TFT-дисплея и SD-карты — применительно к плате Arduino Nano ESP32:

// Target definition
#define RG_TARGET_NAME "ESPLAY-S3"

// Storage
#define RG_STORAGE_DRIVER 1 // 0 = Host, 1 = SDSPI, 2 = SDMMC, 3 = USB, 4 = Flash
#define RG_STORAGE_HOST SDMMC_HOST_SLOT_1 // Used by driver 1 and 2
#define RG_STORAGE_SPEED SDMMC_FREQ_DEFAULT // Used by driver 1 and 2
#define RG_STORAGE_ROOT "/sd" // Storage mount point

// Video
#define RG_SCREEN_DRIVER 0 // 0 = ILI9341
#define RG_SCREEN_HOST SPI2_HOST
#define RG_SCREEN_SPEED SPI_MASTER_FREQ_40M
#define RG_SCREEN_TYPE 0
#define RG_SCREEN_WIDTH 320
#define RG_SCREEN_HEIGHT 240
#define RG_SCREEN_ROTATE 0
#define RG_SCREEN_MARGIN_TOP 0
#define RG_SCREEN_MARGIN_BOTTOM 0
#define RG_SCREEN_MARGIN_LEFT 0
#define RG_SCREEN_MARGIN_RIGHT 0

// Input
#define RG_GAMEPAD_DRIVER 3 // 1 = GPIO, 2 = Serial, 3 = I2C
#define RG_GAMEPAD_HAS_MENU_BTN 0
#define RG_GAMEPAD_HAS_OPTION_BTN 0
#define RG_GAMEPAD_MAP {\
{RG_KEY_SELECT, RG_GPIO_GAMEPAD_SELECT},\
{RG_KEY_A, RG_GPIO_GAMEPAD_A},\
}

// Buttons
#define RG_GPIO_GAMEPAD_SELECT GPIO_NUM_5
#define RG_GPIO_GAMEPAD_A GPIO_NUM_6

// Status LED
#define RG_GPIO_LED GPIO_NUM_45

// I2C BUS
#define RG_GPIO_I2C_SDA GPIO_NUM_11
#define RG_GPIO_I2C_SCL GPIO_NUM_12

// SPI Display
#define RG_GPIO_LCD_MISO GPIO_NUM_47
#define RG_GPIO_LCD_MOSI GPIO_NUM_38
#define RG_GPIO_LCD_CLK GPIO_NUM_48
#define RG_GPIO_LCD_CS GPIO_NUM_21
#define RG_GPIO_LCD_DC GPIO_NUM_18

// SPI SD Card
#define RG_GPIO_SDSPI_MISO GPIO_NUM_47
#define RG_GPIO_SDSPI_MOSI GPIO_NUM_38
#define RG_GPIO_SDSPI_CS GPIO_NUM_9
#define RG_GPIO_SDSPI_CLK GPIO_NUM_48

Для джойстика и кнопок добавьте обработку в файл components/retro-go/rg_input.c:

...
#elif RG_GAMEPAD_DRIVER == 3 // I2C
uint8_t data[3];
if (rg_i2c_read(0x52, -1, &data, 3))
{
    int joyX = data[0];
    int joyY = data[1];
    int btnS = data[2];

    if (joyY > 160) {
        state |= RG_KEY_UP;
    } else if (joyY < 96) {
        state |= RG_KEY_DOWN;
    }

    if (joyX > 160) {
        state |= RG_KEY_RIGHT;
    } else if (joyX < 96) {
        state |= RG_KEY_LEFT;
    }

    if (btnS) {
        state |= RG_KEY_START;
    }
}

for (size_t i = 0; i < keymap_size; ++i)
{
    if (!gpio_get_level(keymap[i].src)) {
        state |= keymap[i].key;
        RG_LOGI("key=%d\n", i);
    }
}
...

Шаг 4. Сборка прошивки#

В Retro-Go есть Python-скрипт, автоматизирующий сборку. Следующая команда собирает приложение-лаунчер Retro-Go и приложение prboom-go (порт DOOM):

$ chmod +x rg_tool.py
$ ./rg_tool.py --target arduino_nano_esp32 build-img prboom-go

Шаг 5. Подготовка SD-карты#

Для хранения настроек и сохранений понадобится SD-карта, отформатированная в FAT32. Кроме того, на карту нужно скопировать WAD-файл DOOM в каталог /roms/doom:

$ cp prboom-go/components/prboom/data/doom1.wad /Volumes/SD/roms/doom

SD-карта вставляется в дисплейный шилд Adafruit TFT.

Шаг 6. Заливка прошивки#

Чтобы загрузить прошивку на Arduino Nano ESP32, достаточно выполнить команду:

$ esptool.py --chip esp32s3 write_flash --flash_size detect 0x00 retro-go_1.39-pre-dirty_esplay-s3.img

Шаг 7. Беспроводное подключение#

Wi-Fi включается автоматически, если создать профиль с настройками в файле /retro-go/config/wifi.json на SD-карте:

{
    "ssid": "your_router_ssid",
    "password": "your_network_password"
}

После успешного подключения консоль синхронизирует время с NTP-сервером, а по IP-адресу интерфейса Wi-Fi платы открывается веб-интерфейс для просмотра и загрузки файлов на SD-карту.

Шаг 8. Корпус#

Для быстрого прототипирования автор взял прозрачный PET-пластик от упаковки: из него сделана передняя панель, которая придала устройству вид и ощущение ручной игровой консоли и заодно добавила джойстику и кнопкам жёсткости. Впрочем, корпус из 3D-принтера был бы вариантом получше.

Передняя панель консоли из прозрачного PET-пластика

Передняя панель из прозрачного PET-пластика от упаковки.

Задняя часть осталась без покрытия.

Задняя сторона консоли без покрытия

Задняя сторона устройства без какого-либо покрытия.

Схема соединений:

Схема подключения Arduino Nano ESP32, TFT-дисплея, джойстика и кнопок

Принципиальная схема соединений всех компонентов консоли.

Итог#

DOOM успешно портирован, и на Arduino Nano ESP32 игра выдаёт приемлемую производительность. Плейлист из DOOM на ESP32 в последние годы не ограничивается этой консолью — например, недавно мы писали о порте Metal Gear Solid на ESP32 и о 2D-движке Deki Engine с 60 FPS на ESP32-S3. Ретро-гейминг на микроконтроллерах явно чувствует себя хорошо.