Build System & Toolchain
Compiler Toolchain
The HaDes-V CPU targets the RV32IM instruction set (RV32I base + M-Extension). The toolchain used is the standard RISC-V GNU toolchain:
CC = /opt/riscv32i/bin/riscv32-unknown-elf-gcc
OBJCOPY = /opt/riscv32i/bin/riscv32-unknown-elf-objcopy
OBJDUMP = /opt/riscv32i/bin/riscv32-unknown-elf-objdump
The cross-compiler targets rv32im with no operating system (bare-metal), producing ELF binaries that are converted to Intel HEX format for loading.
Essentially, this is the exact same toolchain as used for the original HaDes-V lab exercises, with the addition of the -mstrict-align flag to enforce natural alignment of memory accesses.
Optimization Flags
Standard -O2 optimization caused spurious, hard-to-trace runtime issues on the HaDes-V platform. Instead, individual optimization flags were selectively enabled and tested on hardware:
OPT_FLAGS = -fbit-tests
OPT_FLAGS += -finline
OPT_FLAGS += -finline-functions-called-once
OPT_FLAGS += -fcprop-registers
OPT_FLAGS += -fif-conversion
OPT_FLAGS += -fif-conversion2
OPT_FLAGS += -fforward-propagate
OPT_FLAGS += -fdefer-pop
OPT_FLAGS += -fdce
OPT_FLAGS += -fdse
OPT_FLAGS += -fauto-inc-dec
OPT_FLAGS += -fcompare-elim
OPT_FLAGS += -fcombine-stack-adjustments
OPT_FLAGS += -fbranch-count-reg
-fbit-tests is particularly important for DOOM, as the engine makes heavy use of bitfield flags for entity state, door status, and level geometry properties. -finline and -finline-functions-called-once are critical for the rendering hot path, eliminating function call overhead in the inner column-rendering loops.
The -mstrict-align flag is required because the HaDes-V pipeline does not implement hardware support for misaligned loads and stores.
Linker Script (std/hades-v.ld)
The linker script defines two memory regions:
MEMORY {
BRAM (rwx) : ORIGIN = 0x40000, LENGTH = 32K
DDR (rwx) : ORIGIN = 0x80000000, LENGTH = 128M
}
All program sections (.text, .rodata, .data, .bss, .sdata, .sbss) are allocated in DDR2. The BRAM region is reserved for the bootloader and critical interrupt vectors but is not used by the main program. The heap starts at _end:
PROVIDE( _end = . );
The __global_pointer$ symbol is calculated to enable GP-relative addressing for small data sections (.sdata/.sbss), reducing the instruction count for frequently accessed global variables:
__global_pointer$ = MAX((ADDR(.rodata) + 0x800), (ADDR(.sbss) + SIZEOF(.sbss) - 0x800));
The bootloader itself is still built with the original HaDes-V lab template, which places it in BRAM. The bootloader loads the main DOOM binary from the SD card into DDR2 and jumps to its entry point.
WAD Integration
The DOOM WAD file is embedded into the binary at link time using the linker’s binary-to-object capability:
$(BUILD_DIR)/$(C_DIR)/doom/doom1_wad.o: test/c/doom/doom1.wad
$(OBJCOPY) -I binary -O elf32-littleriscv -B riscv \
--rename-section .data=.rodata \
$< $@
This produces an object file containing the WAD data with three exported symbols:
extern const unsigned char _binary_doom1_wad_start[];
extern const unsigned char _binary_doom1_wad_end[];
extern const unsigned char _binary_doom1_wad_size;
The WAD data is placed in .rodata and thus ends up in DDR2 alongside the code and constants.
Makefile Structure
The Makefile was extended from the original lab template:
- Board parameter:
BOARD ?= basys3selects the synthesis target - DOOM target: builds the main DOOM binary combining the DOOM source, the HaDes-V HAL, and the embedded WAD
doom: $(BUILD_DIR)/$(C_DIR)/doom/doom.hex
$(BUILD_DIR)/$(C_DIR)/doom/doom.elf: ... $(DOOM_SOURCES)
$(CC) $(CFLAGS) $(OPT_FLAGS) $(DOOM_SOURCES) -o $@
%.hex: %.elf
$(OBJCOPY) -O ihex $< $@
Synthesis Integration
When building the full bitstream with make synthesis BOARD=nexys4ddr, the DOOM hex file is placed on the SD card and loaded by the BootROM. The FPGA bitstream is targeted at the XC7A100T part with the Nexys 4 DDR constraint file.