Skip to content

ND_WINCHESTER

Source: Verilog/ND-BUS-DEVICES/WINCHESTER/circuit/ND_WINCHESTER.v

Where it sits (Tang): ND120_TANG20K_TOP > ND120_CORE > ND_WINCHESTER - instance path: CORE.gen_wd.WD_500

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

Contains: no other modules.

Module hierarchy - All modules

ND_WINCHESTER symbol

Schematic

Drawn from the Verilog: the yosys netlist of the Tang Nano 20K build, instance CORE.gen_wd.WD_500. 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_WINCHESTER schematic

Parameters

Parameter Default
BASE_ADDR 16'o000500
IDENT_CODE 16'o000001
INT_LEVEL 4'd11
DELAY_TICKS 32'd10
GEO_HEADS 16'd8
GEO_SPT 16'd9
GEO_MAX_CYL 16'd1024
IS_3038 0

Ports

Direction Width Name Description
input 1 sysclk CPU + bus + device domain (ND3202D sysclk/CLOCK_1/CLOCK_2) (from ND120_CORE.clk_cpu)
input 1 sys_rst_n (active low) Active-low reset, from the board's power-on reset (from ND120_CORE.sys_rst_n)
input [15:0] iox_addr captured IOX address (from ND_BUS_SLAVE.iox_addr)
input 1 iox_wr 1-cycle write strobe (from ND_BUS_SLAVE.iox_wr)
input [15:0] iox_wdata write data, valid with iox_wr (from ND_BUS_SLAVE.iox_wdata)
input 1 iox_rd 1-cycle read strobe (from ND_BUS_SLAVE.iox_rd)
output [15:0] iox_rdata
output 1 iox_sel 1 = this core owns the captured IOX address
output [19:0] trace_rec
output 1 trace_we
output 1 trace_done
output 1 dbg_active
output [3:0] int_pending
input 1 ident_strobe 1-cycle IDENT poll (from ND_BUS_SLAVE.ident_strobe)
input [3:0] ident_level binary 10..13, valid with ident_strobe (from ND_BUS_SLAVE.ident_level)
input 1 ident_grant_in
output 1 ident_grant_out
output 1 ident_hit
output [15:0] ident_code
output 1 dma_req
output 1 dma_wr
output [23:0] dma_addr
output [15:0] dma_wdata
input [15:0] dma_rdata
input 1 dma_ack
input 1 dma_err
input 1 dma_busy
output 1 disk_start
output 1 disk_req
output 1 disk_wr
output [15:0] disk_blkaddr1 head b15-8, sector b7-0
output [15:0] disk_blkaddr2 cylinder
output [2:0] disk_unit only bit 0 is used - two units
output [10:0] disk_wordcount words in the current chunk
input 1 disk_done
input 1 disk_err_in
input [3:0] disk_err_code
input [9:0] dbuf_addr
input [15:0] dbuf_wdata
input 1 dbuf_we
output [15:0] dbuf_rdata

Verilog source

Verilog/ND-BUS-DEVICES/WINCHESTER/circuit/ND_WINCHESTER.v on GitHub.

