M-Extension
The RISC-V M-Extension (Standard Extension for Integer Multiplication and Division) provides native hardware support for multiplication and division operations. The original DOOM engine does not use floating-point arithmetic but relies heavily on 16.16 fixed-point arithmetic for its 3D rendering pipeline, BSP tree traversal, and physics calculations. Without the M-Extension, these operations would be implemented with software emulation routines (__mulsi3 and __divsi3), which would cost hundreds of clock cycles per operation and catastrophically reduce performance.
Supported Operations
| Category | Instructions | Use in DOOM |
|---|---|---|
| Multiply | MUL, MULH, MULHSU, MULHU | Fixed-point multiplication, angle/trig lookup scaling |
| Divide | DIV, DIVU | Aspect ratio correction, column/span calculation |
| Remainder | REM, REMU | Modulo operations, buffer index wrapping |
Architecture
The M-Extension is implemented as two separate execution units instantiated within the Execute stage, running in parallel with the standard ALU:
flowchart LR
ALU["ALU (Single-Cycle)"]
MUL["Multiplier<br/>(3-Stage Pipelined)"]
DIV["Divider<br/>(Radix-2 State Machine)"]
MUX["Result MUX"]
ALU --> MUX
MUL --> MUX
DIV --> MUX
Both units are independent and can operate concurrently. The Execute stage monitors their busy/valid signals and only stalls the pipeline when at least one unit is actively computing without a valid result available.
Pipeline Integration
Stall Logic: The Execute stage asserts stall_req_exe when either the Multiplier or Divider is busy but does not yet have a valid_out signal. This completely freezes the Fetch and Decode stages, ensuring no instructions are dropped:
assign stall_req_exe = ((mul_start || mul_busy) && !mul_valid_out) ||
((div_start || div_busy) && !div_valid_out);
Forwarding: The forwarding unit treats MUL and DIV identically to standard ALU R-type instructions. Once the multi-cycle math finishes, the Execute stage outputs valid = 1 and the resulting data is instantly forwarded to subsequent instructions, preventing data hazards.
Busy Tracking: Each unit tracks its own busy state with separate start/busy/valid_out signals. A unit is started by asserting its start signal; it remains busy until it produces a valid output and the pipeline is not stalled.
Verification
The M-Extension was verified at three levels:
- Unit-level testbenches (
tb_multiplier.sv,tb_divider.sv) using Verilator with a dynamic scoreboard queue to handle multi-cycle latency - Assembly tests exercising hazard chains (MUL feeding DIV, feeding MUL) and edge cases (divide-by-zero, signed overflow)
- Hardware acceptance tests (
test_acc.c) that run on the FPGA, printing results over UART and setting LED patterns for quick visual pass/fail confirmation
The Multiplier and Divider are each documented in detail in their own subchapters.