Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Divider

Design

The divider implements a radix-2 non-restoring division algorithm that computes one quotient bit per cycle.

flowchart LR
    IDLE([IDLE])
    CALC([CALC])
    DONE([DONE])

    IDLE -->|valid_in & rs2 != 0| CALC
    IDLE -->|valid_in & rs2 == 0<br/>div-by-zero| DONE
    CALC -->|count == 1| DONE
    CALC -->|count > 1<br/>next AQ| CALC
    DONE -->|implicit next valid_in| IDLE

While a higher-radix algorithm (like SRT Radix-4) could halve the latency, a classic 1-bit-per-cycle state machine was chosen. DOOM performs divisions relatively infrequently, mostly during setup, scaling, and initialization, not in inner-loop rendering, making the reduced implementation complexity favorable over the modest performance gain of a higher-radix design.

Core Algorithm

The non-restoring division operates on a 65-bit register pair AQ (33-bit accumulator + 32-bit quotient) and a 33-bit divisor register M:

assign shifted_AQ = AQ << 1;
assign next_A = AQ[64] ? (shifted_AQ[64:32] + M) : (shifted_AQ[64:32] - M);
assign next_Q0 = ~next_A[32];

At each iteration:

  1. AQ is shifted left by one
  2. If AQ[64] is set (remainder negative), the divisor is added; otherwise it is subtracted
  3. The new quotient bit is the inverse of the sign of the adjusted remainder
  4. The updated values are written back to AQ

After 32 iterations, the final remainder may need correction if it is negative (non-restoring property).

Signed & Special-Case Handling

Signed operations: For DIV and REM, operands are converted to absolute values at the start. The sign of the quotient and remainder is computed and applied at the output:

quotient_neg <= is_signed & (rs1_in[31] ^ rs2_in[31]) & (rs2_in != 0);
rem_neg <= is_signed & rs1_in[31];

Divide-by-zero: The RISC-V specification requires graceful handling rather than exceptions. When a divide-by-zero is detected, the hardware returns:

  • Quotient (DIV): 0xFFFFFFFF (all ones)
  • Remainder (REM): the original dividend value

This is implemented by bypassing the iterative computation and going directly to the DONE state with AQ loaded with the dividend.

Output Selection

The module returns either the quotient or remainder depending on the opcode:

assign result = is_rem_op ? final_remainder : final_quotient;

The is_rem_op signal is registered when the instruction is issued, so the output mux is set correctly by the time the result is ready.

Latency

  • Setup: 1 cycle (IDLE → CALC)
  • Iteration: 32 cycles (one per bit)
  • Pipeline stall overhead: variable, depending on when results are consumed
  • Total: approximately 33 clock cycles per operation

Verification

The divider testbench (rtl/tb_divider.sv) uses the same scoreboard architecture as the multiplier to handle the multi-cycle latency. Tests include:

  • Both signed (DIV, REM) and unsigned (DIVU, REMU) operations with random operands
  • Edge cases: division by zero, division with large/small dividends and divisors
  • Negative dividend and divisor combinations
  • Remainder overflow conditions
  • Hazard chains interleaving MUL and DIV operations