Еще один вариант реализации игры Doom на МК ESP32.

Предыстория
Озадачился собрать такую портативную игрушку, долго штудировал материалы в Сети. Впечатлил такой проект, но погружение выявило недостатки в реализации: довольно экзотические элементы управления, джойстик и пара кнопок с I2C интерфейсом, сложность поиска и цена вызывали сомнения. Кроме того, хотелось реализовать звуковое сопровождение, а также смену карт-сюжетов для игры.
Требования
В итоге сформировались требования к игрушке: небольшой размер с экраном 2,8 дюйма, встроенный звук на динамик 1Вт, и доступ к SD карте без разбора корпуса для замены карты-сюжета. Управление — 6 обычных кнопок, литиевый аккумулятор около 1000mAh.
Принципиальная схема

На рисунке приведена принципиальная схема, пояснения: основной элемент здесь - устройство от Espressif: ESP32 Wrover DevKit SPI 4Mb, дисплей - LCD ILI9341 2,8“ SPI, модуль усилителя НЧ на микросхеме 8002, модуль установки карты microSD. Питать схему напрямую от LiIon не получится, поэтому понадобится DC-DC преобразователь. У меня в наличии оказались только на 5В, его и использовал, а 3.3В для microSD карты снимаю с МК.
Перечень используемых деталей
Наименование |
Описание |
Кол-во |
МК |
ESP32 Wrover DevKit SPI 4Mb |
1 |
LCD |
ILI9341 2,8“ SPI |
1 |
MicroSD |
MicroSD holder |
1 |
DC-DC LiIon 5V |
LiIon => 5V |
1 |
NS8002 |
Модуль усилителя мощности, 3Вт |
1 |
Speaker |
FBF28-6F, 8 Ом, 1.5Вт, 28×5.4мм |
1 |
Battery |
LiIOn LP803040 1200mAh |
1 |
SS12F20G3 |
Движковый переключатель |
2 |
Resistor |
SMD 0805 10k |
6 |
Resistor |
DIP 0.125W 100k |
1 |
Capasitor |
100mkF 6.3x5mm |
2 |
Capasitor |
100nF 0805 |
4 |
PBS-20 |
Гнездо на плату 2.54мм 1х20pin |
1 |
Switch 5-position |
5-позиционный навигационный модуль с джойстиком |
1 |
KLS7-TS1204-7.3-180 |
Кнопка тактовая 12х12 с колпачком |
2 |

Пояснения к списку:
в качестве МК использован модуль ESP32-Wrover, он отличается наличием на борту FLASH‑памяти PSRAM с интефейсом SPI, требуется минимум 4Мб. Для формирования звукового сопровождения имеется встроенный ЦАП;
дисплей — c дравером ILI9341, интерфейс — SPI, диагональ 2,8 дюйма, под него рисовался корпус. Есть возможность использования дисплея с драйвером ST7789, для его использования надо изменить настройки и пересобрать код;
в качестве кнопок управления перемещением использован многопозиционный малогабаритный выключатель, эдакий псевдо‑джойстик;
для вывода звука требуется услилитель, использован готовый модуль на микросхеме NS8002. По просьбам пользователей, предусмотрен выключатель звукового сопровождения.
Процесс монтажа и прошивки
В процессе разработки была создана печатная плата, приведена на рисунке. Плата — односторонняя, разведена вручную в Sprint-Layout для изготовления в домашних условиях методом ЛУТ.
Предчувствую критику к числу перемычек, но при требованиях к размещению МК и SD-холдера ничего лучше не получилось. Пробные запуски на безпаечной плате и макетке показали, что необходимо обеспечивать доступ к интерфейсу USB МК и SD-карте.
Для прошивки МК и проверки результата рекомендую запаять на плату только МК, SD-холдер и гребенки PBS для установки дисплея.

