Skip to content
138 changes: 138 additions & 0 deletions DOOM_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# DOOM on EdgeTX — RadioMaster TX15

Port of the DOOM game engine to the latest EdgeTX codebase, targeting the RadioMaster TX15 (STM32H750, 480x272 LCD).

Based on the earlier [edgetx-doom](https://github.com/DavBfr/edgetx-doom) proof-of-concept, updated to work with the current EdgeTX architecture and APIs.

> **Note:** This port was heavily AI-assisted (GitHub Copilot / Claude). All code changes — API migration, build integration, runtime fixes — were produced in an interactive AI-assisted session.

## What it does

- Runs DOOM (shareware or full) on the TX15 hardware
- Reads `DOOM1.WAD` from the SD card at `/DOOM/DOOM1.WAD`
- Renders at 320×200 internal resolution, scaled to 480×272 via nearest-neighbor
- **Sound effects** — all 109 SFX from the WAD, resampled and mixed in real-time
- **Music** — MUS format playback via triangle/square-wave synthesis (chiptune style)
- **Gimbal controls** — dual-stick layout (left: move/strafe, right: turn) for intuitive FPS gameplay
- Hardware buttons mapped for menu navigation and alternate controls
- Long-press power button to shut down

## Prerequisites

- ARM GNU Toolchain (tested with 14.2.rel1)
- Python 3 with packages: `jinja2`, `pillow`, `lz4`, `clang`, `pyelftools`
- A `DOOM1.WAD` file (shareware or registered)

## How to build

### Configure

```bash
cmake --preset firmware \
-DPCB=TX15 \
-DDEFAULT_MODE=2 \
-DGVARS=YES \
-DWITH_DOOM=ON
```

### Compile

```bash
cd build/fw
make -j$(nproc) firmware
```

The output `firmware.uf2` is ready to flash.

## How to flash

1. Copy `DOOM1.WAD` to `/DOOM/` on the TX15 SD card
2. Power off the TX15
3. Hold trim buttons and connect USB to enter DFU/UF2 mode
4. Copy `firmware.uf2` to the USB mass storage drive
5. Radio reboots into DOOM

## Controls

### Gimbals (Analog Sticks)

| Gimbal | DOOM Action |
| -------- | ------------- |
| Left stick vertical | Move forward / backward |
| Left stick horizontal | Turn left / right |
| Right stick horizontal | Strafe left / right |

### Buttons

| TX15 Button | DOOM Action |
| ----------- | ----------- |
| RTN | Move forward |
| TELE | Move backward |
| PAGE< | Turn left |
| PAGE> | Turn right |
| MDL | Fire |
| ROLL (push) | Use (open doors, switches) |
| SYS | Open menu (Esc) |
| Power (long press) | Shut down |

**Note:**

- Gimbal controls are disabled in menus to avoid navigation conflicts
- ROLL button acts as ENTER in menus for selection/confirmation, and as USE in-game for doors/switches

## Changes to EdgeTX core

All changes are gated behind `#if defined(WITH_DOOM)` — zero impact on normal builds.

| File | Change |
| ------ | -------- |
| `radio/src/CMakeLists.txt` | `WITH_DOOM` option, include doom subdirectory |
| `radio/src/tasks.cpp` | DOOM RTOS task (32KB stack) as alternative to menus/audio tasks |
| `radio/src/edgetx.cpp` | Skip LVGL init when `WITH_DOOM` is defined |
| `radio/src/model_init.cpp` | Skip COLORLCD layout factory when `WITH_DOOM` is defined |
| `radio/src/sdcard.h` | `DOOM_PATH` definition |
| `radio/src/audio.h` | `AUDIO_BUFFER_COUNT = 6` for DOOM builds (covers one game frame) |
| `radio/src/gui/colorlcd/mainview/view_statistics.cpp` | Guard stack display with `#ifndef WITH_DOOM` |

## New files

- `radio/src/doom/` — Full DOOM engine (chocolate-doom based), ~180 source files
- `radio/src/doom/edgetx/` — EdgeTX integration layer:
- `display.cpp` / `display.h` — LCD framebuffer interface
- `doomgeneric.cpp` / `doomgeneric.h` — Hardware init, input, timing
- `doom_main.h` — Entry point declaration
- `sound.cpp` / `sound.h` — SFX audio module (`sound_module_t`)
- `music.cpp` / `music.h` — MUS music module (`music_module_t`)

## Technical details

- **Display**: DOOM writes directly to the LTDC framebuffer in RGB565, bypassing the LVGL GUI stack entirely
- **D-Cache**: `SCB_CleanDCache()` after framebuffer writes so the LTDC peripheral sees CPU writes to SDRAM
- **Watchdog**: `WDG_RESET()` in game loop, sleep/tick functions, and WAD I/O to survive the 500ms hardware watchdog
- **Memory**: DOOM zone allocator uses 2MB from SDRAM heap via `malloc`
- **WAD I/O**: Uses FatFS (`f_open` / `f_read` / `f_lseek`) to read WAD files from the SD card
- **Scaling**: 320×200 → 480×272 nearest-neighbor, palette-indexed to RGB565 conversion per frame

### Input

- **Analog gimbals**: Read via `getADC()` / `evalInputs()` from `calibratedAnalogs[]` array (range: -1024 to +1024)
- **Deadzone**: 6.5% of full range applied to eliminate stick drift
- **Movement threshold**: 20% of full range triggers digital key events (forward/back/strafe)
- **Turning**: Left stick horizontal mapped to DOOM joystick axis (`AD_RH`) for smooth analog turning (inverted: stick left → turn right)
- **Digital buttons**: 16-entry keymap indexed by `EnumKeys` (0-15), supports press/release events
- **Menu behavior**: Analog stick inputs disabled in menus to prevent navigation conflicts; buttons continue to work

### Audio

- **Hardware path**: EdgeTX AudioBufferFifo → DMA → SPI2 (I2S) → TAS2505 codec → speaker
- **Format**: 32 kHz, 16-bit signed, mono
- **SFX pipeline**: WAD lump loading → 8-bit unsigned to 16-bit signed conversion → linear-interpolation resampling (11025 → 32000 Hz) → up to 8-channel mixing → AudioBufferFifo
- **SFX cache**: Up to 64 decoded/resampled sounds kept in memory to avoid re-processing
- **Music pipeline**: MUS format parser → 140 tick/sec sequencer → per-sample waveform synthesis (triangle/square blend for melodic, filtered LFSR noise for percussion) → mixed into the same audio buffer as SFX
- **Music voices**: 16 channels (0-14 melodic, 15 percussion), with per-channel volume, pitch bend, attack/release envelopes, and 1-pole low-pass filtering
- **Music waveforms**: Melodic channels use a 75% triangle / 25% square blend for warm tone with audible bass; percussion uses filtered LFSR noise (±8000 amplitude with 1/2 old + 1/2 new low-pass) for smooth, non-crackly hi-hats and moderate decay
- **Buffer strategy**: 6 × 10 ms buffers (320 samples each) to cover one game frame (~28 ms at 35 fps)

## License

DOOM engine source is licensed under GPLv2. See `radio/src/doom/` file headers for details.
5 changes: 5 additions & 0 deletions radio/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ else()
option(USBJ_EX "Enable USB Joystick Extension" ON)
endif()
option(AUDIO "Enable audio" ON)
option(WITH_DOOM "Enable doom" OFF)

if(NATIVE_BUILD)
option(ALL_LANGUAGES "Enable runtime language selection" ON)
Expand Down Expand Up @@ -204,6 +205,10 @@ if(LUA)
include(lua/CMakeLists.txt)
endif()

if(WITH_DOOM)
include(doom/CMakeLists.txt)
endif()

if(HELI)
add_definitions(-DHELI)
endif()
Expand Down
2 changes: 2 additions & 0 deletions radio/src/audio.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ constexpr uint8_t AUDIO_FILENAME_MAXLEN = (AUDIO_LUA_FILENAME_MAXLEN > AUDIO_MOD

#if defined(SIMU)
#define AUDIO_BUFFER_COUNT (10) // simulator needs more buffers for smooth audio
#elif defined(WITH_DOOM)
#define AUDIO_BUFFER_COUNT (6) // DOOM needs enough buffers to cover one game frame (~28ms)
#elif defined(AUDIO_SPI)
#define AUDIO_BUFFER_COUNT (2) // smaller than Taranis since there is also a buffer on the ADC chip
#elif defined(STORAGE_USE_SPI_FLASH)
Expand Down
18 changes: 18 additions & 0 deletions radio/src/doom/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

set(SRC ${SRC}
doom/doomdef.c doom/hu_lib.c doom/i_timer.c doom/m_fixed.c doom/p_lights.c doom/p_switch.c doom/r_sky.c doom/w_checksum.c
doom/am_map.c doom/hu_stuff.c doom/i_video.c doom/m_menu.c doom/p_map.c doom/p_telept.c doom/r_things.c doom/w_file.c
doom/i_cdmus.c doom/icon.c doom/m_misc.c doom/p_maputl.c doom/p_tick.c doom/s_sound.c doom/w_main.c
doom/d_event.c doom/doomstat.c doom/i_endoom.c doom/m_random.c doom/p_mobj.c doom/p_user.c doom/sha1.c doom/w_wad.c
doom/d_items.c doom/dstrings.c doom/i_input.c doom/info.c doom/memio.c doom/p_plats.c doom/r_bsp.c doom/sounds.c doom/wi_stuff.c
doom/d_iwad.c doom/dummy.c doom/i_joystick.c doom/m_argv.c doom/p_ceilng.c doom/p_pspr.c doom/r_data.c doom/st_lib.c doom/z_zone.c
doom/d_loop.c doom/f_finale.c doom/i_main.c doom/m_bbox.c doom/p_doors.c doom/p_saveg.c doom/r_draw.c doom/st_stuff.c
doom/d_main.c doom/f_wipe.c doom/i_scale.c doom/m_cheat.c doom/p_enemy.c doom/p_setup.c doom/r_main.c doom/statdump.c
doom/d_mode.c doom/g_game.c doom/i_sound.c doom/m_config.c doom/p_floor.c doom/p_sight.c doom/r_plane.c doom/tables.c
doom/d_net.c doom/gusconf.c doom/i_system.c doom/m_controls.c doom/p_inter.c doom/p_spec.c doom/r_segs.c doom/v_video.c doom/edgetx/display.cpp doom/edgetx/doomgeneric.cpp doom/edgetx/sound.cpp doom/edgetx/music.cpp
)

message(STATUS "Build DOOM firmware")

include_directories(doom/include doom/edgetx)
add_compile_definitions(WITH_DOOM)
Loading