nd_storage_tape_adapter¶
Source: Verilog/SD-FAT/circuit/nd_storage_tape_adapter.v
Where it sits (Simulation): ND120_TOP > nd_storage_devices > nd_storage_tape_adapter
- instance path: TAPE_SDFAT_SOURCE.gen_tape.u_tape_adapter
Used in: nd_storage_devices (Simulation, Tang, Nexys, QMTECH), nd_storage_mega65_devices (MEGA65 R6, MEGA65 R3), nd_storage_mister_devices (MiSTer)
Contains: no other modules.
Module hierarchy - All modules

Schematic¶
Drawn from the Verilog: the yosys netlist of the Simulation (Verilator) build, instance TAPE_SDFAT_SOURCE.gen_tape.u_tape_adapter. 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).
Description¶
nd_storage_tape_adapter - byte-stream adapter for the paper-tape reader Design doc: Verilog/docs/nd-storage-design.md section 2.5; spec Verilog/docs/nd-storage-interface-spec.md section 5 (acceptance test 4). Sits between ND_TAPE_400's byte source port (byte_req / byte_valid / byte_data / source_rewind - pin-for-pin, see Verilog/ND-BUS-DEVICES/TAPE-400/circuit/ND_TAPE_400.v) and ONE nd_storage client port. Single clock: clk_cpu (the client-port domain - the CDC lives inside nd_storage). Behavior: - keeps a 32-bit byte position s_bptr; block = s_bptr[26:11], word-in-block = s_bptr[10:1] - on byte_req: position past EOF (s_bptr >= c_size_bytes) or file not open -> SILENCE (no byte_valid, no client-port traffic - the tape's ready-for-transfer flag stays low, the C-model EOF behavior). The zero-padded slot tail past EOF is never served. - block hit (s_have_blk and s_cur_blk matches) -> serve from the local 1024x16 block buffer: byte_valid pulse with byte 2w = word[15:8], byte 2w+1 = word[7:0] (big-endian word order, design doc 4.1 normative) - miss -> one c_req block READ into the local buffer, then serve - source_rewind: position back to 0, buffer invalidated; an in-flight fetch is discarded on completion. NO card access - the image lives in SDRAM. - c_err on a fetch -> drop s_have_blk, stay silent (tape runout semantics); the next byte_req simply retries the fetch. WHY THIS ONE CANNOT TELL THE GUEST WHY IT STOPPED: the byte source port is byte_req / byte_valid / byte_data / source_rewind and nothing else - a real ND-400 paper tape reader has no register in which "the SD card is missing" could be expressed, and inventing one would put a signal on the card that its manual does not define. So the guest still sees exactly the runout silence it saw before, and the reason is instead published on the STICKY diagnostic pair fault / fault_code (nd_storage_status.vh), which no ND logic reads - it exists for a testbench, a probe or a board LED. Cleared by source_rewind and reset. Without it, "the card fell out" and "you reached the end of the tape" are the same event from every observation point. READ-ONLY by construction: c_wr is tied 0 and c_buf_rdata is tied 0, so the step-6 FLAG (never write the partial tail block of a file whose size is not a 2048-byte multiple) holds trivially - there is no write path at all. open_start is a pulse from the board/boot logic; it is passed through as a c_open_req pulse (nd_storage ignores it while the port is busy). Last reviewed: 11-JUL-2026 Ronny Hansen
Ports¶
| Direction | Width | Name | Description |
|---|---|---|---|
| input | 1 |
clk_cpu |
|
| input | 1 |
rst_n (active low) |
|
| input | 1 |
byte_req |
1-cycle pulse: fetch next byte |
| output | 1 |
byte_valid |
1-cycle pulse: byte_data is the byte |
| output | [7:0] |
byte_data |
|
| input | 1 |
source_rewind |
1-cycle pulse: back to byte 0 |
| output | 1 |
fault |
|
| output | [3:0] |
fault_code |
|
| input | 1 |
open_start |
1-cycle pulse: (re)open the file |
| output | 1 |
c_open_req |
|
| input | 1 |
c_open_ok |
|
| input | 1 |
c_open_err |
|
| input | [31:0] |
c_size_bytes |
|
| output | 1 |
c_req |
|
| output | 1 |
c_wr |
tied 0: tape is READ-ONLY |
| output | [15:0] |
c_block |
|
| input | 1 |
c_busy |
|
| input | 1 |
c_done |
1-cycle pulse |
| input | 1 |
c_err |
valid with c_done |
| input | [3:0] |
c_err_code |
valid with c_done when c_err |
| input | [9:0] |
c_buf_addr |
|
| input | [15:0] |
c_buf_wdata |
|
| input | 1 |
c_buf_we |
|
| output | [15:0] |
c_buf_rdata |
never used (no write path) |
Verilog source¶
Verilog/SD-FAT/circuit/nd_storage_tape_adapter.v on GitHub.
Show the Verilog of nd_storage_tape_adapter (228 lines)
/****************************************************************************
** nd_storage_tape_adapter - byte-stream adapter for the paper-tape reader **
** **
** Design doc: Verilog/docs/nd-storage-design.md section 2.5; spec **
** Verilog/docs/nd-storage-interface-spec.md section 5 (acceptance test 4).**
** **
** Sits between ND_TAPE_400's byte source port (byte_req / byte_valid / **
** byte_data / source_rewind - pin-for-pin, see **
** Verilog/ND-BUS-DEVICES/TAPE-400/circuit/ND_TAPE_400.v) and ONE **
** nd_storage client port. Single clock: clk_cpu (the client-port domain **
** - the CDC lives inside nd_storage). **
** **
** Behavior: **
** - keeps a 32-bit byte position s_bptr; block = s_bptr[26:11], **
** word-in-block = s_bptr[10:1] **
** - on byte_req: position past EOF (s_bptr >= c_size_bytes) or file **
** not open -> SILENCE (no byte_valid, no client-port traffic - the **
** tape's ready-for-transfer flag stays low, the C-model EOF **
** behavior). The zero-padded slot tail past EOF is never served. **
** - block hit (s_have_blk and s_cur_blk matches) -> serve from the **
** local 1024x16 block buffer: byte_valid pulse with **
** byte 2w = word[15:8], byte 2w+1 = word[7:0] (big-endian word **
** order, design doc 4.1 normative) **
** - miss -> one c_req block READ into the local buffer, then serve **
** - source_rewind: position back to 0, buffer invalidated; an **
** in-flight fetch is discarded on completion. NO card access - the **
** image lives in SDRAM. **
** - c_err on a fetch -> drop s_have_blk, stay silent (tape runout **
** semantics); the next byte_req simply retries the fetch. **
** **
** WHY THIS ONE CANNOT TELL THE GUEST WHY IT STOPPED: the byte source **
** port is byte_req / byte_valid / byte_data / source_rewind and nothing **
** else - a real ND-400 paper tape reader has no register in which "the **
** SD card is missing" could be expressed, and inventing one would put a **
** signal on the card that its manual does not define. So the guest still **
** sees exactly the runout silence it saw before, and the reason is **
** instead published on the STICKY diagnostic pair fault / fault_code **
** (nd_storage_status.vh), which no ND logic reads - it exists for a **
** testbench, a probe or a board LED. Cleared by source_rewind and reset. **
** Without it, "the card fell out" and "you reached the end of the tape" **
** are the same event from every observation point. **
** **
** READ-ONLY by construction: c_wr is tied 0 and c_buf_rdata is tied 0, **
** so the step-6 FLAG (never write the partial tail block of a file whose **
** size is not a 2048-byte multiple) holds trivially - there is no write **
** path at all. **
** **
** open_start is a pulse from the board/boot logic; it is passed through **
** as a c_open_req pulse (nd_storage ignores it while the port is busy). **
** **
** Last reviewed: 11-JUL-2026 **
** Ronny Hansen **
*****************************************************************************/
`include "nd_storage_status.vh"
module nd_storage_tape_adapter (
input wire clk_cpu,
input wire rst_n,
// Byte source port (to ND_TAPE_400, pin-for-pin)
input wire byte_req, // 1-cycle pulse: fetch next byte
output reg byte_valid, // 1-cycle pulse: byte_data is the byte
output reg [7:0] byte_data,
input wire source_rewind, // 1-cycle pulse: back to byte 0
// Sticky diagnostic (NOT part of the ND-400 device contract - see the
// header). Set the first time a byte request is refused for a reason
// that is not plain end-of-tape; cleared by rewind and reset.
output reg fault,
output reg [3:0] fault_code,
// Board/boot control
input wire open_start, // 1-cycle pulse: (re)open the file
// nd_storage client port (one client, clk_cpu domain)
output reg c_open_req,
input wire c_open_ok,
input wire c_open_err,
input wire [31:0] c_size_bytes,
output reg c_req,
output wire c_wr, // tied 0: tape is READ-ONLY
output reg [15:0] c_block,
input wire c_busy,
input wire c_done, // 1-cycle pulse
input wire c_err, // valid with c_done
input wire [ 3:0] c_err_code, // valid with c_done when c_err
input wire [ 9:0] c_buf_addr,
input wire [15:0] c_buf_wdata,
input wire c_buf_we,
output wire [15:0] c_buf_rdata // never used (no write path)
);
assign c_wr = 1'b0;
assign c_buf_rdata = 16'd0;
// Local copy of the current block (BRAM, written by the client port
// during a fetch, read back one word at a time to serve bytes)
reg [15:0] s_blkbuf[0:1023];
always @(posedge clk_cpu) begin
if (c_buf_we) s_blkbuf[c_buf_addr] <= c_buf_wdata;
end
// FSM
localparam [2:0] A_IDLE = 3'd0, // wait for a pending byte request
A_WAIT = 3'd1, // block fetch in flight (c_req issued)
A_RD1 = 3'd2, // registered buffer read
A_RD2 = 3'd3; // emit byte_valid
reg [ 2:0] s_state;
reg [31:0] s_bptr; // byte position in the file
reg [15:0] s_cur_blk; // block number held in s_blkbuf
reg s_have_blk; // s_blkbuf/s_cur_blk are valid
reg s_pend; // a byte_req is waiting to be answered
reg s_drop; // rewind hit an in-flight fetch: discard it
reg [15:0] s_word; // registered buffer read data
always @(posedge clk_cpu or negedge rst_n) begin
if (!rst_n) begin
byte_valid <= 1'b0;
byte_data <= 8'd0;
c_open_req <= 1'b0;
c_req <= 1'b0;
c_block <= 16'd0;
s_state <= A_IDLE;
s_bptr <= 32'd0;
s_cur_blk <= 16'd0;
s_have_blk <= 1'b0;
s_pend <= 1'b0;
s_drop <= 1'b0;
s_word <= 16'd0;
fault <= 1'b0;
fault_code <= `NDS_ERR_NONE;
end else begin
byte_valid <= 1'b0;
c_req <= 1'b0;
c_open_req <= 1'b0;
if (open_start) c_open_req <= 1'b1;
case (s_state)
A_IDLE: begin
if (s_pend) begin
if (!c_open_ok || (s_bptr >= c_size_bytes)) begin
// EOF / not open: SILENCE - drop the request, no traffic
s_pend <= 1'b0;
// ...but say WHY on the diagnostic pair. Running off the end
// of the tape is normal and is NOT a fault; having no file to
// read is. (Both are true at once when the open failed, since
// c_size_bytes is then 0 - so test !c_open_ok first.)
if (!c_open_ok && !fault) begin
fault <= 1'b1;
fault_code <= `NDS_ERR_NOTOPEN;
end
end else if (s_have_blk && (s_cur_blk == s_bptr[26:11])) begin
s_state <= A_RD1;
end else if (!c_busy) begin
c_req <= 1'b1;
c_block <= s_bptr[26:11];
s_state <= A_WAIT;
end
end
end
A_WAIT: begin
if (c_done) begin
if (s_drop) begin
// rewound while the fetch was in flight: discard
s_drop <= 1'b0;
s_have_blk <= 1'b0;
s_state <= A_IDLE;
end else if (c_err) begin
// silent to the device, retryable on the next byte_req - but
// this is a real storage failure, not the end of the tape
s_have_blk <= 1'b0;
s_pend <= 1'b0;
s_state <= A_IDLE;
if (!fault) begin
fault <= 1'b1;
fault_code <= c_err_code;
end
end else begin
s_have_blk <= 1'b1;
s_cur_blk <= c_block;
s_state <= A_RD1;
end
end
end
A_RD1: begin
s_word <= s_blkbuf[s_bptr[10:1]];
s_state <= A_RD2;
end
A_RD2: begin
byte_valid <= 1'b1;
// big-endian word order: even byte = bits 15:8 (design doc 4.1)
byte_data <= s_bptr[0] ? s_word[7:0] : s_word[15:8];
s_bptr <= s_bptr + 32'd1;
s_pend <= 1'b0;
s_state <= A_IDLE;
end
default: s_state <= A_IDLE;
endcase
// latch a byte request (after the case so a same-cycle serve
// completion cannot eat a new request)
if (byte_req) s_pend <= 1'b1;
// rewind wins over everything in this cycle
if (source_rewind) begin
s_bptr <= 32'd0;
s_have_blk <= 1'b0;
s_pend <= 1'b0;
byte_valid <= 1'b0;
fault <= 1'b0; // new tape pass: the old verdict is stale
fault_code <= `NDS_ERR_NONE;
if ((s_state == A_WAIT) && !c_done) begin
s_drop <= 1'b1; // stay in A_WAIT until the fetch completes
end else begin
s_state <= A_IDLE;
end
end
end
end
endmodule