Устанавливаем среду программирования устройств Espressif esp-idf:
git clone https://github.com/espressif/esp-idf.git cd esp-idf ./install.sh source ./export.sh
В качестве прошивки использован готовый проект. Клонируем и заливаем его следующими командами, USB интерфейс модуля ESP32 при этом должен быть подключен.
git clone https://github.com/jkirsons/doom-espidf cd doom-espidf esptool --chip esp32 --port /dev/ttyUSB0 --baud 115200 --before default_reset --after hard_reset \ write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x1000 \ build/bootloader/bootloader.bin 0x10000 build/esp32-doom.bin 0x8000 build/partitions.bin
Если есть желание, можно попытаться изменить настройки, например номера пинов для управления, дисплей ST7789. Для это придется пересобрать код заново. Вносим исправления в код и выполняем:
make menuconfig make -j4 all
где -j4 число параллельных процессов (ядер на вашем PC)
Результат успешной прошивки
esptool.py v2.8 Serial port /dev/ttyUSB0 Connecting… Chip is ESP32D0WDQ5 (revision 3) Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None Crystal is 40MHz MAC: 28:05:a5:35:13:1c Enabling default SPI flash mode… Configuring flash size… Auto-detected Flash size: 4MB Erasing flash… Compressed 25408 bytes to 15859… Took 0.05s to erase flash block Wrote 25408 bytes (15859 compressed) at 0x00001000 in 1.6 seconds (effective 127.1 kbit/s)… Hash of data verified. Erasing flash… Compressed 835168 bytes to 425572… Took 1.13s to erase flash block Wrote 835168 bytes (425572 compressed) at 0x00010000 in 42.8 seconds (effective 156.0 kbit/s)… Hash of data verified. Erasing flash… Compressed 3072 bytes to 70… Took 0.01s to erase flash block Wrote 3072 bytes (70 compressed) at 0x00008000 in 0.1 seconds (effective 324.6 kbit/s)… Hash of data verified.
Leaving… Hard resetting via RTS pin…
Настало время приготовить SD‑карту для работы. В исходном коде упоминается, что проект — это порт Doom под названием PRBOOM, соответственно для работы нужен файл «prboom.wad» из этого проекта и файл карты‑сюжета IWAD c дефолтным названием «doom.wad». Прога ищет их и загружает при старте, это можно видеть видеть в отладчике при загрузке МК после прошивки. Находим эти файлы в сети, записываем на карту и пытаемся запустить игру, если все в порядке можно переходить к монтажу оставшихся элементов. Если же при включении экран остается белым, то можно провести диагностику: запускаем терминал и пытаемся понять причину. Я наблюдал только ошибки, связанные с флеш‑картой. MicroSD‑холдер — пассивное устройство, поэтому наличие ошибок с поиском файлов при зарузке может быть связано с подключением холдера, его питанием, наличием карты, исправной файловой системы и присутствием файлов. По моему опыту подходят флешки от 4Gb, но далеко не все.
Пример вывода с ошибкой поиска файлов на SD-карте
I (1306) spiram: SPI SRAM memory test OK
I (1306) heap_init: Initializing. RAM available for dynamic allocation:
I (1307) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
I (1313) heap_init: At 3FFC8610 len 000179F0 (94 KiB): DRAM
I (1319) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM I (1326) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM I (1332) heap_init: At 4008DD2C len 000122D4 (72 KiB): IRAM I (1338) cpu_start: Pro cpu start user code I (1343) spiram: Adding pool of 4096K of external SPI memory to heap allocator I (21) cpu_start: Starting scheduler on PRO CPU. I (0) cpu_start: Starting scheduler on APP CPU. I (22) spiram: Reserving pool of 32K of internal memory for DMA/internal allocations spi_lcd_init() *** Display task starting. I (22) gpio: GPIO[32]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 I (42) gpio: GPIO[33]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 I (52) gpio: GPIO[34]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 I (52) gpio: GPIO[35]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 I (62) gpio: GPIO[36]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 I (72) gpio: GPIO[39]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 jsInit: GPIO task created.
prboom v2.5.0 (http://prboom.sourceforge.net/) preinitgfxmask for stdout console output: ICWEFDA M_LoadDefaults: Load system defaults. default file: prboom.cfg IWAD found: DOOM.WAD I_Open: Opening File: DOOM.WAD (as DOOM.WAD) I (112) gpio: GPIO[15]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 E (162) sdmmc_sd: sdmmc_init_sd_if_cond: send_if_cond (1) returned 0x108 I (162) gpio: GPIO[13]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 I (162) gpio: GPIO[2]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 I (172) gpio: GPIO[14]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 Init_SD: Failed to initialize the card. 264 I_Open: Got handle: 0 I_Open: open DOOM.WAD failed Guru Meditation Error: Core 0 panic’ed (LoadProhibited). Exception was unhandled. Core 0 register dump: PC : 0x400efde8 PS : 0x00060730 A0 : 0x800efee0 A1 : 0x3ffda710
A2 : 0x3ffdaa04 A3 : 0x3ffda7bc A4 : 0x0000000c A5 : 0x00000001
A6 : 0x00000000 A7 : 0x3ffbafb4 A8 : 0x0000000c A9 : 0x3ffda700
A10 : 0x3ffda99c A11 : 0x0000280a A12 : 0x0000280a A13 : 0x3ffda730
A14 : 0x3ffda710 A15 : 0x00000008 SAR : 0x00000004 EXCCAUSE: 0x0000001c
EXCVADDR: 0x00000064 LBEG : 0x40088145 LEND : 0x40088155 LCOUNT : 0xfffffffd
ELF file SHA256: f9f820a9d4c78c5d
Backtrace: 0x400efde8:0x3ffda710 0x400efedd:0x3ffda740 0x400e459b:0x3ffda760 0x400f1111:0x3ffda780 0x400e3fbe:0x3ffda810 0x400d4a07:0x3ffda900
Entering gdb stub now. $T0b#e6
Пример вывода успешной загрузки МК
I (1306) spiram: SPI SRAM memory test OK
I (1306) heap_init: Initializing. RAM available for dynamic allocation:
I (1307) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
I (1313) heap_init: At 3FFC8610 len 000179F0 (94 KiB): DRAM
I (1319) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM I (1326) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM I (1332) heap_init: At 4008DD2C len 000122D4 (72 KiB): IRAM I (1338) cpu_start: Pro cpu start user code I (1343) spiram: Adding pool of 4096K of external SPI memory to heap allocator I (21) cpu_start: Starting scheduler on PRO CPU. I (0) cpu_start: Starting scheduler on APP CPU. I (22) spiram: Reserving pool of 32K of internal memory for DMA/internal allocations spi_lcd_init() *** Display task starting. I (22) gpio: GPIO[32]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 I (42) gpio: GPIO[33]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 I (52) gpio: GPIO[34]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 I (52) gpio: GPIO[35]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 I (62) gpio: GPIO[36]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 I (72) gpio: GPIO[39]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:3 jsInit: GPIO task created.
prboom v2.5.0 (http://prboom.sourceforge.net/) preinitgfxmask for stdout console output: ICWEFDA M_LoadDefaults: Load system defaults. default file: prboom.cfg IWAD found: DOOM.WAD I_Open: Opening File: DOOM.WAD (as DOOM.WAD) I (112) gpio: GPIO[15]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 Init_SD: SD card opened. I_Open: Got handle: 0 Size: 4196020 I_Open: Closing File: DOOM.WAD PrBoom (built Dec 15 2025), playing: DOOM Shareware PrBoom is released under the GNU General Public license v2.0. You are welcome to redistribute it under certain conditions. It comes with ABSOLUTELY NO WARRANTY. See the file COPYING for details. V_Init: allocate screens. D_InitNetGame: Checking for network game. W_Init: Init WADfiles. I_Open: Opening File: prboom.wad (as prboom.WAD) I_Open: Got handle: 0 Size: 283028 adding prboom.wad I_Open: Opening File: DOOM.WAD (as DOOM.WAD) I_Open: Got handle: 1 Size: 4196020 adding DOOM.WAD W_InitCache
M_Init: Init miscellaneous info. R_Init: Init DOOM refresh daemon - R_LoadTrigTables: Endianness…ok. R_InitData: R_InitData: Textures R_InitData: Flats R_InitData: Sprites R_InitData: Translucency R_InitTranMap: TRANMAP lump: -1 R_InitTranMap: PLAYPAL lump: 117 R_InitTranMap: PLAYPAL cache: 0x3f83dcac Tranmap build […R_InitData: Colourmaps
R_Init: R_InitPlanes R_InitLightTables R_InitSkyMap R_InitTranslationsTables R_InitPatches P_Init: Init Playloop state. I_Init: Setting up machine state. I (3012) I2S: DMA Malloc info, datalen=blocksize=128, dma_buf_count=4 I (3012) I2S: PLL_D2: Req RATE: 11025, real rate: 694.000, BITS: 16, CLKM: 120, BCK: 60, MCLK: 120.937, SCLK: 22208.000000, diva: 64, divb: 59 I_InitSound: pre-cached all sound data I_InitSound: sound module ready S_Init: Setting up sound. S_Init: default sfx volume 8 HU_Init: Setting up heads up display. I_InitGraphics: 320x240 I_UpdateVideoMode: 320x240 V_InitMode: using 8 bit video mode I_SetRes: Using resolution 320x240 gamepadInit: Initializing game pad. ST_Init: Init status bar. G_DoPlayDemo: playing demo with doom/doom2 v1.9 compatibility P_GetNodesVersion: using normal BSP nodes R_NewVisSprite: reallocing vissprites array to 128
Детали корпуса и процесс сборки
Для проекта был разработан корпус, детали приведены на рисунке.

Пояснения:
дисплей вместе с платой надежно фиксируется в рамке, но его углы надо немного сточить на наждачной бумаге. Придется демонтировать SD‑холдер, в схеме он не используется и не убирается в корпус;
в боковой части корпуса надо вырезать 4 отверстия: слева‑внизу, около псевдо‑джойстика — отверстие для USB‑гнезда для заряда батареи, слева‑вверху — выключателя питания, а справа над платой кнопок — выключатель звука. Установите плату и разметьте отверстие для microSD карты вверху‑ посередине. Не огорчайтесь, если отверстие вышло неровное (у меня они все такие получаются), это исправлется декоративной накладкой, наклейте ее снаружи, можно и внутри. Печатать боковые отверстия не получается, так как страдает прочность боковой стенки в целом. Вырезаю их тонким жалом паяльника, а потом довожу скальпелем и надфилем. Гнезно зарядки и выключатели зафиксированы внутри корпуса горячим клеем.
5-позиционный выключатель вставляется в направляющие и фиксируется, для этого надо расплавить направляющие паяльником;
кнопки «Open» и «Fire» размещены на куске макетной платы, обрезанной в формат корпуса. Плата зафиксирована саморезами в выступающие части корпуса. Добейтесь свободного хода кнопок, возможно придется расточить отверстия;
на плате использован только один разъем для подключения динамика для удобного снятия крышки, все остальные линии управления и питания припаяны прямо на плату;
динамик приклеен на заднюю крышку суперклеем;
декоративные элементы приклеиваются на корпус клеем дихлорэтан;
в крышке намечаются и высверливаются отверстия d=1.5мм для саморезов 1×6мм,, в корпусе же, соответственно, надо высверлить отверстия d=1мм, если этого не сделать саморезы сломаются при закручивании;
выступающие контакты на внешней стороне платы рекомендуется сточить напильником, а плату отмыть от флюса и металлических опилок бензин‑калошей. Это необходимо сделать до установки SMD компонентов;
перед окончательным закрытием корпуса, тщательно проверьте, чтобы внутри ничего не болталось и не гремело, для устранения таких дефектов можно зафиксировать подвижные детали горячем клеем.


Заключение
Итог: при небольших затратах, за несколько часов можно сделать и порадовать себя, близких, детей внуков, правнуков и т. д. К проекту прикладываю только файлы, созданные лично, остальное легко найти в сети. Надеюсь, что информация будет кому-то полезной.
Javian
off Хоть кто-нибудь, запустивший Дум, на таких устройствах, проходил игру до конца? У меня впечатление, что кроме короткой пробежки-демонстрации работоспособности, у авторов желания проходить с начала и до конца эту игру нет.