Show the Verilog of ND_WINCHESTER (1144 lines)
`include "nd_storage_status.vh"
/**************************************************************************
** ND WINCHESTER DISC CONTROLLER, 5 1/4 inch ST506 (3041) / 8 inch (3038) **
**                                                                       **
** DMA block device, IOX 500-507, ident 1, interrupt level 11.           **
**                                                                       **
** Register core made 1:1 faithful to two host-gated C implementations   **
** that are themselves proven byte-identical against each other:         **
**   nd100x  src/devices/winchester/deviceWinchester.c (synchronous)     **
**   portable lib/NDDeviceCore/src/nd_winchester.c     (phase machine)   **
** A shared conformance sequence drives both and the traces are diffed;  **
** this RTL follows the same decisions. Primary source for every rule    **
** below is ND-11.015.01, sections 2.1 and 3.1-3.5 plus 4.1.             **
**                                                                       **
** ---------------------------------------------------------------------- **
** WHY THIS CARD EXISTS ALONGSIDE ND_SMD.v                               **
**                                                                       **
** The families differ in how the 24-bit memory address and the word      **
** count are loaded, and that is what tells them apart:                   **
**                                                                       **
**   NORD-10 large disc   address ONE access,   word count ONE access    **
**   ECC (ND 558/559)     address ONE access,   word count ONE access    **
**   15 MHz SMD (ND 632)  address TWO accesses, word count TWO accesses  **
**   Winchester (this)    address TWO accesses, word count ONE access    **
**                                                                       **
** The ND-120's own MASS STORAGE LOAD microcode (PROM listing, CSA       **
** 002221-002227) writes the core address TWICE and the word count ONCE. **
** That is the Winchester pattern exactly - which is why this core needs **
** NO boot-mode special case at all. ND_SMD.v carries one (s_boot_mode,  **
** the BPUN byte-server plus the "a +1 or +7 write leaves boot mode"     **
** escape) precisely because the microcode's protocol does NOT match its **
** registers. Here the microcode drives the normal register path and a   **
** normal GO, so there is nothing to special-case.                       **
**                                                                       **
** There is also NO register multiplex bit (no CWR): every IOX offset    **
** means exactly one thing, and there is no controller-type strap - the  **
** loading protocol is fixed.                                            **
**                                                                       **
** Register map (offset from base, sec 3.1):                             **
**   +0 R  Read memory address     LO 16 first, then HI 8                **
**   +1 W  Load memory address     HI 8 first, then LO 16                **
**   +2 R  NOT USED - reads 0 ("IOX 502 Not used" in the sec 3.1 table)  **
**   +3 W  Load block address      cylinder b15-5, sector b4-0           **
**   +4 R  Read status register    READING IT RESETS THE FLIP-FLOPS      **
**   +5 W  Load control word       GO / opcode                           **
**   +6 R  Read block address      unconditional - the sec 3.1 table has **
**                                 no test-mode qualifier                **
**   +7 W  Load word count / step count   SINGLE access                  **
**                                                                       **
** NOTE the deliberate asymmetry at +0/+1: address WRITES go HI then LO, **
** address READS come back LO then HI (sec 3.2).                         **
**                                                                       **
** The upper/lower selection flip-flop is reset by ANY of four events    **
** (sec 3.2), all four implemented here: master clear, programmed device **
** clear (control b4), a status register read, and activation (b2).      **
**                                                                       **
** Control word (+5, sec 3.4):                                          **
**   b0 int-enable (on NOT active), b1 error-int enable, b2 ACTIVE (GO), **
**   b3 test mode, b4 device clear, b5-8 head, b9 unit (ONE bit - hence  **
**   two units maximum), b10 unused, b11-13 operation M0..M7,            **
**   b14 seek direction (ZERO = towards cylinder 0), b15 bad track.      **
**                                                                       **
** Status (+4, sec 3.5):                                                 **
**   b0 int-enabled        b1 error-int-enabled   b2 active              **
**   b3 finished with a device operation          b4 inclusive OR (b5-13)**
**   b5 3041: r/w during RTZ  b6 timeout  b7 disk fault                  **
**   b8 address mismatch   b9 CRC error   b10 compare error              **
**   b11 FIFO over/under-run or DMA channel error                        **
**   b12 always 0          b13 3041: always 1 / 3038: r/w gate active    **
**   b14 on cylinder       b15 always 0 - distinguishes this card from   **
**                             the 10 Mb controller                      **
**                                                                       **
** INTERRUPT (sec 4.1), the rule TPE CONFIGURATION probes with:          **
**   "If the controller is ready for an operation (status bit 3 = 1),    **
**    and interrupt is enabled (status bit 0 has been set by control bit **
**    0 = 1), the interrupt signal BINT11 will be active ... The IDENT   **
**    code may now be read by an IDENT PL11 instruction."                **
**   A CONDITION on the current status, not a pulse: activation drops    **
**   b3 and therefore drops the line, which returns at completion. The C **
**   port shipped once without this and TPE reported "No identcode found **
**   on level 11D, expected identcode : 1B".                             **
**                                                                       **
** HARDWARE-CONSTRAINED DIVERGENCE, same as ND_SMD.v: the C models       **
** pre-check that a unit is ATTACHED and (for writes) not WRITE-         **
** PROTECTED using media metadata. This core has no backend metadata     **
** port, so those two faults arrive as disk_err_in during the transfer   **
** and become the same terminated-with-error result. The address-        **
** mismatch bound (b8) IS pre-checked against the GEO_* parameters.      **
**                                                                       **
** MEDIA FILES: this card's images are WDn.IMG, NEVER SMDn.IMG.          **
**                                                                       **
** Thumbwheels (both level 11, sec 3.1):                                 **
**   tw0 disk system 1, base 00500 ident 001                             **
**   tw1 disk system 2, base 00510 ident 005                             **
**   500-507 is the SAME block as the CDC cartridge disc - a machine has **
**   one card or the other, never both.                                  **
**                                                                       **
** Ronny Hansen                                                          **
***************************************************************************/

module ND_WINCHESTER #(
    parameter [15:0] BASE_ADDR   = 16'o000500,
    parameter [15:0] IDENT_CODE  = 16'o000001,
    parameter [3:0]  INT_LEVEL   = 4'd11,
    // How long the controller stays ACTIVE after a GO before it reports
    // completion, in sysclk cycles. Do NOT set this by hand at an
    // instantiation: it is a TIME, so the board wrapper computes it from its
    // own clock frequency. The module default stays small so the unit
    // testbenches run fast. Same rule as ND_SMD.v's DELAY_TICKS.
    parameter [31:0] DELAY_TICKS = 32'd10,
    // Geometry for the address-mismatch bound. Default = the Micropolis 1325
    // (the DISC-74-1): 8 heads, 9 sectors/track, 1024 cylinders, 1024-byte
    // sectors. That is the drive SINTRAN boots from, and the default both C
    // implementations use. A backend owning a different drive passes its own.
    parameter [15:0] GEO_HEADS   = 16'd8,
    parameter [15:0] GEO_SPT     = 16'd9,
    parameter [15:0] GEO_MAX_CYL = 16'd1024,
    // Card model. 0 = 3041 (5 1/4 inch ST506), whose status b13 reads a
    // constant 1; 1 = 3038 (8 inch), whose b13 is the live read/write gate.
    // A parameter rather than an input for the same reason as ND_SMD.v's
    // strap: an unwired input floats and would read as 0 anyway.
    parameter IS_3038 = 0
) (
    input wire sysclk,                 //! CPU + bus + device domain (ND3202D sysclk/CLOCK_1/CLOCK_2) (from ND120_CORE.clk_cpu)
    input wire sys_rst_n,              //! Active-low reset, from the board's power-on reset (from ND120_CORE.sys_rst_n)

    // Device bus (from ND_BUS_SLAVE) - IOX slave side
    input  wire [15:0] iox_addr,       //! captured IOX address (from ND_BUS_SLAVE.iox_addr)
    input  wire        iox_wr,         //! 1-cycle write strobe (from ND_BUS_SLAVE.iox_wr)
    input  wire [15:0] iox_wdata,      //! write data, valid with iox_wr (from ND_BUS_SLAVE.iox_wdata)
    input  wire        iox_rd,         //! 1-cycle read strobe (from ND_BUS_SLAVE.iox_rd)
    output reg  [15:0] iox_rdata,
    output wire        iox_sel,         // 1 = this core owns the captured IOX address

    // ---- on-silicon IOX trace tap -------------------------------------
    // One record per COMPLETED IOX access to this card:
    //   {rw, reg[2:0], data[15:0]}, rw 1 = write (data is iox_wdata),
    //                               rw 0 = read  (data is what we returned).
    // Costs nothing when the consumer ties it off - synthesis prunes it.
    // This exists because ND_WINCHESTER.v is byte-for-byte conformant with
    // the nd100x C model across the whole DISC-TEMA "DU-DI-C" IOX trace and
    // the fault still only appears on silicon, so the sequence the CPU
    // actually issues is the one piece of evidence nobody has. ND_SMD.v had
    // the equivalent (ND120_SMD_TRACE) and that is what made it tractable.
    output wire [19:0] trace_rec,
    output wire        trace_we,
    // Pulses when a device operation COMPLETES (the FinishOperation point).
    // A capture trigger needs this: "the card went quiet" is not enough on
    // its own, because the card is also quiet for many seconds while the
    // diagnostic collects its prompts from the operator.
    output reg         trace_done,
    //! DEBUG (24-AUG-2026): the controller-active flag, for the stage timer
    //! that measures WHERE a disc operation's ~1 s actually goes.
    output wire        dbg_active,
    output wire [3:0]  int_pending,
    input  wire        ident_strobe,   //! 1-cycle IDENT poll (from ND_BUS_SLAVE.ident_strobe)
    input  wire [3:0]  ident_level,    //! binary 10..13, valid with ident_strobe (from ND_BUS_SLAVE.ident_level)
    input  wire        ident_grant_in,
    output wire        ident_grant_out,
    output wire        ident_hit,
    output wire [15:0] ident_code,

    // DMA master client port (to ND_DMA_MASTER)
    output reg         dma_req,
    output reg         dma_wr,
    output reg  [23:0] dma_addr,
    output reg  [15:0] dma_wdata,
    input  wire [15:0] dma_rdata,
    input  wire        dma_ack,
    input  wire        dma_err,
    input  wire        dma_busy,

    // Disk backend: chunk transfers through the internal buffer. Same port
    // shape as ND_SMD.v so the nd_storage adapter pattern carries over; the
    // block address is split into the two words the adapter expects.
    output reg         disk_start,
    output reg         disk_req,
    output reg         disk_wr,
    output wire [15:0] disk_blkaddr1,  // head b15-8, sector b7-0
    output wire [15:0] disk_blkaddr2,  // cylinder
    output wire [2:0]  disk_unit,      // only bit 0 is used - two units
    output wire [10:0] disk_wordcount, // words in the current chunk
    input  wire        disk_done,
    input  wire        disk_err_in,
    // WHY the backend failed (nd_storage_status.vh), valid with disk_done.
    // Mapped below onto the status bits ND-11.015.01 sec 3.5 ALREADY
    // defines for this card - no new status bit is invented here. Before
    // this existed every backend failure became a CRC error, so "no SD
    // card", "WD0.IMG is not on the card" and "block past the end of the
    // image" were one indistinguishable bit at the guest.
    input  wire [3:0]  disk_err_code,
    input  wire [9:0]  dbuf_addr,
    input  wire [15:0] dbuf_wdata,
    input  wire        dbuf_we,
    output reg  [15:0] dbuf_rdata
);

  localparam [10:0] BUF_WORDS = 11'd1024;

  // Device operation codes, control word b11-13 (sec 3.4).
  localparam [2:0] OP_READ       = 3'd0;  // M0
  localparam [2:0] OP_WRITE      = 3'd1;  // M1
  localparam [2:0] OP_READ_PAR   = 3'd2;  // M2
  localparam [2:0] OP_COMPARE    = 3'd3;  // M3
  localparam [2:0] OP_SEEK       = 3'd4;  // M4 - step count in +7
  localparam [2:0] OP_WR_FORMAT  = 3'd5;  // M5
  localparam [2:0] OP_LOAD_CTRL  = 3'd6;  // M6 - 3038 only, NOT activated
  localparam [2:0] OP_RTZ        = 3'd7;  // M7

  // ---- controller registers ----
  reg [15:0] s_mem_addr_lo;   // memory address bits 0-15
  reg [7:0]  s_mem_addr_hi;   // memory address bits 16-23
  reg [15:0] s_word_cnt;      // word count, ALSO the M4 step count
  reg [15:0] s_blkaddr;       // cylinder b15-5, sector b4-0

  wire [10:0] w_cyl    = s_blkaddr[15:5];
  wire [4:0]  w_sector = s_blkaddr[4:0];

  // ---- unit selection + per-unit state ----
  reg        s_sel_unit;      // control word b9 - ONE bit, two units
  reg [3:0]  s_head;          // control word b5-8
  reg        s_seek_dir;      // control word b14: 0 = towards cylinder 0
  reg        s_bad_track;     // control word b15
  reg [1:0]  s_on_cyl;        // per-unit on-cylinder (status b14)

  // M4 is a RELATIVE STEP seek, so each arm's position must be remembered.
  reg [15:0] s_cyl_pos [0:1];

  // ---- status flags (sec 3.5) ----
  reg        s_int_en;        // b0
  reg        s_errint_en;     // b1
  reg        s_active;        // b2
  assign dbg_active = s_active;
  reg        s_rft;           // b3  finished with a device operation
  reg        s_rtz_viol;      // b5
  reg        s_time_out;      // b6
  reg        s_disk_fault;    // b7
  reg        s_addr_mismatch; // b8
  reg        s_crc_err;       // b9
  reg        s_compare_err;   // b10
  reg        s_dma_ch_err;    // b11
  reg        s_rw_gate;       // b13 on the 3038 only
  reg        s_test_mode;     // control b3

  // ---- the upper/lower selection flip-flops (sec 3.2) ----
`ifdef ND120_WD_TRACE
  reg [31:0] s_trace_cyc;     // sysclk counter for the simulation-only trace
