Skip to content

ND_DMA_MASTER

Source: Verilog/ND-BUS-DEVICES/DMA/circuit/ND_DMA_MASTER.v

Where it sits (Simulation): ND120_TOP > ND120_CORE > ND_DMA_MASTER - instance path: CORE.gen_floppy.FLOPPY_DMA_MASTER

Used in: ND120_CORE (Simulation, Tang, Nexys, MiSTer, MEGA65 R6, MEGA65 R3, QMTECH)

Contains: no other modules.

Module hierarchy - All modules

ND_DMA_MASTER symbol

Schematic

Drawn from the Verilog: the yosys netlist of the Simulation (Verilator) build, instance CORE.gen_floppy.FLOPPY_DMA_MASTER. Sub-modules are boxes (click the picture to open it full size; there every sub-module box links to its page, and every wire shows its Verilog name).

ND_DMA_MASTER schematic

Description

ND-100 DMA BUS MASTER The request/grant + memory-reference engine every DMA controller (floppy DMA, SMD) uses to exchange single words directly with the NORD-100 memory system. Protocol per ND-06.016.01 chapter V and docs/nd100-bus-dma.md: 1. Assert BREQ (wired-OR bus request) and wait. 2. The Bus Control Unit answers with BMEM (leading edge freezes request status) and daisy-chains OUTGRANT; the first module with a frozen request receives INGRANT and stops the token. 3. Granted: present the 24-bit physical address on BD with BAPR; BINPUT inactive = memory read, active = memory write. 4. Read: remove the address (no feedback), assert BDAP ("BD free, memory may drive"); memory answers data + BDRY; strobe data on BDRY. Write: present data combined with BDAP; memory strobes on BDAP and answers BDRY = accepted. 5. BDRY leading edge ends the grant; trailing edge releases the bus. One word per allocation - block transfers re-request. BDAP driver note: the master drives BDAP in BOTH directions - verified against PAL_44902A ("PAUSE UNTIL BDAP OCCURS"), see docs/nd100-bus-dma.md section 9 gap 1 resolution. Daisy chain: INGRANT_n in, OUTGRANT_n out. A module that is not requesting passes the token through; a claiming module consumes it. All bus signals active low; BD released = all ones. Client side is a one-word request/ack interface; the device (word counter, core address register) sits on top and re-requests per word. Last reviewed: 11-JUL-2026 Ronny Hansen

Parameters

Parameter Default
TIMEOUT_TICKS 16'd4096
BINPUT_HOLD 0
EARLY_REREQ `ND_DMA_EARLY_REREQ
EARLY_REREQ 0

Verilog source

Verilog/ND-BUS-DEVICES/DMA/circuit/ND_DMA_MASTER.v on GitHub.

Show the Verilog of ND_DMA_MASTER (370 lines)
/**************************************************************************
** ND-100 DMA BUS MASTER                                                 **
**                                                                       **
** The request/grant + memory-reference engine every DMA controller      **
** (floppy DMA, SMD) uses to exchange single words directly with the     **
** NORD-100 memory system. Protocol per ND-06.016.01 chapter V and       **
** docs/nd100-bus-dma.md:                                                **
**                                                                       **
**   1. Assert BREQ (wired-OR bus request) and wait.                     **
**   2. The Bus Control Unit answers with BMEM (leading edge freezes     **
**      request status) and daisy-chains OUTGRANT; the first module      **
**      with a frozen request receives INGRANT and stops the token.      **
**   3. Granted: present the 24-bit physical address on BD with BAPR;    **
**      BINPUT inactive = memory read, active = memory write.            **
**   4. Read: remove the address (no feedback), assert BDAP ("BD free,   **
**      memory may drive"); memory answers data + BDRY; strobe data on   **
**      BDRY. Write: present data combined with BDAP; memory strobes on  **
**      BDAP and answers BDRY = accepted.                                **
**   5. BDRY leading edge ends the grant; trailing edge releases the     **
**      bus. One word per allocation - block transfers re-request.       **
**                                                                       **
** BDAP driver note: the master drives BDAP in BOTH directions -         **
** verified against PAL_44902A ("PAUSE UNTIL BDAP OCCURS"), see          **
** docs/nd100-bus-dma.md section 9 gap 1 resolution.                     **
**                                                                       **
** Daisy chain: INGRANT_n in, OUTGRANT_n out. A module that is not       **
** requesting passes the token through; a claiming module consumes it.   **
**                                                                       **
** All bus signals active low; BD released = all ones. Client side is a  **
** one-word request/ack interface; the device (word counter, core        **
** address register) sits on top and re-requests per word.               **
**                                                                       **
** Last reviewed: 11-JUL-2026                                            **
** Ronny Hansen                                                          **
***************************************************************************/

module ND_DMA_MASTER #(
    // Safety net only: the real timeout guard is the BCU's (memory out
    // of range -> PES bit 14). This local counter stops a hung FSM in
    // sim/tbs; 0 disables it.
    parameter [15:0] TIMEOUT_TICKS = 16'd4096,
    // BINPUT is an ADDRESS-PHASE signal: memory latches the direction
    // at BAPR; after BAPR goes inactive BINPUT carries no meaning
    // (confirmed 11-JUL-2026 against Figure V.4.2).
    //   0 = release BINPUT together with BAPR (correct behavior)
    //   1 = hold BINPUT until the BDRY leading edge (conservative
    //       variant, kept selectable for validation runs)
    parameter BINPUT_HOLD = 0,
    // Early re-request (open question, both variants unit-tested):
    //   0 = a new dma_req is accepted only after dma_ack (BREQ
    //       re-asserted after the BDRY trailing edge)
    //   1 = a dma_req arriving during a transfer is buffered and BREQ
    //       re-asserted already in the cycle tail (overlapping BDRY)
    // `ND_DMA_EARLY_REREQ overrides the default at COMPILE time ONLY - no
    // build defines it, so every normal build keeps 0. The dmaSim P3 teeth
    // target sets it to 1 (with MIN_GAP 0) to reproduce the back-to-back
    // hazard, proving the recovery gap is load-bearing.
