Porting Prompt: RetroCore C# SCSI Controller -> nd100x C¶
You are an expert C systems programmer working inside the nd100x emulator repository
(WSL path ~/repos/nd100x). Your task is to port the RetroCore C# SCSI disk controller
and its SCSI bus/device/disk stack into C, in the exact style of the existing nd100x
device drivers, so that the ported controller reads and writes disk blocks the same way the
existing SMD controller does. The port must be byte-accurate so it can be validated
against RetroCore's known-good behavior.
You are doing the port. This document is self-contained: it embeds the concrete inventory of the C# source, the concrete analysis of the nd100x C target style, a file-by-file port plan, a byte-level accuracy checklist, a validation plan, and the project constraints. You should still open and confirm each referenced file before you rely on it, but you should not need any other context to begin.
1. Goal¶
Add a new block device deviceSCSI to nd100x that emulates the ND-3201/3204 SCSI disk
controller (NCR-5386 SCSI protocol chip + DMA onto the ND-100 IO bus), driving a
Micropolis-1375-class ND SCSI hard disk. When wired in, SINTRAN III must be able to:
@ENTER-DIRECTORY,,DISC-SCSI-1,0 <- mounts the SCSI disk
@DIR <- lists the directory
This already works in RetroCore (C#) after a recent RDIV fix. Your C port must reproduce the same on-wire SCSI behavior and the same block I/O so the SINTRAN mount + directory listing succeeds identically.
Prime directive: FIRST study the existing nd100x SMD driver, adopt its structure, naming, bus-registration, DMA, interrupt, and block read/write patterns, and mirror how the SMD controller reads/writes disk blocks. Only THEN write the SCSI code. The SCSI controller must work "similar to the SMD controller" from the machine's point of view (same block-callback path, same DMA helpers, same interrupt mechanism), while decoding SCSI CDBs internally.
2. Source inventory (RetroCore C#)¶
Repo root: E:\Dev\Repos\Ronny\RetroCore. Study path: Emulated.HW\Common\SCSI\ plus the ND
adapter under Emulated.HW\ND\CPU\NDBUS\. (Ignore any .claude\worktrees copies.)
Architecture: the ND adapter is a dumb NCR-5386 register + DMA bridge. It does NOT decode
SCSI CDBs. The Command Descriptor Block is assembled by SINTRAN's driver and interpreted by
the target device class (SCSIHDD / SCSIHDDMicropolis). Your C port collapses this into
one deviceSCSI that (a) presents the ND IOX register/DMA interface like the adapter and
(b) internally decodes the CDB like the target device. You do NOT need a faithful NCR-5386
register-level emulation for the acceptance test as long as the CDB decode + DMA + block I/O
match; but you MUST reproduce the register map and status/interrupt semantics SINTRAN's driver
depends on.
Active code path (the one that must be byte-accurate):
NDBusDiscControllerSCSI -> SCSIBus -> NCR5386SCSI -> SCSIHDDMicropolis : SCSIHDD :
SCSIFullDevice : SCSIDevice.
Note: SCSIConfigurableHDD / SCSIDeviceConfig / ISCSIConfigurable are a newer data-driven
rewrite whose config dependency is ABSENT from the repo (does not compile-link into the ND
path). Use it only as a cross-reference; the byte-source of truth is SCSIHDD +
SCSIHDDMicropolis.
2.1 ND adapter - NDBusDiscControllerSCSI¶
File: E:\Dev\Repos\Ronny\RetroCore\Emulated.HW\ND\CPU\NDBUS\NDBusDiscControllerSCSI.cs
Class: NDBusDiscControllerSCSI : NDBusDeviceBase. Emulates the ND-3201/3204 card. Creates the
SCSIBus, the NCR5386SCSI chip, and one SCSIHDDMicropolis per unit. Own SCSI ID = 7.
Interrupt level = 11 (disk output-channel). NDBusAddressLength = 63.
Thumbwheel TW2 selects IOX base / IDENT / logical device (octal):
| TW2 | IOX base (octal) | IDENT (octal) | SCSI bus | logical dev |
|---|---|---|---|---|
| 0/4/8/C | 144300 | 140440 | 1 | 2202 |
| 1/5/9/D | 144400 | 140441 | 2 | 2203 |
| 2/6/A/E | 144500 | 140442 | 3 | 2204 |
| 3/7/B/F | 144600 | 140443 | 4 | 2205 |
Register map (Register enum; offset from IOX base; even=read, odd=write):
| Off | Name | Meaning |
|---|---|---|
| 00 | RLMAR | read Memory Address Register bits 0-15 |
| 01 | WLMAR | write MAR bits 0-15 |
| 02 | REDAT | read data buffer (IOX PIO mode) |
| 03 | WRDAT | write data buffer (IOX PIO mode) |
| 04 | RSTAU | read status word |
| 05 | WCONT | write control word |
| 06 | RHMAR | read MAR bits 16-23 |
| 07 | WHMAR | write MAR bits 16-23 (& 0xFF) |
| 0x08/0x0A | RXWC_HI / RXWC | external word count (3204 only) |
| 0x20/0x21 | RNDAT/WNDAT | NCR data reg |
| 0x22/0x23 | RNCOM/WNCOM | NCR command reg |
| 0x24/0x25 | RNCNT/WNCNT | NCR control reg |
| 0x26/0x27 | RDESI/WDESI | destination ID |
| 0x28/0x29 | RAUXS/WAUXS | aux status |
| 0x2A/0x2B | ROIDN/WOIDN | own ID |
| 0x2C | RITRG | interrupt register (read clears NCR interrupt) |
| 0x2E | RSOUI | source ID |
| 0x32 | RDIST | diagnostic status |
| 0x38/0x39 | RTCM/WTCM | transfer counter |
| 0x3A/0x3B | RTC2/WTC2 | transfer counter 2 |
| 0x3C/0x3D | RTCL/WTCL | transfer counter low |
Status word (RSTAU, read): bit0 interrupt-enabled; bit2 active/busy; bit3 ready-for-transfer;
bit4 OR-of-errors (never set in emu); bit5 reset-on-SCSI-bus; bit6 NCR disabled; bit7
single-ended; bit8 data-request-from-NCR; bit9 interrupt-from-NCR; bit10 data-ack; bit11
BERROR (never); bit12 SCSI BSY; bit13 SCSI REQ; bit14 SCSI ACK; bit15 differential.
Reading RSTAU does NOT clear the NCR interrupt - the interrupt is acknowledged only when
RITRG (0x2C) is read (InterruptFromNCR5386 = false).
Control word (WCONT, write): bit0 enable interrupt; bit2 activate (GO); bit3 test mode; bit4
clear-device (zeros MAR + buffer pointers, resets NCR, sets ready-for-transfer); bit5 ND-100
DMA enable; bit6 write-ND-100-memory (DMA direction: 1 = SCSI->ND memory, 0 = ND memory
->SCSI); bit10 reset SCSI bus. Writing bit2=1 starts the transfer and clears ready-for-transfer.
DMA / byte packing (this is the load-bearing detail): the transfer drains all bytes of the
current SCSI phase. Bytes pack into 16-bit ND words big-endian: even byte index -> HIGH
byte of the word, odd byte index -> LOW byte. The MAR increments only after the low (odd)
byte. WriteNextByteDMA does read-modify-write of the target word to preserve the untouched
byte. The MAR is a WORD address; physical byte address = MAR << 1
(DMABus.WriteMemory16(MAR<<1, data) / ReadMemory16(MAR<<1)).
Completion is event-driven: when the NCR signals interrupt, the controller sets active=false, ready-for-transfer=true, and raises interrupt level 11 if enabled.
2.2 SCSIBus¶
File: ...\Common\SCSI\SCSIBus.cs. Port of MAME nscsi_bus; up to 16 devices; OR-ties data
and control lines; notifies devices on control-line changes. For the C port you may reduce this
to a minimal single-initiator/single-target model (the ND adapter is initiator ID 7, one disk
target), OR keep an explicit phase state machine. The acceptance test does not require the full
OR-tied multi-device bus; it requires correct phase sequencing SELECT -> COMMAND -> DATA -> STATUS
-> MESSAGE IN -> BUS FREE for one target.
2.3 SCSIDevice / SCSIFullDevice¶
Files: ...\Common\SCSI\SCSIDevice.cs, ...\Common\SCSI\SCSIFullDevice.cs.
SCSIFullDevice is the target-side phase state machine (step()), driving a buf_control
FIFO with actions BC_MSG_OR_COMMAND, BC_STATUS, BC_DATA_IN, BC_DATA_OUT, BC_MESSAGE_1,
BC_BUS_FREE. Key facts to port:
- scsi_cmdbuf = 4096-byte CDB + data staging buffer. scsi_sense_buffer = 18 bytes.
- CDB length by command group (top 3 bits of opcode): group0=6, group1=10, group2=10, group3=6,
group4=16, group5=12, group6=6, group7=6.
- scsi_data_in(buf, size) queues a DATA IN phase; scsi_data_out(buf, size) queues DATA OUT;
scsi_status_complete(status) queues STATUS + COMMAND-COMPLETE message + BUS FREE.
- Fixed-format sense (set_sense_data): byte0 = 0x70 or 0x71 (valid bit 0x80); byte2 = sense
key + flags; byte7 = 10 (additional length); bytes12-13 = ASC/ASCQ big-endian.
- SBUF ids (SCSIEnums.cs): SBUF_MAIN=0, SBUF_SENSE=1, SBUF_DATA=2.
- Block storage is via callbacks: BlockReadCallback(ControllerId, DeviceUnit, byteOffset, size)
returns byte[]; BlockWriteCallback(ControllerId, DeviceUnit, byteOffset, byte[]);
IsMountedCallback.
2.4 SCSIHDD - the disk command handler (ACTIVE, byte-accurate)¶
File: ...\Common\SCSI\SCSIHDD.cs. SCSIHDD : SCSIFullDevice. This is where you get the exact
CDB decode. m_sectorData = 1024-byte buffer. Geometry in hdinfo.
Command decode (scsi_command()) - EXACT parsing:
| Opcode | Name | LBA | Length | Action |
|---|---|---|---|---|
| 0x00 | TEST UNIT READY | - | - | GOOD if media else CHECK COND |
| 0x03 | REQUEST SENSE | - | cmd[4] | 18-byte sense (see below) |
| 0x04 | FORMAT UNIT | - | - | GOOD |
| 0x08 | READ(6) | get_u24be(1) & 0x1FFFFF (21-bit) |
cmd[4], 0->256 |
read blocks, DATA IN blocks*1024 |
| 0x0A | WRITE(6) | get_u24be(1) & 0x1FFFFF |
cmd[4], 0->256 |
DATA OUT blocks*1024, write |
| 0x12 | INQUIRY | - | cmd[4] |
see INQUIRY bytes below |
| 0x15 | MODE SELECT(6) | - | - | accept params, IGNORE block descriptor (sector size fixed 1024) |
| 0x1A | MODE SENSE(6) | - | - | mode header + block descriptor + pages |
| 0x1B | START STOP UNIT | - | - | GOOD |
| 0x25 | READ CAPACITY | - | - | see READ CAPACITY bytes below |
| 0x28 | READ(10) | get_u32be(2) |
get_u16be(7) |
read, DATA IN blocks*1024 |
| 0x2A | WRITE(10) | get_u32be(2) |
get_u16be(7) |
DATA OUT blocks*1024, write |
| 0x2F | VERIFY(10) | - | - | GOOD |
Illegal / out-of-range fields -> CHECK CONDITION with sense key ILLEGAL_REQUEST + ASC 0x24 (INVALID FIELD IN CDB).
READ CAPACITY (0x25) exact bytes (CommandReadCapacity), 8-byte DATA IN:
bytes 0-3 = last LBA (big-endian u32) = DiskSizeInBlocks = cylinders*heads*sectors - 1
bytes 4-7 = block size (big-endian u32) = 1024
INQUIRY (0x12) exact bytes (CommandInquiry, page 0): buffer zero-filled then bytes 8..35
space-padded (0x20), then:
byte0 = 0x00 device type = direct-access disk
byte1 = 0x00 not removable
byte2 = 0x05 SPC-3
byte3 = 0x01 response data format = CCS
byte4 = 52 additional length
bytes 8-15 = vendor id "NDMICROP" (8 bytes, from SetInquireBuf for the ND disk)
bytes 16-31 = product id "1375" (space-padded to 16)
bytes 32-35 = revision "B0C" + pad
min(cmd[4], 56). (The generic default vendor/product used when no ND
metadata is set is SEAGATE ST225N - NOT used for the ND disk; use the NDMICROP/1375/B0C
identity.) A verified real-hardware INQUIRY exchange: CDB 12 00 00 00 24 00 -> 36 bytes.
REQUEST SENSE (0x03): 18-byte sense, big-endian ASC/ASCQ. After DeviceReset the disk posts
UNIT ATTENTION (sense key 0x06, ASC 0x29 = POWER ON / RESET). Verified real trace:
70 00 06 00 00 00 00 0A 00 00 00 00 29 00 00 00 00 00.
MODE SENSE(6) (0x1A): mode header + 8-byte block descriptor (block count = put_u24be
DiskSizeInBlocks, block length = put_u24be 1024) + pages 0x00/0x01/0x02/0x03/0x04/0x08/0x30.
Only needed if SINTRAN issues it during mount; reproduce faithfully if the trace shows it,
otherwise implement the header + block descriptor + rigid-geometry page 0x04 at minimum.
The function-42 / control-record connect: SINTRAN's ENTER-DIRECTORY mount reads the disk's
last block as the "control record" connect (fn-42). This is simply a READ of the last LBA
via the normal READ path - there is nothing special in the SCSI decode beyond correctly
answering READ CAPACITY (so SINTRAN computes the right last-LBA) and then serving the READ of
that block from the image. See the NDInsight note
SINTRAN/Devices/SCSI/scsi-mount-last-block-is-control-record.md: the last-block read IS the
fn-42 control-record connect (correct behavior). Do NOT describe it as a "geometry probe". The
historical mount bug was block 0 never being read; ensure block 0 and the last block both read
correctly from the image.
Block I/O physical path (readBlock/writeBlock):
location (byte offset) = (long)lba * sectorbytes // sectorbytes = 1024
readBlock: BlockReadCallback(ControllerId, DeviceUnit, location, sectorbytes) -> byte[1024]
writeBlock: BlockWriteCallback(ControllerId, DeviceUnit, location, byte[1024])
scsi_get_data recomputes
clba = lba + pos/1024 and re-reads the block when the LBA changes, returning
m_sectorData[pos % 1024]. scsi_put_data mirrors it and flushes via writeBlock when
offset == 1023.
hdinfo: ushort cylinders, byte heads, ushort sectors, ushort sectorbytes. Defaults for the
ND disk: cyl=898, heads=8, sectors=18, sectorbytes=1024.
DiskSizeInBlocks = cylinders*heads*sectors - 1 (this is actually the LAST LBA, note the -1).
2.5 SCSIHDDMicropolis - the concrete ND disk¶
File: ...\Common\SCSI\SCSIHDDMicropolis.cs. SCSIHDDMicropolis : SCSIHDD. MICORPOL_1375_ND:
heads=8, cyl=898, sectors=18, sectorbytes=1024 -> 129312 sectors * 1024 = 132,415,488 bytes,
last LBA 129311. Identity NDMICROP / 1375 / B0C. Overrides: INQUIRY (rebuilds buffer then
delegates to base), RESERVE(6) 0x16 -> GOOD, SEEK(6) 0x0B (lba = get_u24be(1) & 0x1FFFFF,
range-checked), vendor 0x0C = INIT DRIVE PARAMS (scsi_data_out 8 bytes). DeviceReset posts
UNIT ATTENTION (ASC 0x29).
2.6 Supporting files¶
...\Common\SCSI\SCSISupport.cs:Bufferwrapper with big-endian accessorsput_u16be,put_u24be,put_u32be,get_u16be,get_u24be,get_u32be, plusfill,setText. Port these as small C helpers (or reuse nd100x's existing big-endian word helpers)....\Common\SCSI\SCSIEnums.cs:SCSICommandsopcodes (as tabulated above),SCSIControlbus-signal bits,SCSIStatus, sense keys, ASC codes,SBUF....\Common\SCSI\SCSIHDDImage.cs: internal raw-image reader (seekscur_lba*sectorbytes). NOT used by the ND callback path - block I/O flows through the block callbacks. In nd100x the equivalent is the machine block-callback layer (see section 3), so you do NOT port this file.
3. Target-style analysis (nd100x C) - DO THIS FIRST¶
Repo root (WSL): ~/repos/nd100x = /home/ronny/repos/nd100x. Read with
wsl.exe bash -lc '...' (cat/sed/grep). Before writing any SCSI code, read and internalize
the SMD driver and the device framework. Confirm every path and signature below against the
actual files - they were captured from the current tree but verify before relying on them.
3.1 The Device model (framework)¶
~/repos/nd100x/src/devices/devices_types.h (approx lines 129-172) defines a single Device
struct with function pointers (no C++ classes). Private state lives in void *deviceData.
typedef struct Device {
uint32_t startAddress; // IOX base (octal)
uint32_t endAddress; // startAddress + 7 for an 8-register device
uint16_t interruptBits; // pending IRQ bitmask (levels 10-13)
uint16_t interruptLevel; // default IRQ level (disk = 11)
uint16_t identCode; // IDENT response (octal)
uint16_t logicalDevice;
DeviceType type;
char memoryName[MAX_DEVICE_NAME];
DelayedIoInfo *ioDelays; int ioDelayCount; int ioDelayCapacity;
void (*Reset)(struct Device *self);
uint16_t (*Tick)(struct Device *self); // returns interruptBits
int (*Boot)(struct Device *self, uint16_t device_id);
uint16_t (*Read)(struct Device *self, uint32_t address); // IOX read handler
void (*Write)(struct Device *self, uint32_t address, uint16_t value); // IOX write
uint16_t (*Ident)(struct Device *self, uint16_t level);
void (*Destroy)(struct Device *self);
DeviceClass deviceClass; // use DEVICE_CLASS_BLOCK
size_t blockSizeBytes; // 1024 for the ND SCSI disk
CharacterDeviceCallbacks charCallbacks;
BlockDeviceCallbacks blockCallbacks; // read/write/diskInfo -> machine layer
void *deviceData;
} Device;
size = number of BLOCKS, blockAddress = LBA in
blocks, block size from device->blockSizeBytes:
typedef int (*BlockDeviceReadFunc )(struct Device*, uint8_t *buffer, size_t size, uint32_t blockAddress, int unit);
typedef int (*BlockDeviceWriteFunc)(struct Device*, const uint8_t *buffer, size_t size, uint32_t blockAddress, int unit);
typedef int (*BlockDeviceDiskInfoFunc)(struct Device*, size_t *image_size, bool *is_write_protected, int unit);
3.2 Key framework functions (use these; do NOT reinvent)¶
| Purpose | Function | File |
|---|---|---|
| IOX read/write dispatch | io_op -> IO_Read/IO_Write -> DeviceManager_Read/Write -> Device_Read/Write -> dev->Read/Write |
src/machine/io.c, src/devices/devicemanager.c, src/devices/device.c |
| DMA write word to ND mem | void Device_DMAWrite(uint32_t coreAddress, uint16_t data) |
src/devices/device.c (~916) |
| DMA read word from ND mem | int32_t Device_DMARead(uint32_t coreAddress) |
src/devices/device.c (~922) |
| Raise interrupt | Device_GenerateInterrupt(Device*, uint16_t level) / Device_SetInterruptStatus(Device*, bool, level) |
src/devices/device.c (~800/814) |
| Clear interrupt | Device_ClearInterrupt(Device*, level) |
src/devices/device.c (~785) |
| Deferred completion IRQ | Device_QueueIODelay(Device*, ticks, IODelayedCallback, param, irqlevel) |
src/devices/device.c (~734) |
| Block read (-> image) | Device_ReadBlock -> blockCallbacks.readFunc = machine_block_read |
src/devices/device.c (~1004), src/machine/machine.c (~780) |
| Block write (-> image) | Device_WriteBlock -> machine_block_write |
src/machine/machine.c (~857) |
| Disk info (size/WP) | machine_block_disk_info |
src/machine/machine.c (~899) |
| Big-endian word pack/unpack in a byte buffer | Device_IO_BufferReadWord / Device_IO_BufferWriteWord |
src/devices/device.c (~836-908) |
| Register offset from IOX addr | Device_RegisterAddress |
src/devices/device.c |
| Device init | Device_Init(dev, thumbwheel, DEVICE_CLASS_BLOCK, sectorBytes) |
src/devices/device.c |
Device_DMAWrite/Device_DMARead set gDMAAccess=true around
WritePhysicalMemory/ReadPhysicalMemory(addr & 0xFFFFFF, false) so the transfer bypasses
MMU shadow/page checks. Interrupt levels are restricted to 10-13 (the bit is
1 << level in interruptBits). Disk uses level 11 (matches RetroCore).
3.3 The SMD controller - your structural template¶
~/repos/nd100x/src/devices/smd/deviceSMD.c and deviceSMD.h; geometry helper
diskSMD.c/.h (only DiskSMD_SetDiskType lives there - block transfer logic is in
deviceSMD.c, NOT diskSMD.c). Also read src/devices/floppy/deviceFloppyDMA.c/.h as a second
example.
SMD IOX register map (base 01540 for thumbwheel 0; register = addr - startAddress; even=read,
odd=write; several registers multiplexed by control-word bit 15 registerMultiplexBit):
0 read Core Address / write Load Memory Address low
1 write Load Core Address / Count Mem+WC (test)
2 read Seek Condition / ECC Count
3 write Load Block Address I / II
4 read Status Register / ECC Pattern
5 write Load Control Word
6 read Block Address I / II
7 write Load Word Counter / ECC Control
blockAddressI bits 0-7 = sector, bits 8-15 = head; blockAddressII =
cylinder. SMD status/control words are union { uint16_t raw; struct { ... :1; } bits; }.
Important header gotcha (deviceSMD.h ~line 143): every bitfield member must be typed
uint16_t (not a mixed/enum type), otherwise -mms-bitfields on Windows/MinGW splits the
storage unit and breaks the raw overlay. Follow the same rule for all SCSI register unions.
SMD block transfer (ExecuteGO in deviceSMD.c, triggered when control-word bit 2 active is
written). Copy this pattern exactly:
sector = blockAddressI & 0xFF;
head = (blockAddressI >> 8) & 0xFF;
cylinder = blockAddressII;
long lba = ConvertCHStoLBA(regs, cylinder, head, sector); // (cyl*heads + head)*sectors + sector
long position = lba * bytesPrSector; // bytesPrSector = 1024
wordCounter = (wordCounterHI << 16) | wordCounter;
coreAddress = (coreAddressHiBits << 16) | coreAddress; // WORD address
blockCounter = (wordCounter * 2) / blockSizeBytes; // #blocks to move
// READ:
buffer = malloc(blockCounter * blockSizeBytes);
self->blockCallbacks.readFunc(self, buffer, blockCounter, lba, unit); // = machine_block_read
while (wordCounter > 0) {
uint32_t w = Device_IO_BufferReadWord(self, buffer, buffer_ptr++); // big-endian (hi<<8)|lo
Device_DMAWrite(coreAddress, (uint16_t)w);
coreAddress = IncrementCoreAddress(regs);
wordCounter = DecrementWordCounter(regs);
}
free(buffer);
Device_QueueIODelay(self, IODELAY_HDD_SMD, (IODelayedCallback)SMDReadEnd, unit, self->interruptLevel);
// WRITE: Device_DMARead(coreAddress) each word -> Device_IO_BufferWriteWord into buffer,
// then writeFunc(self, buffer, blockCounter, lba, unit) = machine_block_write,
// then the same Device_QueueIODelay(... SMDReadEnd ...).
// SMDReadEnd clears active, sets readyForTransfer + seek-complete, returns true if IRQ enabled
// (Device_TickIODelay then fires Device_GenerateInterrupt).
Device_IO_BufferReadWord = (buf[N*2]<<8) | buf[N*2+1];
Device_IO_BufferWriteWord writes hi = data>>8 to the even byte. No swap beyond hi/lo packing.
This is IDENTICAL to how the RetroCore DMA packs SCSI bytes into ND words (even byte -> high),
so the two implementations agree byte-for-byte.
Machine-layer fseek math (machine_block_read, src/machine/machine.c ~806-848):
size_t bytes = size * device->blockSizeBytes; // size = #blocks
size_t offset = (size_t)blockAddress * device->blockSizeBytes;
fseek(entry->data.local_file, (long)offset, SEEK_SET);
fread(buffer, 1, bytes, entry->data.local_file); // short read -> zero-filled
fwrite + fflush). The image FILE* is opened once at mount time:
fopen(image_path, "rb+") (falls back to "rb" read-only), machine.c ~500.
SMD boot (SMD_Boot, deviceSMD.c ~773): reads 4 blocks via readFunc, checks non-zero, then
Device_DMAWrite(i, word) for 2048 words into memory at address 0, returns 0. Model SCSI boot
on this if a bootstrap is needed (not required for the ENTER-DIRECTORY acceptance test, but
implement it for parity).
3.4 Factory + registration pattern (copy CreateSMDDevice, deviceSMD.c ~1287)¶
Device *CreateSCSIDevice(uint8_t thumbwheel) {
Device *dev = malloc(sizeof(Device)); memset(dev, 0, sizeof(Device));
SCSIData *data = malloc(sizeof(SCSIData)); memset(data, 0, sizeof(SCSIData));
Device_Init(dev, thumbwheel, DEVICE_CLASS_BLOCK, 1024);
dev->deviceData = data;
dev->Read = SCSI_Read; dev->Write = SCSI_Write; dev->Tick = SCSI_Tick;
dev->Reset = SCSI_Reset; dev->Ident = SCSI_Ident; dev->Boot = SCSI_Boot;
dev->Destroy = SCSI_Destroy;
switch (thumbwheel) { /* set startAddress / identCode / logicalDevice / memoryName */ }
dev->interruptLevel = 11;
dev->endAddress = dev->startAddress + /* register span */;
SCSI_Reset(dev);
return dev;
}
src/devices/devices_types.h: add DEVICE_TYPE_DISC_SCSI to DeviceType, and
#include "./scsi/deviceSCSI.h" at the bottom (where all device headers are included).
2. src/devices/devicemanager.c: add case DEVICE_TYPE_DISC_SCSI: dev = CreateSCSIDevice(thumbwheel);
in CreateDevice(), and a DeviceManager_AddDevice(DEVICE_TYPE_DISC_SCSI, 0); in
DeviceManager_AddAllDevices(). Because deviceClass == DEVICE_CLASS_BLOCK,
DeviceManager_AddDevice auto-wires machine_block_read/write/disk_info - you do NOT open
the image file yourself.
3. src/machine/machine.c: machine_block_read/write/disk_info currently pick the drive array
by device->type (DEVICE_TYPE_DISC_SMD ? DRIVE_SMD : DRIVE_FLOPPY, ~machine.c:783). Add a
DEVICE_TYPE_DISC_SCSI -> DRIVE_SCSI branch and a scsi_drives[] mounted-image array +
mount_scsi(...) helper alongside smd_drives/mount_floppy (~machine.c:241-262), or the
SCSI disk will be treated as a floppy. Follow the existing mount helper exactly.
4. Build system: CMake src/devices/CMakeLists.txt uses file(GLOB ...) per subdir - add a
file(GLOB SCSI_SOURCES "scsi/*.c"), append to SOURCES, add the
mkptypes ... deviceSCSI.c >> devices_protos.h line to the custom command, and add scsi to
both target_include_directories lists. Makefile src/devices/Makefile lists sources
explicitly - add scsi/deviceSCSI.c to SRCS and -I$(SRC_DIR)/scsi to INCLUDES.
devices_protos.h is AUTO-GENERATED by mkptypes - do NOT hand-edit it; put your
CreateSCSIDevice prototype in deviceSCSI.h.
Naming conventions: folder src/devices/scsi/; files deviceSCSI.c/deviceSCSI.h (+ optional
diskSCSI.c/.h for geometry/identity tables, mirroring diskSMD); static internals prefixed
SCSI_* (SCSI_Read, SCSI_Write, SCSI_Tick, SCSI_Reset, SCSI_Ident, SCSI_Boot,
SCSI_Destroy, plus internal SCSI_ExecuteCommand, SCSI_ReadCapacity, SCSI_Inquiry, ...);
shared framework functions stay Device_*; register unions union {uint16_t raw; struct{...} bits;};
register enum SCSIRegisters with octal IOX addresses in comments; device-private state in a
SCSIData typedef stored in deviceData.
4. File-by-file port plan (C# class -> C file/functions)¶
Create everything under ~/repos/nd100x/src/devices/scsi/. You may fold the SCSI stack into
fewer files than the C# side - C uses one Device struct + deviceData, not a class hierarchy.
Recommended layout:
| C# source (RetroCore) | Responsibility | C target |
|---|---|---|
NDBusDiscControllerSCSI.cs |
ND IOX register map, control/status words, DMA byte<->word packing, GO/completion, interrupt level 11 | deviceSCSI.c : SCSI_Read, SCSI_Write, SCSI_Tick, SCSI_Reset, SCSI_Ident, SCSI_Boot, SCSI_Destroy, SCSI_ExecuteGo, DMA pump helpers; deviceSCSI.h register/status/control unions + SCSIRegisters enum + CreateSCSIDevice proto |
SCSIBus.cs + SCSIFullDevice.cs (phase state machine) |
SELECT/COMMAND/DATA/STATUS/MSG/BUS-FREE sequencing, CDB length by group, DATA IN/OUT/STATUS queuing, sense buffer | deviceSCSI.c internal phase state in SCSIData (phase, scsi_cmdbuf[4096], sense[18], FIFO), functions SCSI_StepPhase, SCSI_QueueDataIn, SCSI_QueueDataOut, SCSI_StatusComplete, SCSI_SetSense |
SCSIHDD.cs |
CDB decode (all opcodes in the table), READ CAPACITY, INQUIRY, MODE SENSE, REQUEST SENSE, block read/write via callbacks | deviceSCSI.c : SCSI_ExecuteCommand (opcode switch), SCSI_ReadCapacity, SCSI_Inquiry, SCSI_ModeSense, SCSI_RequestSense, SCSI_ReadBlocks, SCSI_WriteBlocks |
SCSIHDDMicropolis.cs |
Micropolis-1375-ND geometry + identity (NDMICROP/1375/B0C), UNIT ATTENTION on reset, SEEK(6), vendor 0x0C | diskSCSI.c/.h : DiskSCSI_SetDiskType (cyl=898, heads=8, sectors=18, sectorbytes=1024), identity strings, SCSI_DeviceReset posting ASC 0x29 |
SCSISupport.cs (Buffer big-endian accessors) |
u16/u24/u32 big-endian put/get | small static helpers in deviceSCSI.c OR reuse Device_IO_BufferReadWord-style helpers; add scsi_put_u32be/get_u32be/get_u24be/get_u16be |
SCSIEnums.cs |
opcodes, sense keys, ASC codes, status | #define/enum in deviceSCSI.h |
SCSIHDDImage.cs |
raw image reader | NOT ported - use machine block callbacks |
Preserve exact behavior: the register/IO map, the full command set (INQUIRY, READ CAPACITY, READ/WRITE 6/10), the fn-42 control-record last-block read, block size 1024 = 512 words, 21-bit vs 32-bit LBA handling, big-endian multi-byte fields, DMA byte<->word packing, and the level-11 completion interrupt with RITRG-only acknowledge.
5. Byte-level accuracy checklist (validation-critical)¶
Every item here MUST match RetroCore exactly, or SINTRAN's mount / directory listing will fail:
- [ ] Sector size = 1024 bytes = 512 ND words, fixed. MODE SELECT block descriptor is accepted but IGNORED (sector size never changes).
- [ ] READ CAPACITY returns 8 bytes:
[0..3]= last LBA big-endian u32,[4..7]= 1024 big-endian u32. For Micropolis-1375-ND last LBA = 129311 (0x0001F91F). Reporting any other last-LBA -> DISC-TRANSFER-ERROR / STATUS 100020B. - [ ] INQUIRY bytes: byte0=0x00, byte1=0x00, byte2=0x05, byte3=0x01, byte4=52; vendor
"NDMICROP" @8, product "1375" @16 (space-padded), rev "B0C" @32. Return
min(cmd[4],56). - [ ] READ(6)/WRITE(6): LBA = 21-bit
get_u24be(cmd+1) & 0x1FFFFF; length =cmd[4], 0 means 256. READ(10)/WRITE(10): LBA =get_u32be(cmd+2); length =get_u16be(cmd+7). - [ ] All SCSI multi-byte fields are BIG-ENDIAN (MSB first).
- [ ] DMA byte<->word packing is big-endian: first SCSI byte of a word -> HIGH byte; MAR
increments only after the low byte; MAR is a WORD address (physical byte addr =
MAR<<1). This equals the SMDDevice_IO_BufferReadWord/WriteWordconvention - keep them identical. - [ ] ND-100 is 16-bit WORD addressed - never treat the ND side as byte-addressed. The
*2(word->byte) conversion is an emulator implementation detail on the image-buffer side only. - [ ] LBA -> file offset = LBA * 1024 bytes; block read/write go through the machine block
callbacks (
machine_block_read/write), same as SMD. - [ ] Control-record / fn-42 = a READ of the last LBA (and block 0 must read too). No special SCSI opcode; it is a normal READ served from the image.
- [ ] REQUEST SENSE after reset = UNIT ATTENTION (sense key 0x06, ASC 0x29).
- [ ] Completion interrupt = level 11, acknowledged ONLY on
RITRG(0x2C) read, never onRSTAU(0x04) read. - [ ] Status word bit3 = ready-for-transfer, bit2 = active/busy, bit9 = interrupt-from-NCR; control word bit2 = GO, bit5 = DMA enable, bit6 = direction (1 = SCSI->ND memory).
6. Validation plan¶
- Build: build nd100x (CMake and/or Makefile in
src/devices/) after wiring the device in. Fix any prototype/GLOB/include issues.devices_protos.hregenerates viamkptypes. - Mount a SCSI disk image: configure the machine to attach
deviceSCSI(thumbwheel 0, logical device 2202) with the SINTRAN SCSI disk image (the same image RetroCore uses). - Boot SINTRAN from the working boot device, then at the SINTRAN prompt run:
ACCEPTANCE: ENTER-DIRECTORY mounts without error and
@ENTER-DIRECTORY,,DISC-SCSI-1,0 @DIR@DIRreports the directory contents - matching RetroCore's behavior exactly (this already works in RetroCore after the RDIV fix). - Diff against the known-good trace: enable SCSI tracing and compare the CDB sequence and
the byte-level INQUIRY / READ CAPACITY / READ responses against RetroCore's trace and the
NDInsight capture docs under
SINTRAN/Devices/SCSI/(e.g.CAPTURE-WORKING-MOUNT-TRACE.md,scsi-open-last-block-read.md,scsi-transfer-status.md,scsi-mount-last-block-is-control-record.md). Any divergence in opcode, LBA, length, or response bytes is a bug. In particular confirm: READ CAPACITY returns 129311/1024; the mount reads block 0 AND the last block; the completion interrupt fires on level 11. - If SINTRAN reports STATUS 100020B (DISC TRANSFER ERROR), re-check READ CAPACITY last-LBA and the completion/interrupt handshake first - that is the known failure signature.
7. Constraints¶
- Match nd100x's existing C style exactly. C, not C++ (no classes;
Devicestruct + function pointers +void *deviceData). - Follow the existing driver file/proto conventions:
src/devices/scsi/deviceSCSI.c/.h(+ optionaldiskSCSI.c/.h),SCSI_*statics,Device_*framework calls, register unions with all-uint16_tbitfields (the-mms-bitfieldsgotcha), octal IOX comments. - Do NOT hand-edit the auto-generated
devices_protos.h; declare public prototypes in your.hand letmkptypesregenerate. - Use the machine block-callback path for all image I/O (do not open the file inside the driver).
- Reuse
Device_DMAWrite/Device_DMARead/Device_QueueIODelay/Device_GenerateInterrupt/Device_IO_BufferReadWord/Device_IO_BufferWriteWord- do not reinvent DMA, interrupts, or word packing. - Keep as many explanatory comments as the SMD driver has; cite the RetroCore source and the
verified real-hardware traces (INQUIRY
12 00 00 00 24 00; REQUEST SENSE70 00 06 00 00 00 00 0A 00 00 00 00 29 00 00 00 00 00) in comments. - The final arbiter of correctness is behavioral parity with the RetroCore SCSI controller and
the successful SINTRAN
@ENTER-DIRECTORY,,DISC-SCSI-1,0+@DIR.
Quick reference - the two key files to read first¶
- Target style:
~/repos/nd100x/src/devices/smd/deviceSMD.c(+deviceSMD.h,~/repos/nd100x/src/devices/device.c,~/repos/nd100x/src/machine/machine.c). - Byte source of truth:
E:\Dev\Repos\Ronny\RetroCore\Emulated.HW\Common\SCSI\SCSIHDD.cs(+SCSIHDDMicropolis.cs, and the ND adapterE:\Dev\Repos\Ronny\RetroCore\Emulated.HW\ND\CPU\NDBUS\NDBusDiscControllerSCSI.cs).