`endif

  reg        s_ma_write_ff;
  reg        s_ma_read_ff;

  reg        s_irq;           // latched level-11 interrupt line

  assign disk_blkaddr1 = {4'd0, s_head, 3'd0, w_sector};
  assign disk_blkaddr2 = {5'd0, w_cyl};
  assign disk_unit     = {2'd0, s_sel_unit};

  // ---- internal buffer ----
  // Async-read arrays do NOT map to Gowin BSRAM, so this is a simple dual-port
  // RAM: one muxed write port, one registered read port whose address follows
  // the active consumer. Same refactor as ND_SMD.v and ND_FLOPPY_DMA.
  reg [15:0] s_buffer[0:1023];

  // ---- decode ----
  wire s_addressed = (iox_addr[15:3] == BASE_ADDR[15:3]);
  assign iox_sel   = s_addressed;
  wire [2:0] s_reg = iox_addr[2:0];
  wire s_wr_here = iox_wr && s_addressed;
  wire s_rd_here = iox_rd && s_addressed;

  // SIDE EFFECTS MUST BE EDGE-TRIGGERED, NOT LEVEL-TRIGGERED - AND THE READ
  // ALTERNATOR MUST ADVANCE AT THE END OF THE ACCESS, NOT THE START.
  //
  // iox_rd / iox_wr are held for the whole ND-BUS I/O cycle, which is many
  // sysclk periods - the bus runs at the original ND speed while the card is
  // clocked by sysclk. Two separate faults came out of using the level:
  //
  //   1. Every non-idempotent side effect fired once PER SYSCLK for the
  //      duration of the access. The +0 read alternator (s_ma_read_ff) and
  //      the +1 write alternator (s_ma_write_ff) toggled N times instead of
  //      once, so which half of the memory address register you got depended
  //      on how many clocks the bus cycle happened to last.
  //
  //   2. Even toggled exactly once, toggling at the START of a read changes
  //      iox_rdata WHILE the bus is still sampling it: the master sees the
  //      other half. The alternator therefore advances on the END of the
  //      access (s_rd_end), so the selected half is stable for the whole
  //      cycle and the NEXT read gets the other one. The register offset is
  //      latched with it, because iox_addr may already have moved on.
  //
  // Symptom on silicon (05-AUG-2026): DISC-TEMA J02 "DU-DI-C" on
  // DISC-74MB-1 returned all 512 data words correctly and the identical
  // status word 060010, then reported "Memory address Register not as
  // expected" - the two IOX 500 reads came back swapped. Reproduced against
  // the nd100x trace in nd_winchester_oracle_tb.v.
  //
  // Writes keep the RISING edge: iox_wdata is stable when the strobe goes
  // active, and a write has no read-data to hold steady.
  reg       s_iox_rd_q, s_iox_wr_q;
  reg [2:0] s_reg_q;
  always @(posedge sysclk or negedge sys_rst_n) begin
    if (!sys_rst_n) begin
      s_iox_rd_q <= 1'b0;
      s_iox_wr_q <= 1'b0;
      s_reg_q    <= 3'd0;
    end else begin
      s_iox_rd_q <= s_rd_here;
      s_iox_wr_q <= iox_wr;
      if (s_rd_here) s_reg_q <= s_reg;
    end
  end
  wire s_rd_end  = s_iox_rd_q && !s_rd_here;   // end of an addressed read
  wire s_wr_edge = s_wr_here && !s_iox_wr_q;

  // Trace record encoding. {rw, reg} is the top nibble; 0-7 = a READ of
  // +0..+7, 8-F = a WRITE to +0..+7. Two of those 16 codes are impossible on
  // this card and are reused for events that are NOT IOX at all but that
  // software can see just as clearly:
  //
  //   8xxxx  IDENT answered on level 11, xxxx = the code returned
  //   Axxxx  the interrupt line CHANGED, xxxx = 1 raised / 0 dropped
  //
  // (+0 and +2 are read-only, so "write to +0" and "write to +2" can never
  // occur.) They are here because the IOX sequence on silicon now matches the
  // nd100x C model access for access and DISC-TEMA still disagrees with it,
  // so whatever decides the verdict is something an IOX trace cannot show.
  reg s_irq_q;
  always @(posedge sysclk or negedge sys_rst_n) begin
    if (!sys_rst_n) s_irq_q <= 1'b0;
    else            s_irq_q <= s_irq;
  end
  wire s_irq_edge = (s_irq != s_irq_q);

  //   Cxxxx  an IOX to an address that is NOT ours, xxxx = that address.
  // The card sees the whole bus, so this answers "where is the CPU actually
  // going" - the question left open when a mass-storage load (20500&) hangs
  // having touched this card fewer than four times. If the microcode is
  // driving 20501/20503/20507 because bit 13 was never stripped from the
  // typed load code, nothing decodes, the bus times out, and the CPU hangs
  // exactly as observed. ND_SMD.v has the same idea under [SMD-OTHER].
  reg s_foreign_q;
  wire s_foreign = (iox_rd || iox_wr) && !s_addressed;
  always @(posedge sysclk or negedge sys_rst_n) begin
    if (!sys_rst_n) s_foreign_q <= 1'b0;
    else            s_foreign_q <= s_foreign;
  end
  wire s_foreign_edge = s_foreign && !s_foreign_q;

  // An IDENT cycle this card did NOT answer is recorded too (tag 9). Without
  // it the trace cannot tell the two ways an interrupt goes unclaimed apart,
  // and on 09-AUG-2026 a silicon capture of the File System Investigator hit
  // exactly that wall: the irq line rose (tag A) and no tag 8 ever followed,
  // which is equally consistent with "the CPU never ran an IDENT cycle at all"
  // and with "it did, but a device ahead of this one in the grant chain took
  // it" - the floppy is also level 11 and sits earlier in the chain. Tag 9
  // carries grant_in and s_irq, so the answer is read straight off the dump:
  // no tag 9 at all      -> the CPU never identified; look CPU-side.
  // tag 9 with grant=0   -> the grant never arrived; a device ahead consumed it.
  // tag 9 with grant=1,
  //   irq=0              -> our own irq had already dropped before the cycle.
  reg s_idstb_q;
  always @(posedge sysclk or negedge sys_rst_n) begin
    if (!sys_rst_n) s_idstb_q <= 1'b0;
    else            s_idstb_q <= ident_strobe;
  end
  wire s_idstb_edge = ident_strobe && !s_idstb_q &&
                      (ident_level == INT_LEVEL) && !s_ident_answer;

  assign trace_we  = s_wr_edge || (s_rd_here && !s_iox_rd_q)
                     || s_ident_answer || s_idstb_edge || s_irq_edge
                     || s_foreign_edge;
  assign trace_rec = s_wr_edge       ? {1'b1, s_reg, iox_wdata}
                   : (s_rd_here && !s_iox_rd_q) ? {1'b0, s_reg, iox_rdata}
                   : s_ident_answer  ? {4'h8, IDENT_CODE}
                   : s_idstb_edge    ? {4'h9, 10'd0, ident_grant_in, s_irq,
                                        ident_level}
                   : s_irq_edge      ? {4'hA, 15'd0, s_irq}
                                     : {4'hC, iox_addr};

  // ---- status assembly (sec 3.5) ----
  // Bit 4 is the inclusive OR of the error bits. Bit 13 is EXCLUDED even
  // though the manual's wording says "bits 5-13": on a 3041 b13 is always 1,
  // so including it would make the error OR permanently set.
  wire s_incl_or = s_rtz_viol | s_time_out | s_disk_fault | s_addr_mismatch |
                   s_crc_err  | s_compare_err | s_dma_ch_err;

  // Bit 13 is the card identity: the 3041 always reads 1 ("Used to distinguish
  // the 3038 from the 3041"), the 3038 reports its live read/write gate.
  wire s_contr_id = IS_3038 ? s_rw_gate : 1'b1;

  wire [15:0] s_status =
      { 1'b0,             // b15 always 0 - not the 10 Mb controller
        s_on_cyl[s_sel_unit], // b14 on cylinder
        s_contr_id,       // b13 card identity
        1'b0,             // b12 always 0
        s_dma_ch_err,     // b11 FIFO over/under-run or DMA channel error
        s_compare_err,    // b10 compare error
        s_crc_err,        // b9  CRC error
        s_addr_mismatch,  // b8  address mismatch
        s_disk_fault,     // b7  disk fault or missing clocks
        s_time_out,       // b6  timeout
        s_rtz_viol,       // b5  r/w attempted during RTZ (3041)
        s_incl_or,        // b4  inclusive OR of b5-13
        s_rft,            // b3  finished with a device operation
        s_active,         // b2  controller active
        s_errint_en,      // b1  error interrupt enabled
        s_int_en };       // b0  controller-not-active interrupt enabled

  // ---- interrupt / ident (latched line) ----
  assign int_pending = {(INT_LEVEL == 4'd13) && s_irq,
                        (INT_LEVEL == 4'd12) && s_irq,
                        (INT_LEVEL == 4'd11) && s_irq,
                        (INT_LEVEL == 4'd10) && s_irq};
  wire s_ident_answer = ident_strobe && ident_grant_in &&
                        (ident_level == INT_LEVEL) && s_irq;
  assign ident_hit       = s_ident_answer;
  assign ident_code      = s_ident_answer ? IDENT_CODE : 16'd0;
  assign ident_grant_out = ident_grant_in && !s_ident_answer;

  // ---- IOX read mux ----
  // Reads are NOT gated on a selected unit. These are CONTROLLER registers -
  // they exist whether or not a drive is selected, and a probe must be able to
  // tell an absent card from an idle one. Both C implementations shipped a
  // "return 0 when no disk selected" gate here; it was a bug in all of them.
  always @(*) begin
    iox_rdata = 16'd0;
    if (s_rd_here) begin
      case (s_reg)
        // Sec 3.2: the reads are the OPPOSITE order to the writes - the first
        // IOX 500 returns the LOW 16 bits, the second the UPPER 8.
        3'd0: iox_rdata = s_ma_read_ff ? {8'd0, s_mem_addr_hi} : s_mem_addr_lo;
        // +2: "IOX 502 Not used" in the sec 3.1 table. There is no
        // programmer-visible sector counter on this card - the sector counters
        // named in the hardware description are internal. Both C models had a
        // readable counter here until the cross-check sent us to the table.
        3'd2: iox_rdata = 16'd0;
        3'd4: iox_rdata = s_status;
        // +6: "IOX 506 Read Block (disk) address register", with NO test-mode
        // qualifier, so it reads back unconditionally.
        3'd6: iox_rdata = s_blkaddr;
        default: iox_rdata = 16'd0;   // +1/+3/+5/+7 are write-only
      endcase
    end
  end

  // ---- transfer engine ----
  localparam E_IDLE    = 3'd0;
  localparam E_DISK_RD = 3'd1;  // backend: image chunk -> buffer
  localparam E_MEM_WR  = 3'd2;  // DMA: buffer -> ND memory
  localparam E_MEM_RD  = 3'd3;  // DMA: ND memory -> buffer
  localparam E_DISK_WR = 3'd4;  // backend: buffer -> image chunk
  localparam E_DELAY   = 3'd5;  // completion delay -> FinishOperation
  localparam E_MEM_CMP = 3'd6;  // DMA: ND memory vs buffer (M3 compare)

  reg [2:0]  s_eng;
  reg        s_is_cmp;       // this operation is M3, not M0
  reg [10:0] s_chunk_q;
  reg [10:0] s_sec_idx;
  reg [31:0] s_delay_cnt;
  reg        s_dma_wait;
  reg [23:0] s_dma_addr_r;   // running ND word address
  reg [23:0] s_words_left;   // words still to move
  reg        s_unit;         // selected drive for this command

  assign disk_wordcount = s_chunk_q;

  // CHS -> LBA, the same formula the SMD core and both C models use.
  function [31:0] chs2lba;
    input [15:0] cyl;
    input [15:0] head;
    input [15:0] sector;
    begin
      chs2lba = (({16'd0, cyl} * {16'd0, GEO_HEADS}) + {16'd0, head})
                * {16'd0, GEO_SPT} + {16'd0, sector};
    end
  endfunction

  // GO-time address decode (registers are stable outside a control write).
  wire [31:0] w_lba     = chs2lba({5'd0, w_cyl}, {12'd0, s_head}, {11'd0, w_sector});
  wire [31:0] w_max_lba = chs2lba(GEO_MAX_CYL, GEO_HEADS, GEO_SPT);
  wire [23:0] w_words   = {8'd0, s_word_cnt};

  task dma_issue(input wr, input [23:0] a, input [15:0] d);
    begin
      dma_req    <= 1'b1;
      dma_wr     <= wr;
      dma_addr   <= a;
      dma_wdata  <= d;
      s_dma_wait <= 1'b1;
    end
  endtask

  // The four flip-flop reset conditions all call this (sec 3.2).
  task clr_ff;
    begin
      s_ma_write_ff <= 1'b0;
      s_ma_read_ff  <= 1'b0;
    end
  endtask

  // Terminate an operation in error. The specific status bit is set by the
  // caller first. The operation still FINISHES (b3 goes to 1) because the
  // manual's b3 is "finished with a device operation", not "finished
  // successfully" - and because leaving b3 low would wedge a guest waiting on
  // an interrupt that the sec 4.1 condition can then never produce.
  task err_active;
    begin
      s_active <= 1'b0;
      s_rft    <= 1'b1;
      trace_done <= 1'b1;
      clr_ff;
      // A FinishOperation, so the registers are left in their end-of-transfer
      // state exactly as on the clean path (wd_sync_registers in both C
      // models): the address advanced by the words that DID move, count zero.
      s_mem_addr_lo <= s_dma_addr_r[15:0];
      s_mem_addr_hi <= s_dma_addr_r[23:16];
      s_word_cnt    <= 16'd0;
      s_on_cyl[s_unit] <= 1'b0;
      s_eng      <= E_IDLE;
      s_dma_wait <= 1'b0;
      if (s_errint_en) s_irq <= 1'b1;
      else             s_irq <= s_int_en;   // sec 4.1: ready && enabled
    end
  endtask

  // ==== BACKEND FAILURE -> STATUS BIT =====================================
  //
  // WHERE THESE BITS COME FROM: every bit set below is one the WINCHESTER
  // CONTROLLER MANUAL ND-11.015.01, SECTION 3.5 (status register, IOX +4)
  // already defines for this card. They are listed verbatim in this file's
  // header block under "Status (+4, sec 3.5)" and assembled into s_status
  // further down. NOTHING here is invented: this task chooses AMONG the
  // manual's bits, it never adds one, never repurposes one, and never puts
  // the reason code itself anywhere the guest can read it. That is the rule
  // in Verilog/SD-FAT/circuit/nd_storage_status.vh - the storage stack is
  // clean-room and may carry a reason code, a reproduced ND card may not.
  //
  // WHERE THE REASON CODE COMES FROM: nd_storage_engine.v tags the failure
  // at the point it happens (card timeout, CMD17/CMD24 failure, no mount,
  // block past end of file, broken FAT chain, engine watchdog), the code
  // travels engine -> nd_storage -> nd_storage_disc_adapter (the Winchester
  // reuses it at ST506 geometry) -> disk_err_code here.
  //
  // FULL MAPPING - all nine codes of nd_storage_status.vh:
  //
  //   NDS_ERR_NOCARD    b7  disk fault / missing clocks. No SD card in the
  //                         slot: from the guest's side the drive is simply
  //                         not usable, which is exactly what b7 means.
  //   NDS_ERR_NOTOPEN   b7  same bit: WD0.IMG is not on the card (or its
  //                         mount failed), so again there is no usable
  //                         drive behind the interface.
  //   NDS_ERR_RANGE     b8  address mismatch. The manual's b8 is "the
  //                         address searched for was not found on the
  //                         drive", which is precisely a block past the end
  //                         of the image.
  //   NDS_ERR_TIMEOUT   b6  timeout. The engine watchdog fired: the backend
  //                         never answered - the same event b6 exists for.
  //   NDS_ERR_CARDIO    b9  CRC error. The card answered but the data is
  //                         not trustworthy (CMD17/CMD24 error, CRC, card
  //                         stopped mid-block). b9 is this card's
  //                         data-unreadable bit.
  //   NDS_ERR_FATCHAIN  b9  same bit: the FAT chain is broken or circular,
  //                         so the medium cannot yield the requested data.
  //   NDS_ERR_WRPROT    b9  this card has NO write-protect bit in sec 3.5.
  //   NDS_ERR_WRALIGN   b9  A refused write IS a failure to write the
  //                         medium, so it lands on the general media-fault
  //                         bit. The exact reason stays readable on the
  //                         storage seam (WDISK_ERR_CODE) for a testbench
  //                         or a probe - it is only the GUEST that cannot
  //                         tell these two apart, and that is the manual's
  //                         limitation, not a shortcut taken here.
  //   NDS_ERR_NONE          never reaches this task (no error, no call).
  //
  // b4 (inclusive OR of b5-b13) follows automatically from s_incl_or, so
  // any bit chosen here also raises the guest's "something went wrong" bit
  // exactly as the manual specifies.
  //
  // WHY THIS EXISTS: before it, both call sites hard-coded ONE bit, so
  // "no SD card", "WD0.IMG missing", "block past the end of the image",
  // "broken FAT chain" and "the card stopped answering" were a single
  // indistinguishable CRC error at the guest, at the console and in a
  // waveform. That is what made the 08-AUG-2026 zero-read investigation as
  // long as it was.
  // ========================================================================
  task set_backend_fault;
    input [3:0] code;
    begin
      case (code)
        // b7 disk fault / missing clocks (sec 3.5)
        `NDS_ERR_NOCARD,
        `NDS_ERR_NOTOPEN:  s_disk_fault    <= 1'b1;
        // b8 address mismatch (sec 3.5)
        `NDS_ERR_RANGE:    s_addr_mismatch <= 1'b1;
        // b6 timeout (sec 3.5)
        `NDS_ERR_TIMEOUT:  s_time_out      <= 1'b1;
        // b9 CRC error (sec 3.5) - CARDIO, FATCHAIN, WRPROT, WRALIGN
        default:           s_crc_err       <= 1'b1;
      endcase
    end
  endtask

  // ---- buffer RAM ports (BSRAM-mappable: sync write + sync read) ----------
  wire        s_memrd_commit = (s_eng == E_MEM_RD) && s_dma_wait &&
                               dma_ack && !dma_err;
  wire        s_buf_we    = dbuf_we | s_memrd_commit;
  wire [ 9:0] s_buf_waddr = dbuf_we ? dbuf_addr : s_sec_idx[9:0];
  wire [15:0] s_buf_wdata = dbuf_we ? dbuf_wdata : dma_rdata;
  wire [ 9:0] s_buf_raddr = (s_eng == E_MEM_WR)  ? s_sec_idx[9:0] :
                            (s_eng == E_MEM_CMP) ? s_sec_idx[9:0] :
                            (s_eng == E_DISK_WR) ? dbuf_addr      :
                                                   10'd0;
  reg  [15:0] s_buf_dout;
  reg  [ 9:0] s_buf_raddr_q;
  wire        s_buf_valid = (s_buf_raddr_q == s_buf_raddr);

  always @(posedge sysclk) begin
    if (s_buf_we) s_buffer[s_buf_waddr] <= s_buf_wdata;
    s_buf_dout    <= s_buffer[s_buf_raddr];
    s_buf_raddr_q <= s_buf_raddr;
  end

  always @(*) dbuf_rdata = s_buf_dout;

  always @(posedge sysclk or negedge sys_rst_n) begin
    if (!sys_rst_n) begin
      s_mem_addr_lo  <= 16'd0;
      s_mem_addr_hi  <= 8'd0;
      s_word_cnt     <= 16'd0;
      s_blkaddr      <= 16'd0;
      s_sel_unit     <= 1'b0;
      s_head         <= 4'd0;
      s_seek_dir     <= 1'b0;
      s_bad_track    <= 1'b0;
      s_on_cyl       <= 2'd0;
      s_cyl_pos[0]   <= 16'd0;
      s_cyl_pos[1]   <= 16'd0;
      s_int_en       <= 1'b0;
      s_errint_en    <= 1'b0;
      s_active       <= 1'b0;
      s_rft          <= 1'b0;
      s_rtz_viol     <= 1'b0;
      s_time_out     <= 1'b0;
      s_disk_fault   <= 1'b0;
      s_addr_mismatch<= 1'b0;
      s_crc_err      <= 1'b0;
      s_compare_err  <= 1'b0;
      s_dma_ch_err   <= 1'b0;
      s_rw_gate      <= 1'b0;
      s_test_mode    <= 1'b0;
`ifdef ND120_WD_TRACE
      s_trace_cyc    <= 32'd0;
