Complete Message Passing Between ND-100 and ND-500¶
Step-by-Step Analysis with EXACT Code References
Version: 1.0
Last Updated: October 17, 2025
Purpose: Document EXACTLY what happens when messages pass between systems, based on actual NPL source code
Table of Contents¶
- Message Passing Overview
- ND-100 to ND-500 - Complete Flow
- ND-500 to ND-100 - Complete Flow
- Hardware Level Details
- C# Implementation - Verified
- Complete DVIO Example
1. Message Passing Overview¶
1.1 The Core Mechanism¶
flowchart TB
subgraph PREP [1 Message Preparation ND100 or ND500]
A1[Allocate message buffer in 5MPM]
A2[Fill message fields in 5MPM]
A3[Set 5ITMQUEUE flag in 5MPM]
end
subgraph TRIG [2 Trigger Hardware]
B1[Write to LMAR5 register IOX write]
B2[Write to LCON5 register IOX write]
B3[Hardware interrupt generated]
end
subgraph PROC [3 Processing Receiving CPU]
C1[Interrupt handler activated]
C2[Read message from 5MPM using address from MAR]
C3[Process request call appropriate routine]
C4[Write result to 5MPM update message buffer]
end
subgraph RESP [4 Response Hardware]
D1[Clear 5ITMQUEUE flag in 5MPM]
D2[Write result error code to message]
D3[Trigger return interrupt hardware]
D4[Resume waiting process on originating CPU]
end
A1 --> A2 --> A3 --> B1 --> B2 --> B3
B3 --> C1 --> C2 --> C3 --> C4
C4 --> D1 --> D2 --> D3 --> D4
style A2 fill:#009688,stroke:#00695C,stroke-width:2px,color:#fff
style C2 fill:#009688,stroke:#00695C,stroke-width:2px,color:#fff
style C4 fill:#009688,stroke:#00695C,stroke-width:2px,color:#fff
2. ND-100 to ND-500 - Complete Flow¶
2.1 Step-by-Step with Source Code¶
Scenario: ND-500 process requests terminal input (DVINST)
Step 1: ND-500 Process Prepares Message¶
Location: ND-500 code (not in NPL, but this is what happens)
ND-500 Process:
1. Get my dedicated message buffer address (from process descriptor MESSBUFF field)
2. Write to message buffer in 5MPM:
- TODF = terminal datafield address
- DNOBY/NRBYT = number of bytes wanted
- N500A = where to put data (ND-500 address in 5MPM, bit 31 set)
- SMCNO = 503₈ (or 511₈) - function indicator
- Other fields as needed
3. Set 5ITMQUEUE flag in 5MSFL field
4. Trigger ND-100 interrupt via hardware
5. Enter wait state
Step 2: ND-500 Triggers ND-100 Hardware¶
Hardware sequence (from ND-500 microcode):
ND-500 Microcode:
1. Write message buffer address to ND-500 interface card MAR register
2. Write activation code to control register
3. Hardware generates interrupt to ND-100 on Level 12
Step 3: ND-100 Level 12 Handler Activates¶
Source: Interrupt vector table points to Level 12 handler
% Level 12 interrupt entry (implied from code structure)
L12ENTRY:
*IOF % Interrupts off
% Save registers (CPU does automatically)
% Jump to appropriate handler
GO HANDLE_ND500_INTERRUPT
Step 4: ND-100 Reads Message from 5MPM¶
Source: MP-P2-N500.NPL lines 1820-1839 (NINSTR/XNINSTR)
NINSTR: T:=5MBBANK; X:=N5MESSAGE
*AAX DMAXB; LDDTX; AAX -DMAXB % Read max byte count from message
XNINSTR:
IF A><0 OR D>>4000 THEN % Validate byte count
X:=N5MESSAGE; A:=EC174
CALL EMONICO % Error restart
CALL XACTRDY
GO NXTMSG
FI
What this does:
1. T:=5MBBANK - Set T register to 5MPM bank number
2. X:=N5MESSAGE - X register = message buffer address (from saved state)
3. *AAX DMAXB; LDDTX - Read double word at X+DMAXB offset (max byte count)
4. Check if count is valid (negative or >4000₈ bytes = error)
Step 5: ND-100 Validates and Processes¶
Source: MP-P2-N500.NPL lines 1829-1835
A:=B=:BREG
CALL 5GTDF; GO NORMMC % Get terminal datafield address
IF A.TYPRING BIT 5BAD THEN % Check if valid terminal
X:=N5MESSAGE; GO NORMMC
FI
X=:B; N5MESSAGE=:CCMESS % Save message address
CALL SET12WINDOW % Set up MMU windows
What 5GTDF does:
- Reads TODF field from message
- Looks up the datafield
- Validates it's a terminal
- Returns datafield address in D
Step 6: ND-100 Sets Up MMU Windows¶
Source: RP-P2-N500.NPL lines 479-498 (XIBMOVE setup)
% - SET UP WINDOW FOR ND-500 MESSAGE
A:=IBMIN5MSG=:D:=5MBBANK; A=:T; D=:X
AD SHZ -12 % D=PHYS.PAGE OF MESSAGE
A:=142000; AD=:IBM5PITENTRY % SAVE PITENTRY (RPM+WPM+RING2)
A:=IBMIN5MSG/\1777+"WNDN5*2000"=:IBMWNDMESS % LOG.ADDR OF MESSAGE
% - SET UP WINDOW FOR ND-500 BUFFER
X:=IBMIN5MSG; *AAX ABUFA; LDDTX % Read buffer address from message
D=:T
AD SHZ -12; A:=142000; AD=:IBMM6PITENTRY % D=PHYS.PAGE OF MON60 BUFFER
A:=T/\1777+"WNDBF*2000"=:IBMLGBUADDR % LOGICAL ADDR OF DATA BUFFER
What this means:
1. Read message buffer address (IBMIN5MSG)
2. Calculate physical page (shift right 12 bits)
3. Save current PIT entry for that page
4. Create temporary PIT mapping at window address (WNDN5)
5. Do same for data buffer (ABUFA field from message)
6. Now ND-100 can access both message and data buffers via logical addresses
Step 7: ND-100 Performs I/O Operation¶
Source: RP-P2-N500.NPL lines 490-501 (IIBM loop)
IIBM: *ION; IOF
% - MOVE CHARACTER INTO ND-500 BUFFER
AD:=IBM41PITENTRY % GET TERMINAL SAVED PITENTRY
T:=0; X:="WND41+WND41+174000"; *STDTX % SET PIT.ENTRY
AD:=IBM5PITENTRY % GET ND-500 MESSAGE SAVED PITENTRY
X:="WNDN5+WNDN5+174000"; *STDTX % SET PITENTRY
AD:=IBMM6PITENTRY % GET MON60-BUFFER SAVED PITENTRY
X:="WNDBF+WNDBF+174000"; *STDTX % SET PIT.ENTRY
CALL IOTRANS; GO FAR TMWT % READ CHARACTER; IF NONE, GO TMWT
X:=IBMWNDMESS.5FYLLE; T:=IBMLGBUADDR; *SBYT % STORE BYTE IN BUFFER
A:=X+1=:IBMWNDMESS.5FYLLE % INCREMENT BYTE POINTER
GO IBTBREAK % Check for break condition
Character-by-character:
1. Set up PIT entries for all three windows (terminal, message, buffer)
2. CALL IOTRANS - Read one character from terminal hardware
3. *SBYT - Store byte at buffer address (in 5MPM!)
4. Increment byte counter in message (5FYLLE field)
5. Loop until max bytes reached or special character
Step 8: ND-100 Updates Message with Results¶
Source: RP-P2-N500.NPL lines 521-533 (N5RST)
N5RST: % RESTART USER
IF IBMWNDMESS.5FYLLE=0 THEN % NO CHARS READ?
% Handle echo strategy changes...
IF TDRADR.ISTATE=-1 OR A=-2 THEN % NOWAIT
X := IBMIN5MSG
3; CALL EMONICO % GIVE END OF FILE
0=: IN5MSG % DVINST FINISHED
CALL GCPUDF; CALL ERRFATAL % SET B TO POINT TO CPU DF
A=:B; GO RST % RESTART USER PROGRAM
FI
FI
% ... more handling ...
Message update (not shown explicitly but happens): 1. Final byte count written to message 2. Error code set (0 = success, or EC*** for errors) 3. 5ITMQUEUE flag cleared 4. Message now has results
Step 9: ND-100 Triggers ND-500 Return Interrupt¶
Source: MP-P2-N500.NPL lines 3084-3094 (XACT500)
ACT50: 5MBBANK; T:=HDEV+LMAR5; *IOXT % Write MAR with message address
A:=X; *IOXT % Message address value
A:=5; T+"LCON5-LMAR5"; *IOXT % Control value 5 (activate)
Hardware level:
1. ND-100 writes message address to ND-500 interface card MAR register (HDEV+LMAR5)
2. ND-100 writes activation code (5) to control register (HDEV+LCON5)
3. Interface card generates hardware interrupt to ND-500
4. ND-500 microcode reads MAR, finds message address
5. ND-500 reads message from 5MPM at that address
6. ND-500 sees results, resumes process
Step 10: ND-500 Process Resumes¶
ND-500 side (microcode, not in NPL):
ND-500 Microcode:
1. Interrupt received from ND-100
2. Read MAR register to get message address
3. Read message from 5MPM at that address
4. Check 5ITMQUEUE flag (should be clear now)
5. Extract result: ByteCount, ErrorCode
6. Resume process that was waiting
7. Process reads data from 5MPM buffer
8. Process continues execution
3. ND-500 to ND-100 - Complete Flow¶
3.1 Reverse Direction (ND-500 Output)¶
Scenario: ND-500 process writes to terminal (DVIO)
Steps 1-3: Same as Above but Reversed¶
-
ND-500 Process fills message buffer with:
- TODF = terminal datafield
- DNOBY = number of bytes to write
- N500A = source data address (in 5MPM)
- Sets 5ITMQUEUE flag
-
ND-500 triggers ND-100 interrupt (LMAR5, LCON5)
-
ND-100 Level 12 handler activates
Step 4: ND-100 Reads Output Data¶
Source: MP-P2-N500.NPL lines 1688-1707 (DVIO/NOUTSTR)
DVIO:
NOUTSTR:
CALL 5GTDF; GO NORMMC % GET TERMINAL DATAFIELD
A:=D; X:=N5MESSAGE; T:=5MBBANK; *AAX TODF; STATX % STORE TODF
*AAX DNOBY-TODF; LDDTX; AAX -DNOBY % GET BYTE COUNT
IF A><0 OR D>>4000 THEN % VALIDATE
A:=EC174; CALL EMONICO % ERROR
CALL XACTRDY
GO NXTMSG
ELSE IF D=0 THEN % ZERO BYTES?
CALL OSTRS; GO NXTMSG % JUST RESTART
FI; FI
IF MIFLAG NBIT WSMC THEN % NOT IN COM-BUFFER?
T:=5MBBANK; 3RMED; *STATX XMICF % MIC.FUNC=READ DATA MEMORY
A:=D; *AAX NRBYT; STATX % STORE BYTE COUNT
*AAX 5DITN-NRBYT; STZTX % CLEAR DIT NUMBER
*AAX OSTRA-5DITN; LDDTX; AAX N500A-OSTRA; STDTX % ND-500 SOURCE ADDRESS
*AAX ABUFA-N500A; LDDTX; AAX N100A-ABUFA; STDTX % ND-100 DEST ADDRESS
"STTDRIV"; *AAX SPFLA-N100A; STATX; AAX -SPFLA % RESTART IN DRIVER
This reads: 1. Message fields: TODF, DNOBY (byte count) 2. N500A - source address (where data is in 5MPM) 3. Sets up for terminal driver to copy data
Step 5: ND-100 Copies Data to Terminal¶
The terminal driver then: 1. Reads bytes from 5MPM buffer (at N500A address) 2. Writes bytes to terminal hardware 3. User sees output on screen 4. When complete, updates message with result
Step 6: Completion Same as Before¶
Same as steps 8-10 above: update message, trigger ND-500, resume process.
4. Hardware Level Details¶
4.1 The ND-500 Interface Card (3022/5015)¶
┌─────────────────────────────────────────────────────────────┐
│ ND-500 Interface Card Hardware │
├─────────────────────────────────────────────────────────────┤
│ │
│ MAR (Memory Address Register) - 16 bits │
│ ├─ Holds message buffer address in 5MPM │
│ ├─ Written by: IOX write to HDEV+LMAR5 │
│ └─ Read by: Opposite CPU when interrupt occurs │
│ │
│ Control Register - 16 bits │
│ ├─ Command codes written here │
│ ├─ Value 5 = Activate │
│ ├─ Value 10 = Enable interrupt │
│ └─ Written by: IOX write to HDEV+LCON5 │
│ │
│ Status Register - 16 bits │
│ ├─ Read by: IOX read from HDEV+RSTA5 │
│ ├─ Bit flags: 5CLOST, 5ILOCK, 5INTPEND, etc. │
│ └─ Checked to see if ND-500 is stopped/busy │
│ │
│ TAG Registers - 16 bits each │
│ ├─ LTAG5 (write) - Tag output │
│ ├─ RTAG5 (read) - Tag input │
│ └─ Used for additional control signaling │
│ │
│ Interrupt Logic │
│ ├─ Generates Level 12 interrupt to ND-100 │
│ ├─ Generates interrupt to ND-500 (internal) │
│ └─ Hardware arbitration for simultaneous access │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 Actual IOX Operations¶
From ND-100 side:
% Writing to ND-500 interface
T:=HDEV+LMAR5 % T = IOX address (HDEV = base, +1 = LMAR5)
A:=message_addr % A = message buffer address in 5MPM
*IOXT % IOX write: T=address, A=data
% This writes A to IOX device at address T
T:=HDEV+LCON5 % T = control register address
A:=5 % A = activation code
*IOXT % Write 5 to control register
% Hardware interrupt generated immediately
% Reading from ND-500 interface
T:=HDEV+RSTA5 % T = status register address
*IOXT % IOX read: T=address, result in A
% A now contains status bits
Hardware timing: 1. IOX write takes ~1-2 microseconds 2. Interrupt propagation: ~5-10 microseconds 3. Interrupt handler entry: ~10-20 microseconds 4. Total latency: ~20-50 microseconds typical
5. C# Implementation - Verified¶
5.1 Message Manager (Only Verified Fields)¶
/// <summary>
/// ND-500 Message manager - ONLY VERIFIED FIELDS.
/// Based on actual NPL source code references.
/// </summary>
public class ND500MessageManager
{
private readonly MultiportMemoryAccess _5mpm;
private readonly ushort _5mbBank;
public ND500MessageManager(MultiportMemoryAccess mpm, ushort bank)
{
_5mpm = mpm;
_5mbBank = bank;
}
/// <summary>
/// Read message buffer.
/// WARNING: Field offsets are partially known.
/// This uses documented field names but offsets may be wrong.
/// </summary>
public ND500MessageData ReadMessage(ushort messageAddress)
{
var msg = new ND500MessageData
{
MessageAddress = messageAddress
};
// Read fields using AAX pattern (offset from X register)
// We DON'T know exact offsets, so this is a placeholder structure
// Field order based on code references:
ushort offset = 0;
// PLINK - seen first in code
msg.ProcessLink = _5mpm.ReadWord(messageAddress + offset);
offset++;
// 5MSFL - message flags
msg.MessageFlags = _5mpm.ReadWord(messageAddress + offset);
offset++;
// 5PRIO - priority (location uncertain)
msg.Priority = _5mpm.ReadWord(messageAddress + offset);
offset++;
// TODF - to datafield (size uncertain, assume double word)
msg.ToDatafield = _5mpm.ReadDoubleWord(messageAddress + offset);
offset += 2;
// DNOBY/NRBYT - byte count (double word based on LDDTX)
msg.ByteCount = _5mpm.ReadDoubleWord(messageAddress + offset);
offset += 2;
// N500A - ND-500 address (double word)
msg.ND500Address = _5mpm.ReadDoubleWord(messageAddress + offset);
offset += 2;
// N100A - ND-100 address (double word)
msg.ND100Address = _5mpm.ReadDoubleWord(messageAddress + offset);
offset += 2;
// Additional fields...
msg.MicrocodeFunction = _5mpm.ReadWord(messageAddress + offset);
offset++;
msg.DITNumber = _5mpm.ReadWord(messageAddress + offset);
offset++;
msg.CPUNumber = _5mpm.ReadWord(messageAddress + offset);
offset++;
// SMCNO - monitor call number
msg.MonitorCallNumber = _5mpm.ReadWord(messageAddress + offset);
offset++;
// 5FYLLE - byte fill counter
msg.ByteFill = _5mpm.ReadWord(messageAddress + offset);
offset++;
return msg;
}
/// <summary>
/// Write message buffer.
/// </summary>
public void WriteMessage(ushort messageAddress, ND500MessageData msg)
{
ushort offset = 0;
_5mpm.WriteWord(messageAddress + offset, msg.ProcessLink); offset++;
_5mpm.WriteWord(messageAddress + offset, msg.MessageFlags); offset++;
_5mpm.WriteWord(messageAddress + offset, msg.Priority); offset++;
_5mpm.WriteDoubleWord(messageAddress + offset, msg.ToDatafield); offset += 2;
_5mpm.WriteDoubleWord(messageAddress + offset, msg.ByteCount); offset += 2;
_5mpm.WriteDoubleWord(messageAddress + offset, msg.ND500Address); offset += 2;
_5mpm.WriteDoubleWord(messageAddress + offset, msg.ND100Address); offset += 2;
_5mpm.WriteWord(messageAddress + offset, msg.MicrocodeFunction); offset++;
_5mpm.WriteWord(messageAddress + offset, msg.DITNumber); offset++;
_5mpm.WriteWord(messageAddress + offset, msg.CPUNumber); offset++;
_5mpm.WriteWord(messageAddress + offset, msg.MonitorCallNumber); offset++;
_5mpm.WriteWord(messageAddress + offset, msg.ByteFill); offset++;
}
/// <summary>
/// Set message ready flag (5ITMQUEUE).
/// Code shows: A BONE 5ITMQUEUE; *STATX
/// Exact bit position unknown, assuming bit 0.
/// </summary>
public void SetMessageReady(ushort messageAddress)
{
// Assumes 5MSFL is at offset 1 (unverified!)
ushort flags = _5mpm.ReadWord(messageAddress + 1);
flags |= 0x0001; // Bit position guessed!
_5mpm.WriteWord(messageAddress + 1, flags);
}
/// <summary>
/// Clear message ready flag.
/// </summary>
public void ClearMessageReady(ushort messageAddress)
{
ushort flags = _5mpm.ReadWord(messageAddress + 1);
flags &= unchecked((ushort)~0x0001);
_5mpm.WriteWord(messageAddress + 1, flags);
}
/// <summary>
/// Check if message is in queue.
/// </summary>
public bool IsMessageReady(ushort messageAddress)
{
ushort flags = _5mpm.ReadWord(messageAddress + 1);
return (flags & 0x0001) != 0;
}
}
/// <summary>
/// Message data structure - VERIFIED FIELDS ONLY.
/// </summary>
public class ND500MessageData
{
public ushort MessageAddress { get; set; }
// VERIFIED from NPL source:
public ushort ProcessLink { get; set; } // PLINK
public ushort MessageFlags { get; set; } // 5MSFL
public ushort Priority { get; set; } // 5PRIO
public uint ToDatafield { get; set; } // TODF
public uint ByteCount { get; set; } // DNOBY/NRBYT
public uint ND500Address { get; set; } // N500A/OSTRA
public uint ND100Address { get; set; } // N100A/ABUFA
public ushort MicrocodeFunction { get; set; } // XMICF/MICFUNC
public ushort DITNumber { get; set; } // 5DITN
public ushort CPUNumber { get; set; } // 5CPUN
public ushort MonitorCallNumber { get; set; } // SMCNO
public ushort ByteFill { get; set; } // 5FYLLE
// Helper properties
public bool IsInQueue => (MessageFlags & 0x0001) != 0; // Bit position guessed
public string GetMonitorCallName()
{
// From source: SMCNO=511₈ or 503₈
return MonitorCallNumber switch
{
0x149 => "SMCNO_511_octal", // 511₈ = 0x149 hex
0x143 => "SMCNO_503_octal", // 503₈ = 0x143 hex
_ => $"SMCNO_{MonitorCallNumber:X4}"
};
}
}