Skip to content

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

  1. Message Passing Overview
  2. ND-100 to ND-500 - Complete Flow
  3. ND-500 to ND-100 - Complete Flow
  4. Hardware Level Details
  5. C# Implementation - Verified
  6. 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

  1. 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
  2. ND-500 triggers ND-100 interrupt (LMAR5, LCON5)

  3. 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}"
        };
    }
}