ND_FLOPPY_DMA¶
Source: Verilog/ND-BUS-DEVICES/FLOPPY-DMA/circuit/ND_FLOPPY_DMA.v
Where it sits (Simulation): ND120_TOP > ND120_CORE > ND_FLOPPY_DMA
- instance path: CORE.gen_floppy.FLOPPY_1560
Used in: ND120_CORE (Simulation, Tang, Nexys, MiSTer, MEGA65 R6, MEGA65 R3, QMTECH)
Contains: no other modules.
Module hierarchy - All modules

Schematic¶
Drawn from the Verilog: the yosys netlist of the Simulation (Verilator) build, instance CORE.gen_floppy.FLOPPY_1560. 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).
Parameters¶
| Parameter | Default |
|---|---|
BASE_ADDR |
16'o001560 |
IDENT_CODE |
16'o000021 |
INT_LEVEL |
4'd11 |
DELAY_TICKS |
16'd300 |
Verilog source¶
Verilog/ND-BUS-DEVICES/FLOPPY-DMA/circuit/ND_FLOPPY_DMA.v on GitHub.
Show the Verilog of ND_FLOPPY_DMA (845 lines)
`include "nd_storage_status.vh"
/**************************************************************************
** ND-100 FLOPPY DISK CONTROLLER, DMA INTERFACE (3112 / "new controller")**
** **
** Register core, register semantics per the ND-11.021.01 controller **
** manual (3106/3112). Full verified layout, both status words and the **
** octal error table: docs/floppy-3112-register-spec-ND-11.021.md. **
** **
** IMPORTANT - TWO distinct status words (the manual is explicit; the **
** nd100x C model and earlier revisions of this file conflated them): **
** HARDWARE STATUS WORD (§3.7) - what IOX +2 AND +4 return (§3.1 Note **
** 1: reading either gives the same result). b1 RFT/intEnabled, **
** b2 deviceActive, b3 readyForTransfer, b4 OR-of-errors, b6 streamer**
** active, b7 hardError, b14 streamer interface, b15 dualDensity = 1 **
** ALWAYS (SINTRAN detects the 3112 by this bit). NO error code. **
** STATUS WORD 1 (§3.4) - written back to command block CB+6 only. **
** b4 OR-of-errors, b5 deleted, b6 retry, b7 hardError, b8 unused, **
** b9-14 error code, b15 unused. The numeric error code lives ONLY **
** here, never in the IOX register. **
** **
** IOX 1560+0 R read data (no documented idle constant; returns 1) **
** +2 R read hardware status word (§3.7) **
** +3 W control word: b1 enableInterrupt, b2 activateAutoload, **
** b3 testMode, b4 deviceClear, b5 enableStreamer, **
** b8 executeCommand **
** +4 R read hardware status word (same as +2, §3.1 Note 1) **
** +5 W load pointer HIGH (command block address bits 16-23) **
** +7 W load pointer LOW (command block address bits 0-15) **
** STATUS WORD 2 (§3.5.2.2, the format word: b1 bytes/sector, b2 double **
** sided, b3 double density, b9 selected unit) is delivered in memory **
** at CB+7, NOT at an IOX register. **
** **
** Command block, 12 words in ND memory at the pointer (DMA-fetched): **
** w0 command word: b0-5 function, b6-7 drive, b8-9 format, **
** b10 doubleSided, b11 doubleDensity **
** w1 disk address (logical sector), w2 memAddr high (b23-16), **
** w3 memAddr low, w4 options (b15 = 1 word-count / 0 sector-count, **
** b7-0 = count HIGH byte -> 24-bit count), **
** w5 word/sector count, w6 status1 (written back), w7 status2 **
** (written back), w8/w9 last memory address (written back), **
** w10/w11 remaining words (written back) **
** **
** Functions implemented (enough for boot + basic driver use): **
** 0x00 READ DATA: disk -> ND memory by DMA **
** 0x01 WRITE DATA: ND memory -> disk by DMA **
** 0x22 READ FORMAT: media format (disk_media_fmt) into status 2 **
** 0x38 IDENTIFY / others: no-op completion (status writeback only), **
** mirroring the C model's TODO stubs **
** **
** Completion mirrors the C model's ReadEnd: after the E_WBACK status **
** writeback (still busy/not-ready, like the C model's first write) and **
** the completion delay, a FINAL DMA write re-writes CB+6 with the **
** completed status (READY = 1, BUSY = 0) - a driver waiting on FSTA1 **
** in memory sees the controller go ready. **
** BOOT MODE (control b2, used by '1560&'): the microcode mass boot **
** is the BPUN loader pointed at the device - per byte it writes **
** the control word (bit 2 = activate), polls status +2 for the **
** ready bit, and reads the next boot-stream WORD from +0 (the **
** diskette stores one stream byte per 16-bit word, low byte). **
** The controller serves the boot stream from its buffer, **
** refilling a 512-word chunk from the image whenever the **
** pointer crosses a chunk boundary. Reading +0 clears ready; **
** the next activate re-arms it. Device clear or a real **
** executeCommand leaves boot mode. Verified empirically: the **
** 'DMA the block to memory 0' interpretation was WRONG (the **
** CPU never jumped; the microcode wants to read the stream). **
** **
** All memory traffic goes through the ND_DMA_MASTER client port **
** (dma_*): the command-block fetch, the sector data both directions, **
** and the status writeback - one word per bus allocation, exactly like **
** the real controller. **
** **
** Sector geometry per format (deviceFloppyDMA.c): 0 = 512, 1 = 256, **
** 2 = 128, 3 = 1024 bytes per sector; 8 sectors per track model. **
** Disk backend: same style as the PIO core - the backend moves one **
** sector between the image and the internal buffer. A WRITE commits **
** only the words loaded for the final (partial) sector, never a full **
** sector of stale buffer. **
** **
** Robustness: DISK_TIMEOUT (default 0 = off) arms a watchdog on the **
** backend so selecting an absent drive (adapter stays silent) completes **
** with DRIVE_NOT_READY instead of wedging. Error codes follow the **
** deviceFloppyDMA.h octal table (oct 20 not-ready, 41 CB-fetch bus **
** error, 43 data bus error), not invented values. **
** **
** Ident code 021 (octal), interrupt level 11; interrupt on completion **
** when enableInterrupt is set; IDENT clears pending + enable. **
** **
** Last reviewed: 13-JUL-2026 **
** Ronny Hansen **
***************************************************************************/
module ND_FLOPPY_DMA #(
parameter [15:0] BASE_ADDR = 16'o001560,
parameter [15:0] IDENT_CODE = 16'o000021,
parameter [3:0] INT_LEVEL = 4'd11,
parameter [15:0] DELAY_TICKS = 16'd300, // nd100x IODELAY_FLOPPY
// C2: watchdog on the disk backend. The adapter answers ONLY its own
// drive (silence for any other unit), so selecting an absent drive
// would wait on disk_done forever. When non-zero, a backend that does
// not raise disk_done within this many sysclk ticks aborts the command
// with DRIVE_NOT_READY (oct 20) instead of wedging. 0 = disabled (the
// pre-fix behaviour); set it above the real backend's worst-case
// per-sector latency (SD read/write) before enabling on hardware.
parameter [15:0] DISK_TIMEOUT = 16'd0
) (
input wire sysclk,
input wire sys_rst_n,
// Device bus (from ND_BUS_SLAVE) - IOX slave side
input wire [15:0] iox_addr,
input wire iox_wr,
input wire [15:0] iox_wdata,
input wire iox_rd,
output reg [15:0] iox_rdata,
output wire iox_sel, // 1 = this core owns the captured IOX address
output wire [3:0] int_pending,
input wire ident_strobe,
input wire [3:0] 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) - bus master side
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 image backend (one sector per request, same style as the PIO)
output reg disk_req,
output reg disk_wr, // 0 = image -> buffer, 1 = buffer -> image
output wire [15:0] disk_lsect, // logical sector (disk address word)
output wire [1:0] disk_format,
output wire [1:0] disk_drive,
output wire [10:0] disk_wordcount, // words per sector
input wire disk_done,
input wire disk_err_in,
// WHY the backend failed (nd_storage_status.vh), valid with disk_done
// when disk_err_in. Mapped below onto codes from THIS controller's own
// error enum (deviceFloppyDMA.h / nd_floppy_dma.h) - never a new one.
input wire [3:0] disk_err_code,
// Media format of the mounted image, derived from the image size by
// the backend exactly like deviceFloppyDMA.c ExecuteFloppyGo/READ
// FORMAT: {doubleDensity, doubleSided, bytesPerSector[1:0]}.
// Size 315392 (8-inch) -> 4'b0000; size >= 1261568 (5.25" 1.2MB)
// -> 4'b1111. Tie to 4'b1111 if unknown.
input wire [3:0] disk_media_fmt,
input wire [9:0] dbuf_addr,
input wire [15:0] dbuf_wdata,
input wire dbuf_we,
output reg [15:0] dbuf_rdata
);
// ---- registers ----
reg s_int_enabled; // RSR1 b1
reg s_active; // status b2
reg s_rft; // status b3
reg s_hard_err; // status b7
reg [5:0] s_err_code; // Status Word 1 b9-14 (ND-11.021 §3.4/§3.9)
// ==== BACKEND FAILURE -> ERROR CODE =====================================
//
// WHERE THESE CODES COME FROM: this controller does not report failures
// as individual status BITS the way the SMD and Winchester cards do. It
// reports a 6-bit NUMBER in STATUS WORD 1 bits 9-14 (ND-11.021.01 sec
// 3.4/3.9, tabulated in docs/floppy-3112-register-spec-ND-11.021.md),
// written back to the command block at CB+6 - never to an IOX register.
// The legal values are exactly the enum in
// Verilog/ND-BUS-DEVICES/portable/include/nd_floppy_dma.h, which is the
// same table nd100x's deviceFloppyDMA.h uses:
//
// 0 FLOPPY_ERR_OK transfer ok
// 5 FLOPPY_ERR_CRC CRC error - the media fault code
// oct 16 FLOPPY_ERR_WRITE_PROTECTED write-protected diskette
// oct 20 FLOPPY_ERR_DRIVE_NOT_READY drive not attached / not ready
// oct 41 ND-100 BUS ERROR COMMAND FETCH (set at the CB fetch)
// oct 43 ND-100 BUS ERROR DATA TRANSFER (set in the DMA path)
// oct 50 FLOPPY_ERR_NO_BOOTSTRAP no bootstrap on the diskette
// oct 71 FLOPPY_ERR_RAM_ERROR controller RAM error
//
// NOTHING outside that table may be produced here. This function chooses
// AMONG those values, it never adds one, and the storage stack's reason
// code itself never reaches the guest. That rule is stated in
// Verilog/SD-FAT/circuit/nd_storage_status.vh: the SD-FAT 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
// where it happens, and it travels engine -> nd_storage ->
// nd_storage_floppy_adapter -> disk_err_code here.
//
// FULL MAPPING - all nine codes of nd_storage_status.vh:
//
// NDS_ERR_NOCARD oct 20 no SD card in the slot. From the guest's
// side there is no diskette in the drive,
// which is what DRIVE_NOT_READY reports.
// NDS_ERR_NOTOPEN oct 20 FLOPPY1.IMG/FLOPPY2.IMG is not on the card
// (or its mount failed): again, no diskette.
// NDS_ERR_TIMEOUT oct 20 the backend never answered. This card has
// no timeout code of its own, and the
// existing DISK_TIMEOUT watchdog in this file
// already reports oct 20 - kept identical.
// NDS_ERR_RANGE oct 20 a sector past the end of the image. There
// is no seek-error or address-mismatch code
// in this card's table, so it falls back to
// the general not-ready value rather than
// borrow a code that means something else.
// NDS_ERR_WRPROT oct 16 the write path is not built in.
// NDS_ERR_WRALIGN oct 16 a partial/unaligned write was refused. Both
// are "this diskette cannot be written",
// which is exactly WRITE_PROTECTED, and it
// correctly tells the driver the MEDIUM is
// fine - retrying the read will work.
// NDS_ERR_CARDIO 5 the card answered but the data cannot be
// NDS_ERR_FATCHAIN 5 trusted (CMD17/CMD24 failure, CRC, card
// stopped mid-block, broken/circular FAT
// chain). nd_floppy_dma.h documents CRC
// verbatim as "used for a media fault".
// NDS_ERR_NONE never reaches this function (no error).
//
// The BOOT autoload path is NOT routed through here: a backend failure
// during '1560&' must stay oct 50 NO_BOOTSTRAP + hard error + leave boot
// mode, matching both authorities (nd_floppy_dma.c boot_fail() and
// nd100x deviceFloppyDMA.c ExecuteAutoload). See E_DISK_RD below.
//
// WHY THIS EXISTS: before it, every backend failure reported the single
// value oct 20, so a diagnostic could not tell a missing diskette from a
// broken one, nor a refused write from a dead drive.
// ========================================================================
function [5:0] flp_err_code;
input [3:0] code;
begin
case (code)
// oct 20 DRIVE_NOT_READY - no medium, or no answer at all
`NDS_ERR_NOCARD,
`NDS_ERR_NOTOPEN,
`NDS_ERR_TIMEOUT,
`NDS_ERR_RANGE: flp_err_code = 6'o20;
// oct 16 WRITE_PROTECTED - the transfer was refused, medium is fine
`NDS_ERR_WRPROT,
`NDS_ERR_WRALIGN: flp_err_code = 6'o16;
// 5 CRC - this card's media-fault code (CARDIO, FATCHAIN)
default: flp_err_code = 6'd5;
endcase
end
endfunction
reg [7:0] s_ptr_hi; // command block address b23-16
reg [15:0] s_ptr_lo; // command block address b15-0
reg s_test_mode;
// ND-11.021 defines TWO distinct status words - see
// docs/floppy-3112-register-spec-ND-11.021.md:
//
// (a) HARDWARE STATUS WORD (§3.7): returned by IOX +2 AND IOX +4 (§3.1
// Note 1 - "reading either status gives the same result"). bit 15 =
// Dual density controller (the always-1 bit SINTRAN uses to detect the
// 3112 DMA controller); bit 4 = OR of errors; bit 7 = hard error.
// It carries NO numeric error code.
wire s_or_err = (s_err_code != 6'd0) | s_hard_err;
wire [15:0] s_hwstat = {1'b1, // b15 dual density controller
1'b0, // b14 streamer interface
6'b0, // b13-8 (b11 reserved)
s_hard_err, // b7 hard error - DMA transfer
1'b0, // b6 streamer active
1'b0, // b5 not used
s_or_err, // b4 OR of errors
s_rft, // b3 device ready for transfer
s_active, // b2 device active
s_int_enabled, // b1 RFT / interrupt enabled
1'b0}; // b0 not used
//
// (b) STATUS WORD 1 (§3.4): written back into the command block at CB+6.
// bit 8 not used, bits 9-14 = error code from controller, bit 15 not
// used. This is the ONLY place the numeric error code appears.
wire [15:0] s_sw1 = {1'b0, // b15 not used
s_err_code, // b14-9 error code from controller
1'b0, // b8 not used
s_hard_err, // b7 hard error
1'b0, // b6 retry on controller
1'b0, // b5 deleted record
s_or_err, // b4 OR of errors
s_rft, // b3 device ready for transfer
s_active, // b2 device active
s_int_enabled, // b1 RFT / interrupt enabled
1'b0}; // b0 not used
// command block fields (fetched by DMA)
reg [15:0] s_cb[0:5]; // w0-w5 (w6-w11 are write-back only)
wire [5:0] s_func = s_cb[0][5:0];
wire [1:0] s_cb_drive = s_cb[0][7:6];
wire [1:0] s_cb_fmt = s_cb[0][9:8];
wire [15:0] s_disk_addr = s_cb[1];
wire [23:0] s_mem_addr0 = {s_cb[2][7:0], s_cb[3]};
wire [15:0] s_count = s_cb[5];
// STATUS 2 (format word): a real latched register, NOT an echo of the
// command-word format bits. Loaded at command decode with the selected
// unit in bits 8-9 (deviceFloppyDMA.c line 371); READ FORMAT (0x22)
// additionally loads the media format bits 0-3 from disk_media_fmt
// (deviceFloppyDMA.c lines 528-540).
reg [15:0] s_status2;
// geometry: bytes per sector by format -> words per sector
wire [10:0] s_words_per_sector = (s_cb_fmt == 2'd0) ? 11'd256 :
(s_cb_fmt == 2'd1) ? 11'd128 :
(s_cb_fmt == 2'd2) ? 11'd64 : 11'd512;
// C1: command block word 4 (OPWCH) - transfer length control.
// b15 = 1 -> word count, 0 -> sector count (deviceFloppyDMA.c:298-300).
// b7-0 = count HIGH byte -> the count is 24-bit (line 301).
// Total words to move: the 24-bit count directly in word-count mode, or
// count * words-per-sector in sector-count mode (line 346-348). SINTRAN's
// BFDIS driver issues sector-count transfers, so this path must exist.
wire s_is_wc = s_cb[4][15];
wire [23:0] s_wc24 = {s_cb[4][7:0], s_cb[5]};
wire [31:0] s_wtr = s_is_wc ? {8'd0, s_wc24}
: ({8'd0, s_wc24} * {21'd0, s_words_per_sector});
// ---- internal sector buffer (single synchronous-read BSRAM block) ----
// Async-read arrays do NOT map to Gowin BSRAM (BSRAM-BUDGET.md Part 2), so
// the buffer is a simple dual-port RAM: one muxed write port, one registered
// read port (address muxed by engine state - see the RAM port block below
// the register declarations). Writes were already synchronous; dbuf_rdata is
// driven from the registered read there.
reg [15:0] s_buffer[0:1023];
// ---- address decode ----
wire s_addressed = (iox_addr[15:3] == BASE_ADDR[15:3]);
assign iox_sel = s_addressed; // slave gates its BDRY response on this
wire [2:0] s_reg = iox_addr[2:0];
wire s_wr_here = iox_wr && s_addressed;
// ---- interrupt / ident (standard pattern) ----
wire s_pending = s_int_enabled && s_rft;
assign int_pending = {(INT_LEVEL == 4'd13) && s_pending,
(INT_LEVEL == 4'd12) && s_pending,
(INT_LEVEL == 4'd11) && s_pending,
(INT_LEVEL == 4'd10) && s_pending};
wire s_ident_answer = ident_strobe && ident_grant_in &&
(ident_level == INT_LEVEL) && s_pending;
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 ----
always @(*) begin
iox_rdata = 16'd0;
if (iox_rd && s_addressed) begin
case (s_reg)
// +0 outside boot mode: the C model returns the constant 0x0001
3'd0: iox_rdata = s_boot_active ? s_buf_dout : 16'd1;
// §3.1 Note 1 + §3.7: +2 and +4 both return the hardware status word
3'd2: iox_rdata = s_hwstat;
3'd4: iox_rdata = s_hwstat;
default: iox_rdata = 16'd0;
endcase
end
end
// ---- command engine ----
localparam E_IDLE = 4'd0;
localparam E_CB_FETCH = 4'd1; // DMA-read command block words 0-5
localparam E_DISK_RD = 4'd2; // backend: image sector -> buffer
localparam E_MEM_WR = 4'd3; // DMA-write buffer -> ND memory
localparam E_MEM_RD = 4'd4; // DMA-read ND memory -> buffer
localparam E_DISK_WR = 4'd5; // backend: buffer -> image sector
localparam E_WBACK = 4'd6; // DMA-write status words 6-11
localparam E_DELAY = 4'd7; // completion delay -> interrupt
localparam E_FINAL = 4'd8; // re-write CB+6 with completed status
// (READY=1, BUSY=0) - C model ReadEnd
reg [3:0] s_eng;
reg [2:0] s_cb_idx; // command-block word index during fetch
reg [31:0] s_words_left; // total transfer words remaining (C1: 24-bit
// count x words/sector can exceed 16 bits)
reg [15:0] s_disk_to; // C2: backend watchdog countdown
reg [10:0] s_sec_idx; // word index within the current sector
reg [15:0] s_lsect; // current logical sector
reg [23:0] s_mem_ptr; // current ND memory address
reg [2:0] s_wb_idx; // writeback word index (0-5 -> w6-w11)
reg [15:0] s_delay_cnt;
reg s_dma_wait; // a dma_req is outstanding
reg s_autoload; // a boot-chunk fetch is in flight
reg s_boot_active; // boot byte-server engaged
reg [9:0] s_bootptr; // word index into the current boot chunk
reg s_final_wb; // command block present: E_DELAY -> E_FINAL
assign disk_lsect = s_lsect;
assign disk_format = s_cb_fmt;
assign disk_drive = s_cb_drive;
// C3: a WRITE commits exactly the words loaded for this sector (s_chunk_q),
// NOT a full sector - otherwise a partial-sector tail leaks stale buffer
// (previous sector or reset garbage) onto the disk. A READ fills the full
// sector into the buffer (harmless; only s_chunk_q words reach memory).
assign disk_wordcount = disk_wr ? s_chunk_q : s_words_per_sector;
// words in the CURRENT sector transfer - latched at sector start
// (computing it from the live word counter would shrink it mid-sector)
reg [10:0] s_chunk_q;
// one-clock req pulse toward the DMA master; response by dma_ack
task automatic 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
// ---- sector-buffer RAM ports (synchronous, BSRAM-inferable) -------------
// WRITE port: backend fill (dbuf_we) or the E_MEM_RD DMA commit, muxed - the
// two are mutually exclusive engine phases. READ port: one registered read
// whose address follows the active consumer - E_MEM_WR walks the sector for
// the DMA-out, E_DISK_WR serves the backend readout (dbuf_addr), otherwise
// the boot-stream pointer. s_buf_valid marks s_buf_dout as current for the
// address requested THIS cycle (s_buf_dout holds s_buffer[s_buf_raddr_q]);
// consumers that need the freshest word gate on it, adding the one cycle of
// read latency. The floppy adapter's F_PULL present/settle/sample walk
// already tolerates the registered readout (nd_storage_floppy_adapter.v).
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_DISK_WR) ? dbuf_addr :
s_bootptr;
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_int_enabled <= 1'b0;
s_active <= 1'b0;
s_rft <= 1'b1;
s_hard_err <= 1'b0;
s_err_code <= 6'd0;
s_ptr_hi <= 8'd0;
s_ptr_lo <= 16'd0;
s_test_mode <= 1'b0;
s_eng <= E_IDLE;
s_cb_idx <= 3'd0;
s_words_left <= 32'd0;
s_disk_to <= 16'd0;
s_sec_idx <= 11'd0;
s_lsect <= 16'd0;
s_mem_ptr <= 24'd0;
s_wb_idx <= 3'd0;
s_delay_cnt <= 16'd0;
s_chunk_q <= 11'd0;
s_dma_wait <= 1'b0;
s_autoload <= 1'b0;
s_boot_active <= 1'b0;
s_bootptr <= 10'd0;
s_final_wb <= 1'b0;
s_status2 <= 16'd0;
dma_req <= 1'b0;
dma_wr <= 1'b0;
dma_addr <= 24'd0;
dma_wdata <= 16'd0;
disk_req <= 1'b0;
disk_wr <= 1'b0;
end else begin
dma_req <= 1'b0;
disk_req <= 1'b0;
// backend fill (dbuf_we) into the sector buffer is handled by the
// synchronous RAM write port above (s_buf_we mux)
// boot stream readout: +0 read consumes the word, clears ready
if (s_boot_active && iox_rd && s_addressed && (s_reg == 3'd0)) begin
s_bootptr <= s_bootptr + 10'd1;
s_rft <= 1'b0;
end
// ---- IOX register writes ----
if (s_wr_here) begin
case (s_reg)
3'd3: begin // control word
s_int_enabled <= iox_wdata[1];
s_test_mode <= iox_wdata[3];
if (iox_wdata[4]) begin // device clear
s_rft <= 1'b1;
s_active <= 1'b0;
s_hard_err <= 1'b0;
s_err_code <= 6'd0;
s_boot_active <= 1'b0;
s_eng <= E_IDLE;
s_dma_wait <= 1'b0;
end
// autoload has PRIORITY over execute when both bits are set
// (C model if/else order in FloppyDMA_Write)
if (iox_wdata[2] && s_eng == E_IDLE) begin
// BOOT MODE activate: arm the next boot-stream word
if (!s_boot_active) begin
// first activate: fetch chunk 0 into the buffer
s_boot_active <= 1'b1;
s_bootptr <= 10'd0;
s_active <= 1'b1;
s_rft <= 1'b0;
s_hard_err <= 1'b0;
s_err_code <= 6'd0;
s_autoload <= 1'b1;
s_cb[0] <= 16'h0300; // format 3 (1024 B/sector)
s_lsect <= 16'd0;
s_chunk_q <= 11'd512;
disk_req <= 1'b1;
disk_wr <= 1'b0;
s_disk_to <= DISK_TIMEOUT;
s_eng <= E_DISK_RD;
end else if (s_bootptr == 10'd512) begin
// chunk exhausted: fetch the next one (re-assert the
// boot geometry - a normal command may have changed
// the format field in between)
s_bootptr <= 10'd0;
s_active <= 1'b1;
s_rft <= 1'b0;
s_autoload <= 1'b1;
s_cb[0] <= 16'h0300;
s_lsect <= s_lsect + 16'd1;
s_chunk_q <= 11'd512;
disk_req <= 1'b1;
disk_wr <= 1'b0;
s_disk_to <= DISK_TIMEOUT;
s_eng <= E_DISK_RD;
end else begin
s_rft <= 1'b1; // next word already buffered
end
end else if (iox_wdata[8] && s_eng == E_IDLE) begin // execute
s_boot_active <= 1'b0;
s_active <= 1'b1;
s_rft <= 1'b0;
s_hard_err <= 1'b0;
s_err_code <= 6'd0;
s_autoload <= 1'b0;
s_cb_idx <= 3'd0;
s_eng <= E_CB_FETCH;
end
end
3'd5: s_ptr_hi <= iox_wdata[7:0];
3'd7: s_ptr_lo <= iox_wdata;
default: ;
endcase
end
// ---- engine ----
case (s_eng)
E_IDLE: ;
// fetch command block words 0-5 by DMA
E_CB_FETCH: begin
if (!s_dma_wait && !dma_busy && s_cb_idx < 3'd6) begin
dma_issue(1'b0, {s_ptr_hi, s_ptr_lo} + {21'd0, s_cb_idx}, 16'd0);
end else if (s_dma_wait && dma_ack) begin
s_dma_wait <= 1'b0;
if (dma_err) begin
// ND-100 BUS ERROR COMMAND FETCH = oct 41 (deviceFloppyDMA.h);
// a genuine memory-contact failure -> also flag hard error b7
s_hard_err <= 1'b1;
s_err_code <= 6'o41;
s_eng <= E_WBACK;
s_wb_idx <= 3'd0;
end else begin
s_cb[s_cb_idx[2:0]] <= dma_rdata;
s_cb_idx <= s_cb_idx + 3'd1; // idx 6 = decode cycle below
end
end
// decode once the last word has landed (registered s_cb ready
// the cycle after the final ack)
if (s_cb_idx == 3'd6) begin
s_lsect <= s_disk_addr;
s_mem_ptr <= s_mem_addr0;
s_words_left <= s_wtr; // C1: word- or sector-count transfer
// status 2: selected unit in bits 8-9 (C model command setup);
// READ FORMAT loads the media format bits on top - the 8-inch
// format (all-zero descriptor) is a plain assignment in the C
// model, so the unit bits are cleared for that case too
if (s_func == 6'h22)
s_status2 <= (disk_media_fmt == 4'b0000) ? 16'd0 :
({6'd0, s_cb_drive, 8'd0} | {12'd0, disk_media_fmt});
else
s_status2 <= {6'd0, s_cb_drive, 8'd0};
s_chunk_q <= (s_wtr > {21'd0, s_words_per_sector}) ?
s_words_per_sector : s_wtr[10:0];
s_sec_idx <= 11'd0;
if (s_func == 6'h00 && s_wtr != 32'd0) begin
disk_req <= 1'b1;
disk_wr <= 1'b0;
s_disk_to <= DISK_TIMEOUT;
s_eng <= E_DISK_RD;
end else if (s_func == 6'h01 && s_wtr != 32'd0) begin
s_eng <= E_MEM_RD;
end else begin
// IDENTIFY and the other functions: complete with clean
// status (the C model's stubs do the same)
s_eng <= E_WBACK;
s_wb_idx <= 3'd0;
end
end
end
// wait for the backend to fill the buffer with one sector
E_DISK_RD: begin
if (disk_done) begin
if (disk_err_in) begin
if (s_autoload) begin
// BOOT autoload backend failure = boot_fail(). Match BOTH
// authorities: the portable core nd_floppy_dma.c boot_fail()
// (lines 603-617) and nd100x deviceFloppyDMA.c ExecuteAutoload
// set NO_BOOTSTRAP (oct 50, deviceFloppyDMA.h
// FLOPPY_ERR_NO_BOOTSTRAP) + hard error, and LEAVE boot mode
// (boot_active := false). Releasing RFT in E_DELAY frees the
// microcode's forever-poll on +2 instead of hanging silently;
// clearing s_boot_active makes a later +0 read return the idle
// constant 1 again (not stale buffer) and lets a fresh activate
// re-enter boot cleanly. A normal (non-boot) read keeps oct 20.
s_err_code <= 6'o50;
s_hard_err <= 1'b1;
s_boot_active <= 1'b0;
s_autoload <= 1'b0;
s_delay_cnt <= DELAY_TICKS;
s_final_wb <= 1'b0; // boot path: no command block
s_eng <= E_DELAY;
end else begin
// backend not-ready / read failure. deviceFloppyDMA.c sets
// only the code and always DRIVE_NOT_READY; here the reason
// picks it, and a missing medium still lands on oct 20.
s_err_code <= flp_err_code(disk_err_code);
s_eng <= E_WBACK;
s_wb_idx <= 3'd0;
end
end else if (s_autoload) begin
// boot chunk buffered: serve it via +0 reads
s_autoload <= 1'b0;
s_active <= 1'b0;
s_rft <= 1'b1;
s_eng <= E_IDLE;
end else begin
s_sec_idx <= 11'd0;
s_eng <= E_MEM_WR;
end
end else if (DISK_TIMEOUT != 16'd0) begin
// C2 watchdog: backend never answered -> no wedge
if (s_disk_to == 16'd1) begin
if (s_autoload) begin
// BOOT autoload timed out = boot_fail() (same authorities as
// the disk_err_in branch above): NO_BOOTSTRAP (oct 50) + hard
// error, leave boot mode, RFT released in E_DELAY. A normal
// read keeps DRIVE_NOT_READY (oct 20).
s_err_code <= 6'o50;
s_hard_err <= 1'b1;
s_boot_active <= 1'b0;
s_autoload <= 1'b0;
s_delay_cnt <= DELAY_TICKS;
s_final_wb <= 1'b0;
s_eng <= E_DELAY;
end else begin
s_err_code <= 6'o20; // DRIVE_NOT_READY, no wedge
s_eng <= E_WBACK;
s_wb_idx <= 3'd0;
end
end else if (s_disk_to != 16'd0) begin
s_disk_to <= s_disk_to - 16'd1;
end
end
end
// DMA-write the buffered sector words to ND memory
E_MEM_WR: begin
if (!s_dma_wait && !dma_busy && s_buf_valid) begin
// s_buf_dout is the registered read of s_buffer[s_sec_idx]
dma_issue(1'b1, s_mem_ptr, s_buf_dout);
end else if (s_dma_wait && dma_ack) begin
s_dma_wait <= 1'b0;
if (dma_err) begin
// ND-100 BUS ERROR DATA TRANSFER = oct 43 (deviceFloppyDMA.h)
s_hard_err <= 1'b1;
s_err_code <= 6'o43;
s_eng <= E_WBACK;
s_wb_idx <= 3'd0;
end else begin
s_mem_ptr <= s_mem_ptr + 24'd1;
s_words_left <= s_words_left - 32'd1;
if (s_sec_idx + 11'd1 >= s_chunk_q || s_words_left == 32'd1) begin
if (s_words_left == 32'd1) begin
if (s_autoload) begin
s_delay_cnt <= DELAY_TICKS;
s_final_wb <= 1'b0; // boot path: no command block
s_eng <= E_DELAY;
end else begin
s_eng <= E_WBACK;
s_wb_idx <= 3'd0;
end
end else begin
s_lsect <= s_lsect + 16'd1;
s_chunk_q <= ((s_words_left - 32'd1) > {21'd0, s_words_per_sector}) ?
s_words_per_sector : s_words_left[10:0] - 11'd1;
disk_req <= 1'b1;
disk_wr <= 1'b0;
s_disk_to <= DISK_TIMEOUT;
s_eng <= E_DISK_RD;
end
end else begin
s_sec_idx <= s_sec_idx + 11'd1;
end
end
end
end
// DMA-read ND memory words into the buffer (write function)
E_MEM_RD: begin
if (!s_dma_wait && !dma_busy) begin
dma_issue(1'b0, s_mem_ptr, 16'd0);
end else if (s_dma_wait && dma_ack) begin
s_dma_wait <= 1'b0;
if (dma_err) begin
// ND-100 BUS ERROR DATA TRANSFER = oct 43 (deviceFloppyDMA.h)
s_hard_err <= 1'b1;
s_err_code <= 6'o43;
s_eng <= E_WBACK;
s_wb_idx <= 3'd0;
end else begin
// dma_rdata -> s_buffer[s_sec_idx] is committed by the synchronous
// RAM write port above (s_memrd_commit qualifier)
s_mem_ptr <= s_mem_ptr + 24'd1;
s_words_left <= s_words_left - 32'd1;
if (s_sec_idx + 11'd1 >= s_chunk_q || s_words_left == 32'd1) begin
disk_req <= 1'b1;
disk_wr <= 1'b1;
s_disk_to <= DISK_TIMEOUT;
s_eng <= E_DISK_WR;
end else begin
s_sec_idx <= s_sec_idx + 11'd1;
end
end
end
end
// wait for the backend to write the buffer to the image
E_DISK_WR: begin
if (disk_done) begin
if (disk_err_in) begin
// write failure: code only, like C, but the reason chooses it
// (a refused unaligned write is WRITE_PROTECTED, not a dead drive)
s_err_code <= flp_err_code(disk_err_code);
s_eng <= E_WBACK;
s_wb_idx <= 3'd0;
end else if (s_words_left == 32'd0) begin
s_eng <= E_WBACK;
s_wb_idx <= 3'd0;
end else begin
s_lsect <= s_lsect + 16'd1;
s_sec_idx <= 11'd0;
s_chunk_q <= (s_words_left > {21'd0, s_words_per_sector}) ?
s_words_per_sector : s_words_left[10:0];
s_eng <= E_MEM_RD;
end
end else if (DISK_TIMEOUT != 16'd0) begin
// C2 watchdog on the write backend
if (s_disk_to == 16'd1) begin
s_err_code <= 6'o20;
s_eng <= E_WBACK;
s_wb_idx <= 3'd0;
end else if (s_disk_to != 16'd0) begin
s_disk_to <= s_disk_to - 16'd1;
end
end
end
// DMA-write status words w6-w11 back into the command block
E_WBACK: begin
if (!s_dma_wait && !dma_busy) begin
case (s_wb_idx)
3'd0: dma_issue(1'b1, {s_ptr_hi, s_ptr_lo} + 24'd6, s_sw1);
3'd1: dma_issue(1'b1, {s_ptr_hi, s_ptr_lo} + 24'd7, s_status2);
3'd2: dma_issue(1'b1, {s_ptr_hi, s_ptr_lo} + 24'd8,
{8'd0, s_mem_ptr[23:16]});
3'd3: dma_issue(1'b1, {s_ptr_hi, s_ptr_lo} + 24'd9,
s_mem_ptr[15:0]);
3'd4: dma_issue(1'b1, {s_ptr_hi, s_ptr_lo} + 24'd10, 16'd0);
default: dma_issue(1'b1, {s_ptr_hi, s_ptr_lo} + 24'd11,
s_words_left[15:0]);
endcase
end else if (s_dma_wait && dma_ack) begin
s_dma_wait <= 1'b0;
if (s_wb_idx == 3'd5) begin
s_delay_cnt <= DELAY_TICKS;
s_final_wb <= 1'b1; // re-write CB+6 after the delay
s_eng <= E_DELAY;
end
s_wb_idx <= s_wb_idx + 3'd1;
end
end
// completion delay, then ready + interrupt condition; a normal
// command (s_final_wb) goes on to re-write CB+6 with the
// completed status, like the C model's ReadEnd
E_DELAY: begin
if (s_delay_cnt != 16'd0) s_delay_cnt <= s_delay_cnt - 16'd1;
else begin
s_active <= 1'b0;
s_rft <= 1'b1;
s_eng <= s_final_wb ? E_FINAL : E_IDLE;
end
end
// final status re-write (C model ReadEnd): one DMA write of
// CB+6 with s_sw1 now evaluating READY=1 (b3), BUSY=0 (b2 clear)
E_FINAL: begin
if (!s_dma_wait && !dma_busy) begin
dma_issue(1'b1, {s_ptr_hi, s_ptr_lo} + 24'd6, s_sw1);
end else if (s_dma_wait && dma_ack) begin
s_dma_wait <= 1'b0;
s_final_wb <= 1'b0;
s_eng <= E_IDLE;
end
end
default: s_eng <= E_IDLE;
endcase
// IDENT answered: clear the enable bit (standard rule)
if (s_ident_answer) begin
s_int_enabled <= 1'b0;
end
end
end
endmodule