`ifdef ND_DMA_EARLY_REREQ
    parameter EARLY_REREQ = `ND_DMA_EARLY_REREQ,
`else
    parameter EARLY_REREQ = 0,
`endif
    // Recovery gap between granted cycles, in sysclk ticks. MEASURED on
    // the real CPU-board RTL (full-RTL gate): the memory-side grant and
    // decode chain (BLRQ/BCGNT 25/50ns stages) needs time to unwind
    // after BDRY before it can latch the next externally strobed
    // address - a back-to-back re-request wins the bus grant but the
    // RAM cycle never happens (every second read lost). Real ND-100
    // controllers re-request at 1.4us+ periods (manual II.4 examples),
    // so hardware never hit this. The request is ACCEPTED at any time;
    // only the BREQ assertion is deferred.
    //
    // UNITS - READ THIS: the quantity that matters is a real-TIME interval,
    // not a tick count. The target is the ND-100 controller re-request
    // period, ~1.4us (manual II.4). "32 ticks" is only that interval
    // expressed in sysclk periods AT THE SIM CLOCK. This design does not pin
    // one CPU/bus clock frequency (see the clocking docs), so the equivalent
    // tick count is clock-dependent, NOT a physical constant - 32 is the
    // sim-era default. At a known silicon clock, re-derive it from the time
    // (ticks = ceil(1.4us * f_sysclk)); do not carry 32 over blindly.
    // `ND_DMA_MIN_GAP_TICKS overrides the default at COMPILE time ONLY - no
    // build defines it, so every normal build keeps 32. The dmaSim P3 teeth
    // target sets it to 0 (with EARLY_REREQ 1) to reproduce "every second
    // read lost", proving the recovery gap is load-bearing.
`ifdef ND_DMA_MIN_GAP_TICKS
    parameter [7:0] MIN_GAP_TICKS = `ND_DMA_MIN_GAP_TICKS
