ND-3201 SCSI/Floppy Controller - Board Reference¶
Board: ND-3201, PN 350001 (also 3204/3205/3206/3207 variants) Firmware: 45900E.bin (8KB ROM, Z80) - drives the FLOPPY half only (see below) Purpose: SCSI and floppy disk controller for Norsk Data ND-100 minicomputer
THE ARCHITECTURE: Two Independent Halves¶
This is the single most important fact about the board, and an earlier revision of this document got it backwards. Read this before anything else.
The ND-3201 is two independent controllers sharing one PCB and one host connector:
| Half | Hardware | Who drives it |
|---|---|---|
| SCSI | NCR 5386 | The ND-100 host. The NCR register file is hardware-decoded straight onto the ND-100 IOX bus. SINTRAN's driver is the SCSI protocol engine - it issues NCR commands, services NCR interrupts, and programs the transfer counter and MAR itself, phase by phase. |
| Floppy | Z80 + AM9517 DMA + FD1797 | The Z80 firmware (45900E.bin). Classic intelligent-controller model: the host writes a command block, the Z80 executes it and signals completion. |
The Z80 never touches the NCR 5386. Not once, anywhere in the 8KB ROM.
Evidence (VERIFIED - exhaustive byte-level I/O sweep of 45900E.bin)¶
Every D3/DB/ED 4x/ED 5x/ED 6x/ED 7x I/O opcode in ram:0000-1fff was located and its
port resolved (including the (C)-register forms, traced back to what C holds). Findings:
- The claimed "NCR window" at Z80 ports 0x20-0x3D is a complete AM9517/8237 DMA register
file - every control register at its exact datasheet offset (0x28 Command/Status, 0x29
Request, 0x2A Single Mask, 0x2B Mode, 0x2C Clear Byte Pointer, 0x2D Master Clear, channel n
addr/count at 0x20+2n / 0x21+2n through the byte-pointer flip-flop).
IN A,(0x28)at ram:012a reads it as Status. Under the old NCR reading, that same instruction would read NCR "Diagnostic Status" while 0x29 received0x4|channelas an NCR command - incoherent. - The NCR5386 signature is absent. No port anywhere receives an NCR command code (0x00/0x01/0x03/0x04/0x08/0x09/0x0B/0x24/0x54/0x94/0xA4), and no port is read in the "write command -> later read interrupt register" pattern that defines NCR usage.
- The only command-code-shaped byte stream goes to port 0x70, and they are FD179x opcodes -
0x02 Restore, 0x12/0x18/0x1C Seek, 0x88/0x8C Read Sector, 0xC4 Read Address, 0xD0/0xD4 Force
Interrupt, 0xF0/0xF2 Write Track - confirmed by the adjacent 0x71/0x72/0x73 Track/Sector/Data
triple and the
BIT 0,ABusy poll on 0x70. - The DMA exists to serve the floppy. AM9517 channels are programmed immediately before every FD1797 data-moving command, and the current-address register is polled during Write Track (ram:1248). This explains what the 8237 is for without SCSI existing at all.
- No unattributed port block remains in which an NCR could hide. The only partly-characterised ports are 0x75 and 0x76 (one write site each) - two isolated bytes, far too narrow for a 16-register NCR file.
Independent corroboration¶
- The SINTRAN NPL driver (
IP-P2-SCSI-DRIV.NPL) drives the NCR directly over IOX. Its symbols map one-for-one onto NCR 5386 registers and match this document's ND-100 IOX table exactly:WNCOM=43₃ ("WRITE NCR COMMAND REGISTER") = IOX 0x23,RITRG=54₃ ("READ INTERRUPT REGISTER") = IOX 0x2C,RAUXS=50₃ = IOX 0x28,RNDAT=40₃ = IOX 0x20. The driver issues "Message Accepted TO NCR", "Set ATN TO NCR", "DMA MODE + TRANSFER INFO", and "SET RESET ON SCSI BUS" itself. A Z80-mediated SCSI path is impossible against this source. - The ND-3106/3112 manual (ND-11.021.1), cited later in this document, confirms the Z80-side
ports 0x50-0x57, the command-block format,
FDVSEL(port 0x74 = Floppy Drive Select) and the CTC assignment as identical to those floppy controllers. The Z80 half of the ND-3201 is essentially an ND-3112 floppy controller.
Consequence for SCSI debugging¶
The Z80 firmware is not in the SCSI path. Questions about what the board does when the ND-100 writes WCONT during a SCSI operation are answered by the SINTRAN driver and the NCR 5386 datasheet - not by this ROM. See "What WCONT Actually Does" below.
Open questions (INFERRED / UNVERIFIED)¶
- This ROM image maps only 0x0000-0x3FFF. A second or bank-switched ROM cannot be excluded from the image alone, though nothing observed suggests one.
- Ports 0x75/0x76 have a single write site each and are not fully characterised.
- Whether the floppy half answers on a separate IOX device address from the SCSI half (TH3 sets the floppy device number) is not verified here, though it would explain how both halves coexist behind one WCONT/RSTAU-shaped interface.
Document Status and Retractions¶
Corrected on the basis of the byte-level sweep above. The following claims from the earlier revision were DISPROVED and have been removed or rewritten. They are listed so that anyone holding a copy of the old text knows not to trust it:
| Retracted claim | Reality |
|---|---|
| Z80 ports 0x20-0x3D are NCR 5386 registers | They are the AM9517 DMA controller (0x20-0x2D) |
| NCR INT -> CTC1 Ch3 -> dynamic Z80 ISR chain services SCSI phases | The Z80 never accesses the NCR. The chain does not exist |
| WCONT Active pulses CTC1 Ch0 in counter mode (TC=1), waking the Z80 for every command | CTC1 Ch0 is programmed in timer mode, prescaler 16, TC=16 (control word 0x95 at ram:0109). No 0xC5 counter-mode word is written to port 0x10 anywhere. That code is an interrupt-plumbing selftest: it installs a stub ISR, starts a timer, and checks the ISR ran (LED error 2 on failure) |
| SRAM 0x2000 is a host command mailbox polled by the idle loop | ram:024a reads (0x2000) and calls an error trap (0x1d1c = RST 08 + code 0x95) if it is non-zero. It is a RAM-corruption watchdog. Nothing writes a command there; nothing dispatches on its value |
| The Z80 command flow (ISR 0x02AB decode/dispatch) governs SCSI I/O | That flow is the floppy half's command path (ND-3112-style command block via ports 0x50-0x57) |
| "Change 4: Implement ExecuteGo() properly" in the C# recommendations | Based on the false Z80-command model. Retracted - see the C# section |
~25 function names (all ncr5386_*, most scsi_* / nd100_bus_*) |
Misnamed. Corrected in the Ghidra DB and in the Function Reference below |
Claims that were independently corroborated and are retained: the ND-100 IOX register map (matches the NPL driver), the RSTAU/WCONT bit tables, the POST and event error codes (traced to ND-11.021.1), the 7-segment font, and the memory map.
Every claim below should be labelled VERIFIED (read from bytes/source) or INFERRED (reasoned).
Section Map - which half does each section describe?¶
This document grew as a Z80 firmware analysis, so its sections are not physically grouped by half. Rather than claim a tidy split that does not exist, here is what each section actually covers:
| Section | Half | Trust |
|---|---|---|
| Thumbwheel Switches | Both (TH1/TH2 = SCSI, TH3 = floppy) | Good |
| Hardware on Card, Memory Map | Board | Good |
| Z80 I/O Port Map | Floppy (Z80-side ports) | Corrected - 0x20-0x2D is AM9517, not NCR |
| RST / IM2 Vector Tables, Boot Sequence | Floppy (Z80 POST) | Good, but ISR purposes were inferred from the false NCR premise |
| ND-100 to Z80 Command Flow, Trigger Mechanism | Floppy | Trigger mechanism DISPROVED; command flow is the floppy protocol, not SCSI |
| Z80 to ND-100 Response Flow | Floppy | Good |
| ND-100 Side Register Map (IOX), RSTAU/WCONT bits | SCSI | Solid - corroborated by IP-P2-SCSI-DRIV.NPL |
| POST / Event Error Codes | Floppy | Solid - traced to ND-11.021.1 |
| Function Reference | Floppy | 21 names corrected; ~13 still suspect |
| RAM Variable Reference | Floppy | Names inferred; treat with care |
| Deep Dive: NCR 5386 Interrupt Path | - | RETRACTED IN FULL - fiction |
| Deep Dive: FD1797 / AM9517 / CTC / Floppy Boot | Floppy | Good |
| What WCONT Actually Does | Both | Solid - the authoritative answer for SCSI |
| C# Emulator Analysis / Recommended Changes | SCSI | Several issues retracted - read the warnings |
| ND-3106/3112 Manual Confirmations | Floppy | Solid - independent manual |
If you are debugging SCSI: read "THE ARCHITECTURE", "What WCONT Actually Does", and "ND-100 Side Register Map (IOX)". Ignore everything Z80.
If you are implementing floppy support: almost everything else applies, but re-verify names against the Ghidra DB, which is now more accurate than this document's prose.
Thumbwheel Switches¶
| Switch | Function | How it works |
|---|---|---|
| TH1 | SCSI device number (SCSI ID) | Wired to NCR 5386 ID strapping pins (pins 12-14). Uses "strapped ID" mode -- the chip reads its own SCSI ID from hardware pins without Z80 involvement. The Z80 firmware never reads this value. |
| TH2 | SCSI IDENT (IOX address) | Sets address comparator in bus interface glue logic. Pure hardware -- the Z80 is completely unaware of which IDENT the board responds to. |
| TH3 | Floppy device number | Not read by Z80. Likely configures floppy drive select logic in glue logic (port 0x74/0x75 decode) or affects which IOX sub-address the floppy registers respond to on the ND-100 side. |
TH2 IDENT and IOX Address Mapping¶
| TH2 Setting | IOX Range (octal) | IDENT (octal) | IDENT (hex) | Device |
|---|---|---|---|---|
| 0, 4, 8, C | 144.300-144.377 | 140440 | 0xC120 | SCSI Bus 1 |
| 1, 5, 9, D | 144.400-144.477 | 140441 | 0xC121 | SCSI Bus 2 |
| 2, 6, A, E | 144.500-144.577 | 140442 | 0xC122 | SCSI Bus 3 |
| 3, 7, B, F | 144.600-144.677 | 140443 | 0xC123 | SCSI Bus 4 |
The IOX base address 0xC8C0 (octal 144300 = SCSI Bus 1) is hardcoded in the ROM identity data block at address 0x15F7. Only this one bus address exists in ROM 45900E -- the other three bus addresses (0xC900/0xC940/0xC980) are absent. This means either separate ROMs are built per bus position, or the ND-100 already knows the IOX base from the thumbwheel-selected IDENT code and ignores this field.
Thumbwheel Reading -- The Z80 Does NOT Read Any Thumbwheel¶
Every IN instruction (both IN A,(n) and IN r,(C) forms) in the entire 8KB ROM was checked. The complete set of Z80 I/O read ports is:
| Port | Device |
|---|---|
| 0x10 | CTC1 Ch0 |
| 0x16-0x17 | CTC2 Ch2/Ch3 (calibration) |
| 0x20-0x2D | AM9517 DMA registers (CORRECTED - not NCR 5386) |
| 0x50-0x57 | ND-100 bus interface |
| 0x70-0x71 | FD1797 status/track |
| 0x77 | Glue logic status |
No other ports are read. No thumbwheel input port exists in the firmware.
All Z80 configuration comes from the ND-100 host via command data read from ports 0x50-0x54. The SINTRAN driver on the ND-100 already knows the SCSI ID, device type, and geometry, and sends it as part of the command block.
Hardware on Card¶
| Chip | Function |
|---|---|
| Z80 CPU | Main controller processor |
| NCR 5386 | SCSI protocol controller |
| AM9517 | DMA controller |
| FD1797 | Floppy disc controller |
| Z80 CTC x2 | Counter/Timer (8 channels total) |
| TMM2063 | 8KB SRAM (0x2000-0x3FFF) |
| 3-digit 7-segment LED | POST error display |
Memory Map¶
| Address Range | Type | Purpose |
|---|---|---|
| 0x0000-0x1FFF | ROM | Firmware (45900E.bin) |
| 0x2000-0x206F | RAM | Stack area (SP=0x2070, grows down) |
| 0x2070-0x207F | RAM | IM2 interrupt vector table |
| 0x2080-0x20FF | RAM | Controller state (IX=0x2080 base) |
| 0x2100-0x219F | RAM | Drive control block (IY=0x2100 base) |
| 0x2200-0x22FF | RAM | SCSI command buffer |
| 0x22A0+ | RAM | SCSI data buffer |
| 0x2750+ | RAM | SCSI status buffer |
Z80 I/O Port Map¶
CTC x2 (ports 0x10-0x17)¶
| Port | Chip | Channel | Function |
|---|---|---|---|
| 0x10 | CTC1 | Ch0 | ND-100 command trigger (counter mode, external CLK/TRG) |
| 0x11 | CTC1 | Ch1 | Timeout/error timer |
| 0x12 | CTC1 | Ch2 | Transfer phase timer |
| 0x13 | CTC1 | Ch3 | SCSI phase timer |
| 0x14 | CTC2 | Ch0 | SCSI reselection timer |
| 0x15 | CTC2 | Ch1 | Timeout/error timer |
| 0x16 | CTC2 | Ch2 | CTC clock calibration |
| 0x17 | CTC2 | Ch3 | 7-segment display refresh |
AM9517/8237 DMA Controller (ports 0x20-0x2D) - VERIFIED¶
This block was previously and wrongly documented as "NCR 5386 SCSI Controller (ports 0x20-0x3D)". It is the AM9517 DMA controller, at base 0x20. There is no NCR 5386 in the Z80's I/O space. See "THE ARCHITECTURE" at the top of this document.
| Port | R/W | 8237 offset | Register | Evidence |
|---|---|---|---|---|
| 0x20 | R/W | base+0x00 | Channel 0 Base/Current Address | ram:1248: IN A,(0x20) current-address readback during Write Track |
| 0x21 | R/W | base+0x01 | Channel 0 Base/Current Word Count | ram:0718 INC C from addr port, then OUT (C),E; OUT (C),D |
| 0x22-0x27 | R/W | base+0x02..07 | Channels 1-3 Address / Word Count | ram:0718: port = (A AND 3) * 2 + 0x20 |
| 0x28 | W | base+0x08 | Command Register | ram:0709: LD A,0x20; OUT (0x28),A |
| 0x28 | R | base+0x08 | Status Register | ram:012a: IN A,(0x28), ram:1dfc |
| 0x29 | W | base+0x09 | Request Register | ram:0141: LD A,0x4; OR C; OUT (0x29),A (bit 2 = set/clear, bits 0-1 = channel) |
| 0x2A | W | base+0x0A | Single Mask Register | ram:0714: OUT (0x2a),A (A = channel); ram:1e00: LD A,0xf; OUT (0x2a),A |
| 0x2B | W | base+0x0B | Mode Register | ram:0718: OUT (0x2b),A then AND 0x3 to extract the channel from the mode byte |
| 0x2C | W | base+0x0C | Clear Byte Pointer flip-flop | ram:0718: OUT (0x2c),A immediately before the 16-bit addr/count writes |
| 0x2D | W | base+0x0D | Master Clear | ram:0709: LD A,0x20; OUT (0x2d),A |
The 16-bit address and count are each written low-byte-then-high-byte to the same port, which is only meaningful because of the byte-pointer flip-flop cleared via 0x2C. That sequence is the signature that identifies this block beyond doubt.
The DMA serves the floppy: channels are programmed immediately before every FD1797 command that
moves data, and the current-address register is polled during Write Track (ram:1248).
Ports 0x2E-0x3F are never accessed by the ROM.
NOTE - do not confuse the two 0x20-0x3D ranges. The ND-100's IOX offsets 0x20-0x3D really are the NCR 5386 register file (see Part 1). The Z80's I/O port range 0x20-0x2D is the AM9517. The earlier revision of this document appears to have assumed the Z80 ports mirrored the IOX offsets and copied the NCR table across. They are unrelated address spaces on opposite sides of the board.
7-Segment Display (ports 0x40-0x41)¶
| Port | R/W | Function |
|---|---|---|
| 0x40 | W | Digit select (active display segment mask: 0x7F/0xBF/0xDF) |
| 0x41 | W | Segment data (7-segment pattern) |
7-segment encoding table at ROM 0x1B2E:
| Digit | Pattern |
|---|---|
| 0 | 0x3F |
| 1 | 0x06 |
| 2 | 0x5B |
| 3 | 0x4F |
| 4 | 0x66 |
| 5 | 0x6D |
| 6 | 0x7D |
| 7 | 0x07 |
| 8 | 0x7F |
| 9 | 0x6F |
ND-100 Bus Interface (ports 0x50-0x57)¶
These ports bridge the Z80 and the ND-100 host. Read and write access different registers at the same address.
| Port | Read Function | Write Function |
|---|---|---|
| 0x50 | Command word high byte (from ND-100) | DMA address low byte (to ND-100) |
| 0x51 | Block address low byte (from ND-100) | DMA address mid byte (to ND-100) |
| 0x52 | Block address mid byte (from ND-100) | DMA address high byte (to ND-100) |
| 0x53 | Block address high byte (from ND-100) | Bus mode control register |
| 0x54 | Command word low byte (from ND-100) | Status/flags (to ND-100) |
| 0x55 | DMA address readback low | SCSI ID (to ND-100) |
| 0x56 | DMA address readback mid | Sense/error flags (to ND-100) |
| 0x57 | DMA address readback high | Completion status (to ND-100) |
FD1797 Floppy Disc Controller (ports 0x70-0x73)¶
| Port | R/W | FD1797 Register | Description |
|---|---|---|---|
| 0x70 | R/W | Command / Status | Write: FD1797 commands (Restore, Seek, Read Sector, etc.). Read: status (bit 0=Busy, bit 7=Not Ready) |
| 0x71 | R/W | Track Register | Current track number. Read to verify position after seek |
| 0x72 | W | Sector Register | Sector number for read/write operations (never read by firmware) |
| 0x73 | W | Data Register | Target track number for Seek commands (never read - data transfer via AM9517 DMA) |
Port 0x70 serves dual purpose: the FD1797 always receives the write, AND the board glue logic monitors certain values (0xD0, 0xD4) as bus status signals to the ND-100 interface.
Board Glue Logic (ports 0x74-0x77)¶
| Port | R/W | Function | Details |
|---|---|---|---|
| 0x74 | W | Drive select + DMA direction | Bit 4: DMA direction toggle. Bit 5: DMA control. Written with drive mask from dcb_drive_select (0x2104) |
| 0x75 | W | Secondary control (side select?) | Written 0x00 during init |
| 0x76 | W | Timer/prescaler control | Only written during CTC calibration |
| 0x77 | R/W | Drive status / motor control | Read: bit 0=FDC busy/IRQ, bit 1=head loaded/WP. Write: motor control, bit 7 cleared for bus output |
RST Vector Table¶
| Vector | Address | Name | Purpose |
|---|---|---|---|
| RST 0 | 0x0000 | RST0 | Reset: init ports, jump to MAIN |
| RST 1 | 0x0008 | save_context_and_enter_cmd_loop | Save all regs + SP, enter command processor at 0x1D4C |
| RST 2 | 0x0010 | save_registers | Prologue: push DE, BC, AF, IX |
| RST 3 | 0x0018 | restore_registers | Epilogue: pop IX, AF, BC, DE |
| RST 4 | 0x0020 | ctc_write_01_03 | Write 0x01 then 0x03 to CTC port in C (reset/stop) |
| RST 5 | 0x0028 | ctc_write_c5_01 | Program CTC port C: control=0xC5 (counter mode, IRQ), constant=1 |
| RST 6 | 0x0030 | isr_save_and_reti | ISR entry: RETI + save registers |
| RST 7 | 0x0038 | (redirect) | JP 0x1D1C (event code 0x95 -> command processor) |
| NMI | 0x0066 | nmi_watchdog_handler | Decrement counter at 0x20AA; zero triggers reset |
IM2 Interrupt Vector Table¶
The Z80 runs in IM2 mode with I=0x20. The vector table is at RAM 0x2070-0x207F, initialized from ROM at 0x0076.
CTC1 vector base = 0x70, CTC2 vector base = 0x78.
| CTC Channel | Port | Vector Addr | ISR Address | Function |
|---|---|---|---|---|
| CTC1 Ch0 | 0x10 | 0x2070 | 0x02AB | Command receive ISR (ND-100 command handler) |
| CTC1 Ch1 | 0x11 | 0x2072 | 0x1D26 | Error/panic handler |
| CTC1 Ch2 | 0x12 | 0x2074 | 0x0BB8 | Transfer phase ISR (dynamically modified) |
| CTC1 Ch3 | 0x13 | 0x2076 | 0x0505 | SCSI phase ISR (dynamically modified) |
| CTC2 Ch0 | 0x14 | 0x2078 | 0x0AF3 | SCSI reselection handler |
| CTC2 Ch1 | 0x15 | 0x207A | 0x1D26 | Error/panic handler |
| CTC2 Ch2 | 0x16 | 0x207C | 0x141F | CTC calibration ISR |
| CTC2 Ch3 | 0x17 | 0x207E | 0x1AD5 | 7-segment display refresh |
Note: Vectors at 0x2074 and 0x2076 are dynamically modified during SCSI operations to point to different ISRs depending on the current transfer phase.
Boot Sequence¶
flowchart TD
RST0["RST0 (0x0000) - OUT 0x74=0x01, OUT 0x70=0x02"]
MAIN["MAIN (0x003B) - OUT 0x54=0x08, SP=0x2070"]
RAM["MainLoop (0x0086) - RAM Test: 0x00 / 0xFF / addr"]
ERR["Error Display - 7-seg error 0-7, infinite loop"]
POSTINIT["Post-RAM Init - IX=0x2080, IY=0x2100, init CTC"]
NCRTEST["scsi_select_and_verify - Test NCR 5386"]
SELF["selftest_and_init - Bus check, CTC calibrate, data line test"]
CTRLINIT["init_controller_state - probe RAM, clear state, restore vectors"]
IDLE["Main Idle Loop - EI, HALT, process_pending_command"]
RST0 --> MAIN --> RAM
RAM -->|FAIL| ERR
RAM -->|PASS| POSTINIT --> NCRTEST
NCRTEST -->|FAIL| ERR
NCRTEST -->|PASS| SELF
SELF -->|FAIL| ERR
SELF --> CTRLINIT --> IDLE
- RST0 (0x0000) -- Writes 0x01 to port 0x74, 0x02 to port 0x70, jumps to MAIN
- MAIN (0x003B) -- Configures ports 0x54-0x56, sets SP=0x2070, jumps to MainLoop
- MainLoop (0x0086) -- RAM test with three patterns:
- Pattern 1: Fill 0x00, verify (checksum with 0x55 seed)
- Pattern 2: Fill 0xFF, verify
- Pattern 3: Fill with address high byte, verify
- probe_ram_size (0x027B) -- Scans RAM from 0x2000 up, writes 0x55 pattern to detect mirroring. Validates usable size is between 1KB and 8KB
- init_ctc_channels (0x0759) -- Resets all 8 CTC channels, sets IM2 mode (I=0x20), copies ROM vector table to 0x2070
- scsi_select_and_verify (0x012E) -- Tests NCR 5386 connectivity by selecting and reading back ID
- selftest_and_init (0x0161) -- Checks ND-100 bus (port 0x77/0x70), calibrates CTC clock, tests bus data lines with pattern 0x55/0xAA/0x0F/0xF0
- Main idle loop (0x0244) --
EI; HALT; CALL process_pending_command; JR HALT
Detailed Boot Sequence (Z80 Firmware)¶
sequenceDiagram
participant Z80 as Z80 CPU
participant P74 as Port 0x74 (Glue: Drive Select)
participant P70 as Port 0x70 (FD1797 Cmd)
participant P54 as Port 0x54-0x56 (ND-100 Status)
participant CTC as CTC1/CTC2 (8 channels)
participant NCR as NCR 5386 (Port 0x20-0x3D)
participant P77 as Port 0x77 (Glue: Status)
participant SEG as Port 0x40-0x41 (7-seg)
Note over Z80: RST0 (0x0000)
Z80->>P74: OUT 0x74 = 0x01
Z80->>P70: OUT 0x70 = 0x02
Note over Z80: MAIN (0x003B)
Z80->>P54: OUT 0x54=0x08 (self-test flag)
Z80->>P54: OUT 0x55=0x00, OUT 0x56=0x00
Z80->>Z80: SP = 0x2070
Note over Z80: RAM Test (0x0086)
Z80->>Z80: Fill 0x2000-0x3FFF with 0x00, verify
Z80->>Z80: Fill with 0xFF, verify
Z80->>Z80: Fill with addr high byte, verify
Note over Z80: Post-RAM Init (0x00E8)
Z80->>Z80: IX=0x2080, IY=0x2100
Z80->>CTC: init_ctc_channels: reset all, IM2 mode, I=0x20
Z80->>CTC: Set CTC1 Ch0/2/3 vectors to 0x015E (RETI)
Z80->>NCR: ncr5386_reset_parity: OUT 0x2D=0x20, OUT 0x28=0x20
Z80->>NCR: scsi_select_and_verify: test select + ID readback
Note over Z80: selftest_and_init (0x0161)
Z80->>P77: IN (0x77) - check FDC busy
Z80->>P70: IN (0x70) - check bus busy, wait
Z80->>CTC: calibrate_ctc_clock: 6-step binary search
Z80->>P70: OUT 0x73=0x01, OUT 0x70=0x12 (seek track 1)
Z80->>P54: Test bus data lines via write_and_readback_port
Note over Z80: Final Init (0x0227)
Z80->>Z80: init_controller_state: probe RAM, clear state
Z80->>CTC: Restore CTC vector table (0x02AB for Ch0)
Z80->>SEG: setup_7seg_display_scsi_id
Z80->>CTC: Program CTC1 Ch0: control=0x95, constant=0x10
Note over Z80: Idle Loop (0x0244)
Z80->>Z80: EI, HALT - waiting for CTC interrupt
C# Equivalent of Boot Sequence¶
The C# emulator does not emulate the Z80 boot. Here is what an equivalent Reset() or ClearDevice() method should logically do to match the firmware's end state:
/// <summary>
/// Simulates the Z80 firmware boot sequence end-state.
/// On real hardware this takes thousands of Z80 clock cycles.
/// Called on power-on reset or WCONT Clear Device (bit 4).
/// </summary>
private void SimulateZ80Boot()
{
// Step 1: RST0 + MAIN - initial port states
// Port 0x74 = 0x01, Port 0x70 = 0x02
// Port 0x54 = 0x08 (self-test in progress flag to ND-100)
// Port 0x55 = 0x00, Port 0x56 = 0x00
// The ND-100 can read RSTAU and see the controller is in self-test
// Step 2: RAM test - we skip this (no Z80 RAM to test)
// Step 3: CTC init - we don't emulate CTC timers
// On real HW: IM2 mode set, vector table at 0x2070 initialized
// Step 4: NCR 5386 reset and selftest
ncr5386.DeviceReset();
// Firmware writes 0x20 to Aux Status (reset parity)
ncr5386.Write((byte)SCSIRegisters.AuxilaryStatus, 0x20);
// Firmware writes 0x0F to Own ID register (enable all IDs initially)
ncr5386.Write((byte)SCSIRegisters.IDRegister, 0x0F);
// Step 5: CTC calibration - skip (no CTC to calibrate)
// Step 6: Clear all controller state
regs.Clear();
bufferPointer = 0;
readbufferPointer = 0;
dma_bytes_written = 0;
dma_bytes_read = 0;
// Step 7: Set the proper SCSI Own ID from thumbwheel
ncr5386.Write((byte)SCSIRegisters.IDRegister, regs.TW1);
// Step 8: Controller is now ready
// On real HW: Z80 enters HALT loop, CTC1 Ch0 ISR (0x02AB) handles commands
regs.readyForTransfer = true;
// If interrupts were enabled before clear, notify ND-100
// (On real HW: the Z80 would only set ready after full boot)
if (regs.interruptEnabled)
{
SetInterruptBit(true);
}
}
ND-100 to Z80 Command Flow¶
sequenceDiagram
participant ND100 as ND-100 (SINTRAN)
participant HW as Board Hardware
participant CTC as CTC1 Ch0
participant Z80 as Z80 CPU
participant NCR as NCR 5386
ND100->>HW: IOX WRDAT/WLMAR (write data + address)
ND100->>HW: IOX WCONT bit 2 (Active) + bit 0 (Enable IRQ)
HW->>HW: Latch data into shared registers (ports 0x50-0x54)
HW->>CTC: Pulse CLK/TRG input
CTC->>Z80: IM2 interrupt (vector 0x70 -> ISR 0x02AB)
Z80->>HW: IN (0x50), IN (0x54) - read command word
Z80->>Z80: Decode command type (SCSI/floppy/identity)
alt SCSI Command (bit 0 = 1)
Z80->>HW: DMA read config from ND-100 memory
Z80->>NCR: Program NCR registers (Own ID, transfer counter)
Z80->>Z80: ISR chain: 0x1298 -> 0x12BF -> 0x0505
NCR-->>Z80: INT (via CTC1 Ch3) on each phase complete
Z80->>HW: OUT (0x54-0x57) - write results
Z80->>HW: OUT (0x70) = 0xD4 (DMA ready) or 0xD0 (done)
else Floppy Command (bit 4 = 1)
Z80->>HW: IN (0x51-0x53) - read block address
Z80->>HW: DMA read 12-byte config from ND-100
Z80->>HW: OUT (0x74) - drive select
Z80->>HW: OUT (0x73) - FD1797 data (track)
Z80->>HW: OUT (0x70) - FD1797 command (seek/read/write)
else Identity Query
Z80->>HW: DMA write ROM identity block to ND-100 memory
end
HW->>HW: Port 0x70 write detected -> set RSTAU[3] Ready, clear RSTAU[2] Active
HW->>ND100: Assert Level 11 interrupt (RSTAU[0] was set)
ND100->>HW: IOX RSTAU - read status
ND100->>HW: IOX RITRG - read NCR interrupt register (acknowledges)
Trigger Mechanism - RETRACTED (the CTC1 Ch0 counter-mode theory is DISPROVED)¶
The earlier revision claimed CTC1 Ch0 was configured in counter mode with time constant 1, pulsed by the glue logic on every WCONT-Active write, and that this is what wakes the Z80 for every host command. This is wrong. VERIFIED from bytes:
ram:00fa: 11 5e 01 LD DE,0x15e ; stub ISR = "XOR A; RETI" at ram:015e
ram:00fd: ed 53 70 20 LD (0x2070),DE ; IM2 vector slot for CTC1 ch0 (I=0x20, vector 0x70)
ram:0109: 3e 95 LD A,0x95 ; <-- CTC control word
ram:010b: d3 10 OUT (0x10),A
ram:010d: 3e 10 LD A,0x10 ; <-- time constant = 16
ram:010f: d3 10 OUT (0x10),A
ram:0111: fb EI
ram:0112: 06 16 LD B,0x16
ram:0114: 10 fe DJNZ 0x0114 ; delay, allow the timer to fire
ram:0116: a7 AND A ; did the stub ISR run and zero A?
ram:0117: c2 b2 01 JP NZ,0x01b2 ; no -> LED Error 2
Decoding 0x95 = 1001_0101: D0=1 control word, D1=0 no reset, D2=1 time constant follows, D3=0 auto trigger, D4=1 rising edge, D5=0 prescaler /16, D6=0 -> TIMER mode, D7=1 interrupt enable.
So CTC1 Ch0 is in timer mode, prescaler 16, time constant 16 - not counter mode, not TC=1.
No 0xC5 counter-mode control word is written to port 0x10 anywhere in the ROM. What this code
actually is: an interrupt-plumbing selftest. It installs a stub ISR that zeroes A, starts a
timer, delays, and verifies the ISR ran - proving IM2 + CTC interrupts work. Failure lights LED
error 2.
The actual mechanism by which the host wakes the Z80 for a floppy command is not fully
determined. What is VERIFIED: ram:0441 does OUT (0x53),A; HALT - it arms the ND-100 interface
control register and halts awaiting a host-driven interrupt. The command data itself moves over
ports 0x50-0x57 (see the ND-3106/3112 manual section), not through an SRAM mailbox.
None of this applies to SCSI operations, which the ND-100 drives directly against the NCR 5386.
Command Receive ISR (0x02AB)¶
The ISR reads command data from the shared registers:
RST 0x30 ; save registers + RETI
IN A, (0x54) ; read command low byte from ND-100
LD L, A
IN A, (0x50) ; read command high byte from ND-100
LD H, A
BIT 0, A ; check bit 0
JR NZ, SCSI_path ; bit 0 = SCSI command
Command Dispatch¶
Based on the command word read from ports 0x50/0x54:
| Port 0x50 Bits | Command Type | Handler Address | Description |
|---|---|---|---|
| Bit 0 = 1 | SCSI disk I/O | 0x02DE | DMA reads config from ND-100 memory, processes SCSI operation |
| Bit 1 = 1 | Bus control | 0x1B4A | Special bus operations |
| Bit 4 = 1 | Device init | 0x0383 or 0x08A1 | Floppy init (0x08A1) or SCSI device init (0x0383) |
| Bits 1-4 = 0 | Diagnostic | 0x15B6 or 0x0376 | Identity/diagnostic response |
The dispatch at 0x036A uses a computed jump:
LD (0x2196), BC ; store handler address
RST 0x18 ; restore registers
PUSH HL
LD HL, (0x2196) ; load handler address
EX SP, HL ; swap onto stack
EI
RET ; "return" to handler
Z80 to ND-100 Response Flow¶
Completion Signaling¶
After processing, the command handler writes results and signals completion:
; Write result data to shared registers
OUT (0x54), A ; flags/status byte
OUT (0x55), A ; SCSI ID
OUT (0x56), A ; sense/error info
OUT (0x57), A ; completion status (bit 0 = error, bit 1 = special)
; Signal completion via port 0x70
CALL nd100_start_dma_transfer ; writes 0xD4 -> data transfer ready
; or
CALL disconnect_nd100_bus ; writes 0xD0 -> command complete, no data
Port 0x70 Command Values¶
| Value | Meaning | When Used |
|---|---|---|
| 0x00 | Idle / clear | Init, after bus operations |
| 0x10 | Seek / bus select | start_nd100_bus_transfer |
| 0x18 | Bus command with data | nd100_bus_issue_cmd_and_wait |
| 0x88/0xA8 | Read/Write sector | nd100_block_transfer_loop |
| 0xC4 | DMA transfer phase | nd100_bus_dma_transfer |
| 0xD0 | Disconnect (done, no data) | disconnect_nd100_bus |
| 0xD4 | DMA start (data ready) | nd100_start_dma_transfer |
| 0xF0/0xF2 | SCSI execute command | scsi_execute_io_operation |
Interrupt to ND-100¶
The write to port 0x70 causes the board hardware to:
- Set RSTAU bit 3 ("Ready for transfer") / clear bit 2 ("Active")
- If RSTAU bit 0 ("Enable Interrupt") was set by the ND-100 -> assert level 11 interrupt to ND-100
Full Round-Trip Sequence Diagram¶
ND-100 Board HW CTC1 Ch0 Z80
| | | |
|-- IOX WCONT (Active) --> | | |
| |-- latch regs -----> | |
| |-- pulse CLK/TRG --> | |
| | |-- count 1->0 --> |
| | | (INT vec 0x70) |
| | | |-- ISR 0x02AB
| | | |-- IN (0x50-0x54)
| | | |-- process cmd
| | | |-- OUT (0x54-0x57)
| |<--- OUT (0x70) -----------------------------|
| | | |
| |-- set RSTAU[3] ---> | |
| |-- assert IRQ 11 --> | |
|<-- Level 11 interrupt -- | | |
| | | |
|-- IOX RSTAU (read) ----> | | |
ND-100 Side Register Map (IOX)¶
These are the registers visible to the ND-100 host via IOX instructions. The SINTRAN driver uses these symbol names (from embedded driver source in the emulator):
| IOX Offset | Octal | Symbol | R/W | Description |
|---|---|---|---|---|
| 0x00 | 00 | RLMAR | R | Read Memory Address Register bits 0-15 |
| 0x01 | 01 | WLMAR | W | Write Memory Address Register bits 0-15 |
| 0x02 | 02 | REDAT | R | Read Data (16-bit, IOX mode only) |
| 0x03 | 03 | WRDAT | W | Write Data (16-bit, IOX mode only) |
| 0x04 | 04 | RSTAU | R | Read Status |
| 0x05 | 05 | WCONT | W | Write Control |
| 0x06 | 06 | RHMAR | R | Read Memory Address Register bits 16-23 |
| 0x07 | 07 | WHMAR | W | Write Memory Address Register bits 16-23 |
| 0x08 | 10 | RXWC_HI | R | Read External Wordcount bits 16-23 (3204 only) |
| 0x0A | 12 | RXWC | R | Read External Wordcount bits 0-15 (3204 only) |
| 0x20 | 40 | RNDAT | R | Read NCR Data Register |
| 0x21 | 41 | WNDAT | W | Write NCR Data Register |
| 0x22 | 42 | RNCOM | R | Read NCR Command Register |
| 0x23 | 43 | WNCOM | W | Write NCR Command Register |
| 0x24 | 44 | RNCNT | R | Read NCR Control Register |
| 0x25 | 45 | WNCNT | W | Write NCR Control Register |
| 0x26 | 46 | RDESI | R | Read Destination ID Register |
| 0x27 | 47 | WDESI | W | Write Destination ID Register |
| 0x28 | 50 | RAUXS | R | Read Auxiliary Status |
| 0x29 | 51 | WAUXS | W | Write Auxiliary Status |
| 0x2A | 52 | ROIDN | R | Read Own ID Number |
| 0x2B | 53 | WOIDN | W | Write Own ID Number |
| 0x2C | 54 | RITRG | R | Read Interrupt Register |
| 0x2E | 56 | RSOUI | R | Read Source ID |
| 0x32 | 62 | RDIST | R | Read Diagnostic Status |
| 0x38 | 70 | RTCM | R | Read Transfer Counter MSB |
| 0x39 | 71 | WTCM | W | Write Transfer Counter MSB |
| 0x3A | 72 | RTC2 | R | Read Transfer Counter 2nd |
| 0x3B | 73 | WTC2 | W | Write Transfer Counter 2nd |
| 0x3C | 74 | RTCL | R | Read Transfer Counter LSB |
| 0x3D | 75 | WTCL | W | Write Transfer Counter LSB |
RSTAU Status Register Bits (IOX+4, Read)¶
| Bit | Name | Description |
|---|---|---|
| 0 | Interrupt Enabled | From WCONT bit 0 |
| 2 | Busy (Active) | Controller is processing a command |
| 3 | Ready for Transfer | Controller has completed and is ready |
| 4 | Error | OR of error conditions |
| 5 | Reset on SCSI bus | SCSI bus reset detected (*) |
| 6 | NCR 5386 disabled | NCR chip is in disabled state |
| 7 | Single-ended | Single-ended SCSI driver selected |
| 8 | Data Request | Data request from NCR 5386 |
| 9 | NCR Interrupt | Interrupt from NCR 5386 (*) |
| 10 | Data Acknowledge | Data acknowledge to NCR 5386 |
| 11 | BERROR | ND-100 Bus DMA error |
| 12 | BSY | BSY signal from SCSI bus |
| 13 | REQ | REQ signal from SCSI bus |
| 14 | ACK | ACK signal from SCSI bus |
| 15 | Differential | Differential SCSI receivers selected |
(*) These bits generate an interrupt to ND-100 level 11 when bit 0 (Enable Interrupt) is set.
WCONT Control Register Bits (IOX+5, Write)¶
| Bit | Name | Description |
|---|---|---|
| 0 | Enable Interrupt | Allow interrupt to ND-100 level 11 |
| 2 | Active | Start the specified operation |
| 3 | Test | Test mode (MAR increments on read) |
| 4 | Clear Device | Clear controller registers |
| 5 | ND-100 DMA Enable | Allow DMA transfers to/from ND-100 memory |
| 6 | Write ND-100 Memory | Data direction: write into ND-100 memory |
| 10 | Reset SCSI Bus | Assert reset on the SCSI bus |
POST Error Codes¶
On failure, the firmware halts and displays an error code on the 3-digit 7-segment LED display in an infinite loop.
| Error Code (C reg) | Address | Cause |
|---|---|---|
| 0 | 0x01AA | RAM checksum fail (0x55 pattern mismatch) |
| 1 | 0x01AE | RAM write/read verify fail |
| 2 | 0x01B2 | CTC or bus initialization fail |
| 3 | 0x01B6 | NCR 5386 SCSI select/verify fail |
| 4 | 0x01BA | CTC clock calibration fail |
| 7 | 0x01BE | Unknown/fatal error |
Event/Error Code Table (0x1CFB-0x1D3A)¶
Each entry is RST 0x08 followed by a code byte. RST 0x08 saves all registers, captures SP, and enters the command processor at scsi_command_entry_point (0x1D4C) which reads the code byte from the return address.
The error code byte encodes two fields: - Bits 7:6 = severity/mode: 0x00=info (return), 0x40=warning (restart op), 0x80=fatal (full reinit) - Bits 5:0 = error code (matches ND-3106/3112 manual error codes exactly)
The error code is reported to the ND-100 via port 0x55 (DHI) as error_code * 2 (shifted left by 1).
Confirmed Error Codes (from ND-11.021.1 manual)¶
| Address | Byte | Octal | Error Code | Severity | Confirmed Meaning | Firmware Trigger |
|---|---|---|---|---|---|---|
| 0x1CFB | 0x85 | 05 | 0x05 | Fatal | CRC error | NCR parity check failed |
| 0x1CFD | 0x86 | 06 | 0x06 | Fatal | Sector not found | SCSI bus error in reselection handler |
| 0x1CFF | 0x87 | 07 | 0x07 | Fatal | Track not found | Seek retry count exhausted |
| 0x1D01 | 0x88 | 10 | 0x08 | Fatal | Format not found | Seek verify failed after retries |
| 0x1D03 | 0x89 | 11 | 0x09 | Fatal | Diskette defect / Record Not Found | FD1797 status bits 3 or 4 in block transfer |
| 0x1D05 | 0x8A | 12 | 0x0A | Fatal | Format mismatch / Track mismatch | set_status_and_restart (stores B into IY+5) |
| 0x1D0A | 0x8B | 13 | 0x0B | Fatal | Illegal format specified | Invalid format code in init_floppy_drive |
| 0x1D0C | 0x8C | 14 | 0x0C | Fatal | Single sided diskette / Unexpected disconnect | check_scsi_bus_change detects disconnect |
| 0x1D0E | 0x8D | 15 | 0x0D | Fatal | Double sided diskette / Unexpected reconnect | check_scsi_bus_change detects reconnect |
| 0x1D10 | 0x8E | 16 | 0x0E | Fatal | Write protected | FD1797 status bit 6 or SCSI unit attention |
| 0x1D12 | 0x8F | 17 | 0x0F | Fatal | Deleted record / SCSI bus error | scsi_reselection_handler bus error |
| 0x1D14 | 0x90 | 20 | 0x10 | Fatal | Drive not ready | FD1797 status bit 7 or bus timeout |
| 0x1D16 | 0x91 | 21 | 0x11 | Fatal | Controller busy on start | FD1797 busy bit stuck / bus busy after selection |
| 0x1D18 | 0x92 | 22 | 0x12 | Fatal | Lost data (over/underrun) | FD1797 status bit 2 in block transfer |
| 0x1D1A | 0x93 | 23 | 0x13 | Fatal | Track zero not detected / Restore failed | Reselection retry exhausted |
| 0x1D1C | 0x95 | 25 | 0x15 | Fatal | Microprogram out of range (RST 38H) | Z80 executed 0xFF from invalid memory |
| 0x1D1E | 0x96 | 26 | 0x16 | Fatal | Watchdog timeout (~10 sec) | NMI counter at 0x20AA reached zero |
| 0x1D20 | 0x97 | 27 | 0x17 | Fatal | Undefined error / Read-write retries exhausted | scsi_execute_io_operation 3 retries failed |
| 0x1D22 | 0x98 | 30 | 0x18 | Fatal | Track/sector out of range | compute_chs_from_lba exceeds media max |
| 0x1D24 | 0x9A | 32 | 0x1A | Fatal | Compare error / Bus attention | CTC2 Ch0 ISR timeout |
| 0x1D26 | 0x9B | 33 | 0x1B | Fatal | Internal DMA error | NCR transfer counter mismatch |
| 0x1D28 | 0x20 | 40 | 0x20 | Info | ND-100 bus error - command fetch | DMA address readback mismatch |
| 0x1D2C | 0x22 | 42 | 0x22 | Info | ND-100 bus error - data transfer | |
| 0x1D2E | 0xA3 | 43 | 0x23 | Fatal | Illegal command | End of dispatch table (0xFF sentinel) |
| 0x1D32 | 0xA6 | 46 | 0x26 | Fatal | Address register error | DMA address verify mismatch |
| 0x1D34 | 0xA8 | 50 | 0x28 | Fatal | No bootstrap found on diskette | Boot block parse error |
| 0x1D36 | 0xA9 | 51 | 0x29 | Fatal | Wrong bootstrap version | Incompatible floppy monitor |
Error Code to Port 0x55 (DHI) Mapping¶
The error code (lower 6 bits) is shifted left by 1 and written to port 0x55 (DHI). This is what the ND-100 sees when it reads the command block status:
| Error Code | Port 0x55 Value | Meaning |
|---|---|---|
| 0x05 | 0x0A | CRC error |
| 0x06 | 0x0C | Sector not found |
| 0x07 | 0x0E | Track not found |
| 0x08 | 0x10 | Format not found |
| 0x09 | 0x12 | Diskette defect |
| 0x0A | 0x14 | Format mismatch |
| 0x0B | 0x16 | Illegal format |
| 0x0C | 0x18 | Single sided / disconnect |
| 0x0D | 0x1A | Double sided / reconnect |
| 0x0E | 0x1C | Write protected |
| 0x0F | 0x1E | Deleted record / bus error |
| 0x10 | 0x20 | Drive not ready |
| 0x11 | 0x22 | Controller busy |
| 0x12 | 0x24 | Lost data |
| 0x13 | 0x26 | Track 0 not detected |
| 0x15 | 0x2A | Microprogram crash |
| 0x16 | 0x2C | Watchdog timeout |
| 0x17 | 0x2E | Undefined / retries exhausted |
| 0x18 | 0x30 | Track/sector out of range |
| 0x1A | 0x34 | Compare error |
| 0x1B | 0x36 | Internal DMA error |
| 0x20 | 0x40 | ND-100 bus error (cmd fetch) |
| 0x22 | 0x44 | ND-100 bus error (data) |
| 0x23 | 0x46 | Illegal command |
| 0x26 | 0x4C | Address register error |
| 0x28 | 0x50 | No bootstrap found |
| 0x29 | 0x52 | Wrong bootstrap version |
Function Reference¶
NAMING CORRECTION. The names in the tables below came from the original speculative pass and many are wrong - they say "scsi"/"ncr5386" for code that is actually floppy or DMA. 21 functions were renamed in the Ghidra DB (
45900E.bin) to match the verified evidence. Where a table below still shows an old name, the map here wins.
Applied renames (Ghidra DB is authoritative)¶
| Address | Old name (WRONG) | Corrected name |
|---|---|---|
| 0x012e | scsi_select_and_verify | dma9517_channel_register_selftest |
| 0x03f7 | scsi_data_transfer | nd100_arm_iface_and_halt_for_host |
| 0x0614 | scsi_start_io_operation | nd100_write_completion_status |
| 0x06a1 | nd100_start_dma_transfer | fd1797_force_interrupt_d4 |
| 0x06b6 | scsi_enable_selection | dma9517_mask_channel_and_nd100_ctrl |
| 0x0708 | ncr5386_reset_parity | dma9517_master_clear_and_init |
| 0x0711 | ncr5386_set_own_id_and_program | dma9517_program_and_unmask_channel |
| 0x0717 | ncr5386_program_transfer | dma9517_load_mode_addr_count |
| 0x0732 | ncr5386_read_verify_transfer | dma9517_readback_and_verify_addr_count |
| 0x0aff | clear_nd100_bus_attention | floppy_glue_control_write_74 |
| 0x0b46 | start_nd100_bus_transfer | fd1797_seek_to_track |
| 0x0bae | disconnect_nd100_bus | fd1797_force_interrupt_d0 |
| 0x0e8b | nd100_bus_dma_transfer | fd1797_read_address_with_side_select |
| 0x0ee8 | nd100_bus_issue_cmd_and_wait | fd1797_seek_step_issue_command |
| 0x0fe6 | nd100_block_transfer_loop | fd1797_read_write_sector_loop |
| 0x1050 | scsi_select_target | floppy_drive_motor_select_and_busy_poll |
| 0x1090 | nd100_bus_output_and_wait | floppy_drive_select_and_wait_ready |
| 0x10ba | check_scsi_bus_change | floppy_read_status_disk_change |
| 0x11ce | scsi_execute_io_operation | fd1797_format_write_track |
| 0x1afa | setup_7seg_display_scsi_id | ctc2_program_channel3 |
| 0x1d4c | scsi_command_entry_point | controller_full_reset_and_init |
Note the two most misleading of these: scsi_execute_io_operation (0x11ce) is the floppy FORMAT
routine (FD1797 Write Track 0xF0/0xF2 with DMA address polling), and setup_7seg_display_scsi_id
(0x1afa) is CTC2 channel 3 programming - nothing to do with either the display or SCSI.
Still-suspect names (NOT yet renamed - no byte-level evidence gathered)¶
These carry SCSI-flavoured names but, given that no NCR access exists in this ROM, are very likely misnamed too. Treat with suspicion until swept:
scsi_transfer_with_disconnect (0x03f1), setup_dma_and_start_scsi (0x045a),
scsi_select_and_send (0x0a37), scsi_reselection_handler (0x0a5d),
setup_ncr_dma_pointer (0x0b0b), set_scsi_target_id (0x103c),
dispatch_by_scsi_id (0x1a95), add_scsi_id_offset_to_hl (0x10df),
scsi_init_data_buffer (0x06c9), scsi_setup_dma_and_select (0x06d2),
setup_scsi_params_table_a (0x10e9), setup_scsi_buffers_from_table (0x10ee),
copy_cdb_and_setup_lba (0x0aa2).
Boot and Initialization¶
| Address | Name | Description |
|---|---|---|
| 0x0000 | RST0 | Reset vector, init ports 0x74/0x70, jump to MAIN |
| 0x003B | MAIN | Set SP, configure ports 0x54-0x56, jump to RAM test |
| 0x0086 | MainLoop | POST RAM test with 3 patterns (0x00, 0xFF, address) |
| 0x0161 | selftest_and_init | POST: bus check, CTC calibration, data line test, enter idle loop |
| 0x024A | process_pending_command | Check 0x2000 for pending command, dispatch if non-zero |
| 0x0253 | init_controller_state | Set IX=0x2080, probe RAM, clear state, wait for bus ready |
| 0x027B | probe_ram_size | Detect RAM top by writing 0x55, validate size bounds |
| 0x0759 | init_ctc_channels | Reset all CTC channels, set IM2 mode, copy vector table |
| 0x1395 | calibrate_ctc_clock | Binary search CTC calibration over 6 steps targeting 0x4000 |
SCSI Operations¶
| Address | Name | Description |
|---|---|---|
| 0x1D4C | scsi_command_entry_point | Main command processor: save context, extract ID/mode, dispatch |
| 0x0614 | scsi_start_io_operation | Prepare SCSI I/O, write status to ports 0x54-0x57, start transfer |
| 0x11CE | scsi_execute_io_operation | Core SCSI I/O: send CDB, handle phases via HALT+IRQ, retry x3 |
| 0x03F1 | scsi_transfer_with_disconnect | SCSI DMA transfer entry with disconnect/reselect enabled |
| 0x03F7 | scsi_data_transfer | DMA transfer via ND-100 bus, wait for interrupt completion |
| 0x0A37 | scsi_select_and_send | Arbitrate, select SCSI target, dispatch command |
| 0x0A5D | scsi_reselection_handler | Handle SCSI bus reselection, restore state, re-initiate |
| 0x06B6 | scsi_enable_selection | Enable SCSI bus selection via NCR Own ID register |
| 0x06C9 | scsi_init_data_buffer | Reset data buffer pointer to 0x2200, fall through to DMA setup |
| 0x06D2 | scsi_setup_dma_and_select | Program NCR transfer registers, handle bus selection |
| 0x012E | scsi_select_and_verify | POST: select target and verify ID readback |
| 0x103C | set_scsi_target_id | Convert SCSI ID bitmask to target number |
| 0x1050 | scsi_select_target | SCSI arbitration/selection through bus interface |
| 0x10BA | check_scsi_bus_change | Detect bus sense mismatch, call disconnect/reconnect handlers |
| 0x138A | store_status_by_unit | Store status byte in per-unit table at 0x2125 |
AM9517 DMA Functions (was: "NCR 5386 Functions" - ENTIRELY MISNAMED)¶
There are no NCR 5386 functions in this ROM. Every function previously in this section is AM9517 DMA code. Renamed in the Ghidra DB:
| Address | Old name (WRONG) | Corrected name | What it actually does |
|---|---|---|---|
| 0x0708 | ncr5386_reset_parity | dma9517_master_clear_and_init |
AM9517 Master Clear (0x2D) + Command register write 0x20 (0x28) |
| 0x0711 | ncr5386_set_own_id_and_program | dma9517_program_and_unmask_channel |
Program channel addr/count, then unmask via Single Mask (0x2A) |
| 0x0717 | ncr5386_program_transfer | dma9517_load_mode_addr_count |
Write Mode (0x2B), clear byte pointer (0x2C), load addr+count at 0x20+2n |
| 0x0732 | ncr5386_read_verify_transfer | dma9517_readback_and_verify_addr_count |
Read back current addr/count, verify vs expected |
ND-100 Bus Interface¶
| Address | Name | Description |
|---|---|---|
| 0x048C | set_nd100_dma_address | Program DMA addr ports 0x50-0x52, verify via 0x55-0x57 |
| 0x04C3 | read_nd100_dma_address | Read current DMA address from ports 0x55-0x57 |
| 0x04CC | update_dma_transfer_remaining | Decrement remaining count, flag completion or adjust final block |
| 0x06A1 | nd100_start_dma_transfer | Write 0xD4 to port 0x70 (DMA start), set phase counter |
| 0x06B0 | nd100_resume_transfer | Re-issue bus command to continue/retry transfer |
| 0x0B46 | start_nd100_bus_transfer | Set up ports 0x70-0x74, handle reselection, enable IRQ |
| 0x0BAE | disconnect_nd100_bus | Write 0xD0 to port 0x70 (disconnect/done) |
| 0x0CC5 | resume_nd100_bus_transfer | Restore bus state, reprogram NCR DMA, resume transfer |
| 0x0FE6 | nd100_block_transfer_loop | Block-level sector transfer loop between SCSI and ND-100 |
| 0x1090 | nd100_bus_output_and_wait | Output to port 0x77/0x74, poll for ACK with 100-iteration timeout |
| 0x0F1E | store_cmd_and_poll_nd100 | Save command to 0x2108, poll port 0x70 bit 0 until not busy |
| 0x0F21 | poll_nd100_bus_ready | Poll port 0x70 bit 0, check bit 7 for errors |
| 0x0F15 | wait_nd100_bus_with_delay | Poll bus ready then add timed delay |
| 0x0E8B | nd100_bus_dma_transfer | Multi-phase DMA transfer with NCR 5386 setup |
| 0x0ED4 | nd100_bus_cmd_48 | Issue bus command 0x48 (short transfer) |
| 0x0ED8 | nd100_bus_cmd_reselect | Initiate SCSI reselection via bus interface |
| 0x0EE8 | nd100_bus_issue_cmd_and_wait | Output data+command to bus, wait, handle reselection retries |
| 0x1590 | send_default_buffer_to_nd100 | Set HL=0x2200, fall through to send_data_to_nd100_bus |
| 0x1593 | send_data_to_nd100_bus | Send DE bytes via port 0x57 with interrupt handshaking |
Floppy Operations¶
| Address | Name | Description |
|---|---|---|
| 0x08A1 | init_floppy_drive | Full FD1797 init: reset, clear DCB, set format (26/15/8 spt) |
| 0x09C7 | get_head_count | Extract head count from config bits 7:6 of 0x2081 |
| 0x09D4 | build_drive_select_mask | Build FD1797 drive select bitmask with density/motor flags |
| 0x09F4 | compute_chs_from_lba | Convert linear block address to cylinder/head/sector |
| 0x0E08 | clear_drive_select | Clear drive status byte 0x2105, fall through to seek |
| 0x0E0E | seek_and_select_drive | Seek/head-positioning with retry and recalibration |
Disk Geometry and Buffers¶
| Address | Name | Description |
|---|---|---|
| 0x07A4 | setup_disk_geometry | Init geometry from device descriptor: sector size, buffer, track layout |
| 0x078F | advance_write_buffer_index | Increment ring buffer write pointer with wrap |
| 0x0795 | advance_read_buffer_index | Increment ring buffer read pointer with wrap |
| 0x0AA2 | copy_cdb_and_setup_lba | Copy SCSI CDB to working buffer, byte-swap LBA |
| 0x0AD1 | init_disk_transfer_state | Compute CHS, clear status accumulators and flags |
| 0x0AFF | clear_nd100_bus_attention | Deassert attention bit on port 0x74 |
| 0x0B0B | setup_ncr_dma_pointer | Compute and program NCR DMA address registers |
| 0x0CBA | get_drive_table_entry_ptr | Return pointer into sector table at 0x2139 by sector number |
| 0x10E9 | setup_scsi_params_table_a | Load parameter table A (0x115D), fall through to buffer setup |
| 0x10EE | setup_scsi_buffers_from_table | Init SCSI RAM buffers and data structures from config table |
| 0x1143 | copy_counted_blocks | Copy multiple sized data blocks from table to destination |
| 0x1150 | memfill_byte_to_de | Fill BC bytes at (DE) with single byte from (HL) |
| 0x1157 | calc_remaining_size | Compute DE = DE - BC (remaining buffer space) |
| 0x045A | setup_dma_and_start_scsi | Set up DMA address, issue SCSI command to NCR 5386 |
Display¶
| Address | Name | Description |
|---|---|---|
| 0x1AFA | setup_7seg_display_scsi_id | Convert SCSI ID to 7-seg patterns, program CTC3 for refresh |
| 0x1B23 | digit_to_7seg_pattern | Lookup table: digit (0-7) -> 7-segment bit pattern |
| 0x1B38 | set_7seg_bits | OR bits into display state, write to port 0x41 |
| 0x1B3F | clear_7seg_bits | AND-complement bits from display state, write to port 0x41 |
Utility¶
| Address | Name | Description |
|---|---|---|
| 0x004B | WaitLoopB | Nested delay: A outer loops x B inner loops |
| 0x0057 | WaitLoop_A | Inner delay loop: B iterations of LD R,A; LD R,A; NOP; NOP |
| 0x0060 | ctc_write_port_pair_tail | Shared tail: OUT (C),E; OUT (C),D; POP DE; RET |
| 0x087A | divide_24by16 | 24-bit / 16-bit unsigned division, quotient in HL |
| 0x12D4 | short_delay | Busy-wait: 16 DJNZ iterations (~50us at 4MHz) |
| 0x10DF | add_scsi_id_offset_to_hl | Add SCSI target ID (from 0x2103) to HL for array indexing |
| 0x1A87 | write_and_readback_port | Write A to port C, wait, read back from port B |
| 0x1A95 | dispatch_by_scsi_id | Scan dispatch table for matching SCSI ID, redirect execution |
| 0x1D05 | set_status_and_restart | Store B as status code, re-enter command processor |
RAM Variable Reference¶
Controller State (IX = 0x2080)¶
| Address | Name | Description |
|---|---|---|
| 0x2080 | controller_config | Configuration byte (bit 2: drive ready, bit 3: density) |
| 0x2081 | drive_config_byte | Bits 7:6 = head count encoding |
| 0x2082 | cylinder_position | Current cylinder/head position (byte-swapped) |
| 0x2085 | scsi_cdb_working | 7-byte SCSI CDB working copy |
| 0x208C | scsi_id_times2 | Current SCSI ID * 2 |
| 0x208D | io_flags | I/O operation flags (bit 3: data, bit 4: error, bit 7: abort) |
| 0x208E | scsi_status_copy | Copy of SCSI status |
| 0x208F | scsi_sense_copy | Copy of SCSI sense data |
| 0x2091 | geometry_working | 7-byte working copy of drive geometry |
| 0x2098 | media_descriptor | Media type descriptor (from port 0x50/0x54 command word) |
| 0x209D | device_type | Device type code (0x1E = special, 0x2C = floppy) |
DMA and Transfer State¶
| Address | Name | Description |
|---|---|---|
| 0x20A5 | state_flag_a5 | General state flag |
| 0x20AF | ram_top_address | Detected top of usable RAM |
| 0x20B1 | ram_usable_size | Usable RAM size |
| 0x20B3 | ram_size_minus_2200 | RAM available for buffers |
| 0x20B5 | sector_size | Computed sector size in bytes |
| 0x20B7 | ring_buf_max_count | Ring buffer maximum entry count |
| 0x20B8 | ring_buf_write_idx | Ring buffer write pointer |
| 0x20B9 | ring_buf_read_idx | Ring buffer read pointer |
| 0x20BA | scsi_bus_status | Current SCSI bus status |
| 0x20BB | transfer_result | Result code of last transfer |
| 0x20BC | nd100_bus_mode | ND-100 bus mode register cache |
| 0x20BF | scsi_target_id | Current SCSI target ID |
| 0x20C0 | dma_buffer_address | Z80 DMA buffer address (usually 0x2200) |
| 0x20C2 | dma_transfer_count | Current DMA block transfer count |
| 0x20C4 | nd100_dma_addr_lo | ND-100 DMA address bits 0-15 |
| 0x20C6 | nd100_dma_addr_hi | ND-100 DMA address bits 16-23 |
| 0x20C7 | dma_remaining_lo | Remaining transfer count bits 0-15 |
| 0x20C9 | dma_remaining_hi | Remaining transfer count bits 16-23 |
| 0x20CA | current_track_ptr | Current track pointer |
| 0x20D5 | state_flag_d5 | State flag |
| 0x20D6 | reselection_flag | SCSI reselection pending flag |
| 0x20DA | total_capacity | Total device capacity |
Display and Watchdog¶
| Address | Name | Description |
|---|---|---|
| 0x20A4 | display_state | Current 7-segment display register state |
| 0x20A7 | display_digit_ones | 7-seg pattern for ones digit |
| 0x20A8 | display_digit_tens | 7-seg pattern for tens digit |
| 0x20A9 | display_digit_prefix | 7-seg pattern for prefix digit (0 or E) |
| 0x20AA | nmi_watchdog_counter | NMI-decremented watchdog; zero triggers reset |
Drive Control Block (IY = 0x2100)¶
| Address | Name | Description |
|---|---|---|
| 0x2100 | drive_control_block | Base of drive control block |
| 0x2101 | dcb_head_count_alt | Alternate head count storage |
| 0x2102 | dcb_select_mask_alt | Alternate drive select mask |
| 0x2103 | dcb_head_count | Number of heads |
| 0x2104 | dcb_drive_select | Drive select bitmask for FD1797 |
| 0x2105 | dcb_drive_status | Current drive status byte |
| 0x2106 | motor_status | Motor on/off status |
| 0x2107 | sector_count_in_block | Sector count within current block |
| 0x2108 | last_bus_command | Last command written to port 0x70 |
| 0x2109 | last_bus_status | Status read back from port 0x70 |
| 0x210B | media_type_copy | Copy of media type from 0x2098 |
| 0x210D | scsi_config_flags | SCSI configuration flags |
| 0x210E | drive_error_flags | Drive error accumulator |
| 0x2113 | drive_ready_status | Drive ready/status byte |
| 0x2115 | scsi_interrupt_status | NCR 5386 interrupt status byte |
| 0x2120 | error_merge_byte | Merged error flags for status reporting |
SCSI Buffer Configuration¶
| Address | Name | Description |
|---|---|---|
| 0x2124 | current_drive_select | Currently selected drive byte |
| 0x2125 | per_unit_status_table | Per-unit status array (indexed by unit number) |
| 0x2129 | target_descriptor_table | SCSI target descriptor array |
| 0x2130 | ctc_calibration_value | CTC clock calibration result |
| 0x2131 | scsi_cmd_buffer_size | Size of SCSI command buffer (at 0x2200) |
| 0x2133 | scsi_data_buffer_size | Size of SCSI data buffer (at 0x22A0) |
| 0x2135 | scsi_status_buffer_size | Size of SCSI status buffer (0x50 = 80 bytes) |
| 0x2137 | scsi_params_base_ptr | Pointer to SCSI parameter table base |
| 0x2139 | sector_table | Sector mapping table |
Context Save Area¶
| Address | Name | Description |
|---|---|---|
| 0x2178 | saved_disk_params | 12-byte saved disk parameters (for disconnect/reconnect) |
| 0x2184 | saved_context | 14-byte saved CPU context |
| 0x218E | saved_sp | Saved stack pointer |
| 0x2192 | saved_iy | Saved IY register |
| 0x2194 | scsi_device_id | SCSI device ID from command |
| 0x2195 | cmd_mode_flags | Command mode flags (0x00=return, 0x40=normal, 0x80=target) |
Deep Dive: NCR 5386 Interrupt Path to ND-100 - RETRACTED IN FULL¶
DISPROVED. DO NOT USE. Retained only so that readers holding the old text can see exactly what was withdrawn and why.
This entire section is built on the claim that the Z80 reads and writes NCR 5386 registers at ports 0x20-0x3D. It does not. An exhaustive byte-level sweep of every I/O opcode in the ROM found no NCR access of any kind: that port range is the AM9517 DMA controller, and the only command-code stream in the ROM goes to the FD1797 at port 0x70. See "THE ARCHITECTURE" at the top of this document.
Consequently the following are all fiction: the NCR INT -> CTC1 Ch3 wiring, the dynamic ISR chain at (0x2076) "servicing SCSI phases", the three-phase SCSI ISR sequence (0x1298 / 0x12BF / 0x0505), and the claim that NCR interrupts reach the ND-100 only after Z80 processing.
What is actually true: the NCR 5386 interrupts the ND-100 directly (RSTAU bit 9 = "NCR Interrupt"; level 11 when WCONT bit 0 enabled). SINTRAN's
SCINThandler services it. The Z80 is not in this path at any point.The CTC channels and ISRs named below are real, but they serve the floppy half. Their true assignment is not established; the earlier mapping was derived from the false NCR premise and should not be trusted.
How NCR 5386 interrupts reach the Z80 - RETRACTED (see warning above)¶
flowchart LR
NCR["NCR 5386 INT pin"] -->|pulse| CTC["CTC1 Ch3, port 0x13, TC=1"]
CTC -->|IM2 vector 0x76| VEC["RAM 0x2076 - dynamic ISR addr"]
VEC -->|Phase 1| ISR1["ISR 0x1298 - Send CDB bytes"]
VEC -->|Phase 2| ISR2["ISR 0x12BF - DMA data transfer"]
VEC -->|Phase 3| ISR3["ISR 0x0505 - Status/completion"]
ISR1 -->|update 0x2076| VEC
ISR2 -->|update 0x2076| VEC
ISR3 --> DONE["Mainline resumes after HALT"]
DONE --> PORT70["OUT 0x70 = 0xD0 or 0xD4"]
PORT70 --> ND100["ND-100 Level 11 IRQ"]
The NCR 5386 INT pin is wired to CTC1 Channel 3 CLK/TRG input (port 0x13). It does NOT connect directly to the Z80's INT pin.
When the NCR 5386 asserts INT (SCSI operation complete, reselection, bus service, etc.): 1. CTC1 Ch3 counts down 1->0 (counter mode, TC=1) 2. Fires IM2 interrupt with vector from 0x2076 3. Z80 executes the dynamic ISR pointed to by 0x2076
Dynamic ISR chaining during SCSI I/O¶
The vector at 0x2076 is dynamically modified during SCSI operations. scsi_execute_io_operation (0x11CE) uses three SCSI phases, each with its own ISR:
Phase 1 - Command out (0x2076 -> 0x1298 or 0x12A6):
ISR 0x1298: Sends command bytes to NCR data ports (0x20/0x21),
writes Own ID register (0x2A/0x2B),
stores next ISR address into (0x2076), EI, RETI
ISR 0x12A6: Same but also writes to port 0x73 (drive data),
used for wide/special commands
Phase 2 - Data transfer (0x2076 -> 0x12BF):
ISR 0x12BF: If B > 0 (more blocks): manipulates return stack
so RETI returns to polling loop at 0x1245
If B = 0 (last block): writes transfer counter to NCR,
chains to next ISR via (0x2076) update
Phase 3 - Status/completion (0x2076 -> 0x0505):
ISR 0x0505: Default disk I/O handler - reads status from
port 0x28, handles seek completion and sector positioning
Between phases, the mainline code HALTs waiting for each NCR interrupt.
How it reaches the ND-100¶
The NCR 5386 interrupt does NOT propagate directly to the ND-100. The Z80 processes it first:
NCR 5386 INT pin
|
v
CTC1 Ch3 CLK/TRG (port 0x13, counter mode, TC=1)
|
v
Z80 IM2 interrupt -> ISR from (0x2076)
- Processes NCR data (reads/writes NCR registers 0x20-0x3D)
- Chains ISRs for multi-phase SCSI operations
- After all phases: mainline code resumes after HALT
|
v
Z80 writes port 0x70 (0xD0=done, 0xD4=DMA ready, etc.)
|
v
Board hardware sets RSTAU[3] (Ready), clears RSTAU[2] (Active)
If RSTAU[0] (Enable Interrupt) was set -> assert ND-100 level 11 IRQ
SCSI reselection interrupt path¶
CTC2 Ch0 (port 0x14, ISR at 0x0AF3) handles SCSI reselection events separately. This ISR: - Immediately writes 0xD0 to port 0x70 (disconnect/abort) - Clears drive select (port 0x74) - Enters event handler 0x1D24
This suggests the NCR 5386 has a separate reselection signal routed to CTC2 Ch0, distinct from its main INT output on CTC1 Ch3.
Uncertainty¶
- I cannot verify from firmware alone which NCR 5386 pin connects to which CTC channel. The firmware evidence strongly suggests Ch3 for normal operations and CTC2 Ch0 for reselection, but confirming requires the board schematic.
- The NCR 5386 has multiple interrupt sources (Function Complete, Bus Service, Disconnect, Reconnect, etc.) but only one INT pin. The two CTC channels may be driven by different board logic signals derived from NCR status, not directly from the INT pin.
Deep Dive: FD1797 Floppy Disc Controller¶
This and the AM9517 / CTC / Floppy Boot deep-dives below describe the floppy half - what the 45900E.bin ROM actually implements. Per the ND-3106/3112 manual section near the end of this document, the Z80-side interface is identical to those floppy controllers - port 0x74 is literally
FDVSEL(Floppy Drive Select). The Z80 half of the ND-3201 is essentially an ND-3112.
Confirmed Port Mapping¶
Ports 0x70-0x73 are the FD1797's four registers. This is confirmed by matching command encodings:
| Z80 Port | FD1797 Register | Evidence |
|---|---|---|
| 0x70 | Command/Status | All FD1797 commands written here; status polled here |
| 0x71 | Track Register | Read to verify current track position |
| 0x72 | Sector Register | Written with sector number before read/write |
| 0x73 | Data Register | Written with target track before seek commands |
FD1797 Commands Used in Firmware¶
| Command | Value | Locations | Description |
|---|---|---|---|
| Restore | 0x00 | 0x08BF, 0x026E | Seek to track 0, polls busy bit |
| Seek | 0x12 | 0x0192 | Seek with head load (h=1) |
| Seek | 0x18 | 0x0EF0 | Seek with head load + verify |
| Seek | 0x1C | 0x0DED | Seek with flags from (IY+0x14) |
| Step-In | 0x48 | 0x0ED4 | Step toward higher tracks |
| Step-In | 0x4C | 0x0DC9 | Step-in with verify |
| Read Sector | 0x88/0x8C | 0x0C5C-0x0CA3 | Bit 2=multi-sector, bit 0=side |
| Read Address | 0xC4 | 0x0EB3 | Read sector ID field |
| Force Interrupt | 0xD0 | 0x0AF4, 0x0BB4 | Abort current operation |
| Force Interrupt | 0xD4 | 0x06A6 | Interrupt on index pulse |
| Write Track | 0xF0/0xF2 | 0x121E | Format track (F2 = with side select) |
Glue Logic Ports (0x74-0x77)¶
These are NOT FD1797 registers. They are board-level control/status registers implemented in glue logic (PAL/GAL or discrete):
| Port | Direction | Function | Evidence |
|---|---|---|---|
| 0x74 | Write only | Drive select + DMA direction control | Bit 5 controls DMA direction; cleared at reset; set with drive mask from 0x2104 |
| 0x75 | Write only | Secondary control (side select?) | Written 0x00 during init at 0x08B9, 0x0257 |
| 0x76 | Write only | Timer/prescaler control | Only written during CTC calibration at 0x13BF |
| 0x77 | Read/Write | Drive status input + motor control | Read: bit 0=FDC busy, bit 1=head loaded/WP; Write: motor control |
Port 0x70 Dual Purpose¶
Port 0x70 serves as BOTH the FD1797 command register AND is monitored by the ND-100 bus interface glue logic. When the Z80 writes a command to port 0x70: - The FD1797 executes the floppy command (seek, read, write, etc.) - The board glue logic simultaneously interprets certain values (especially 0xD0/0xD4) as bus status signals to the ND-100 interface
I don't know the exact mechanism of this dual routing. It could be: - Simple address decode: FD1797 always receives the write, AND a latch captures it for the bus interface - Or the glue logic uses port 0x74 state to determine routing
Deep Dive: AM9517 DMA Controller¶
flowchart LR
subgraph Z80_Side["Z80 CPU"]
Z80["Z80 programs FD1797 via port 0x70-0x73"]
Z80RAM["Z80 RAM 0x2000-0x3FFF"]
end
subgraph DMA["AM9517 DMA Controller"]
DREQ["DREQ input"]
DACK["DACK output"]
ADDR["Address bus"]
DATA["Data bus"]
end
subgraph FD["FD1797 Floppy Controller"]
CMD["Command/Status port 0x70"]
DRQ["DRQ output"]
FDATA["Data Register"]
end
subgraph GLUE["Glue Logic"]
DIR["Port 0x74 bit 4/5 = DMA direction"]
end
Z80 -->|OUT 0x70 = Read/Write Sector cmd| CMD
CMD -->|byte ready| DRQ
DRQ --> DREQ
DACK --> FDATA
FDATA <-->|byte| DATA
DATA <-->|byte| Z80RAM
ADDR -->|address| Z80RAM
Z80 -->|OUT 0x74| DIR
DIR -->|direction control| DMA
style DMA fill:#ff9
Not Directly Z80-Accessible¶
There are zero Z80 I/O port instructions to any AM9517 registers in the firmware. The AM9517 is entirely hardware-controlled by the board glue logic.
The AM9517 sits between the FD1797 DRQ output and the Z80 RAM bus: - FD1797 DRQ pin -> AM9517 DREQ input - AM9517 address bus -> Z80 RAM address bus - AM9517 data bus -> Z80 RAM data bus
During floppy read/write sector operations: 1. Z80 sets up the transfer via port 0x74 (DMA direction, drive select) 2. Z80 issues Read Sector or Write Sector command to FD1797 via port 0x70 3. Z80 HALTs waiting for interrupt 4. FD1797 asserts DRQ for each byte 5. AM9517 handles byte-by-byte DMA between FD1797 and Z80 RAM autonomously 6. When the sector transfer completes, FD1797 deasserts BUSY and asserts INTRQ 7. The interrupt wakes the Z80
The Z80 firmware never reads FD1797 port 0x73 (Data Register) via IN instruction during normal operation - 0 matches found. This confirms all data transfer goes through the AM9517 DMA, not programmed I/O.
How Port 0x74 Controls DMA Direction¶
In nd100_bus_dma_transfer (0x0E8B), port 0x74 is written with:
- Bit 4 toggled: alternates transfer direction (read from disk vs. write to disk)
- Bit 5: set/cleared based on port 0x77 status
This configures the AM9517's direction control through the glue logic.
Deep Dive: CTC Channel Usage¶
flowchart TB
subgraph CTC1["CTC1 (ports 0x10-0x13)"]
C1C0["Ch0 - Timer - ND-100 cmd reception - ISR 0x02AB"]
C1C1["Ch1 - Timer - Calibration reference - ISR 0x1D26"]
C1C2["Ch2 - External? - SCSI bus phase - ISR 0x0BB8"]
C1C3["Ch3 - External - NCR INT handler - ISR DYNAMIC"]
end
subgraph CTC2["CTC2 (ports 0x14-0x17)"]
C2C0["Ch0 - SCSI timeout watchdog - ISR 0x0AF3"]
C2C1["Ch1 - UNUSED - ISR 0x1D26 error trap"]
C2C2["Ch2 - Counter - Calibration high byte - ISR 0x141F"]
C2C3["Ch3 - Timer - 7-seg display refresh - ISR 0x1AD5"]
end
subgraph Triggers["External Trigger Sources"]
NDBUS["ND-100 Bus Interface Logic"]
NCRINT["NCR 5386 INT pin"]
NCRSEL["NCR 5386 Reselection?"]
end
NDBUS -.->|pulse on WCONT Active| C1C0
NCRINT -.->|INT assertion| C1C3
NCRSEL -.->|timeout or reselect| C2C0
C2C3 -.->|ZC/TO cascade| C2C2
CTC1 (ports 0x10-0x13)¶
CTC1 Ch0 (port 0x10) - ND-100 Command Reception¶
- Mode: Timer mode (control word 0x95), prescaler 16, time constant 0x10 (16)
- Period: 16 x 16 = 256 clock cycles
- ISR: 0x02AB (command receive handler)
- Purpose: Generates periodic interrupts to wake the Z80 from HALT. The ISR reads command data from the ND-100 bus interface (ports 0x50-0x54).
- Note: During early init, vector temporarily set to 0x015E (RETI, do nothing)
- Uncertainty: Earlier analysis suggested counter mode with external trigger from ND-100 bus. The control word 0x95 decodes as timer mode. I cannot determine from firmware alone whether the CLK/TRG pin also receives an external signal that would re-trigger the timer. The practical effect is the same: the Z80 wakes periodically and checks for commands.
CTC1 Ch1 (port 0x11) - Calibration Reference Timer¶
- Mode: Timer mode (control word 0xB5), prescaler 256, time constant 0xFF (255)
- Period: 256 x 255 = 65,280 clock cycles
- ISR: 0x1D26 (error handler - should never fire during normal operation)
- Purpose: Used during CTC clock calibration only. Provides a known time reference.
- Dynamic: Vector at 0x2072 temporarily changed to 0x141F during calibration, then restored.
CTC1 Ch2 (port 0x12) - SCSI Bus Phase Monitor¶
- Mode: Unknown - no explicit OUT to port 0x12 found in firmware
- ISR: 0x0BB8 (reads port 0x70 status, handles DMA direction, manages sector counting)
- Purpose: Fires on SCSI bus phase changes during I/O operations
- Uncertainty: May be triggered via CTC cascade (Ch1 ZC/TO -> Ch2 CLK/TRG) or by external hardware. No direct programming found.
CTC1 Ch3 (port 0x13) - NCR 5386 Interrupt Handler¶
- Mode: Unknown - no explicit OUT to port 0x13 found in firmware
- ISR: 0x0505 (default), dynamically changed to 0x1298/0x12A6/0x12BF during SCSI I/O
- Purpose: Primary NCR 5386 interrupt receiver. Handles SCSI data transfer phases.
- Dynamic: Vector at 0x2076 is the most heavily modified vector - switched between ISRs during every SCSI operation.
- Uncertainty: Same as Ch2 - no direct programming found, likely triggered by NCR 5386 INT via external CLK/TRG wiring.
CTC2 (ports 0x14-0x17)¶
CTC2 Ch0 (port 0x14) - SCSI Timeout / Reselection Watchdog¶
- Mode: Unknown - only vector base written
- ISR: 0x0AF3 (writes 0xD0 to port 0x70, clears port 0x74, resets)
- Purpose: SCSI operation timeout. When it fires, aborts the current SCSI operation.
- Uncertainty: Programming may happen through RST 0x20 dispatch (C=0x14 seen in ISR).
CTC2 Ch1 (port 0x15) - Unused¶
- Mode: Not programmed
- ISR: 0x1D26 (error handler - safety trap)
- Purpose: Unused. If it ever fires, indicates a hardware fault.
CTC2 Ch2 (port 0x16) - Calibration Counter (High Byte)¶
- Mode: Counter mode (0x57), time constant 0xFF (255) during calibration
- ISR: 0x141F (calibration completion - reads counter values, stops timers)
- Purpose: Forms cascaded 16-bit counter with CTC2 Ch3 for clock measurement
- Active only during: calibrate_ctc_clock (0x1395)
CTC2 Ch3 (port 0x17) - 7-Segment Display Refresh¶
- Mode: Timer mode (0xB5), prescaler 256, time constant 0x80 (128)
- Period: 256 x 128 = 32,768 clock cycles
- ISR: 0x1AD5 (cycles through 3 display digits, outputs patterns to ports 0x40/0x41)
- Purpose: Multiplexes the 3-digit 7-segment display showing SCSI ID
- Also used during: CTC calibration as cascaded counter low byte (Ch3 ZC/TO -> Ch2 CLK/TRG)
Deep Dive: Floppy Boot / Init Path¶
flowchart TD
CMD["ND-100 writes WCONT with Active"]
ISR["CTC1 Ch0 ISR at 0x02AB"]
READ["IN port 0x50, IN port 0x54"]
CHK0{"Port 0x50 bit 0?"}
CHK4{"Port 0x50 bit 4?"}
SCSI["SCSI command path"]
DEVTYPE{"Device type 0x209D?"}
FLOPPY["init_floppy_drive (0x08A1)"]
SCSIINIT["SCSI-style init (0x0383)"]
FD1797["FD1797: Restore, Seek, Read/Write"]
AM9517["AM9517 DMA: byte transfer"]
COMPLETE["OUT port 0x70 - signal completion"]
CMD --> ISR --> READ --> CHK0
CHK0 -->|1 = SCSI| SCSI
CHK0 -->|0| CHK4
CHK4 -->|1 = device init| DEVTYPE
CHK4 -->|0| IDENTITY["Identity query (0x15B6)"]
DEVTYPE -->|0x2C = floppy| FLOPPY
DEVTYPE -->|0x1E = SCSI disk| SCSIINIT
FLOPPY --> FD1797 --> AM9517 --> COMPLETE
SCSIINIT --> COMPLETE
How a Floppy Command Reaches the Z80¶
When the ND-100 sends a floppy command via IOX WCONT:
- CTC1 Ch0 fires, ISR at 0x02AB reads ports 0x50/0x54
- Port 0x50 bit 0 = 0 (NOT a SCSI command)
- Port 0x50 bit 4 = 1 (device init command)
- ISR reads additional data from ports 0x51-0x53 (block address)
- DMA reads 12 bytes of device config from ND-100 memory into 0x2080
- Checks device type at 0x209D:
- 0x1E -> handler at 0x0383 (SCSI device via floppy-style init)
- Other (0x2C = floppy) -> handler at 0x08A1 (
init_floppy_drive)
Floppy Init Handler (0x08A1 = init_floppy_drive)¶
- Sets IY = 0x2100 (drive control block)
- Calls
disconnect_nd100_bus(writes 0xD0 to port 0x70 = FD1797 Force Interrupt) - Checks motor status at 0x2106
- If motor running: writes 0x00 to ports 0x74 (drive deselect), 0x75 (side deselect), 0x70 (FD1797 Restore), polls port 0x70 bit 0 until not busy
- Clears 29 bytes of drive control block at 0x2100
- Copies media type from 0x2098 to 0x210B
- If device type = 0x2C (floppy):
- Calls
get_head_count(extracts from config bits 7:6 of 0x2081) - Calls
build_drive_select_mask(sets density, motor, drive bits) - Derives sector count from format code table at 0x9C4: 26 (8" SD), 15 (5.25" DD), 8 (5.25" SD)
- Calls
setup_disk_geometryto compute buffer allocation and track layout - Calls
compute_chs_from_lbafor initial head positioning
- Calls
Floppy-Style SCSI Init Handler (0x0383)¶
This handles device type 0x1E (used for SCSI disks that use the floppy-like init path):
- Calls
setup_disk_geometry(0x07A4) - Reads port 0x77 bit 7 to select between two command templates (at 0x0C85 or 0x0C81)
- Builds a command block at 0x2200 with transfer parameters
- Copies 16 bytes from saved context (0x2184) and 12 bytes from saved disk params (0x2178) into the command buffer
- Byte-swaps the command block entries (big-endian CDB format)
- Calculates transfer size from remaining DMA count (0x20C7-0x20C9), caps at 0x17 (23) sectors
- Calls
scsi_data_transfer(0x03F7) to DMA the command data to ND-100 memory - Jumps to
scsi_start_io_operation(0x0614) to begin the SCSI operation
ND-3201 vs. ND-3112 Floppy Controller¶
The ND-3201 floppy implementation is completely different from the standalone ND-3112 floppy controller (NDBusFloppyDMA.cs):
| Aspect | ND-3112 (NDBusFloppyDMA.cs) | ND-3201 (this firmware) |
|---|---|---|
| CPU | None (hardware state machine) | Z80 |
| IOX Interface | Direct register mapping | Z80-mediated shared registers |
| DMA to ND-100 | Built-in DMA logic | Via ND-100 bus interface (ports 0x50-0x57) |
| Floppy DMA | Integrated | AM9517 (hardware-controlled, not Z80-accessible) |
| SCSI | None | NCR 5386 |
| Command Processing | Hardware state machine | Z80 firmware (ISR at 0x02AB dispatches) |
The ND-3201's floppy operations go through the same Z80 command processing pipeline as SCSI operations. The Z80 receives the command from the ND-100, programs the FD1797 and AM9517 for the floppy transfer, then signals completion back to the ND-100 via port 0x70.
What WCONT Actually Does¶
This section supersedes the "What the Z80 Does When WCONT is Written" material that follows it.
WCONT (IOX+5) is written by the ND-100. What happens next depends on which half of the board the operation belongs to, and the earlier revision conflated them.
For a SCSI operation (VERIFIED against IP-P2-SCSI-DRIV.NPL)¶
The Z80 is not involved. WCONT is a hardware control latch on the SCSI half. The ND-100 drives the NCR 5386 itself, and the interrupt it is waiting for comes from the NCR, not from firmware.
The decisive evidence is the tail of SINTRAN's interrupt handler SCINT:
SCINT: T:=HDEV+RSTAU; *IOXT % READ DEVICE STATUS
IF A=:SCSSR BIT 11 THEN % INTERRUPT FROM NCR
...decode phase, service it, issue the next NCR command...
FI
5\/SCCCW; T:=HDEV+WCONT; *IOXT % ACTIVATE+ENABLE INTERRUPT <-- line 187
GO SCWTI % park, wait for next interrupt
Line 187 is the unconditional common tail: every path that services an interrupt falls into it
on the way out. SCCCW is zeroed on handler entry (0=:SCCCW, line 139) and set to 40₃
only when a data transfer needs the DMA/direction bit (line 746). Therefore:
| Observed WCONT value | Meaning |
|---|---|
0x0005 (5 \/ 0) |
SCINT exit re-arm, no transfer pending. Enable-Interrupt + Active only |
0x0065 (5 \/ 40₃) |
Same tail, during a data transfer (adds DMA-enable + write-direction) |
A bare WCONT=5 is not a request for work. It means "I have finished servicing this interrupt;
re-arm the controller so a future NCR event can interrupt me," followed by parking at SCWTI.
SCWTI is the driver's normal wait-for-interrupt exit - every route out of the handler ends
GO SCWTI ("BUSY RETURN", "NO RETURN"), so "parked at SCWTI" is the ordinary idle state between
phases, not evidence of a hang.
Consequence: if the last interrupt serviced was a target Disconnect and nothing is queued, the
correct hardware behaviour after that final WCONT=5 is silence. No NCR event exists, so no
interrupt should fire. A controller that manufactured a completion interrupt here would be wrong:
SCINT would re-enter, read RSTAU, find the NCR-interrupt bit (11) clear, service nothing, fall to
line 187, re-arm, and park again - an interrupt storm that accomplishes nothing.
Note also line 147: on Disconnect the driver calls DCTHR (disconnect logical thread) and goes to
BUSFP -> SELEC, which checks the arbitration queue for the next operation. If no further
work was queued, the driver is correctly idle - and the reason nothing was queued is a CPU-side
decision in SINTRAN's connect/verify layer, not a controller defect.
For a floppy operation (INFERRED)¶
The Z80 half plausibly does use an ND-3112-style command-block model (host writes a command block, controller executes, signals completion via port 0x70 -> RSTAU Ready -> level 11). The material below describes that path. Treat it as the floppy protocol, and note that its specific trigger mechanism (CTC1 Ch0 counter-mode pulse) is DISPROVED - see "Trigger Mechanism" above.
WCONT Bit 4: Clear Device (VERIFIED, applies to both halves)¶
Asserts the Z80 RESET pin, so the floppy half reboots through its POST. This part of the original analysis stands.
What the Z80 Does When WCONT is Written - PARTIALLY RETRACTED¶
WARNING. The SCSI branch of everything below is DISPROVED - the Z80 never touches the NCR 5386. Read "What WCONT Actually Does" above instead. The floppy and Clear Device paths remain broadly valid, but the trigger mechanism described (CTC1 Ch0 counter mode, TC=1) is refuted, and the function names cited were misnamed (see the Function Reference).
WCONT Bit 2: Active (Start Operation) - SCSI branch RETRACTED¶
flowchart TD
WCONT["ND-100 writes WCONT with bit 2 Active"]
LATCH["Board latches WRDAT/WLMAR/WHMAR into shared regs"]
PULSE["Board pulses CTC1 Ch0 CLK/TRG"]
WAKE["Z80 wakes from HALT"]
ISR["ISR 0x02AB reads ports 0x50/0x54"]
DECODE{"Command type?"}
SCSI_DMA["DMA read 0x800 bytes from ND-100 into 0x2200"]
SCSI_CFG["Copy 12-byte config to 0x2080"]
SCSI_ID["set_scsi_target_id, clear_drive_select"]
FLOPPY_RD["Read block addr from ports 0x51-0x53"]
FLOPPY_CFG["DMA read 12-byte config into 0x2080"]
FLOPPY_INIT["init_floppy_drive or SCSI-style init"]
IDENT["DMA write ROM identity block to ND-100"]
IO_OP["scsi_start_io_operation (0x0614)"]
RESULTS["OUT 0x54-0x57 - write results"]
SIGNAL{"Error?"}
DMA_START["OUT 0x70 = 0xD4 - DMA ready"]
DISCONNECT["OUT 0x70 = 0xD0 - done"]
ND100_IRQ["Board sets RSTAU Ready, clears Active, IRQ level 11"]
WCONT --> LATCH --> PULSE --> WAKE --> ISR --> DECODE
DECODE -->|bit 0 = SCSI| SCSI_DMA --> SCSI_CFG --> SCSI_ID --> IO_OP
DECODE -->|bit 4 = device init| FLOPPY_RD --> FLOPPY_CFG --> FLOPPY_INIT --> IO_OP
DECODE -->|identity query| IDENT --> IO_OP
IO_OP --> RESULTS --> SIGNAL
SIGNAL -->|No| DMA_START --> ND100_IRQ
SIGNAL -->|Yes| DISCONNECT --> ND100_IRQ
When the ND-100 sets the Active bit, the board hardware: 1. Latches whatever data was previously written to WRDAT/WLMAR/WHMAR into shared registers 2. Sends a pulse to CTC1 Ch0 CLK/TRG input 3. Z80 wakes from HALT, CTC fires IM2 interrupt -> ISR at 0x02AB
The Z80 ISR at 0x02AB then executes this sequence:
Step 1: Read command word
IN A, (0x54) ; command low byte from ND-100
IN A, (0x50) ; command high byte from ND-100
Store to media_descriptor (0x2098)
Step 2: Decode command type from port 0x50 bits
bit 0 = 1 -> SCSI disk I/O command
bit 1 = 1 -> Bus control / special command
bit 4 = 1 -> Device init command
all zero -> Identity / diagnostic query
Step 3: For SCSI commands (bit 0 set):
a) DMA read 0x800 bytes from ND-100 memory (at MAR) into Z80 RAM at 0x2200
using scsi_transfer_with_disconnect (0x03F1)
b) Read ND-100 DMA address from 0x20C4-0x20C6
c) If address is non-zero, loop back to (a) for more data
d) Copy 12-byte default config from ROM 0x0377 to controller_config (0x2080)
e) Set SCSI target ID = 1 via set_scsi_target_id (0x103C)
f) Call clear_drive_select (0x0E08)
g) Dispatch to scsi_start_io_operation (0x0614)
Step 4: For device init (bit 4 set):
a) Reset CTC1 Ch3 ISR vector (0x2076) to default 0x0505
b) Read block address: port 0x51 -> low, 0x52 -> mid, 0x53 -> high
Store at block_address_lo (0x209A) and block_address_hi (0x209C)
c) DMA read 12 bytes of device config from ND-100 memory into 0x2080
using scsi_transfer_with_disconnect (0x03F1)
d) Extract device type from drive_config_byte (0x2081) bits 5:0
e) If device type = 0x1E: dispatch to handler 0x0383 (SCSI-style init)
If device type = other: dispatch to init_floppy_drive (0x08A1)
Step 5: For identity query (bits 0-4 all zero, port 0x51=0, port 0x52=4):
Dispatch to identity handler (0x15B6) which DMAs the ROM identity
data block (from 0x15DF, contains IOX base 0xC8C0) to ND-100 memory
Step 6: Dispatch mechanism (at 0x036A):
The handler address (in BC) is pushed onto the stack via EX SP,HL
then RET jumps to it. The handler eventually calls
scsi_start_io_operation (0x0614) which:
a) Writes status/results to ports 0x54-0x57
b) Either calls nd100_start_dma_transfer (OUT 0xD4 to port 0x70)
or calls disconnect_nd100_bus (OUT 0xD0 to port 0x70)
c) Board hardware sees port 0x70 write -> sets RSTAU[3] Ready,
clears RSTAU[2] Active -> if RSTAU[0] IRQ enabled -> ND-100 level 11
WCONT Bit 4: Clear Device¶
flowchart TD
CLEAR["ND-100 writes WCONT bit 4 = Clear Device"]
RESET["Board asserts Z80 RESET pin"]
RST0["RST0: OUT 0x74=0x01, OUT 0x70=0x02"]
MAIN["MAIN: OUT 0x54=0x08 (self-test flag), SP=0x2070"]
RAMTEST["RAM Test: fill+verify 0x2000-0x3FFF"]
CTCINIT["init_ctc_channels: IM2, vector table"]
NCRTEST["Test NCR 5386 connectivity"]
SELFTEST["selftest_and_init: bus, CTC calibrate, data lines"]
REINIT["init_controller_state: probe RAM, clear, restore vectors"]
IDLE["Main Idle Loop: EI, HALT, ready for commands"]
CLEAR --> RESET --> RST0 --> MAIN --> RAMTEST --> CTCINIT
CTCINIT --> NCRTEST --> SELFTEST --> REINIT --> IDLE
style CLEAR fill:#f66
style IDLE fill:#6f6
Clear Device causes a hard reset of the controller. Based on the Z80 firmware's reset sequence, the board hardware most likely asserts the Z80 RESET pin, causing the Z80 to restart from address 0x0000.
The Z80 then executes the full boot sequence:
Step 1: RST0 (0x0000) - Hardware init
OUT (0x74), 0x01 ; initial drive select state
OUT (0x70), 0x02 ; initial FD1797 state (Restore command variant)
Step 2: MAIN (0x003B) - Port init
OUT (0x54), 0x08 ; status to ND-100: "self-test in progress"
OUT (0x55), 0x00 ; clear SCSI ID
OUT (0x56), 0x00 ; clear sense flags
SP = 0x2070 ; initialize stack
Step 3: MainLoop (0x0086) - RAM test
Fill 0x2000-0x3FFF with 0x00, verify (checksum with 0x55 seed)
Fill with 0xFF, verify
Fill with address high byte, verify
On failure: display error code on 7-seg LEDs, halt forever
Step 4: Post-RAM-test init (0x00E8)
SP = 0x2070
IX = 0x2080 (controller state base)
IY = 0x2100 (drive control block base)
Call init_ctc_channels (0x0759): reset all CTC, set IM2, copy vector table
Set CTC1 Ch0/Ch2/Ch3 vectors to 0x015E (RETI, safe during init)
Call ncr5386_reset_parity (0x0708)
Test NCR 5386 via scsi_select_and_verify (0x012E)
Read port 0x28 (NCR aux status, flush pending state)
Step 5: selftest_and_init (0x0161)
Check port 0x77 bit 0 (FDC busy), wait to clear
Read port 0x70 bit 0 (bus busy), wait with timeout
Call calibrate_ctc_clock (0x1395): binary search CTC calibration
Write 0x01 to port 0x73, 0x12 to port 0x70 (seek to track 1)
Enable interrupts, wait with timeout
Test bus data lines with patterns (0x55, 0xAA, 0x0F, 0xF0)
On failure: display error code 0-7 on 7-seg LEDs
Step 6: Final init (0x0227)
Call init_controller_state (0x0253):
Probe RAM size, clear state vars, init CTC (restores vector table)
Set 0x2126-0x2128 = 0xFF (mark all units as uninitialized)
Clear port 0x74 (drive deselect)
Clear port 0x53 (ND-100 bus mode)
Call setup_7seg_display_scsi_id (0x1AFA): show SCSI ID on display
Program CTC1 Ch0 (port 0x10): control=0x95, constant=0x10
Step 7: Enter idle loop (0x0243)
EI ; enable interrupts
HALT ; sleep until CTC interrupt
CALL process_pending_command ; check for work
JR back to HALT ; repeat
Total time: thousands of Z80 clock cycles (RAM test alone is ~64K writes+reads)
The ND-100 can detect when boot completes because the CTC1 Ch0 ISR (0x02AB) only runs after the idle loop is entered. Before that, the CTC1 Ch0 vector points to 0x015E (RETI, do nothing). The SINTRAN driver typically waits for a timeout after Clear Device before sending the next command.
WCONT Bit 4: Clear Device - The Reinit Path (0x80 mode)¶
There is also a soft reinit path in the Z80 firmware. When scsi_command_entry_point (0x1D4C) is entered with mode 0x80 (instead of a hard reset), it executes:
0x1DED: DI ; disable interrupts
0x1DEF: OUT (0x74), 0 ; clear drive select
0x1DF1: OUT (0x77), 0 ; clear motor control
Clear state_flag_d5, current_drive_select, dcb_drive_select
0x1DFC: IN A, (0x28) ; read NCR aux status (flush)
0x1DFE: LD A, 0x0F
0x1E00: OUT (0x2A), A ; write 0x0F to NCR Own ID (all IDs enabled)
0x1E02: CALL disconnect_nd100_bus ; OUT 0xD0 to port 0x70
0x1E05: CALL init_controller_state ; probe RAM, clear state, init CTC
0x1E08: CALL calibrate_ctc_clock ; recalibrate timers
0x1E0B: CALL ncr5386_reset_parity ; reset NCR parity state
0x1E0E: CALL scsi_start_io_operation ; process initial status
0x1E11: JP 0x023B ; enter main idle loop
This soft reinit skips the RAM test and POST error checking, but still recalibrates the CTC and reinitializes all controller state. It takes less time than a hard reset but still involves significant Z80 processing.
WCONT Bit 3: Test Mode¶
Test mode is handled entirely by the board hardware and glue logic. The Z80 firmware has no special handling for test mode. In test mode, reading RLMAR (IOX+0) causes the MAR to auto-increment, and if DMA Enable is also set, a memory read or write occurs. This is used for memory diagnostics without involving the Z80.
WCONT Bit 5+6: DMA Enable + Write Direction¶
These bits control the AM9517 DMA controller and bus interface glue logic directly. The Z80 firmware does not see these bits. They configure whether DMA transfers go between the NCR 5386 and ND-100 memory, and in which direction.
WCONT Bit 10: Reset SCSI Bus¶
This asserts the SCSI bus reset signal. The Z80 may or may not be involved. The NCR 5386 chip detects the reset and would interrupt the Z80 via CTC1 Ch3, entering the soft reinit path (mode 0x80).
C# Emulator Analysis (NDBusDiscControllerSCSI.cs)¶
Comparison of the C# emulator at RetroCore: Emulated.HW\ND\CPU\NDBUS\NDBusDiscControllerSCSI.cs against the actual Z80 firmware behavior.
Architecture Difference¶
flowchart TB
subgraph REAL["Real Hardware"]
ND1["ND-100"] -->|IOX| BUS1["Board Bus Interface"]
BUS1 -->|shared regs| Z80_1["Z80 CPU running firmware"]
Z80_1 -->|I/O ports 0x20-0x3D| NCR1["NCR 5386"]
Z80_1 -->|I/O ports 0x70-0x73| FD1["FD1797"]
FD1["FD1797"] <-->|DRQ/DACK| AM1["AM9517 DMA"]
AM1 <--> Z80RAM1["Z80 RAM"]
NCR1 <-->|SCSI bus| DISK1["SCSI Disk"]
FD1 <-->|floppy cable| FLOP1["Floppy Drive"]
Z80_1 -->|port 0x70 write| BUS1
BUS1 -->|Level 11 IRQ| ND1
end
subgraph EMUL["C# Emulator"]
ND2["ND-100"] -->|IOX Read/Write| CTRL["NDBusDiscControllerSCSI"]
CTRL -->|passthrough| NCR2["NCR5386SCSI class"]
NCR2 <-->|emulated| DISK2["SCSIHDDMicropolis"]
CTRL -->|DMARead/DMAWrite| MEM["ND-100 Memory"]
CTRL -->|SetInterruptBit| ND2
end
style Z80_1 fill:#ff9
style CTRL fill:#9ff
The C# emulator does NOT emulate the Z80. It emulates the controller at a higher level, passing NCR 5386 register reads/writes through directly from the ND-100 IOX interface to the NCR5386SCSI class. The real hardware has the Z80 as an active intermediary that receives commands, manages multi-phase SCSI transfers, handles floppy operations, and signals completion.
This means the SINTRAN driver talks "through" the emulated controller directly to the NCR 5386 emulation, whereas on real hardware the SINTRAN driver talks to the controller board, and the Z80 firmware mediates all NCR 5386 access.
Issue 1: SetSCSIIdNumber writes to wrong NCR register¶
File: NDBusDiscControllerSCSI.cs, lines 792 and 804 Severity: Bug
// Current code (WRONG):
ncr5386.Write((byte)SCSIRegisters.SourceID, (byte)regs.TW1);
// Should be:
ncr5386.Write((byte)SCSIRegisters.IDRegister, (byte)regs.TW1);
Why: The Source ID register (RSOUI, IOX offset 0x2E, octal 56) is a read-only register on the NCR 5386. It returns the SCSI ID of the device that selected or reselected the controller. Writing to it has no effect on the real chip.
The Z80 firmware writes the Own ID to the ID Register (WOIDN, port 0x2B). The SINTRAN driver also writes to WOIDN (not Source ID). The correct C# register is SCSIRegisters.IDRegister.
This bug exists in both SetSCSIIdNumber() (line 792) and Reset() (line 804).
Issue 2: InterruptFromNCR5386 cleared on every RSTAU read¶
File: NDBusDiscControllerSCSI.cs, line 905 Severity: Potential bug - may cause lost interrupts
// In the RSTAU read handler:
regs.InterruptFromNCR5386 = false; // Clear flag
Why: On the real hardware, the NCR 5386 interrupt flag is acknowledged by reading the NCR's Interrupt Register (RITRG, IOX offset 0x2C), not by reading the controller's status register (RSTAU). The SINTRAN interrupt handler reads RSTAU first, then reads RITRG:
SCINT: T:= HDEV + RSTAU; *IOXT % READ STATUS
...
T+"RAUXS-WCONT"; *IOXT % READ AUX STATUS
T+"RITRG-RAUXS"; *IOXT % READ INTERRUPT REG (acknowledges NCR IRQ)
If any code path reads RSTAU without following up with RITRG, or reads RSTAU twice (e.g., polling), the interrupt flag is lost on the first read. The flag should be cleared when RITRG is read, not when RSTAU is read.
On the real hardware, the NCR 5386 interrupt flag in RSTAU is a hardware signal -- it stays asserted as long as the NCR 5386's INT pin is active. It only clears when the NCR interrupt is acknowledged via RITRG read.
Issue 3: ExecuteGo() is empty¶
File: NDBusDiscControllerSCSI.cs, lines 1326-1348 Severity: Design limitation (works only because SINTRAN programs NCR directly)
private void ExecuteGo()
{
dma_bytes_written = dma_bytes_read = 0;
// ... debug logging ...
return; // <-- Returns immediately, all code below is dead
// Dead code behind #if _GO_ ...
}
Why this works anyway: The SINTRAN driver programs the NCR 5386 directly via IOX registers (WNCOM, WDESI, WNDAT, transfer counters, etc.) and the C# emulator passes all those writes through to the NCR5386SCSI class. The NCR emulation handles the SCSI protocol autonomously. The controller emulator just manages DMA between NCR and ND-100 memory via StepGoState().
Why this differs from real hardware - RETRACTED. The original claimed the Z80 "programs the NCR 5386 itself (ISRs at 0x1298/0x12A6/0x12BF)", "manages retries", and "handles disconnect/reconnect" for SCSI. All false - the Z80 never touches the NCR (VERIFIED, see "THE ARCHITECTURE").
Why it actually works - and why it is not merely "architecturally valid for emulation purposes": the "Why this works anyway" paragraph above is not a lucky accident, it is a correct description of the hardware. On the real board the NCR 5386 is decoded straight onto the ND-100 IOX bus and SINTRAN is the SCSI protocol engine. An emulator that passes IOX writes through to an NCR model and lets the driver run the protocol is faithful, not a shortcut.
An empty ExecuteGo() is therefore not a design limitation for the SCSI path. StepGoState()
raising the IRQ only when regs.InterruptFromNCR5386 is set matches the hardware contract: the
board completes a GO when the NCR has something to report, and stays silent otherwise. See
"What WCONT Actually Does".
The residual limitation is real but is about FLOPPY, not SCSI: the emulator has no Z80, so any SINTRAN code that drives the floppy half through the ND-3112-style command word on ports 0x50-0x54 has nothing to talk to. That is Issue 7 (No floppy support), and it is the correct place for this concern.
Issue 4: NCR interrupt path skips Z80 processing - NOT AN ISSUE (RETRACTED)¶
This was never a defect. The emulator is right and this "issue" was the false model talking.
There is no Z80 processing to skip. The NCR 5386 interrupts the ND-100 directly (RSTAU bit 9, level 11 when WCONT bit 0 is set), and SINTRAN's
SCINTservices it. The claimed chainNCR INT -> CTC1 Ch3 -> Z80 ISR (0x1298 -> 0x12BF -> 0x0505) -> port 0x70 -> ND-100does not exist: the Z80 never reads or writes an NCR register anywhere in the ROM (VERIFIED by exhaustive I/O sweep).The premise "the Z80 only signals the ND-100 after the FULL operation completes" is also backwards for SCSI. Per
IP-P2-SCSI-DRIV.NPL, the driver wants an interrupt per SCSI phase -SCINTdecodes the phase, issues the next NCR command, re-arms (line 187) and parks atSCWTI. Signalling "after any single NCR interrupt" is not the emulator seeing intermediate states by accident; it is exactly the contract the driver is written against.The device trace corroborates this: 24 NCR interrupts = 24 controller completions = 24 RITRG acks, perfectly balanced, with the READ_6 data DMA'd successfully.
Original text (RETRACTED), preserved for reference:
File: NDBusDiscControllerSCSI.cs, lines 750-767 and 1220-1234 Severity: Timing/behavioral difference
On the real hardware:
NCR 5386 INT -> CTC1 Ch3 -> Z80 ISR chain (processes data, manages phases)
-> Z80 writes port 0x70 -> board sets RSTAU[3] -> ND-100 level 11 IRQ
In the C# emulator:
NCR interrupt -> set flag -> next Clock() -> StepGoState()
-> if InterruptFromNCR5386 && active: set readyForTransfer, clear active
-> if interruptEnabled: SetInterruptBit(true) -> immediate ND-100 IRQ
The Z80 firmware performs significant work between NCR interrupt and ND-100 notification: 1. Reads NCR status registers 2. Transfers data bytes via ISR chains (0x1298 -> 0x12BF -> 0x0505) 3. Manages command/data/status/message phases 4. Only signals the ND-100 after the FULL operation completes
The C# emulator signals the ND-100 after any single NCR interrupt, which may happen mid-operation (e.g., after command phase but before data phase). This could cause the SINTRAN driver to see intermediate states.
Issue 5: Ncr5386_OnInterrupt immediate propagation commented out¶
File: NDBusDiscControllerSCSI.cs, lines 759-767 Severity: Uncertain - may be intentional
// This code is commented out:
/*
if (regs.InterruptFromNCR5386 && regs.interruptEnabled)
{
SetInterruptBit(true);
}
*/
The immediate interrupt propagation is disabled. Instead, StepGoState() handles it on the next Clock() cycle. This adds a one-cycle delay. I don't know if this was disabled to fix a specific timing issue or if it should be re-enabled.
Issue 6: Clear Device immediately sets readyForTransfer¶
File: NDBusDiscControllerSCSI.cs, lines 1108-1127 Severity: Minor timing issue
if ((value & 1 << 4) != 0) // Clear Device
{
regs.MemoryAddressLSB = 0;
regs.MemoryAddressMSB = 0;
bufferPointer = 0;
readbufferPointer = 0;
ncr5386.DeviceReset();
regs.readyForTransfer = true; // <-- Immediately ready
}
On the real hardware, after a Clear Device, the Z80 runs its full POST sequence (RAM test, CTC init, NCR 5386 selftest, CTC calibration) before becoming ready. This takes thousands of Z80 clock cycles. The immediate readyForTransfer = true could cause the ND-100 driver to send the next command before the controller has properly re-initialized.
For emulation purposes this is probably fine since there's no Z80 to re-initialize, but if timing-sensitive SINTRAN code checks for a delay after Clear Device, it would behave differently.
Issue 7: No floppy support¶
Severity: Feature gap (probably intentional)
The C# emulator has no FD1797, AM9517, or floppy path. The Z80 firmware handles floppy through the same command receive ISR (port 0x50 bit 4 = device init, device type 0x2C = floppy). For systems that need floppy boot from the SCSI controller card, this would need to be added.
Issue 8: DMA byte ordering uncertainty¶
File: NDBusDiscControllerSCSI.cs, lines 1282-1320 Severity: Uncertain
// ReadNextByteDMA - even byte reads high byte of word:
data = (byte)(dma_read_data >> 8);
// WriteNextByteDMA - even byte writes to high byte of word:
dma_write_data = (ushort)((memData & 0x00FF) | ((data & 0xFF) << 8));
The emulator reads/writes the high byte first (even offset = MSB, odd offset = LSB). The ND-100 is big-endian and the SCSI bus transfers bytes MSB-first, so this is likely correct. However, I cannot verify the byte ordering from the Z80 firmware alone because the real DMA goes through the AM9517 hardware which may or may not swap bytes. If data appears byte-swapped in the emulator, this is the place to check.
What Looks Correct¶
- IOX register mapping (RLMAR through WTCL) matches SINTRAN source symbols exactly
- RSTAU status register bit layout (bits 0-15) matches hardware documentation
- WCONT control register bit layout matches
- NCR 5386 register passthrough (IOX offsets 0x20-0x3D) correctly maps to NCR registers
- InterruptLevel = 11 is correct for disk devices
- IDENT clears interruptEnabled matches expected ND-100 bus behavior
- NDBusAddressLength = 63 is correct (IOX range covers 0x00-0x3F = 64 addresses, but 0-based = 63)
- NCR register read/write mapping matches the SINTRAN symbols and Z80 firmware port usage
Recommended Changes¶
Change 1: Fix SetSCSIIdNumber register (BUG FIX)¶
What: Change SCSIRegisters.SourceID to SCSIRegisters.IDRegister in two places.
Where: Lines 792 and 804 in NDBusDiscControllerSCSI.cs
Why: Source ID is read-only on the NCR 5386. The Own ID must be written to the ID Register. The Z80 firmware writes to port 0x2B (WOIDN = ID Register), and the SINTRAN driver also uses WOIDN. Writing to Source ID is a no-op on real hardware, meaning the emulated NCR 5386 may not have the correct Own ID set.
How to test:
1. Boot SINTRAN with the SCSI controller
2. Verify the controller responds to the correct SCSI ID (should be 7 for host)
3. If the NCR5386SCSI class ignores writes to SourceID, this bug has no effect. If it honors them (stores the value), then the Own ID is set via the wrong register and the actual IDRegister may be unset.
4. Check: does ncr5386.Read((byte)SCSIRegisters.IDRegister) return the TW1 value after SetSCSIIdNumber() is called? If not, the bug is active.
Change 2: Move InterruptFromNCR5386 clear to RITRG read (BUG FIX)¶
What: Remove regs.InterruptFromNCR5386 = false; from the RSTAU read handler. Add it to the RITRG read handler instead.
Where: - Remove from line 905 (case Register.RSTAU) - Add to line 978-980 (case Register.RITRG), after the NCR read
Why: On real hardware, RSTAU bit 9 reflects the NCR 5386 INT pin state -- it stays set as long as the NCR is asserting interrupt. The interrupt is acknowledged (and the NCR clears INT) when the Interrupt Register is read via RITRG. Clearing the flag on RSTAU read means any double-read of RSTAU would lose the interrupt.
How to test: 1. Boot SINTRAN and run SCSI disk operations 2. Watch for "lost interrupt" symptoms: operations that hang waiting for an interrupt that was already cleared 3. Add logging to the RITRG read handler to verify it's being called and the flag is cleared there 4. Verify the SINTRAN interrupt handler sequence: RSTAU read -> RAUXS read -> RITRG read (the flag should survive until the RITRG read)
Change 3: Consider re-enabling immediate NCR interrupt propagation (EVALUATION)¶
What: Evaluate whether the commented-out immediate interrupt code in Ncr5386_OnInterrupt() (lines 759-767) should be re-enabled.
Where: Lines 759-767
Why: Currently, NCR interrupts are delayed until the next Clock() cycle via StepGoState(). If the SINTRAN driver expects to see the interrupt on the same IOX cycle that triggered the NCR command, the one-cycle delay could cause timing issues. However, if re-enabling caused problems before (which is why it was commented out), the root cause should be investigated.
How to test: 1. Uncomment the immediate propagation code 2. Run the full SINTRAN boot and disk test suite 3. If it works, keep it enabled. If operations hang or produce errors, the delay is needed and the issue is elsewhere (possibly in the NCR5386SCSI state machine).
Change 4: Implement ExecuteGo() properly - RATIONALE RETRACTED¶
The justification below is DISPROVED. It rests on "the Z80 firmware actively manages the command lifecycle" for SCSI. It does not - the Z80 never touches the NCR 5386 (see "THE ARCHITECTURE"). The parenthetical in the original text, "The C# emulator works because SINTRAN programs the NCR directly", was the correct insight and should have been the conclusion.
Do NOT make a bare
WCONT=Activeproduce a completion interrupt. Per "What WCONT Actually Does", aWCONT=5with no NCR command loaded isSCINT's exit re-arm (NPL line 187). Silence is the correct response. Manufacturing an interrupt there causesSCINTto re-enter, find RSTAU bit 11 clear, service nothing, re-arm, and park - an interrupt storm.The existing emulator behaviour -
StepGoState()raising the ND-100 IRQ only whenregs.InterruptFromNCR5386is set - already matches the hardware contract: a GO completes when the NCR has something to report. The sample code below happens to preserve that gate (if (regs.InterruptFromNCR5386)), so it is not itself harmful, but it should be adopted (if at all) as state-machine tidying, not as "matching the Z80 firmware".The doc-comment in the sample code below is factually wrong on points 1-4 and must not be pasted into the source.
What: Replace the empty ExecuteGo() with proper state management.
Where: Line 1326 in NDBusDiscControllerSCSI.cs
Why (ORIGINAL, RETRACTED): The empty ExecuteGo() relies on StepGoState() to handle everything, but the state transitions don't match the real hardware. On the real board, the Z80 firmware actively manages the command lifecycle. The C# emulator works because SINTRAN programs the NCR directly, but the state machine (active/ready/interrupt) should still behave correctly.
Sample implementation based on Z80 firmware behavior:
/// <summary>
/// Execute the command that was loaded via IOX register writes.
///
/// On real hardware, writing WCONT with Active bit causes the board to:
/// 1. Latch command data into shared registers (Z80 ports 0x50-0x54)
/// 2. Pulse CTC1 Ch0 to wake the Z80 from HALT
/// 3. Z80 ISR at 0x02AB reads command, programs NCR, manages transfer phases
/// 4. Z80 writes completion to port 0x70 -> board sets Ready, clears Active
///
/// In this emulator, SINTRAN programs the NCR directly via IOX passthrough.
/// The NCR5386SCSI class handles the SCSI protocol autonomously.
/// ExecuteGo() just needs to manage the controller state machine correctly.
/// </summary>
private void ExecuteGo()
{
dma_bytes_written = 0;
dma_bytes_read = 0;
// If the NCR already has a pending interrupt (from a previous operation),
// the Z80 firmware would process it immediately and signal ready.
if (regs.InterruptFromNCR5386)
{
regs.active = false;
regs.readyForTransfer = true;
if (regs.interruptEnabled)
{
SetInterruptBit(true);
}
return;
}
// If DMA is enabled, the NCR5386SCSI class will handle the SCSI protocol.
// StepGoState() on each Clock() will transfer DMA bytes between NCR and
// ND-100 memory. When NCR signals completion (interrupt), StepGoState()
// will set readyForTransfer and generate the ND-100 interrupt.
//
// If DMA is NOT enabled, SINTRAN is doing register-level IOX access to
// the NCR. The NCR interrupt will eventually fire when the SCSI operation
// completes, and StepGoState() handles it.
//
// In both cases, the actual work happens in Clock() -> StepGoState().
// Nothing more to do here.
}
How to test: 1. Boot SINTRAN with SCSI disk attached 2. Verify disk operations (read, write, format) complete without hanging 3. Check that the controller returns to ready state after each operation 4. Verify interrupt timing: ND-100 receives level 11 interrupt after each op completes 5. Test Clear Device (WCONT bit 4) followed by a new command
Change 5: Clear Device should delay readyForTransfer (LOW PRIORITY)¶
What: After Clear Device, don't immediately set readyForTransfer = true. Instead, optionally add a small delay (a few Clock() cycles) before setting it.
Where: Line 1126
Why: On real hardware, the Z80 runs a full POST after reset (~thousands of cycles). If SINTRAN code checks for a delay after Clear Device, immediate ready could cause issues. However, this is likely not a problem in practice.
How to test: 1. Only implement if Clear Device operations cause problems (SINTRAN sends a command before the controller has re-initialized) 2. If no issues observed, leave as-is
Additional C# Emulator Impacts from Firmware Analysis¶
Beyond the bugs documented in the Recommended Changes section above, the firmware analysis revealed these additional behavioral differences:
Missing SCSI Operation Timeout¶
On the real hardware, CTC2 Ch0 (ISR at 0x0AF3) acts as a watchdog timer for SCSI operations. If the NCR 5386 doesn't complete within the timeout, the ISR fires and aborts:
0x0AF3: LD A, 0xD0 ; Force Interrupt / abort
OUT (0x70), A ; signal ND-100: disconnect
CALL clear_nd100_bus_attention ; clear drive select on port 0x74
CALL event 0x1D24 ; enter error handler
The C# emulator has no timeout mechanism in StepGoState() or Clock(). If an NCR operation hangs (target doesn't respond, SCSI bus stuck), the controller stays in active=true forever, blocking all further operations.
Suggested fix: Add a cycle counter in Clock() that increments while regs.active is true. If it exceeds a threshold (derived from CTC2 Ch0's time constant on real hardware), force regs.active = false, set regs.readyForTransfer = true, and generate an interrupt with an error status.
Bulk DMA Transfer in Single Clock Cycle¶
At line 1238 in StepGoState():
while (regs.DataRequestFromNCR5386 && regs.active)
{
// transfers ALL pending bytes instantly
}
On real hardware, DMA happens byte-by-byte through the AM9517, with the Z80 ISR chains managing each phase. The entire transfer is spread across many clock cycles. The instant-transfer behavior is probably fine for SINTRAN, but could break timing-sensitive diagnostic software that polls RSTAU between DMA bytes.
Missing SCSI Bus Reset Interrupt to ND-100¶
When WCONT bit 10 is set (Reset SCSI Bus), the C# emulator sets regs.resetOnSCSIBus = true (line 1130) and calls ncr5386.InitiateResetSCSIBus() (line 1134). RSTAU bit 5 will show the reset state. But there is no SetInterruptBit(true) call for the reset case.
On real hardware, RSTAU bit 5 (Reset on SCSI bus) generates an ND-100 level 11 interrupt if bit 0 (Enable Interrupt) is set. The Z80 firmware handles this through the soft reinit path (mode 0x80 in scsi_command_entry_point), which calls disconnect_nd100_bus (writes 0xD0 to port 0x70), triggering the ND-100 interrupt.
Suggested fix: After setting regs.resetOnSCSIBus = true, check if regs.interruptEnabled and call SetInterruptBit(true).
Missing Identity/Diagnostic Query Response¶
The Z80 firmware has an identity handler at 0x15B6 that responds to diagnostic queries from the ND-100. When the ND-100 sends a specific command word (port 0x50 bits 0-4 all zero, port 0x51=0, port 0x52=4), the Z80 DMAs a 0xC00-byte identity data block from ROM (address 0x15DF) to ND-100 memory. This block contains the IOX base address (0xC8C0) and controller identification data.
The C# emulator has no equivalent. If SINTRAN or diagnostic software queries the controller identity, there would be no response. This only affects device discovery and diagnostics, not normal disk I/O.
Two Interrupt Sources in RSTAU¶
The RSTAU register documentation states:
(*) Gives Interrupt to ND-100 if bit 0 (Enable Interrupt) is set.
This applies to both: - Bit 5: Reset on SCSI bus - Bit 9: Interrupt from NCR 5386
The C# emulator handles the NCR interrupt path (bit 9) but does not generate ND-100 interrupts for the SCSI bus reset path (bit 5). Both should trigger SetInterruptBit(true) when regs.interruptEnabled is set.
Prioritized Change Summary¶
All issues found in the C# emulator, ordered by impact:
| Priority | Issue | Location | Risk if Unfixed |
|---|---|---|---|
| HIGH | SetSCSIIdNumber writes to SourceID (read-only) instead of IDRegister | Lines 792, 804 | NCR 5386 may have wrong Own ID, causing SCSI arbitration/selection failures |
| HIGH | InterruptFromNCR5386 cleared on RSTAU read instead of RITRG read | Line 905 | Interrupts lost if RSTAU is read more than once before RITRG, causing hung operations |
| MEDIUM | No SCSI operation timeout (CTC2 Ch0 watchdog missing) | StepGoState/Clock | Controller hangs permanently if NCR operation never completes |
| MEDIUM | SCSI bus reset doesn't generate ND-100 interrupt | Lines 1130-1136 | ND-100 not notified of bus reset, driver may wait forever for status |
| LOW | ExecuteGo() empty - document architecture and add state handling | Line 1326 | Future maintainability; works now because SINTRAN programs NCR directly |
| LOW | No identity/diagnostic query response | N/A (missing feature) | Diagnostics and device discovery fail; normal I/O unaffected |
| LOW | Bulk DMA transfer in single Clock() cycle | Line 1238 | Timing difference; only matters for diagnostic software |
| LOW | Clear Device immediately sets readyForTransfer | Line 1126 | Timing difference; Z80 takes thousands of cycles to reboot |
| INFO | Ncr5386_OnInterrupt immediate propagation commented out | Lines 759-767 | Unknown if intentional; may need re-evaluation |
| INFO | DMA byte ordering (MSB first) | Lines 1282-1320 | Appears correct for ND-100 big-endian, but verify if data appears swapped |
IOX Register Deep Dive: Z80 Firmware Behavior per Register¶
This section documents exactly what happens in the Z80 firmware for each IOX register, what the C# emulator currently does, and C# code showing what it should do based on the firmware analysis.
Architecture: The Shared Register Block¶
The ND-100 accesses the controller via IOX instructions. The board has a shared register block between the ND-100 bus and the Z80:
ND-100 (IOX) Board Hardware Z80 (I/O ports)
+-------------------+
IOX+0 RLMAR (Read) <----| MAR Low latch |<--- OUT (0x50), L
IOX+1 WLMAR (Write) ---->| |
+-------------------+
IOX+2 REDAT (Read) <----| Data Buffer |<--- OUT (0x54/0x55), data
IOX+3 WRDAT (Write) ---->| (ring buffer) |---> IN (0x54), data
+-------------------+
IOX+4 RSTAU (Read) <----| Status Latch |<--- OUT (0x53/0x57), status
IOX+5 WCONT (Write) ---->| Control Logic |---> CTC1 Ch0 trigger
+-------------------+
IOX+6 RHMAR (Read) <----| MAR High latch |<--- OUT (0x52), A
IOX+7 WHMAR (Write) ---->| |
+-------------------+
IOX+0x20-0x3D <-->| NCR 5386 direct |<--> Z80 ports 0x20-0x3D
+-------------------+
The Z80 ports 0x50-0x57 map to the shared register block as follows:
| Z80 Port | Write Function | Read Function |
|---|---|---|
| 0x50 | MAR low byte (DMA addr) | Command word high byte (from ND-100) |
| 0x51 | MAR mid byte (DMA addr) | Command param byte 1 (from ND-100) |
| 0x52 | MAR high byte (DMA addr) | Command param byte 2 (from ND-100) |
| 0x53 | Bus mode control (0x00=clear, 0x90=latch/readback) | Command param byte 3 (from ND-100) |
| 0x54 | Status/flags to ND-100 | Command word low byte (from ND-100) |
| 0x55 | SCSI ID / mid status to ND-100 | MAR readback low |
| 0x56 | Sense/direction flags to ND-100 | MAR readback mid |
| 0x57 | Completion/error status to ND-100 | MAR readback high |
IOX+0 RLMAR: Read Memory Address Register (bits 0-15)¶
ND-100 reads this to get the current DMA address.
Z80 firmware writes the MAR via ports 0x50-0x52 in set_nd100_dma_address (0x048C):
XOR A; OUT (0x53), A ; clear control register
LD C, 0x50
OUT (C), L ; port 0x50 = MAR low byte
INC C
OUT (C), H ; port 0x51 = MAR mid byte
INC C
OUT (C), A ; port 0x52 = MAR high byte
LD A, 0x90
OUT (0x53), A ; latch the address (0x90 = activate)
CALL read_nd100_dma_address ; verify by reading back from 0x55-0x57
XOR A; OUT (0x53), A ; clear control register
C# emulator current code:
case Register.RLMAR:
rval = regs.MemoryAddressLSB;
if (regs.testMode) regs.IncrementMarRegister();
break;
Assessment: The C# code is correct. The auto-increment in test mode matches the hardware documentation. The Z80 firmware doesn't read RLMAR - it writes the MAR via ports 0x50-0x52 and the ND-100 reads it via IOX.
IOX+1 WLMAR: Write Memory Address Register (bits 0-15)¶
ND-100 writes this to set the DMA starting address.
Z80 firmware: Does not directly see this write. The board hardware latches the value. When the ND-100 subsequently writes WCONT with Active, the CTC triggers the Z80, and the Z80 reads the command data from ports 0x50-0x54.
C# emulator current code:
case Register.WLMAR:
regs.MemoryAddressLSB = value;
break;
Assessment: Correct. Simple latch.
IOX+2 REDAT: Read Data¶
ND-100 reads data from the controller's data buffer.
Z80 firmware writes data to the ND-100 via ports 0x54/0x55 using interrupt-driven OUTI instructions in ISRs at 0x15A8 and 0x15B0, and via send_data_to_nd100_bus (0x1593) which writes to port 0x57 as block acknowledgment.
The data transfer uses a ring buffer approach with the Z80 writing 2-byte values (port 0x55 = high, port 0x54 = low) via OUTI instructions during interrupt service.
C# emulator current code:
case Register.REDAT:
rval = DataBuffer[readbufferPointer];
readbufferPointer = (readbufferPointer + 1) & BUFFER_MAX;
break;
Assessment: Functionally correct as a ring buffer read. The Z80 firmware populates the buffer via ISR-driven port writes.
IOX+3 WRDAT: Write Data¶
ND-100 writes data to the controller's data buffer.
Z80 firmware reads command data from ports 0x50-0x54. When the ND-100 writes WRDAT followed by WCONT with Active, the data appears on the Z80's read ports.
C# emulator current code:
case Register.WRDAT:
DataBuffer[bufferPointer] = value;
bufferPointer = (bufferPointer + 1) & BUFFER_MAX;
break;
Assessment: Correct. Stores into ring buffer.
IOX+4 RSTAU: Read Status¶
ND-100 reads this to check controller status and interrupt source.
Z80 firmware controls the status bits by writing to ports 0x53 (bus mode), 0x54-0x57 (status/results), and 0x70 (FD1797/completion). The board glue logic assembles the RSTAU bits from these Z80 port writes and hardware signals.
Key Z80 actions that affect RSTAU:
- Writing 0xD0 to port 0x70 (disconnect_nd100_bus): clears Active (bit 2), sets Ready (bit 3)
- Writing 0xD4 to port 0x70 (nd100_start_dma_transfer): signals DMA ready
- Port 0x53 writes: affect bus mode/status bits
- NCR 5386 INT pin: sets bit 9 via hardware
C# emulator current code:
case Register.RSTAU:
if (regs.interruptEnabled) rval |= 1 << 0;
if (regs.active) rval |= 1 << 2;
if (regs.readyForTransfer) rval |= 1 << 3;
if (regs.resetOnSCSIBus) rval |= 1 << 5;
if (ncr5386.ChipDisabled()) rval |= 1 << 6;
if (regs.DataRequestFromNCR5386) rval |= 1 << 8;
if (regs.InterruptFromNCR5386) rval |= 1 << 9;
if (regs.DataAcknowledgeToNCR5386) rval |= 1 << 10;
if (ncr5386.SCSI_BSY()) rval |= 1 << 12;
if (ncr5386.SCSI_REQ()) rval |= 1 << 13;
if (ncr5386.SCSI_ACK()) rval |= 1 << 14;
regs.InterruptFromNCR5386 = false; // BUG: should clear on RITRG read
break;
Recommended C# code:
case Register.RSTAU:
if (regs.interruptEnabled) rval |= 1 << 0;
if (regs.active) rval |= 1 << 2;
if (regs.readyForTransfer) rval |= 1 << 3;
// Bit 4: OR of errors - not modeled (BERROR never happens in emulator)
if (regs.resetOnSCSIBus) rval |= 1 << 5;
if (ncr5386.ChipDisabled()) rval |= 1 << 6;
// Bit 7: Single-ended - not modeled
if (regs.DataRequestFromNCR5386) rval |= 1 << 8;
if (regs.InterruptFromNCR5386) rval |= 1 << 9;
if (regs.DataAcknowledgeToNCR5386) rval |= 1 << 10;
// Bit 11: BERROR - never happens in emulator
if (ncr5386.SCSI_BSY()) rval |= 1 << 12;
if (ncr5386.SCSI_REQ()) rval |= 1 << 13;
if (ncr5386.SCSI_ACK()) rval |= 1 << 14;
// Bit 15: Differential - not modeled
// DO NOT clear InterruptFromNCR5386 here.
// On real hardware, RSTAU bit 9 reflects the NCR INT pin state.
// It stays set as long as NCR is asserting interrupt.
// The interrupt is acknowledged by reading RITRG (IOX+0x2C),
// which causes the NCR to deassert INT.
break;
IOX+5 WCONT: Write Control¶
ND-100 writes this to command the controller.
This is the most complex register. See the "What the Z80 Does When WCONT is Written" section above for the full Z80 firmware trace.
C# emulator current code (simplified):
case Register.WCONT:
regs.interruptEnabled = (value & 1 << 0) != 0;
regs.active = (value & 1 << 2) != 0;
regs.testMode = (value & 1 << 3) != 0;
regs.DMAEnable = (value & 1 << 5) != 0;
regs.WriteNDMemory = (value & 1 << 6) != 0;
if (regs.testMode) { /* immediate DMA read/write */ }
if ((value & 1 << 4) != 0) { /* Clear Device: reset NCR, set ready */ }
regs.resetOnSCSIBus = ((value & 1 << 10) != 0);
if (regs.resetOnSCSIBus) { ncr5386.InitiateResetSCSIBus(); regs.readyForTransfer = true; }
if (regs.active) { regs.readyForTransfer = false; ExecuteGo(); }
else { if (interruptEnabled && readyForTransfer) SetInterruptBit(true); }
break;
Recommended C# code based on firmware analysis:
case Register.WCONT:
regs.interruptEnabled = (value & 1 << 0) != 0;
regs.active = (value & 1 << 2) != 0;
regs.testMode = (value & 1 << 3) != 0;
regs.DMAEnable = (value & 1 << 5) != 0;
regs.WriteNDMemory = (value & 1 << 6) != 0;
// Test Mode: immediate single-word DMA (no Z80 involvement on real HW)
if (regs.testMode)
{
uint dma_address = regs.MAR;
if (regs.WriteNDMemory)
{
DMAWrite(dma_address, regs.ReadWriteData);
}
else
{
regs.ReadWriteData = (ushort)DMARead(dma_address);
}
}
// Clear Device: on real HW this asserts Z80 RESET, full reboot sequence
// Z80 does: RAM test, CTC init, NCR selftest, CTC calibration
// then writes 0x08 to port 0x54 (self-test status) during boot
// and enters HALT idle loop when ready
if ((value & 1 << 4) != 0)
{
regs.MemoryAddressLSB = 0;
regs.MemoryAddressMSB = 0;
bufferPointer = 0;
readbufferPointer = 0;
ncr5386.DeviceReset();
regs.readyForTransfer = true;
// On real HW: Z80 takes thousands of cycles before ready.
// Consider adding a delay counter before setting readyForTransfer.
}
// Reset SCSI Bus
regs.resetOnSCSIBus = ((value & 1 << 10) != 0);
if (regs.resetOnSCSIBus)
{
ncr5386.InitiateResetSCSIBus();
regs.readyForTransfer = true;
// On real HW, RSTAU bit 5 generates ND-100 interrupt if bit 0 set
if (regs.interruptEnabled)
{
SetInterruptBit(true);
}
}
// Activate: start command processing
// On real HW: board latches data, pulses CTC1 Ch0, Z80 ISR reads
// command from ports 0x50-0x54, dispatches to handler, processes
// SCSI/floppy operation, writes results to ports 0x54-0x57,
// writes completion to port 0x70, board sets Ready and interrupts ND-100
if (regs.active)
{
regs.readyForTransfer = false;
dma_bytes_written = 0;
dma_bytes_read = 0;
// If NCR already has pending interrupt, complete immediately
if (regs.InterruptFromNCR5386)
{
regs.active = false;
regs.readyForTransfer = true;
if (regs.interruptEnabled) SetInterruptBit(true);
}
// Otherwise: NCR5386SCSI handles the SCSI protocol.
// StepGoState() on each Clock() transfers DMA bytes and
// detects NCR completion.
}
else
{
// Not activating - just enabling interrupt.
// If already ready, signal immediately.
// This matches SINTRAN pattern: "5; T:= HDEV + WCONT; *IOXT"
// which writes 5 (enable IRQ + active) after SELEC completes.
if (regs.interruptEnabled && regs.readyForTransfer)
{
SetInterruptBit(true);
}
}
break;
IOX+6 RHMAR: Read Memory Address Register (bits 16-23)¶
C# emulator: rval = regs.MemoryAddressMSB; -- Correct.
IOX+7 WHMAR: Write Memory Address Register (bits 16-23)¶
C# emulator: regs.MemoryAddressMSB = (ushort)(value & 0xFF); -- Correct.
IOX+0x08 RXWC_HI: Read External Wordcount (bits 16-23, ND-3204 only)¶
C# emulator: rval = regs.ExternalWordcountMSB; -- Correct. Only used on ND-3204.
IOX+0x0A RXWC: Read External Wordcount (bits 0-15, ND-3204 only)¶
C# emulator: rval = regs.ExternalWordcountLSB; -- Correct. Only used on ND-3204.
IOX+0x20 RNDAT: Read NCR Data Register¶
C# emulator: rval = ncr5386.Read((byte)SCSIRegisters.DataRegister); -- Correct passthrough.
Z80 firmware also reads this via port 0x20 (IN A,(0x20)) in scsi_execute_io_operation at 0x1248 during the status phase to read SCSI status bytes.
IOX+0x21 WNDAT: Write NCR Data Register¶
C# emulator: ncr5386.Write((byte)SCSIRegisters.DataRegister, (byte)value); -- Correct passthrough.
IOX+0x22 RNCOM: Read NCR Command Register¶
C# emulator: rval = ncr5386.Read((byte)SCSIRegisters.CommandRegister); -- Correct.
Note: Per NCR 5386 datasheet, the Command Register is reset when the chip sets an interrupt, so the read value may not reflect the last command written.
IOX+0x23 WNCOM: Write NCR Command Register¶
C# emulator: ncr5386.Write((byte)SCSIRegisters.CommandRegister, (byte)value); -- Correct.
This is the register that starts NCR operations. SINTRAN writes commands like: - 0x03: Set ATN - 0x04: Message Accepted - 0x00: Disconnect - Select With/Without ATN - Transfer Info (single byte or DMA)
IOX+0x24/0x25 RNCNT/WNCNT: NCR Control Register¶
C# emulator: Correct passthrough. Bits: 0=Select enable, 1=Reselect enable, 2=Parity enable.
IOX+0x26/0x27 RDESI/WDESI: Destination ID Register¶
C# emulator: Correct passthrough. SINTRAN writes the target SCSI ID here before Select commands.
IOX+0x28/0x29 RAUXS/WAUXS: Auxiliary Status Register¶
C# emulator: Correct passthrough.
Z80 firmware reads this at port 0x28 (IN A,(0x28)) in:
- ncr5386_reset_parity (0x0708): writes 0x20 to port 0x28 to reset diagnostic state
- ISR 0x0505: reads status to determine SCSI bus phase
- scsi_command_entry_point (0x1DFC): reads to flush/acknowledge NCR state during reinit
IOX+0x2A/0x2B ROIDN/WOIDN: Own ID Register¶
C# emulator: Correct passthrough for WOIDN.
Z80 firmware writes port 0x2A (WOIDN) in:
- ncr5386_set_own_id_and_program (0x0711): sets Own ID for transfer operations
- scsi_enable_selection (0x06B6): writes 0x02 to enable selection
- scsi_command_entry_point (0x1E00): writes 0x0F during reinit (enable all IDs)
BUG REMINDER: SetSCSIIdNumber() writes to SCSIRegisters.SourceID instead of SCSIRegisters.IDRegister. Fix at lines 792 and 804.
IOX+0x2C RITRG: Read Interrupt Register¶
C# emulator current code:
case Register.RITRG:
rval = ncr5386.Read((byte)SCSIRegisters.InterruptRegister);
break;
Recommended C# code:
case Register.RITRG:
rval = ncr5386.Read((byte)SCSIRegisters.InterruptRegister);
// Reading the Interrupt Register acknowledges the NCR interrupt.
// The NCR deasserts INT, which clears RSTAU bit 9.
// This is where InterruptFromNCR5386 should be cleared,
// NOT in the RSTAU read handler.
regs.InterruptFromNCR5386 = false;
break;
Why: On real hardware, reading RITRG causes the NCR 5386 to deassert its INT pin, which clears the hardware signal that feeds into RSTAU bit 9. The SINTRAN interrupt handler reads RSTAU first (to check what happened), then reads RITRG (to acknowledge). Clearing the flag on RSTAU read would lose it before RITRG is read.
IOX+0x2E RSOUI: Read Source ID¶
C# emulator: rval = ncr5386.Read((byte)SCSIRegisters.SourceID); -- Correct. Read-only register returning the ID of the selecting/reselecting device.
IOX+0x32 RDIST: Read Diagnostic Status¶
C# emulator: rval = ncr5386.Read((byte)SCSIRegisters.DiagnosticStatus); -- Correct. Bit 7 (SLFCO) = selftest complete.
IOX+0x38/0x39 RTCM/WTCM: Transfer Counter MSB¶
C# emulator: Correct passthrough. SINTRAN sets this as part of the SCSI Select command sequence.
IOX+0x3A/0x3B RTC2/WTC2: Transfer Counter 2nd Byte¶
C# emulator: Correct passthrough.
IOX+0x3C/0x3D RTCL/WTCL: Transfer Counter LSB¶
C# emulator: Correct passthrough.
Summary: Z80 Port to IOX Register Mapping¶
This table shows how Z80 port writes/reads map to what the ND-100 sees via IOX:
| Z80 Action | Z80 Port | Direction | IOX Effect | IOX Register |
|---|---|---|---|---|
| Write MAR low | OUT (0x50), L | Z80 -> HW | Sets MAR low byte | Readable via RLMAR (IOX+0) |
| Write MAR mid | OUT (0x51), H | Z80 -> HW | Sets MAR mid byte | Readable via RLMAR (IOX+0) |
| Write MAR high | OUT (0x52), A | Z80 -> HW | Sets MAR high byte | Readable via RHMAR (IOX+6) |
| Latch MAR | OUT (0x53), 0x90 | Z80 -> HW | Activates readback | Enables RLMAR/RHMAR |
| Clear control | OUT (0x53), 0x00 | Z80 -> HW | Clears bus mode | - |
| Write status | OUT (0x53), mode | Z80 -> HW | Sets SCSI selection state | Affects RSTAU bits |
| Write flags | OUT (0x54), flags | Z80 -> HW | Device select / status | Readable via REDAT? |
| Write SCSI ID | OUT (0x55), id | Z80 -> HW | SCSI target ID | Readable via REDAT? |
| Write direction | OUT (0x56), dir | Z80 -> HW | Transfer direction flag | Affects RSTAU? |
| Write completion | OUT (0x57), status | Z80 -> HW | Error/done flags | Affects RSTAU ready |
| Write FD1797 cmd | OUT (0x70), cmd | Z80 -> HW | FD1797 command + bus status | Sets RSTAU Active/Ready |
| Read cmd high | IN A, (0x50) | HW -> Z80 | Gets ND-100 command word high | Written via WRDAT/WLMAR |
| Read param 1 | IN A, (0x51) | HW -> Z80 | Gets command parameter | Written via WRDAT |
| Read param 2 | IN A, (0x52) | HW -> Z80 | Gets command parameter | Written via WRDAT |
| Read param 3 | IN A, (0x53) | HW -> Z80 | Gets command parameter | Written via WRDAT |
| Read cmd low | IN A, (0x54) | HW -> Z80 | Gets ND-100 command word low | Written via WRDAT/WLMAR |
| Read MAR back | IN A, (0x55) | HW -> Z80 | Readback MAR low | After 0x90 latch |
| Read MAR back | IN A, (0x56) | HW -> Z80 | Readback MAR mid | After 0x90 latch |
| Read MAR back | IN A, (0x57) | HW -> Z80 | Readback MAR high | After 0x90 latch |
Debugging: BUS RESET Loop¶
Observed Symptom¶
The emulated SCSI controller loops indefinitely with this pattern every ~5 seconds:
BUS RESET -> 4s -> SELECTION:0 -> ReadCapacity -> STATUS:0 GOOD -> 5s -> BUS RESET -> repeat
The ReadCapacity SCSI command succeeds every time on the SCSI bus (correct 8-byte response, STATUS 0 GOOD, MESSAGE_IN 0x00 = Command Complete). But SINTRAN resets the bus again ~5 seconds later.
Root Cause: RSTAU Read Clears NCR Interrupt Flag Too Early¶
sequenceDiagram
participant SINT as SINTRAN Driver
participant CTRL as C# Controller
participant NCR as NCR5386SCSI
SINT->>CTRL: IOX WNCOM = Select (target 0)
CTRL->>NCR: Write CommandRegister
NCR->>NCR: Runs full SCSI transaction synchronously
Note over NCR: Select -> ReadCapacity -> DATA_IN -> STATUS -> MSG_IN
NCR-->>CTRL: OnInterrupt(1)
Note over CTRL: InterruptFromNCR5386 = true
SINT->>CTRL: IOX WCONT = 5 (Active + Enable IRQ)
Note over CTRL: active=true, readyForTransfer=false
Note over CTRL: Clock() -> StepGoState()
Note over CTRL: Sees InterruptFromNCR5386=true
Note over CTRL: active=false, readyForTransfer=true
CTRL-->>SINT: SetInterruptBit(true) -> Level 11 IRQ
SINT->>CTRL: IOX RSTAU (read status)
Note over CTRL: Returns bit 9 set (NCR interrupt)
Note over CTRL: BUG: InterruptFromNCR5386 = false (cleared!)
SINT->>CTRL: IOX RAUXS (read NCR aux status)
SINT->>CTRL: IOX RITRG (read NCR interrupt register)
Note over SINT: Processes the interrupt...
Note over SINT: But the flag is already gone from RSTAU
Note over SINT: Expects next operation to complete...
Note over SINT: 5 second timeout expires
SINT->>CTRL: IOX WCONT bit 10 = RESET SCSI BUS
Note over SINT: Loop repeats
The Specific Code Bug¶
In NDBusDiscControllerSCSI.cs line 905, inside the RSTAU read handler:
// CURRENT CODE (BUGGY):
case Register.RSTAU:
if (regs.interruptEnabled) rval |= 1 << 0;
if (regs.active) rval |= 1 << 2;
if (regs.readyForTransfer) rval |= 1 << 3;
// ...
if (regs.InterruptFromNCR5386) rval |= 1 << 9;
// ...
regs.InterruptFromNCR5386 = false; // <- THIS CAUSES THE LOOP
break;
On real hardware, RSTAU bit 9 is a hardware signal that mirrors the NCR 5386 INT pin. It stays asserted as long as the NCR's INT pin is active. Reading RSTAU does NOT clear it. The NCR only deasserts INT when its Interrupt Register (RITRG, IOX+0x2C) is read.
Why This Causes the 5-Second Loop¶
The SINTRAN interrupt handler (SCINT) follows this exact sequence:
SCINT: T:= HDEV + RSTAU; *IOXT % 1. Read status -> sees bit 9
"0"; T:= HDEV + WCONT; *IOXT % 2. Clear controller
T+"RAUXS-WCONT"; *IOXT % 3. Read NCR aux status
T+"RITRG-RAUXS"; *IOXT % 4. Read NCR interrupt reg (acknowledge)
Step 1 reads RSTAU and sees the NCR interrupt (bit 9). But the C# emulator immediately clears the flag. After step 2 clears the controller, SINTRAN continues processing. But subsequent state checks may fail because the flag is gone.
More critically: if SINTRAN's processing path re-reads RSTAU (for any reason), or if StepGoState() needs the flag for state transitions, it's already cleared. The controller gets stuck in an incomplete state, and SINTRAN's 5-second watchdog timer fires.
The Fix¶
// FIXED CODE:
case Register.RSTAU:
if (regs.interruptEnabled) rval |= 1 << 0;
if (regs.active) rval |= 1 << 2;
if (regs.readyForTransfer) rval |= 1 << 3;
if (regs.resetOnSCSIBus) rval |= 1 << 5;
if (ncr5386.ChipDisabled()) rval |= 1 << 6;
if (regs.DataRequestFromNCR5386) rval |= 1 << 8;
if (regs.InterruptFromNCR5386) rval |= 1 << 9;
if (regs.DataAcknowledgeToNCR5386) rval |= 1 << 10;
if (ncr5386.SCSI_BSY()) rval |= 1 << 12;
if (ncr5386.SCSI_REQ()) rval |= 1 << 13;
if (ncr5386.SCSI_ACK()) rval |= 1 << 14;
// DO NOT clear InterruptFromNCR5386 here!
// On real hardware this is a live signal from the NCR INT pin.
break;
case Register.RITRG:
rval = ncr5386.Read((byte)SCSIRegisters.InterruptRegister);
// Clear the flag HERE - reading RITRG acknowledges the NCR interrupt,
// causing the NCR to deassert INT, which clears RSTAU bit 9.
regs.InterruptFromNCR5386 = false;
break;
Additional Check: NCR Chip Disabled¶
If ncr5386.ChipDisabled() returns true, RSTAU bit 6 would be set. The SINTRAN handler checks:
IF X:= 64 /\ A >< 0 THEN % 64 decimal = 0x40 = bit 6
If bit 6 is set (NCR disabled), SINTRAN would treat the controller as non-functional and skip interrupt processing. Verify that ChipDisabled() returns false during normal operation.
Additional Check: Interrupt Propagation Timing¶
The commented-out code in Ncr5386_OnInterrupt() (lines 759-767) means NCR interrupts are delayed until the next Clock() cycle. If Clock() doesn't run between the NCR completion and SINTRAN's WCONT write, the state transition may not happen in time.
Recommended fix for Ncr5386_OnInterrupt:
private void Ncr5386_OnInterrupt(byte intr)
{
if (intr != 0)
{
regs.InterruptFromNCR5386 = true;
// Complete the operation immediately if controller is active.
// On real hardware, the Z80 ISR chain handles this over many
// cycles, but the end result is the same: active->false,
// readyForTransfer->true, then ND-100 gets interrupt.
if (regs.active)
{
regs.active = false;
regs.readyForTransfer = true;
}
if (regs.interruptEnabled)
{
SetInterruptBit(true);
}
}
}
What SINTRAN Expects to See in RSTAU¶
After a successful SCSI command, SINTRAN's interrupt handler expects:
| Bit | Value | Meaning |
|---|---|---|
| 0 | 1 | Interrupt Enabled (we set it via WCONT) |
| 2 | 0 | NOT Busy (command complete) |
| 3 | 1 | Ready for Transfer (data available in DMA memory) |
| 9 | 1 | NCR Interrupt (SCSI operation complete) |
If any of these are wrong, SINTRAN either waits (bit 2 set = busy) or can't determine what happened (bit 9 clear = no interrupt source).
What Causes SINTRAN to Issue BUS RESET¶
SINTRAN writes WCONT bit 10 (SCSI Bus Reset) in these situations:
- Initialization: First boot, SINTRAN always resets the bus to discover devices
- Select timeout: SELEC routine sets a 5-second timer (
-5 =:TMR). If selection doesn't complete, it resets - Error recovery: After detecting a bus error, protocol violation, or unexpected disconnect
- No interrupt after command: If the controller stays busy (RSTAU bit 2 set) for too long
- RSTAU shows unexpected state: If the status bits don't match expected completion pattern
In your log, cause #2 or #4 is most likely - SINTRAN's 5-second timeout expires because the completion interrupt didn't propagate correctly to the ND-100.
Floppy Boot Process¶
Complete Floppy Boot Flow¶
sequenceDiagram
participant ND as ND-100 (SINTRAN)
participant CTRL as Controller (Z80)
participant FD as FD1797
participant DMA as AM9517
participant DISK as Floppy Drive
Note over CTRL: Power-on Reset (RST0)
CTRL->>FD: OUT 0x70 = 0x02 (Restore variant)
CTRL->>ND: OUT 0x54 = 0x08 (self-test flag)
Note over CTRL: RAM Test + POST
CTRL->>FD: OUT 0x70 = 0x00 (Restore to track 0)
CTRL->>FD: Wait for BUSY clear
CTRL->>FD: OUT 0x73 = 0x01, OUT 0x70 = 0x12 (Seek track 1, verify)
CTRL->>FD: IN 0x71 - verify track register = 1
Note over CTRL: Idle loop - waiting for ND-100
ND->>CTRL: IOX WCONT (Active) - device init command
Note over CTRL: ISR 0x02AB reads port 0x50 bit 4 = device init
CTRL->>CTRL: Read ports 0x51-0x53 (block address)
CTRL->>ND: DMA read 12-byte device config into 0x2080
Note over CTRL: Device type = 0x2C (floppy)
Note over CTRL: Dispatch to init_floppy_drive (0x08A1)
CTRL->>FD: OUT 0x70 = 0xD0 (Force Interrupt - abort pending)
CTRL->>FD: OUT 0x74 = 0x00, OUT 0x75 = 0x00 (deselect drives)
CTRL->>FD: OUT 0x70 = 0x00 (Restore - seek track 0)
CTRL->>FD: Wait for BUSY clear
CTRL->>CTRL: Clear drive control block (28 bytes at 0x2100)
CTRL->>CTRL: Extract head count from config bits 7:6
CTRL->>CTRL: Build drive select mask (density, motor, drive bits)
CTRL->>CTRL: Lookup sector count from table at 0x09C4
CTRL->>CTRL: Setup disk geometry (sector size, buffers, track layout)
CTRL->>ND: Signal completion via port 0x70
Note over ND: SINTRAN sends read command
ND->>CTRL: IOX WCONT (Active) - read sectors
CTRL->>CTRL: ISR dispatches to nd100_block_transfer_loop
loop For each sector
CTRL->>FD: IN 0x71 - check current track
CTRL->>FD: OUT 0x73 = target track, OUT 0x70 = 0x18 (Seek)
CTRL->>FD: Wait for seek complete
CTRL->>FD: OUT 0x72 = sector number
CTRL->>FD: OUT 0x70 = 0x88 (Read Sector)
FD->>DMA: DRQ for each byte
DMA->>CTRL: Byte transferred to Z80 RAM
FD-->>CTRL: Interrupt on completion
CTRL->>CTRL: Check FD1797 status (port 0x70)
end
CTRL->>ND: DMA write sector data to ND-100 memory
CTRL->>ND: OUT 0x70 = 0xD4, signal completion
Floppy Format Table¶
The firmware supports three floppy formats, selected by the lower nibble of the config byte:
| Format Code | Sectors/Track | Bytes/Sector | Media | Table at 0x09C4 |
|---|---|---|---|---|
| 0 | 26 (0x1A) | 128 | 8" Single Density | 0x09C4 |
| 1 | 15 (0x0F) | 256 | 5.25" Double Density or 8" DD | 0x09C5 |
| 2 | 8 (0x08) | 512 | 5.25" Double Density | 0x09C6 |
Drive Select Mask (built by build_drive_select_mask at 0x09D4)¶
| Bit | Meaning | Source |
|---|---|---|
| 0-3 | Drive select (one-hot: 0x01=drive 0, etc.) | Head count shifted |
| 4 | Motor on | Set when config bit 3 is NOT set |
| 7 | Mini-floppy / 8" density flag | Set when media descriptor bit 2 is set |
FD1797 Status Bits and Error Mapping¶
After Read/Write Sector commands, port 0x70 status maps to errors:
| Bit | Mask | FD1797 Meaning | Firmware Action | Error Event |
|---|---|---|---|---|
| 0 | 0x01 | BUSY | Poll until clear | 0x91 if stuck |
| 2 | 0x04 | Lost Data | Abort transfer | 0x92 (0x1D18) |
| 3 | 0x08 | CRC Error | Retry with recalibrate | Part of 0x89 |
| 4 | 0x10 | Record Not Found | Retry seek | 0x89 (0x1D03) |
| 5 | 0x20 | Record Type / Deleted | Used for EOF detection | Not an error |
| 6 | 0x40 | Write Protect | Abort write | 0x8E (0x1D10) |
| 7 | 0x80 | Not Ready | Abort operation | 0x90 (0x1D14) |
Status mask 0xBC (bits 7,5,4,3,2) is checked after Read Sector at 0x101C. Status mask 0x18 (bits 4,3) is checked after Seek at 0x0E32. Status mask 0x9C (bits 7,4,3,2) is checked after Read Address at 0x0EBF.
Floppy Error Codes Sent to ND-100¶
All errors go through scsi_start_io_operation (0x0614) which writes to ports 0x54-0x57:
| Port | Content | Meaning |
|---|---|---|
| 0x54 | io_flags (0x208D) | Bit 3=complete, bit 4=check condition, bit 7=fatal |
| 0x55 | error_code * 2 (from 0x2194 << 1) | The error code shifted left by 1 |
| 0x56 | 0x00 or 0x80 | Bit 7 = SCSI device flag (0 for floppy) |
| 0x57 | completion status | 0x00=success, 0x01=error, 0x03=fatal error |
Error code in port 0x55 is the event code lower 6 bits, shifted left. Examples:
| Event | Code byte | Lower 6 bits | Port 0x55 value | Meaning |
|---|---|---|---|---|
| 0x1D03 | 0x89 | 0x09 | 0x12 (18 dec) | CRC error / Record Not Found |
| 0x1D0A | 0x8B | 0x0B | 0x16 (22 dec) | Invalid format code |
| 0x1D10 | 0x8E | 0x0E | 0x1C (28 dec) | Write protect |
| 0x1D14 | 0x90 | 0x10 | 0x20 (32 dec) | Drive not ready |
| 0x1D16 | 0x91 | 0x11 | 0x22 (34 dec) | FD1797 busy timeout |
| 0x1D18 | 0x92 | 0x12 | 0x24 (36 dec) | Lost data |
| 0x1D1A | 0x93 | 0x13 | 0x26 (38 dec) | Restore failed |
| 0x1D20 | 0x97 | 0x17 | 0x2E (46 dec) | Read/write retries exhausted |
| 0x1D22 | 0x98 | 0x18 | 0x30 (48 dec) | Sector out of range |
| 0x1CFD | 0x86 | 0x06 | 0x0C (12 dec) | SCSI bus error |
Your Specific Error Codes¶
Error code 0x20 (decimal 32) = port 0x55 value 0x20: This is event 0x90 (lower 6 bits = 0x10, shifted left = 0x20). It means "Drive Not Ready" - triggered when FD1797 status bit 7 (Not Ready) is set. Causes: - Floppy drive motor not spinning - No disk inserted - Drive door open - Drive not connected
Error code 0x06 (decimal 6) = port 0x55 value 0x06: Lower 6 bits = 0x03, so the event code would be 0x83 or similar. But 0x06 doesn't directly match an event table entry as a port 0x55 value. If 0x06 is seen in the sense byte (port 0x56 or 0x208F), it means SCSI Sense Key 0x06 = "Unit Attention" - the SCSI target device was reset or media was changed. This comes from the NCR 5386 reading the actual sense data from the SCSI target disk, not from the floppy path.
Complete Error Code Reference¶
| Event Addr | Code Byte | Port 0x55 | Trigger | FD1797 Status | Meaning |
|---|---|---|---|---|---|
| 0x1CFB | 0x85 | 0x0A | NCR parity check | - | SCSI parity error |
| 0x1CFD | 0x86 | 0x0C | scsi_reselection_handler | - | SCSI bus error |
| 0x1CFF | 0x87 | 0x0E | - | - | Seek retry exhausted |
| 0x1D01 | 0x88 | 0x10 | seek_and_select_drive | Bit 3+4 | Seek verify failed |
| 0x1D03 | 0x89 | 0x12 | nd100_block_transfer_loop | Bit 3 or 4 | CRC / Record Not Found |
| 0x1D05 | 0x8A | 0x14 | set_status_and_restart | - | Track mismatch |
| 0x1D0A | 0x8B | 0x16 | init_floppy_drive | - | Invalid format code |
| 0x1D0C | 0x8C | 0x18 | check_scsi_bus_change | - | Unexpected disconnect |
| 0x1D0E | 0x8D | 0x1A | check_scsi_bus_change | - | Unexpected reconnect |
| 0x1D10 | 0x8E | 0x1C | scsi_execute_io_operation | - | Unit attention / Write protect |
| 0x1D12 | 0x8F | 0x1E | scsi_reselection_handler | - | SCSI bus error (reselect) |
| 0x1D14 | 0x90 | 0x20 | poll_nd100_bus_ready, scsi_select_target | Bit 7 | Drive Not Ready / Bus timeout |
| 0x1D16 | 0x91 | 0x22 | scsi_select_target | Bit 0 stuck | FD1797 busy timeout |
| 0x1D18 | 0x92 | 0x24 | nd100_block_transfer_loop | Bit 2 | Lost Data |
| 0x1D1A | 0x93 | 0x26 | nd100_bus_issue_cmd_and_wait | - | Reselection retry exhausted |
| 0x1D1C | 0x95 | 0x2A | process_pending_command | - | Command re-entry |
| 0x1D1E | 0x96 | 0x2C | NMI watchdog | - | Watchdog timeout |
| 0x1D20 | 0x97 | 0x2E | scsi_execute_io_operation | - | I/O retries exhausted (3x) |
| 0x1D22 | 0x98 | 0x30 | compute_chs_from_lba | - | Sector out of range |
| 0x1D24 | 0x9A | 0x34 | CTC2 Ch0 ISR | - | SCSI operation timeout |
| 0x1D26 | 0x9B | 0x36 | ncr5386_read_verify_transfer | - | Transfer counter mismatch |
| 0x1D28 | 0x20 | 0x40 | set_nd100_dma_address verify | - | DMA address readback fail |
| 0x1D2C | 0x22 | 0x44 | - | - | (reserved) |
| 0x1D2E | 0x22 | 0x44 | dispatch_by_scsi_id | - | End of dispatch table |
| 0x1D32 | 0xA6 | 0x4C | set_nd100_dma_address | - | DMA address mismatch |
Confirmed from ND-3106/3112 Manual (ND-11.021.1)¶
The ND-3106/3112 floppy/streamer controller manual confirms the ND-100 bus interface architecture is shared across ND controllers. Key findings that apply to the ND-3201:
Port 0x50-0x57 Register Names (confirmed identical)¶
| Port | Write Name | Write Function | Read Name | Read Function |
|---|---|---|---|---|
| 0x50 | ADL | DMA Address bits 0-7 | CW1 | Control Word low (bit 0=autoload, 4=activate, 5=ENI, 6=DMA enabled, 7=BERROR) |
| 0x51 | ADM | DMA Address bits 8-15 | POL | Pointer/data bits 0-7 |
| 0x52 | ADH | DMA Address bits 16-23 | POM | Pointer/data bits 8-15 |
| 0x53 | DD-T | DMA Direction+Test (bit 0=dir, 1=enable DMA, 7=NTEST) | POH | Pointer bits 16-23 |
| 0x54 | DLO | Data Out bits 0-7 | CW2 | Control Word bits 9-15 |
| 0x55 | DHI | Data Out bits 8-15 | - | ND-100 MAR bits 0-7 |
| 0x56 | NSTAT | Status for ND-100 (maps to HW status bits 5,6,8-13) | - | ND-100 MAR bits 8-15 |
| 0x57 | NFINI | Set RFT + error flags (bit 0=error, bit 1=hard error) | - | ND-100 MAR bits 16-23 |
Writing to port 0x57 (NFINI) always sets RFT (Ready For Transfer) and generates an ND-100 interrupt if enabled. This is the trigger mechanism.
Command Block Format (6 words in ND-100 memory)¶
| Offset | Content |
|---|---|
| +0 | Command Word (bits 0-5=function, 6-7=unit, 8-9=bytes/sector, 10=sides, 11=density) |
| +1 | Device Address bits 0-15 (logical sector number) |
| +2 | Device Address bits 16-23 (high) + Memory Address bits 16-23 (low byte) |
| +3 | Memory Address bits 0-15 |
| +4 | Options (bit 15=wordcount mode) + Word Count bits 16-23 |
| +5 | Word Count bits 0-15 |
| +6 | Status Word 1 (written by controller on completion) |
| +7 | Status Word 2 (written by controller on completion) |
| +10 oct | Last Device Address |
| +11 oct | Last Memory Address |
| +12 oct | Remaining Word Count high |
| +13 oct | Remaining Word Count low |
Error Codes (confirmed matching ND-3201 firmware events)¶
The 3106/3112 error codes stored in Status Word 1 bits 9-14 match the ND-3201 event table exactly:
| Octal | Hex | 3106/3112 Meaning | ND-3201 Event | Match |
|---|---|---|---|---|
| 05 | 0x05 | CRC error | 0x1CFB (0x85) | Partial |
| 06 | 0x06 | Sector not found | 0x1CFD (0x86) | YES |
| 07 | 0x07 | Track not found | 0x1CFF (0x87) | YES |
| 10 | 0x08 | Format not found | 0x1D01 (0x88) | YES |
| 11 | 0x09 | Diskette defect | 0x1D03 (0x89) | YES |
| 12 | 0x0A | Format mismatch | 0x1D05 (0x8A) | YES |
| 13 | 0x0B | Illegal format | 0x1D0A (0x8B) | YES |
| 14 | 0x0C | Single sided diskette | 0x1D0C (0x8C) | YES |
| 15 | 0x0D | Double sided diskette | 0x1D0E (0x8D) | YES |
| 16 | 0x0E | Write protected | 0x1D10 (0x8E) | YES |
| 17 | 0x0F | Deleted record | 0x1D12 (0x8F) | - |
| 20 | 0x10 | Drive not ready | 0x1D14 (0x90) | YES |
| 21 | 0x11 | Controller busy | 0x1D16 (0x91) | YES |
| 22 | 0x12 | Lost data | 0x1D18 (0x92) | YES |
| 23 | 0x13 | Track 0 not detected | 0x1D1A (0x93) | YES |
| 25 | 0x15 | Microprogram crash (Z80 hit 0xFF = RST 38H) | 0x1D1C (0x95) | YES |
| 26 | 0x16 | Watchdog timeout (~10 sec) | 0x1D1E (0x96) | YES |
| 27 | 0x17 | Undefined error | 0x1D20 (0x97) | YES |
| 30 | 0x18 | Track/sector out of range | 0x1D22 (0x98) | YES |
| 32 | 0x1A | Compare error | 0x1D24 (0x9A) | YES |
| 33 | 0x1B | Internal DMA error | 0x1D26 (0x9B) | YES |
| 40 | 0x20 | ND-100 bus error - command fetch | 0x1D28 (0x20) | YES |
| 42 | 0x22 | ND-100 bus error - data transfer | 0x1D2C (0x22) | YES |
| 43 | 0x23 | Illegal command | 0x1D2E (0xA3) | YES |
| 46 | 0x26 | Address register error | 0x1D32 (0xA6) | YES |
| 50 | 0x28 | No bootstrap found | 0x1D34 (0xA8) | YES |
| 51 | 0x29 | Wrong bootstrap version | 0x1D36 (0xA9) | YES |
CTC Channel Assignment (3106/3112)¶
| Port | 3106/3112 Function | ND-3201 Equivalent |
|---|---|---|
| 0x10 | CTC Ch0: Interrupt from ND-100 | CTC1 Ch0: ND-100 command reception |
| 0x11 | CTC Ch1: Streamer exception | CTC1 Ch1: Calibration reference |
| 0x12 | CTC Ch2: FD1797 interrupt | CTC1 Ch2: SCSI bus phase |
| 0x13 | CTC Ch3: DMA controller interrupt | CTC1 Ch3: NCR 5386 interrupt |
| 0x14 | CTC Ch4: Compare error | CTC2 Ch0: SCSI timeout |
| 0x15 | CTC Ch5: Streamer ready | CTC2 Ch1: Unused |
| 0x16 | CTC Ch6: Timer | CTC2 Ch2: Calibration counter |
| 0x17 | CTC Ch7: Timer | CTC2 Ch3: Display refresh |
CTC Ch0 (port 0x10) = ND-100 command trigger is confirmed identical on both boards.
Port 0x74 (FDVSEL) Drive Select Bits (confirmed)¶
| Bit | Function |
|---|---|
| 0-3 | Drive select (active low, directly from FD1797 active decode) |
| 4 | Motor control |
| 5 | DMA direction |
| 7 | Mini-floppy / density flag |
Port 0x77 Read (Drive Status, confirmed)¶
Read from port 0x77 returns floppy drive status signals.
Port 0x53 (DD-T) DMA Direction Details (confirmed)¶
| Bit | Function |
|---|---|
| 0 | DMA direction: 0 = Z80 to ND-100, 1 = ND-100 to Z80 |
| 1 | Enable DMA transfer between ND-100 and Z80 |
| 4-5 | Test register select |
| 7 | NTEST enable (0x90 = enable test/readback mode) |
This confirms the ND-3201 port 0x53 value 0x90 = bit 7 (NTEST) + bit 4 (test select) = enable readback mode. And value 0x02 = bit 1 = enable DMA.
Watchdog Timer¶
The 3106/3112 has a watchdog timer that triggers error code 26 (octal) = 0x16 after ~10 seconds. The ND-3201 has the same via NMI at 0x0066 (event 0x96 = code 0x16). The timeout value is stored at RAM 0x20AA and decremented by NMI.
Debugging: Intermittent BUS RESET on Real Hardware¶
The BUS RESET loop on real hardware (not the emulator) with an intermittent pattern ("comes and goes") points to timing-related issues. The SCSI transaction completes successfully on the bus but the controller fails to signal completion to the ND-100 in time.
Possible Causes on Real Hardware¶
flowchart TD
SYMPTOM["Intermittent BUS RESET loop
ReadCapacity succeeds but controller
doesn't signal completion in time"]
CAUSE1["NMI Watchdog Timeout
Counter at 0x20AA reaches 0
Event 0x96 -> full reinit
~10 second timeout"]
CAUSE2["CTC2 Ch0 Timeout
ISR 0x0AF3 fires
Writes 0xD0 to port 0x70
Aborts SCSI operation"]
CAUSE3["CTC Clock Calibration Drift
calibrate_ctc_clock at 0x1395
Marginal calibration result
All timeouts slightly off"]
CAUSE4["SCSI Bus Signal Integrity
Marginal termination
Cable length/quality
NCR 5386 sees parity errors"]
CAUSE5["NCR 5386 INT Timing
INT assertion too brief
CTC1 Ch3 misses the pulse
Z80 never processes completion"]
CAUSE6["Z80 ISR Chain Stall
Dynamic ISR at 0x2076
Phase ISR doesn't complete
Stuck waiting for NCR state"]
SYMPTOM --> CAUSE1
SYMPTOM --> CAUSE2
SYMPTOM --> CAUSE3
SYMPTOM --> CAUSE4
SYMPTOM --> CAUSE5
SYMPTOM --> CAUSE6
style CAUSE3 fill:#ff9
style CAUSE5 fill:#ff9
Most Likely Cause: CTC Calibration + NCR INT Timing¶
The "comes and goes" nature suggests a marginal timing condition:
-
CTC calibration at boot (0x1395) measures the actual clock frequency and adjusts timer constants. If the result is near a boundary, some boots get a slightly different calibration, affecting all timeouts.
-
NCR 5386 INT pulse width - The NCR's INT output is connected to CTC1 Ch3 CLK/TRG. The CTC needs to see the pulse for at least one Z80 clock cycle to register it. If the NCR deasserts INT too quickly (e.g., because its internal state machine moves to the next phase), the CTC might miss it on some attempts.
-
Temperature sensitivity - Both CTC timing and SCSI bus signal levels are temperature-dependent. A cold board might calibrate differently than a warm one.
What to Check on the Real Hardware¶
-
Watch the 7-segment display during the loop - does it show an error code? The error codes match the 3106/3112 manual exactly (see table above).
-
Check SCSI bus termination - both ends of the bus must be properly terminated. Missing or weak termination causes signal reflections.
-
Check SCSI cable length - total bus length must not exceed spec (6 meters for single-ended).
-
Try with just one SCSI device - remove all other devices to eliminate bus contention.
-
Check the CTC clock crystal - if the oscillator is marginal, CTC calibration will be unreliable.
-
The 5-second interval in your log matches the NMI watchdog/CTC timeout, not the ND-100 microcode timeout. This suggests the Z80 firmware itself is aborting the operation, not the ND-100.
References¶
- ND-12.048 ND-100 SCSI Reference Guide
- ND-11.021.1 EN Floppy and Streamer Controller 3106/3112
- NCR 5386 SCSI Controller Users Guide (May 1985)
- FD1797 Floppy Disc Controller datasheet
- Z80 CTC Counter/Timer Circuit datasheet
- AM9517 DMA Controller datasheet
- ndwiki.org/wiki/3201