`endif
      trace_done     <= 1'b0;
      s_ma_write_ff  <= 1'b0;   // master clear is reset condition #1 (sec 3.2)
      s_ma_read_ff   <= 1'b0;
      s_irq          <= 1'b0;
      s_eng          <= E_IDLE;
      s_is_cmp       <= 1'b0;
      s_chunk_q      <= 11'd0;
      s_sec_idx      <= 11'd0;
      s_delay_cnt    <= 32'd0;
      s_dma_wait     <= 1'b0;
      s_dma_addr_r   <= 24'd0;
      s_words_left   <= 24'd0;
      s_unit         <= 1'b0;
      dma_req        <= 1'b0;
      dma_wr         <= 1'b0;
      dma_addr       <= 24'd0;
      dma_wdata      <= 16'd0;
      disk_start     <= 1'b0;
      disk_req       <= 1'b0;
      disk_wr        <= 1'b0;
    end else begin
      dma_req    <= 1'b0;
      disk_start <= 1'b0;
      disk_req   <= 1'b0;
      trace_done <= 1'b0;

      // ---- read-strobe side effects (END of access: see the note above) ----
      if (s_rd_end) begin
        case (s_reg_q)
          // Successive +0 reads alternate LO then HI (sec 3.2).
          3'd0: s_ma_read_ff <= ~s_ma_read_ff;
          // A status read is flip-flop reset condition #3 (sec 3.2).
          3'd4: clr_ff;
          default: ;
        endcase
      end

      // ---- IOX register writes (EDGE: see the note at s_wr_edge) ----
      if (s_wr_edge) begin
        case (s_reg)
          // +1  Load memory address: TWO accesses, HI 8 then LO 16 (sec 3.2).
          3'd1: begin
            if (s_ma_write_ff) begin
              s_mem_addr_lo <= iox_wdata;          // second write: LO 16
              s_ma_write_ff <= 1'b0;
            end else begin
              s_mem_addr_hi <= iox_wdata[7:0];     // first write: HI 8
              s_ma_write_ff <= 1'b1;
            end
          end

          // +3  Load block address: cylinder b15-5, sector b4-0 (sec 3.3).
          3'd3: s_blkaddr <= iox_wdata;

          // +5  Load control word (GO / opcode), sec 3.4.
          3'd5: begin
            s_int_en    <= iox_wdata[0];
            s_errint_en <= iox_wdata[1];
            s_test_mode <= iox_wdata[3];
            s_head      <= iox_wdata[8:5];
            s_sel_unit  <= iox_wdata[9];
            s_seek_dir  <= iox_wdata[14];
            s_bad_track <= iox_wdata[15];

            // Device clear (b4). Sec 3.4: "To clear the disk drive, it may be
            // necessary to execute two consecutive device clear before reading
            // a correct status."
            //
            // Processed INLINE - it must NOT skip the ready/interrupt update
            // below, because a single control word may carry device clear AND
            // the interrupt enable together, which is how a probe usually
            // opens. Both C models take the same shape, following the
            // paper-tape reader's "processed inline, does NOT break".
            if (iox_wdata[4]) begin
              s_active       <= 1'b0;
              s_mem_addr_lo  <= 16'd0;
              s_mem_addr_hi  <= 8'd0;
              s_word_cnt     <= 16'd0;
              s_blkaddr      <= 16'd0;
              s_rtz_viol     <= 1'b0;
              s_time_out     <= 1'b0;
              s_disk_fault   <= 1'b0;
              s_addr_mismatch<= 1'b0;
              s_crc_err      <= 1'b0;
              s_compare_err  <= 1'b0;
              s_dma_ch_err   <= 1'b0;
              clr_ff;
              s_eng          <= E_IDLE;
              s_dma_wait     <= 1'b0;
              s_irq          <= 1'b0;
            end

            // Activation (b2). Sec 3.4: every operation code is activated by
            // loading the code with the activate bit, EXCEPT M6.
            //
            // M6 only sets additional control bits (3038 only) - it is a
            // control-bit load, not an operation - so a control word carrying
            // it takes the NON-activating path below and leaves the card
            // ready. The manual never says what the hardware does if you
            // activate M6 anyway ("no activation should be made" is guidance
            // to the programmer); both C models make the same choice, and the
            // cross-check trace keeps all three from drifting apart.
            if (iox_wdata[2] && iox_wdata[13:11] != OP_LOAD_CTRL) begin
              // Activation is flip-flop reset condition #4 (sec 3.2).
              clr_ff;
              s_active <= 1'b1;
              s_rft    <= 1'b0;
              s_unit   <= iox_wdata[9];
              s_on_cyl[iox_wdata[9]] <= 1'b1;

              s_rtz_viol      <= 1'b0;
              s_time_out      <= 1'b0;
              s_disk_fault    <= 1'b0;
              s_addr_mismatch <= 1'b0;
              s_crc_err       <= 1'b0;
              s_compare_err   <= 1'b0;
              s_dma_ch_err    <= 1'b0;

              // Sec 4.1 is a condition on the CURRENT status: activation drops
              // b3, so the line drops with it and returns at completion.
              s_irq <= 1'b0;

              s_dma_addr_r <= {s_mem_addr_hi, s_mem_addr_lo};
              s_words_left <= w_words;

              case (iox_wdata[13:11])
                OP_SEEK: begin
                  // M4 is a RELATIVE STEP seek (sec 3.4.5): step count in the
                  // word-count register, direction in b14 where ZERO means
                  // towards cylinder 0. Clamped at both stops - running the
                  // arm past either one is not a status-reported error, the
                  // drive simply stops.
                  if (!iox_wdata[14]) begin
                    s_cyl_pos[iox_wdata[9]] <=
                        (s_cyl_pos[iox_wdata[9]] > s_word_cnt) ?
                        (s_cyl_pos[iox_wdata[9]] - s_word_cnt) : 16'd0;
                  end else begin
                    s_cyl_pos[iox_wdata[9]] <=
                        ((s_cyl_pos[iox_wdata[9]] + s_word_cnt) > GEO_MAX_CYL ||
                         (s_cyl_pos[iox_wdata[9]] + s_word_cnt) <
                          s_cyl_pos[iox_wdata[9]]) ?
                        GEO_MAX_CYL : (s_cyl_pos[iox_wdata[9]] + s_word_cnt);
                  end
                  // A zero-step seek moves nothing: the drive only re-checks
                  // its position and completes. See the OP_RTZ note - the
                  // completion delay is only earned by ARM MOTION.
                  s_delay_cnt <= (s_word_cnt == 16'd0) ? 32'd8 : DELAY_TICKS;
                  s_eng       <= E_DELAY;
                end

                OP_RTZ: begin
                  // EVERY RTZ earns DELAY_TICKS, including one whose arm is
                  // already at cylinder 0.
                  //
                  // 06-AUG-2026 this charged a fixed 8 ticks when the arm was
                  // already home, on the reasoning that a track-0 sensor check
                  // is not a seek and that the full delay desynced DISC-TEMA
                  // into reporting "Memory address Register not as expected".
                  // That was a workaround, and the symptom it worked around
                  // has since been root-caused elsewhere: the real fault was
                  // an IOX write zeroing the A register, fixed as the CDLBD
                  // 74646 pin-node correction in BIF_DPATH_9.v.
                  //
                  // The 8-tick path is itself a bug, and the nd100x oracle
                  // proves it. In oracle/fsi-lifi-nd100x.trace the File System
                  // Investigator writes 034005 and its next status read
                  // returns 060005 - ACTIVE, seek still running. On silicon
                  // the same read returns 060011, already finished, because
                  // 8 ticks at 6.75 MHz is 1.2 us and no guest can poll that
                  // fast. A real drive unloads and reseeks track 0 on every
                  // RTZ; it is never instantaneous. Measured by
                  // sim/nd_winchester_rtz_tb.v, which reproduces the silicon
                  // divergence and fails on the old 8-tick behaviour.
                  s_cyl_pos[iox_wdata[9]] <= 16'd0;
                  s_rtz_viol  <= 1'b0;
                  s_delay_cnt <= DELAY_TICKS;
                  s_eng       <= E_DELAY;
                end

                OP_READ, OP_WRITE, OP_COMPARE: begin
                  // Test mode bypasses the bounds check (sec 3.4).
                  if (!iox_wdata[3] &&
                      ((w_lba > w_max_lba) ||
                       ({11'd0, s_head}  >= GEO_HEADS) ||
                       ({11'd0, w_sector} >= GEO_SPT))) begin
                    s_addr_mismatch <= 1'b1;
                    s_active <= 1'b0;
                    s_rft    <= 1'b1;
                    trace_done <= 1'b1;
                    s_on_cyl[iox_wdata[9]] <= 1'b0;
                    s_eng      <= E_IDLE;
                    s_dma_wait <= 1'b0;
                    // EVERY completion is a FinishOperation, and a
                    // FinishOperation always clears the upper/lower selection
                    // flip-flops and zeroes the word counter - both C models
                    // route this path through wd_finish_operation(), which
                    // calls wd_clear_flipflops() + wd_sync_registers().
                    // Leaving them alone here is what made a LATER IOX 500
                    // read return the wrong half: the transfer and the status
                    // word stay perfectly correct, so the fault surfaces only
                    // as DISC-TEMA's "Memory address Register not as
                    // expected" long after the probe that caused it. The
                    // address registers themselves are NOT advanced - nothing
                    // was transferred, which is also what wd_sync_registers
                    // computes here (mem_addr still equals what was loaded).
                    clr_ff;
                    s_word_cnt <= 16'd0;
                    if (iox_wdata[1]) s_irq <= 1'b1;
                    else              s_irq <= iox_wdata[0];
                  end else if (w_words != 24'd0) begin
                    s_chunk_q <= (w_words > {13'd0, BUF_WORDS}) ?
                                 BUF_WORDS : w_words[10:0];
                    s_sec_idx <= 11'd0;
                    // M3 takes the READ path off the disc - it has to, it is
                    // reading the sector - and then compares in E_MEM_CMP
                    // instead of storing in E_MEM_WR.
                    s_is_cmp <= (iox_wdata[13:11] == OP_COMPARE);
                    if (iox_wdata[13:11] == OP_READ ||
                        iox_wdata[13:11] == OP_COMPARE) begin
                      disk_start <= 1'b1;
                      disk_req   <= 1'b1;
                      disk_wr    <= 1'b0;
                      s_eng      <= E_DISK_RD;
                    end else begin
                      disk_start <= 1'b1;
                      s_eng      <= E_MEM_RD;
                    end
                  end else begin
                    // A zero-length transfer still completes.
                    s_delay_cnt <= DELAY_TICKS;
                    s_eng       <= E_DELAY;
                  end
                end

                default: begin
                  // M2 read-parity / M5 write-format: documented stubs,
                  // acknowledged as no-data completions so the guest proceeds.
                  //
                  // M3 COMPARE IS NO LONGER ONE OF THEM. It was, and the note
                  // here claimed that matched both C models - it does not.
                  // nd100x's deviceWinchester.c reads the sector, DMA-reads
                  // the same words back out of ND memory, compares them, and
                  // then writes the ADVANCED core address into the memory
                  // address register like any other transfer. The stub left
                  // that register holding the address the guest had loaded.
                  //
                  // SINTRAN reads +0 back after every operation, so the stub
                  // is visible to it. Measured 10-AUG-2026: booting WD0.IMG
                  // with '20500&', the Tang and nd100x request the SAME 124
                  // disc addresses and then split at exactly the group where
                  // SINTRAN reads block 0 twice and compares it. nd100x moves
                  // on; the Tang re-issued block 0 eleven times and gave up
                  // with SINTRAN's own 'TRANSFER ERROR' message.
                  s_delay_cnt <= DELAY_TICKS;
                  s_eng       <= E_DELAY;
                end
              endcase

            end else if (!iox_wdata[4]) begin
              // A control word that does NOT activate (and is not a device
              // clear, which handled its own interrupt above): the controller
              // stays idle and is by definition ready for an operation, so
              // status b3 goes to 1 and the sec 4.1 condition is re-evaluated.
              // THIS is what makes TPE CONFIGURATION find the card.
              //
              // ONLY WHEN IT REALLY IS IDLE. "Stays idle" is a PREMISE, not a
              // consequence, and it is one this RTL can violate where the C
              // models cannot: their Wd_ExecuteGO is synchronous, so by the
              // time any later control word arrives the operation has already
              // finished and active is 0. Here an operation takes real time,
              // so a non-activating word can land mid-transfer - and setting
              // b3 then reports ACTIVE and FINISHED at the same instant,
              // which no real card can do.
              //
              // Measured on silicon 05-AUG-2026 with ND120_WD_TRACE_DUMP:
              // DISC-TEMA "DU-DI-C" issues GO (+5 = 034005), polls +4 and
              // gets 060005 (active), writes a non-activating +5 = 0, and the
              // next +4 read returned 060014 - b2 AND b3 both set. The oracle
              // never emits that word; its whole status set for this run is
              // 020010 / 020011 / 060011 / 060010 / 060005.
              if (!s_active) s_rft <= 1'b1;
              s_irq <= iox_wdata[0];
            end else begin
              // Device clear AND the interrupt enable in one word: the clear
              // ran above; still evaluate sec 4.1 with ready now true.
              s_rft <= 1'b1;
              s_irq <= iox_wdata[0];
            end
          end

          // +7  Load word count / step count - a SINGLE access.
          // THE register that differs from the 15 MHz SMD card, and the reason
          // the ND-120 mass-load microcode works here: it writes the word count
          // once, with 002000 (1024 words). For M4 the same register carries
          // the STEP COUNT (sec 3.4.5).
          3'd7: s_word_cnt <= iox_wdata;

          default: ;
        endcase
      end

      // ---- transfer engine ----
      case (s_eng)
        E_IDLE: ;

        E_DISK_RD: begin
          if (disk_done) begin
            if (disk_err_in) begin
              set_backend_fault(disk_err_code);
              err_active;
            end else begin
              s_sec_idx <= 11'd0;
              s_eng     <= s_is_cmp ? E_MEM_CMP : E_MEM_WR;
            end
          end
        end

        E_MEM_WR: begin
          // s_buf_valid: wait the one read-latency cycle after entering the
          // state / advancing s_sec_idx so s_buf_dout holds THIS word.
          if (!s_dma_wait && !dma_busy && s_buf_valid) begin
            dma_issue(1'b1, s_dma_addr_r, s_buf_dout);
          end else if (s_dma_wait && dma_ack) begin
            s_dma_wait <= 1'b0;
            if (dma_err) begin
              s_dma_ch_err <= 1'b1;   // b11: the fault came from the ND bus
              err_active;
            end else begin
              s_dma_addr_r <= s_dma_addr_r + 24'd1;
              s_words_left <= s_words_left - 24'd1;
              if (s_sec_idx + 11'd1 >= s_chunk_q || s_words_left == 24'd1) begin
                if (s_words_left == 24'd1) begin
                  s_delay_cnt <= DELAY_TICKS;
                  s_eng       <= E_DELAY;
                end else begin
                  s_chunk_q <= ((s_words_left - 24'd1) > {13'd0, BUF_WORDS}) ?
                               BUF_WORDS : (s_words_left[10:0] - 11'd1);
                  disk_req  <= 1'b1;
                  disk_wr   <= 1'b0;
                  s_eng     <= E_DISK_RD;
                end
              end else begin
                s_sec_idx <= s_sec_idx + 11'd1;
              end
            end
          end
        end

        // M3 compare (sec 3.4.7): "No data transfer to the computer memory is
        // performed" - the words go the OTHER way. The sector is already in
        // the buffer from E_DISK_RD; each word is DMA-READ back out of ND
        // memory and compared, and the running address advances exactly as it
        // does on a read. A mismatch sets status b10 and stops there, leaving
        // the address at the word that failed - which is what the C model's
        // break-out-of-the-loop produces.
        //
        // s_buf_valid is the same one-cycle read-latency guard E_MEM_WR uses:
        // without it the first comparison would run against the PREVIOUS
        // buffer word and report a mismatch on identical data.
        E_MEM_CMP: begin
          if (!s_dma_wait && !dma_busy && s_buf_valid) begin
            dma_issue(1'b0, s_dma_addr_r, 16'd0);
          end else if (s_dma_wait && dma_ack) begin
            s_dma_wait <= 1'b0;
            if (dma_err) begin
              s_dma_ch_err <= 1'b1;
              err_active;
            end else if (dma_rdata != s_buf_dout) begin
              s_compare_err <= 1'b1;
              err_active;
            end else begin
              s_dma_addr_r <= s_dma_addr_r + 24'd1;
              s_words_left <= s_words_left - 24'd1;
              if (s_sec_idx + 11'd1 >= s_chunk_q || s_words_left == 24'd1) begin
                if (s_words_left == 24'd1) begin
                  s_delay_cnt <= DELAY_TICKS;
                  s_eng       <= E_DELAY;
                end else begin
                  s_chunk_q <= ((s_words_left - 24'd1) > {13'd0, BUF_WORDS}) ?
                               BUF_WORDS : (s_words_left[10:0] - 11'd1);
                  disk_req  <= 1'b1;
                  disk_wr   <= 1'b0;
                  s_eng     <= E_DISK_RD;
                end
              end else begin
                s_sec_idx <= s_sec_idx + 11'd1;
              end
            end
          end
        end

        E_MEM_RD: begin
          if (!s_dma_wait && !dma_busy) begin
            dma_issue(1'b0, s_dma_addr_r, 16'd0);
          end else if (s_dma_wait && dma_ack) begin
            s_dma_wait <= 1'b0;
            if (dma_err) begin
              s_dma_ch_err <= 1'b1;
              err_active;
            end else begin
              // The buffer write happens through the muxed RAM write port
              // (s_memrd_commit mirrors this exact condition).
              s_dma_addr_r <= s_dma_addr_r + 24'd1;
              s_words_left <= s_words_left - 24'd1;
              if (s_sec_idx + 11'd1 >= s_chunk_q || s_words_left == 24'd1) begin
                disk_req <= 1'b1;
                disk_wr  <= 1'b1;
                s_eng    <= E_DISK_WR;
              end else begin
                s_sec_idx <= s_sec_idx + 11'd1;
              end
            end
          end
        end

        E_DISK_WR: begin
          if (disk_done) begin
            if (disk_err_in) begin
              // Same mapping as the read path, so a write failure is as
              // diagnosable as a read failure. The adapter refuses a
              // partial-block write by design, so an unaligned Winchester
              // write arrives here as a clean, visible error rather than
              // silent corruption - and now says WHICH refusal it was.
              set_backend_fault(disk_err_code);
              err_active;
            end else if (s_words_left == 24'd0) begin
              s_delay_cnt <= DELAY_TICKS;
              s_eng       <= E_DELAY;
            end else begin
              s_chunk_q <= (s_words_left > {13'd0, BUF_WORDS}) ?
                           BUF_WORDS : s_words_left[10:0];
              s_sec_idx <= 11'd0;
              s_eng     <= E_MEM_RD;
            end
          end
        end

        // Completion delay, then FinishOperation.
        E_DELAY: begin
          if (s_delay_cnt != 32'd0) begin
            s_delay_cnt <= s_delay_cnt - 32'd1;
          end else begin
            s_active <= 1'b0;
            s_rft    <= 1'b1;
            trace_done <= 1'b1;
            clr_ff;
            s_mem_addr_lo <= s_dma_addr_r[15:0];
            s_mem_addr_hi <= s_dma_addr_r[23:16];
            s_word_cnt    <= 16'd0;
            s_on_cyl[s_unit] <= 1'b1;
            s_eng         <= E_IDLE;
            // Sec 4.1 with ready now true: the line follows the enable.
            s_irq         <= s_int_en;
          end
        end

        default: s_eng <= E_IDLE;
      endcase

`ifdef ND120_WD_TRACE
      // Simulation-only IOX trace, the same facility ND_SMD.v carries under
      // ND120_SMD_TRACE. That trace is what made the SMD tractable: it shows
      // what the real diagnostic actually emits, register by register, with
      // the controller state that decides what each access MEANS. Reading the
      // RTL against the C model settles nothing when the two agree and the
      // silicon still disagrees - only the sequence does.
      // s_trace_cyc counts sysclk edges; $time is useless (no timescale).
      s_trace_cyc <= s_trace_cyc + 32'd1;
      if (s_wr_edge)
        $display("[WD] cyc=%0d WR +%0d val=%o maw_ff=%b active=%b rft=%b",
                 s_trace_cyc, s_reg, iox_wdata, s_ma_write_ff, s_active, s_rft);
      if (s_rd_here && !s_iox_rd_q)
        $display("[WD] cyc=%0d RD +%0d -> %o mar_ff=%b active=%b rft=%b",
                 s_trace_cyc, s_reg, iox_rdata, s_ma_read_ff, s_active, s_rft);
      // Every IOX the CPU issues to a DISC-range address that is NOT ours -
      // answers "which device is the test program actually driving".
      if ((iox_rd || iox_wr) && !s_addressed &&
          iox_addr >= 16'o000400 && iox_addr < 16'o002000)
        $display("[WD-OTHER] cyc=%0d %s dev=%06o data=%06o",
                 s_trace_cyc, iox_wr ? "WR" : "RD", iox_addr, iox_wdata);
`endif

      // IDENT answered: clear interrupt-enable and drop the line.
      if (s_ident_answer) begin
        s_int_en <= 1'b0;
        s_irq    <= 1'b0;
      end
    end
  end

endmodule