`else
    parameter [7:0] MIN_GAP_TICKS = 8'd32
`endif
) (
    input wire sysclk,
    input wire sys_rst_n,

    // Client (device core) side: one word per request
    input  wire        dma_req,       // pulse: start transfer (when !dma_busy)
    input  wire        dma_wr,        // with req: 0 = memory read, 1 = memory write
    input  wire [23:0] dma_addr,      // physical memory address
    input  wire [15:0] dma_wdata,     // write data
    output reg  [15:0] dma_rdata,     // read data, valid with dma_ack
    output reg         dma_ack,       // pulse: transfer complete
    output reg         dma_err,       // with ack: local timeout hit
    output wire        dma_busy,

    // ND-100 bus, master side
    output reg         BREQ_n,        // bus request (wired-OR line)
    input  wire        INGRANT_n,     // grant token in (OUTGRANT of the module before us / the BCU)
    output wire        OUTGRANT_n,    // grant token out (to the next module)
    input  wire        BMEM_n,        // memory cycle enable from the BCU; leading edge freezes requests
    output reg  [23:0] BD_23_0_n_OUT, // we drive the bus (address, write data); FFFFFF when released
    input  wire [23:0] BD_23_0_n_IN,  // bus as driven by others (read data from memory)
    output reg         BAPR_n,        // address present strobe
    output reg         BINPUT_n,      // direction: low = write (input to memory), high = read
    output reg         BDAP_n,        // data present / BD-free strobe (master-driven both ways)
    input  wire        BDRY_n         // data ready from memory
);

  localparam ST_IDLE  = 3'd0;
  localparam ST_REQ   = 3'd1;  // BREQ out, waiting for BMEM+INGRANT
  localparam ST_ADDR  = 3'd2;  // address on BD with BAPR
  localparam ST_DATA  = 3'd3;  // BDAP out, waiting for BDRY
  localparam ST_END   = 3'd4;  // BDRY seen: strobe/complete, wait release

  reg [2:0]  s_state;
  reg        s_wr;
  reg [23:0] s_addr;
  reg [15:0] s_wdata;
  reg [15:0] s_rd_capture;  // last driven bus value seen in the data window
  reg        s_rd_captured;
  reg [1:0]  s_rd_idle_cnt; // consecutive undriven-bus ticks while BDRY idle
  reg        s_pend;        // EARLY_REREQ: buffered next request
  reg        s_pend_wr;
  reg [23:0] s_pend_addr;
  reg [15:0] s_pend_wdata;
  reg        s_prev_bmem_n;
  reg [15:0] s_tick_cnt;
  reg [7:0]  s_gap_cnt;     // recovery gap countdown (MIN_GAP_TICKS)
  reg [1:0]  s_phase_cnt;   // small strobe-width counter

  assign dma_busy = (s_state != ST_IDLE);

  wire s_bmem_fall = (BMEM_n == 1'b0) && (s_prev_bmem_n == 1'b1);

  // Request freeze (F.2, Figure V.4.1 "DMA REQ. STATUS IS FROZEN"):
  // only a request asserted BEFORE the leading edge of BMEM takes part
  // in the current grant round. s_req_frozen latches our status at the
  // BMEM edge; s_frozen_eff covers the edge cycle itself (the register
  // updates one clock later).
  reg  s_req_frozen;
  wire s_frozen_eff = s_req_frozen |
                      (s_bmem_fall && (s_state == ST_REQ) && (BREQ_n == 1'b0));

  // Daisy chain: combinational pass-through, like the search chain on
  // the real backplane. We consume the token only when FROZEN-in and
  // waiting (Figure V.5.1: an un-frozen requester must keep connecting
  // INGRANT to OUTGRANT), and for the whole transfer once granted - if
  // the token leaked past a granted master mid-cycle, the next
  // requester downstream would start its own cycle on the occupied bus.
  wire s_claiming = ((s_state == ST_REQ) && s_frozen_eff) ||
                    (s_state == ST_ADDR) || (s_state == ST_DATA) ||
                    (s_state == ST_END);
  assign OUTGRANT_n = INGRANT_n | s_claiming;

  always @(posedge sysclk or negedge sys_rst_n) begin
    if (!sys_rst_n) begin
      s_state       <= ST_IDLE;
      s_wr          <= 1'b0;
      s_addr        <= 24'd0;
      s_wdata       <= 16'd0;
      s_req_frozen  <= 1'b0;
      s_rd_capture  <= 16'd0;
      s_rd_captured <= 1'b0;
      s_pend        <= 1'b0;
      s_pend_wr     <= 1'b0;
      s_pend_addr   <= 24'd0;
      s_pend_wdata  <= 16'd0;
      s_prev_bmem_n <= 1'b1;
      s_tick_cnt    <= 16'd0;
      s_gap_cnt     <= 8'd0;
      s_phase_cnt   <= 2'd0;
      dma_rdata     <= 16'd0;
      dma_ack       <= 1'b0;
      dma_err       <= 1'b0;
      BREQ_n        <= 1'b1;
      BD_23_0_n_OUT <= 24'hFFFFFF;
      BAPR_n        <= 1'b1;
      BINPUT_n      <= 1'b1;
      BDAP_n        <= 1'b1;
    end else begin
      dma_ack <= 1'b0;

      if (s_gap_cnt != 8'd0) s_gap_cnt <= s_gap_cnt - 8'd1;

      // Freeze latch: sampled at the BMEM leading edge, cleared when the
      // round is over (BMEM inactive)
      if (BMEM_n == 1'b1) s_req_frozen <= 1'b0;
      else if (s_bmem_fall)
        s_req_frozen <= (s_state == ST_REQ) && (BREQ_n == 1'b0);

      // EARLY_REREQ: buffer one request arriving while busy
      if (EARLY_REREQ != 0 && dma_req && (s_state != ST_IDLE) && !s_pend) begin
        s_pend       <= 1'b1;
        s_pend_wr    <= dma_wr;
        s_pend_addr  <= dma_addr;
        s_pend_wdata <= dma_wdata;
      end

      case (s_state)
        ST_IDLE: begin
          dma_err <= 1'b0;
          if (dma_req) begin
            s_wr     <= dma_wr;
            s_addr   <= dma_addr;
            s_wdata  <= dma_wdata;
            s_state  <= ST_REQ;
            s_tick_cnt <= 16'd0;
          end
        end

        // Wait for the allocation: BMEM active AND our request frozen in
        // at its leading edge AND the grant token reaching us. A request
        // raised after the BMEM edge waits for the next round (F.2).
        ST_REQ: begin
          // assert the request once the recovery gap has expired
          if (BREQ_n == 1'b1 && s_gap_cnt == 8'd0) begin
            BREQ_n <= 1'b0;
          end
          if ((BMEM_n == 1'b0) && (INGRANT_n == 1'b0) && s_frozen_eff) begin
            // Granted: drop the request, start the address cycle
            BREQ_n        <= 1'b1;
            BD_23_0_n_OUT <= ~s_addr;
            BAPR_n        <= 1'b0;
            BINPUT_n      <= s_wr ? 1'b0 : 1'b1;  // low = write
            s_phase_cnt   <= 2'd2;
            s_state       <= ST_ADDR;
          end
        end

        // Hold address + BAPR for two clocks, then move to the data part
        ST_ADDR: begin
          if (s_phase_cnt != 2'd0) begin
            s_phase_cnt <= s_phase_cnt - 2'd1;
          end else begin
            BAPR_n <= 1'b1;
            if (BINPUT_HOLD == 0) begin
              // Direction was latched by memory at BAPR; BINPUT has no
              // meaning in the data phase - release it with BAPR
              BINPUT_n <= 1'b1;
            end
            if (s_wr) begin
              // Write: data on BD combined with BDAP
              BD_23_0_n_OUT <= ~{8'd0, s_wdata};
            end else begin
              // Read: remove the address, BD free for memory
              BD_23_0_n_OUT <= 24'hFFFFFF;
            end
            BDAP_n  <= 1'b0;
            s_rd_captured <= 1'b0;
            s_rd_idle_cnt <= 2'd0;
            s_state <= ST_DATA;
          end
        end

        // Wait for memory's BDRY. Read data: on the real backplane the
        // data holds through BDRY, so strobing at the BDRY leading edge
        // equals taking the LAST DRIVEN value of the data window - and
        // the latter also tolerates our zero-delay RTL, where the
        // internal BDRY25/BDRY50 delay chains make the board release
        // its data drivers before the externally visible BDRY edge
        // (measured in the full-RTL gate; see docs/nd100-bus-dma.md).
        ST_DATA: begin
          // Capture only GENUINELY-DRIVEN read data by rejecting BOTH idle
          // patterns of this inverted wired-AND bus:
          //   0xFFFFFF = idle-high (tb model + the DMA's own idle drive + a
          //              memory word of 0, which drives ~0 = 0xFFFFFF), and
          //   0x000000 = released/undriven (ND120_TOP+FPGA "drive 0 when
          //              disabled", and the pre-data phase there).
          // Real non-zero data is neither (the upper byte is driven 0xFF, e.g.
          // data 0x0F00 -> bus 0xFFF0FF; even data 0xFFFF -> bus 0xFF0000).
          // For a data value of 0 the bus is 0xFFFFFF at the BDRY edge in BOTH
          // environments, so the `~BD_23_0_n_IN` fallback below yields 0 - no
          // capture needed. This makes the capture value-independent and works
          // for both the standalone tbs (data presented AT BDRY) and ND120_TOP
          // (data presented BEFORE BDRY, released to 0xFFFFFF at the edge).
          // The old `!= 0xFFFFFF` captured the ND120_TOP pre-data 0x000000 as
          // garbage 0xFFFF, corrupting zero-word reads (FLOMON command block
          // sector -> 65535 -> floppy boot hang); `!= 0x000000` alone broke the
          // tbs (idle-high captured as 0). Rejecting both is correct everywhere.
          if (!s_wr && (BD_23_0_n_IN != 24'hFFFFFF) && (BD_23_0_n_IN != 24'h000000)) begin
            s_rd_capture  <= ~BD_23_0_n_IN[15:0];
            s_rd_captured <= 1'b1;
            s_rd_idle_cnt <= 2'd0;
          end else if (!s_wr && s_rd_captured && (s_rd_idle_cnt != 2'd3)) begin
            // Undriven tick: age the capture. A captured value is only
            // trusted at the BDRY edge while FRESH (bus driven within the
            // last 2 ticks) - see the acceptance test below.
            s_rd_idle_cnt <= s_rd_idle_cnt + 2'd1;
          end
          if (BDRY_n == 1'b0) begin
            if (!s_wr) begin
              // Accept the captured value only if the drive window ran
              // (near-)contiguously into this BDRY edge: the board may
              // release its data drivers up to ~2 ticks before the edge
              // (measured 0-1 ticks), but a CPU-fetch TRANSIENT leaking
              // through the BIF transceiver sits at least a memory-access
              // time (~6 ticks) before BDRY, so an age limit of 2 rejects
              // every transient - including TRAINS of them, which the
              // earlier clear-after-2-idle-ticks rule could miss when a
              // new flicker kept resetting the idle counter. For a read
              // of a ZERO word the answer drives ~0 = 24'hFFFFFF
              // (idle-indistinguishable), so a stale capture used to win
              // there; with the age limit the fallback 0 wins instead.
              dma_rdata <= (s_rd_captured && (s_rd_idle_cnt <= 2'd2))
                               ? s_rd_capture
                               : ~BD_23_0_n_IN[15:0];
            end
            // Leading edge of BDRY terminates the grant: release our
            // strobes and data
            BDAP_n        <= 1'b1;
            BD_23_0_n_OUT <= 24'hFFFFFF;
            BINPUT_n      <= 1'b1;
            s_state       <= ST_END;
          end
        end

        // Trailing edge of BDRY releases the bus; complete toward the client
        ST_END: begin
          if (EARLY_REREQ != 0 && s_pend) begin
            BREQ_n <= 1'b0;  // re-assert in the cycle tail (overlaps BDRY)
          end
          if (BDRY_n == 1'b1) begin
            dma_ack   <= 1'b1;
            s_gap_cnt <= MIN_GAP_TICKS;
            if (EARLY_REREQ != 0 && s_pend) begin
              s_wr    <= s_pend_wr;
              s_addr  <= s_pend_addr;
              s_wdata <= s_pend_wdata;
              s_pend  <= 1'b0;
              s_tick_cnt <= 16'd0;
              s_state <= ST_REQ;
            end else begin
              s_state <= ST_IDLE;
            end
          end
        end

        default: s_state <= ST_IDLE;
      endcase

      // Local hang guard (sim safety net; the real guard is the BCU's)
      if (TIMEOUT_TICKS != 16'd0 && s_state != ST_IDLE) begin
        s_tick_cnt <= s_tick_cnt + 16'd1;
        if (s_tick_cnt >= TIMEOUT_TICKS) begin
          BREQ_n        <= 1'b1;
          BAPR_n        <= 1'b1;
          BDAP_n        <= 1'b1;
          BINPUT_n      <= 1'b1;
          BD_23_0_n_OUT <= 24'hFFFFFF;
          dma_err       <= 1'b1;
          dma_ack       <= 1'b1;
          s_pend        <= 1'b0;
          s_gap_cnt     <= MIN_GAP_TICKS;
          s_state       <= ST_IDLE;
        end
      end

      s_prev_bmem_n <= BMEM_n;
    end
  end

endmodule