ND-100 Instruction Semantics — Derived from the nd100x CPU Implementation¶
Purpose. Ground-truth reference for translating carved ND-100 disassembly into
correct pseudo-C. Every statement below is taken from the executable behavior of the
nd100x emulator CPU core, cited to ~/repos/nd100x/src/cpu/<file>:<line>. Where the
emulator behavior is ambiguous or unimplemented it is flagged UNRESOLVED.
All numeric literals are octal unless suffixed differently (ND-100 convention). Register width is 16 bits. Word-addressed memory. Two's-complement arithmetic.
0. Register / status model (facts)¶
Register index encoding (used by ROP, SKP, IRW/IRR, bit ops). Source: cpu_types.h:163-170.
| idx | reg | idx | reg |
|---|---|---|---|
| 0 | STS | 4 | L |
| 1 | D | 5 | A |
| 2 | P | 6 | T |
| 3 | B | 7 | X |
Disassembler letter suffixes map to these indices (cpu_disasm.c:40,49,50):
DA/SA = A (5), DT/ST = T (6), DX/SX = X (7), DD/SD = D (1),
DB/SB = B (3), DL/SL = L (4), DP/SP = P (2), index 0 = STS (printed DS/0).
Status register bits (cpu_types.h:233-245): bit0 PTM, bit1 TG, bit2 K (the "one‑bit
accumulator" used by bit instructions), bit3 Z (error), bit4 Q (dynamic overflow),
bit5 O (static overflow, sticky), bit6 C (carry), bit7 M (multi‑shift link).
Bits 8-11 = program level, bit14 PONI, bit15 IONI (these live in the shared high
byte reg_STS).
Carry/overflow core — do_add(a,b,k) (cpu_instr.c:82-105): computes tmp = a+b+k
as 32-bit; sets C if bit16+ nonzero; sets O and Q on signed overflow (operands
same sign, result different sign); clears Q otherwise; O is sticky — never cleared
here. Returns low 16 bits. This is the shared adder used by ADD/SUB/AAx/RADD/RSUB/MIN.
Sign extension of 8-bit fields — signExtend(x) (cpu_instr.c:70-79): treats bit7 as
sign, fills bits 8-15. Used for every 8-bit displacement/argument.
1. Translation cheat-sheet (most common forms in MON handlers)¶
P below = address of the instruction word itself. disp = 8-bit displacement,
sign-extended. mem[] = word memory. ind() = one indirect word fetch.
| Disassembly | C meaning |
|---|---|
LDA disp |
A = mem[P + disp] |
LDA ,B disp |
A = mem[B + disp] |
LDA ,X disp |
A = mem[X + disp] |
LDA I disp |
A = mem[ mem[P + disp] ] |
LDA I ,B disp |
A = mem[ mem[B + disp] ] |
LDA ,X ,B disp |
A = mem[B + X + disp] |
LDA I ,X disp |
A = mem[ X + mem[P + disp] ] |
LDA I ,B ,X disp |
A = mem[ X + mem[B + disp] ] |
STA … / STT / STX |
mem[EA] = A / T / X |
STZ … |
mem[EA] = 0 |
STD I ,B ,X disp |
EA = X + mem[B+disp]; mem[EA]=A; mem[EA+1]=D |
LDD ,B disp |
A = mem[B+disp]; D = mem[B+disp+1] |
ADD ,B disp |
A = do_add(A, mem[B+disp], 0) |
SUB ,B disp |
A = do_add(A, ~mem[B+disp], 1) (= A − mem) |
AND ,B disp |
A &= mem[B+disp] |
ORA ,B disp |
A \|= mem[B+disp] |
MIN ,B disp |
t=mem[EA]+1; mem[EA]=t; if(t==0) PC++ (skip) |
MPY ,B disp |
A = (int16)A * (int16)mem[EA] (sets O,Q) |
JMP … |
PC = EA |
JPL I disp |
L = returnaddr; PC = mem[P+disp] |
JAF disp |
if (A != 0) PC += disp |
JAZ disp |
if (A == 0) PC += disp (also sets C = (A==0)) |
JAN/JAP disp |
if (A<0)/(A>=0) PC += disp |
JXZ/JXN disp |
if (X==0)/(X<0) PC += disp |
JPC disp |
X++; if (X>=0) PC += disp |
JNC disp |
X++; if (X<0) PC += disp |
SAA n |
A = signext8(n) |
SAT/SAX/SAB n |
T/X/B = signext8(n) |
AAA n |
A = do_add(A, signext8(n), 0) |
AAX/AAT/AAB n |
X/T/B = do_add(reg, signext8(n), 0) |
RADD SD DA |
A = do_add(A, D, 0) (A = A + D) |
RADD CLD SD DA |
A = D (COPY: dest cleared, then +source) |
COPY SA DX (=RADD CLD SA DX) |
X = A |
RSUB SX DA |
A = do_add(A, ~X, 1) (A = A − X) |
RADD ADC SD DA |
A = do_add(A, D, C) (add with carry) |
SWAP SA DT |
t=T; T=A; A=t (exchange) |
RCLR DA (=RADD CLD 0 DA w/ sr=0) |
A = 0 |
RINC DA (=RADD AD1 0 DA) |
A = do_add(A,0,1) (A+1) |
RDCR DA (=RADD CM1 0 DA) |
A = do_add(A,~0,0) (A−1) |
EXIT (=RADD CLD SL DP) |
PC = L (return) |
STATX n |
phys[(T<<16)|(X + (n>>3))] = A |
STZTX n |
phys[EL] = 0 |
STDTX n |
phys[EL] = A; phys[EL+1] = D |
LDATX n |
A = phys[EL] |
LDXTX n |
X = phys[EL] |
LDDTX n |
A = phys[EL]; D = phys[EL+1] |
LDBTX n |
B = 0177000 \| (2 * phys[EL]) |
SKP IF DA EQL ST |
if (A == T) PC++ |
SKP IF DA GRE SX |
if ((int16)A >= (int16)X) PC++ |
BSET ONE 5 DA |
A bit5 = 1 |
BSET ZRO 5 DA |
A bit5 = 0 |
BSKP ONE 0 DA |
if (A bit0 == 1) PC++ |
BSKP ZRO 0 DA |
if (A bit0 == 0) PC++ |
SHA n / SHA SHR n |
shift A left/right n places |
SHA ROT n / ZIN / LIN |
rotate / zero-in / link-in shift |
IRW 120 DB |
reg[level=120>>3][B] = A (inter-level write) |
IRR 120 DB |
A = reg[level][B] (inter-level read) |
147440 (RADD sr=SL dr=STS) |
NOOP |
2. Memory-reference instructions + addressing modes¶
2.1 Effective address (THE authoritative formula)¶
Source: New_GetEffectiveAddr (cpu.c:186-233). disp = signExtend(instr & 0xFF);
P = (gPC - 1) = address of the instruction word itself (PC was pre-incremented in
do_op, cpu.c:162). Selector = bits 8-10 ((instr>>8)&7). Disassembler string map is
cpu_disasm.c:45.
| bits 8-10 | disasm | Effective address EA |
|---|---|---|
| 0 | (none) | EA = P + disp |
| 1 | ,B |
EA = B + disp |
| 2 | I |
EA = ind(P + disp) |
| 3 | I ,B |
EA = ind(B + disp) |
| 4 | ,X |
EA = X + disp |
| 5 | ,X ,B |
EA = B + X + disp |
| 6 | I ,X |
EA = X + ind(P + disp) |
| 7 | I ,B ,X |
EA = X + ind(B + disp) |
ind(w) = a single indirect word fetch (ReadIndirectVirtualMemory, cpu.c:205,210,223,228).
Order of operations (critical): In the indexed-indirect modes 6 and 7 the indirection
happens first, then the X register is added to the fetched pointer (post-indexing).
There is only one level of indirection (no chained indirection in the emulator).
Mode 5 (,X ,B) is pure pre-indexing with no indirection: B + X + disp.
P-relative (mode 0) accesses the primary page table; all other modes set use_apt=true,
i.e. the final data access uses the alternative page table. This distinction rarely
matters for pseudo-C data-flow but is noted for completeness.
2.2 The instructions¶
All resolve EA = New_GetEffectiveAddr(...) then:
| Mnemonic | Opcode base | Operation | Source |
|---|---|---|---|
STZ |
000000 | mem[EA] = 0 |
cpu_instr.c:539-543 |
STA |
004000 | mem[EA] = A |
:547-552 |
STT |
010000 | mem[EA] = T |
:556-560 |
STX |
014000 | mem[EA] = X |
:564-568 |
STD |
020000 | mem[EA] = A; mem[EA+1] = D |
:572-577 |
LDD |
024000 | A = mem[EA]; D = mem[EA+1] |
:615-621 |
STF |
030000 | mem[EA]=T; mem[EA+1]=A; mem[EA+2]=D |
:581-587 |
LDF |
034000 | T=mem[EA]; A=mem[EA+1]; D=mem[EA+2] |
:625-632 |
MIN |
040000 | t=mem[EA]+1; mem[EA]=t; if(t==0) PC++ |
:785-795 |
LDA |
044000 | A = mem[EA] |
:591-595 |
LDT |
050000 | T = mem[EA] |
:599-603 |
LDX |
054000 | X = mem[EA] |
:607-611 |
ADD |
060000 | A = do_add(A, mem[EA], 0) |
:799-805 |
SUB |
064000 | A = do_add(A, ~mem[EA], 1) (= A−mem) |
:809-814 |
AND |
070000 | A &= mem[EA] (no status change) |
:818-822 |
ORA |
074000 | A \|= mem[EA] (no status change) |
:826-830 |
MPY |
120000 | A = (int16)A * (int16)mem[EA]; sets Q, O on |prod|>32767 |
:3101-3120 |
JMP |
124000 | PC = EA |
:917-926 |
JPL |
134000 | L = PC(return); PC = EA |
:453-464 |
FAD/FSB/FMU/FDV |
100000/104000/110000/114000 | 48-bit float T:A:D op mem[EA..EA+2] | :834-913 |
Notes: STD/LDD store A into the low word and D into the high word+1 (A first, then D).
STD writes mem[EA]=A, mem[EA+1]=D. LDD mirrors it. AND/ORA do not touch any
status bit. SUB is implemented as add of one's-complement + 1 (proper two's-complement
subtract, sets C/O/Q per do_add). JPL stores the return address (the word after JPL —
gL = gPC after PC pre-increment) into L, then jumps.
FDV sets Z on divide-by-zero (cpu_instr.c:906-909). Float format is 48-bit
(T=exponent/sign+mantissa hi, A, D) — see float.c if float translation is required
(out of scope here).
3. Register-operation (ROP) class — base 144000-147777¶
Dispatched by regop (cpu_instr.c:1686-1769); table entry cpu_instr.c:3439.
This is the #1 source of mistranslation. Read carefully.
3.1 Field layout of the 16-bit ROP word¶
bit: 10 9 8 7 6 5 4 3 2 1 0
RAD ADC AD1 CM1 CLD [ sr ] [ dr ]
regop): RAD=(op>>10)&1 (:1692), CM1=(op>>7)&1 (:1693),
CLD=(op>>6)&1 (:1694), sr=(op>>3)&7 (:1696), dr=op&7 (:1697).
For the arithmetic sub-op the code switches on (op & 0x0380)>>7 (bits 7,8,9), i.e. the
triple {ADC, AD1, CM1}; for the logical sub-op it switches on (op & 0x0300)>>8
(bits 8,9).
3.2 Operand fetch (both classes)¶
source = (sr == 0) ? 0 : reg[CurrLEVEL][sr]— when the source field is 0 (STS), the literal value 0 is used, NOT the STS register. (:1699)destination = CLD ? 0 : reg[CurrLEVEL][dr]— CLD clears the destination operand to 0 BEFORE the operation (does not read the dr register). (:1700)- If
dr == 0(destination = STS): logical/arithmetic ops are suppressed (guarded byif (dr != 0)), except the arithmetic path withdr==0clears carry:C = 0(:1758-1761). This is why147440(dr=0) is a NOOP that also clears C only when it reaches the arithmetic path — for147440, sub-op field = 6 = NOOP, so nothing changes.
3.3 Logical operations (RAD = 0), sub-op = bits 8,9¶
Effective only when dr != 0. Let s = CM1 ? ~source : source. (:1704-1726)
| bits 8,9 | mnem | operation |
|---|---|---|
| 0 | SWAP |
t = reg[dr]; reg[dr] = s; reg[sr] = CLD ? 0 : t — exchange (dr gets source, sr gets old dr) |
| 1 | RAND |
reg[dr] &= s; if (CLD) reg[dr] = 0 |
| 2 | REXO |
reg[dr] = CLD ? s : (reg[dr] ^ s) (XOR) |
| 3 | RORA |
reg[dr] = CLD ? s : (reg[dr] \| s) (OR) |
SWAP writes s into dr and the old dr value into sr; with CM1 the value put into dr is
complemented; with CLD the value put into sr is 0. Logical ops do not change C/O/Q/Z.
3.4 Arithmetic operations (RAD = 1), sub-op = bits 7,8,9¶
Effective only when dr != 0. dest = CLD ? 0 : reg[dr], source per §3.2.
All go through do_add so C/O/Q are affected per §0. (:1727-1762)
| bits 7,8,9 | disasm | operation | note |
|---|---|---|---|
| 0 | RADD |
reg[dr] = do_add(dest, source, 0) |
dest + source |
| 1 | RADD CM1 |
do_add(dest, ~source, 0) |
dest − source − 1 |
| 2 | RADD AD1 |
do_add(dest, source, 1) |
dest + source + 1 |
| 3 | RADD AD1 CM1 |
do_add(dest, ~source, 1) |
= RSUB: dest − source |
| 4 | RADD ADC |
do_add(dest, source, C) |
add-with-carry |
| 5 | RADD ADC CM1 |
do_add(dest, ~source, C) |
subtract-with-borrow |
| 6 | NOOP | nothing | |
| 7 | NOOP | nothing |
The disassembler prints sub-op 3 as RSUB (cpu_disasm.c:475-477) and sub-op 0 with
CLD as EXIT when the whole word equals 0146142 (cpu_disasm.c:457-461).
3.5 The critical example: RADD CLD SD DA¶
sr = SD = 1 (D), dr = DA = 5 (A), CLD = 1, arithmetic sub-op 0.
dest = CLD ? 0 = 0; source = reg[D] = D; reg[A] = do_add(0, D, 0) = D.
Result: A = D (a plain copy). This is the COPY idiom: COPY <sr> <dr> assembles to
RADD CLD <sr> <dr>, meaning dst = src (destination cleared, then source added to 0).
do_add(0,src,0) still updates C/O/Q as a side effect (C stays 0, O/Q cleared unless src
overflow — for a copy of a value there is no signed overflow, so O/Q are cleared, C=0).
3.6 Distinctions to remember¶
RADD SD DA(no CLD) =A = A + D.RADD CLD SD DA=A = D. The only difference is CLD, which decides whether the destination register participates or is zeroed.COPY=RADD CLD(dst := src).SWAP= full exchange (dst↔src), a logical opcode, not arithmetic.RADD(no CLD) = arithmetic accumulate.RCLR dr=RADD CLDwithsr=0→dst = do_add(0,0,0) = 0.RINC dr=RADD AD1withsr=0→dst = dst + 1.RDCR dr=RADD CM1withsr=0→dst = dst + ~0 = dst − 1.EXIT=RADD CLD SL DP(0146142) →P = L(procedure return).
3.7 RMPY / RDIV (same register fields, separate opcodes)¶
RMPY sr dr(base 141200, mask FFC0,cpu_instr.c:3061-3096): signed 16×16→32,A = high16, D = low16;source=(sr==0)?0:reg[sr],dest=(dr==0)?0:reg[dr]; setsCif product exceeds 16 bits. (Note: the emulator'srmpysets C but the sign is applied after the C test; O/Q are not set in this active version — the olderrmpy_orgset O/Q. Table entry usesrmpy.)RDIV sr(base 141600, mask FFC0,cpu_instr.c:2990-3018): 32-bit dividend(A<<16)|Ddivided byreg[sr](0 if sr=0). Quotient→A, remainder→D. Divide-by-zero or |quotient|≥32768 setsZand returns without writing A/D. SetsCif quotient > 16 bits.
4. Argument instructions — set / add signed 8-bit argument¶
arg = signExtend(operand & 0xFF). Sources: cpu_instr.c:159-254; table :3487-3494.
| Mnemonic | Opcode base | Operation | Source |
|---|---|---|---|
SAA n |
170400 | A = arg (via setreg) |
:230-233 |
SAB n |
170000 | B = arg |
:237-239 |
SAT n |
171000 | T = arg |
:243-246 |
SAX n |
171400 | X = arg |
:251-253 |
AAA n |
172400 | A = do_add(A, arg, 0) |
:161-167 |
AAB n |
172000 | B = do_add(B, arg, 0) |
:171-177 |
AAT n |
173000 | T = do_add(T, arg, 0) |
:181-187 |
AAX n |
173400 | X = do_add(X, arg, 0) |
:191-198 |
SAx simply loads the sign-extended argument (no status change). AAx uses do_add, so
C/O/Q are affected. The 8-bit field means the argument range is −200..+177 (octal).
5. T/X-indexed physical transfers — base 143300-143306¶
Privileged (each calls CheckPriv). Dispatched by opcode with mask 0xFFC7, so the
variable field is bits 3-5. Table cpu_instr.c:3424-3430.
5.1 The effective address EL (authoritative)¶
calcEL(displacement) (cpu_instr.c:109-118), with
displacement = (operand >> 3) & 0x07 (:644,657,670,694,...):
EL = ( (T & 0xFF) << 16 ) | ( (X + displacement) & 0xFFFF )
EL = EL & 0xFFFFFF // 24-bit physical address
So EL is a 24-bit PHYSICAL address: high 8 bits = T & 0xFF, low 16 bits =
(X + displacement) & 0xFFFF. The displacement is the 3-bit field (0-7), taken from bits
3-5 of the instruction. In listings the suffix is the raw octal of that field region, e.g.
LDDTX 20 → 020 octal >> 3 = 2 → calcEL(2) (see the SETPT microcode transcription,
cpu_instr.c:1287,1296,1308). Access is via ReadPhysicalMemory/WritePhysicalMemory
(ReadEL/WriteEL, cpu_instr.c:121-130) — it bypasses the page tables.
5.2 The instructions¶
| Mnemonic | Opcode | Operation | Source |
|---|---|---|---|
LDATX n |
143300 | A = phys[EL] |
:689-698 |
LDXTX n |
143301 | X = phys[EL] |
:711-720 |
LDDTX n |
143302 | A = phys[EL]; D = phys[EL+1] |
:733-745 |
LDBTX n |
143303 | B = 0177000 \| ((2 * phys[EL]) & 0xFFFF) |
:759-773 |
STATX n |
143304 | phys[EL] = A |
:652-661 |
STZTX n |
143305 | phys[EL] = 0 |
:639-648 |
STDTX n |
143306 | phys[EL] = A; phys[EL+1] = D |
:665-675 |
Notes:
* LDDTX/STDTX operate on the pair (EL, EL+1), low word first. STDTX stores A into
EL and D into EL+1 (matches STD's A-then-D convention).
* LDBTX doubles the fetched word (2*phys[EL], wrapped to 16 bits) and ORs in 0177000
(0xFE00). This forms a page-table-entry pointer with the top bits forced set. The ND
hardware had a documented bug here (see cpu_instr.c:1272,1300) — the value is used as an
in-page-table address by CLEPT/SETPT.
* Opcodes 143307 (STBTX) and the n-field pattern: the emulator implements 143300-143306;
there is no STBTX (143307) handler in the table (cpu_instr.c:3424-3430). If a carve
contains 143307, treat as UNRESOLVED (emulator: no dispatch entry; would fall through to
illegal_instr).
6. Conditional jumps & the SKP class¶
6.1 Register-conditional jumps (CJP family), base 130000-133777¶
Target is P-relative: PC = do_add(P, signExtend(disp8), 0) where P = address of the
jump instruction (CJP, cpu_instr.c:331-343). Range −200..+177 (octal). Table
:3285-3306.
| Mnemonic | Opcode base | Condition to jump | Source |
|---|---|---|---|
JAP d |
130000 | A >= 0 (bit15 == 0) |
:352-356 |
JAN d |
130400 | A < 0 (bit15 == 1) |
:365-369 |
JAZ d |
131000 | A == 0 — also sets C = (A==0) |
:378-384 |
JAF d |
131400 | A != 0 (filled) |
:393-396 |
JPC d |
132000 | X++ first, then jump if X >= 0 |
:406-411 |
JNC d |
132400 | X++ first, then jump if X < 0 |
:421-425 |
JXZ d |
133000 | X == 0 |
:446-449 |
JXN d |
133400 | X < 0 (bit15 == 1) |
:434-437 |
JPC/JNC always increment X (even when the branch is not taken). JAZ has the side
effect of writing the carry bit. The others touch no status.
6.2 SKP — skip on register comparison, base 140000 (mask F8C0)¶
ndfunc_skp → IsSkip (cpu_instr.c:2197-2254), dispatch :3309. Disassembly
SKP IF <dst> <cond> <src> (cpu_disasm.c:181). Fields: sr=(instr>>3)&7, dr=instr&7;
source=(sr==0)?0:reg[sr], desti=(dr==0)?0:reg[dr] (STS never read, 0 used instead,
:2204-2205). If the condition holds, skip the next instruction (gPC++, :471-472).
Flags computed on desti vs source (:2210-2215): z = (desti==source);
signed sgr = (int16)desti − (int16)source; o = signed-overflow(sgr);
c = ((desti−source) < 0) ? 0 : 1 (unsigned "≥"); s = bit15 of (int16)(desti−source).
Condition = bits 8-10 ((instr>>8)&7), mnemonics cpu_disasm.c:48:
| bits 8-10 | disasm | skip if |
|---|---|---|
| 0 | EQL |
desti == source |
| 1 | GEQ |
!s (signed desti ≥ source, sign-only test) |
| 2 | GRE |
!(s ^ o) (signed desti ≥ source, overflow-correct) |
| 3 | MGRE |
c (unsigned desti ≥ source) |
| 4 | UEQ |
desti != source |
| 5 | LSS |
s (signed desti < source) |
| 6 | LST |
s ^ o (signed less, overflow-correct) |
| 7 | MLST |
!c (unsigned desti < source) |
Example: SKP IF DA EQL ST → dr=A, sr=T → if (A == T) PC++.
SKP IF DA GRE SX → if ((int16)A >= (int16)X) PC++.
The disassembler order is dst then src; in C the comparison reads desti <cond> source.
7. Bit instructions — base 174000-177777¶
do_bops (cpu_instr.c:2256-2321), dispatch :3496. Fields: bn=(op>>3)&0x0F
(bit number 0-15), dr=op&7 (register). Sub-op = bits 7-10 ((op & 0x0780)>>7). K = STS
bit2, the one-bit accumulator. Disassembly <bop> <bitnum> D<reg> (cpu_disasm.c:653-656);
when dr==0 (STS) the bit is named symbolically (SSK, SSZ, …, cpu_disasm.c:52).
| sub-op | disasm | operation |
|---|---|---|
| 0 | BSET ZRO b Dr |
reg[dr] bit b = 0 |
| 1 | BSET ONE b Dr |
reg[dr] bit b = 1 |
| 2 | BSET BCM b Dr |
reg[dr] bit b ^= 1 (complement) |
| 3 | BSET BAC b Dr |
reg[dr] bit b = K |
| 4 | BSKP ZRO b Dr |
if (reg[dr] bit b == 0) PC++ |
| 5 | BSKP ONE b Dr |
if (reg[dr] bit b == 1) PC++ |
| 6 | BSKP BCM b Dr |
if ((bit ^ 1) == K) PC++ |
| 7 | BSKP BAC b Dr |
if (bit == K) PC++ |
| 8 | BSTC |
reg bit = K^1; K = 1 |
| 9 | BSTA |
reg bit = K; K = 0 |
| 10 | BLDC |
K = bit ^ 1 |
| 11 | BLDA |
K = bit |
| 12 | BANC |
K = (bit^1) & K |
| 13 | BAND |
K = bit & K |
| 14 | BORC |
K = (bit^1) \| K |
| 15 | BORA |
K = bit \| K |
So BSKP ZRO 0 DA = if ((A & 1)==0) PC++; BSET ONE 5 DA = A |= (1<<5).
The bit number is bits 3-6 (4 bits, 0-15); the register is bits 0-2. When dr==0 the target
is the STS register bit (e.g. SSK sets the K bit).
8. Shift instructions — base 154000-155777¶
ndfunc_shifts (cpu_instr.c:258-281) selects the target by bits 7-8
((op>>7)&3): 0=SHT(T), 1=SHD(D), 2=SHA(A), 3=SAD(A:D 32-bit pair). Dispatch
:3477-3480. The work is ShiftReg (:2323-2354) / ShiftDoubleReg (:2356-2387).
Field decode (ShiftReg): isneg=(op>>5)&1 (bit5 = direction sign); when isneg the count
is (~((op&0x3F)|0xFFC0)+1) i.e. the magnitude of a 6-bit two's-complement count; otherwise
count = op & 0x3F. isneg set = shift RIGHT (disassembler prints SHR <n>,
cpu_disasm.c:577); clear = shift LEFT. Shift type = bits 9-10 ((op>>9)&3).
| type (bits 9,10) | disasm | left (isneg=0) fill of bit0 | right (isneg=1) fill of bit15 |
|---|---|---|---|
| 0 | (plain) | 0 (arithmetic/logical left) | sign (msb preserved → arithmetic right) |
| 1 | ROT |
rotated-out bit → bit0 | rotated-out bit → bit15 |
| 2 | ZIN |
0 | 0 |
| 3 | LIN |
M bit → bit0 |
M bit → bit15 |
The shift runs one bit position per iteration for count iterations. The last bit shifted
out is written to the M status bit (setbit(_STS,_M,tmp), :2352). For plain right
shift the sign bit (msb) is replicated, so type-0 right shift is arithmetic; type-0
left shift feeds 0 into bit0. SAD does the same over the 32-bit (A<<16)|D value
(ShiftDoubleReg), writing A/D back (:274-277).
Example: SHA 3 = A <<= 3 (zero-filled). SHA SHR 3 = arithmetic A >>= 3 (sign kept).
SHA ROT SHR 1 = rotate right 1. SHD ZIN 4 = D <<= 4 zero-fill (same as plain here).
9. System / misc instructions¶
9.1 IOX / IOXT (privileged), base 164000 / opcode 150415¶
IOX <dev>(164000, mask F800):A = io_op(operand & 0x07FF, A). The 11-bit device address is embedded in the instruction. (cpu_instr.c:1025-1033)IOXT(150415):A = io_op(T, A)— device address taken from the T register. (cpu_instr.c:1037-1046) Both first test the "in-memory IO" window 100000-100777 viaUpdateMemoryIO(cpu_instr.c:982-1001). Both privileged.
9.2 MON — monitor call, base 153000 (mask FF00)¶
ndfunc_mon (cpu_instr.c:202-226): monitor_number = operand & 0x1FF (9-bit, sign-
extended into T if bit8 set). Loads reg[14][T] = monitor_number and triggers interrupt
level 14 (interrupt(14, 1<<1)). In C terms: MON n transfers control to the level-14
monitor with the call number (sign-extended 9-bit) in T. The number printed in disassembly
is the 9-bit field.
9.3 WAIT / give-up-priority, base 151000 (mask FF00)¶
DoWAIT (cpu_instr.c:1979-2007), privileged. If the interrupt system is off (!IONI)
the emulator stops the CPU (gA = exit code). Otherwise it clears the current level's PID
bit (gPID &= ~(1<<CurrLEVEL)) — relinquishing the CPU to a lower level — and requests a
priority recalculation. Level 0 cannot go lower (returns). For pseudo-C, model WAIT as
"yield / dismiss current interrupt level".
9.4 MST / MCL — masked set/clear of an internal register¶
Opcode bases 150300 (MST) / 150200 (MCL), mask FFF0; low nibble selects the register.
DoMST (cpu_instr.c:1821-1847) / DoMCL (:1784-1810), privileged. Only STS, PID, PIE
are implemented.
| low nibble | reg | MST (set) |
MCL (clear) |
|---|---|---|---|
| 01 | STS | STS_lo \|= (A & 0xFF) |
STS_lo &= ~(A & 0xFF) |
| 06 | PID | PID \|= A |
PID &= ~A |
| 07 | PIE | PIE \|= A |
PIE &= ~A |
Other nibbles: no operation. So MST PID = "set the PID bits named by A"; MCL PIE = "clear
the PIE bits named by A". STS masking touches only the low 8 bits.
9.5 ION / IOF / PON / POF / PION / PIOF — interrupt & paging control¶
Single opcodes (cpu_instr.c:1171-1229; table :3445-3461).
| Mnemonic | Opcode | Effect |
|---|---|---|
ION |
150402 | IONI = 1 (interrupt system on); request PK recalc |
IOF |
150401 | IONI = 0 (privileged) |
PON |
150410 | PONI = 1 (paging on) |
POF |
150404 | PONI = 0 (paging off, privileged) |
PION |
150412 | IONI = 1; PONI = 1 |
PIOF |
150405 | IONI = 0; PONI = 0 (privileged) |
SEX |
150406 | SEXI = 1 (extended 24-bit addressing) |
REX |
150407 | SEXI = 0 |
9.6 IRW / IRR — inter-register write / read¶
Bases 153400 (IRW) / 153600 (IRR), mask FF80. Fields: level=(op>>3)&0x0F, reg=op&7.
Both privileged. Disassembly IRW <op&0x78 octal> D<reg> (cpu_disasm.c:566-570) — note
the printed number is level<<3 (the raw field, octal), e.g. IRW 120 DB → level = 0120>>3
= 12 (octal), register B.
IRW(cpu_instr.c:1096-1119):reg[level][dr] = A. Special cases: writing A→A on the same level is a NOP; writing P→P on the same level is a NOP; writing STS updates only the low 8 bits (reg[level][STS] = A & 0xFF).IRR(cpu_instr.c:1129-1145):A = reg[level][sr]; ifsr==0(STS) thenA = reg[level][STS] & 0xFF(high byte cleared).
So IRW 120 DB = reg[level 12][B] = A; IRR 120 DB = A = reg[level 12][B].
9.7 TRA / TRR — transfer to/from internal (system) register¶
Bases 150000 (TRA, read → A) / 150100 (TRR, write ← A), mask FFF0; low nibble picks the
register. Privileged. DoTRA (cpu_instr.c:1857-1946), DoTRR (:2049-2113). Register
names cpu_disasm.c:42-43.
TRA <reg>: A = <internal reg>. Notable: TRA STS (01) merges the level STS low byte with
the shared system high byte; TRA PGS/TRA PEA have unlock side effects; TRA IIC (05)
reads then clears IIC/IID; TRA PGC/TRA PCR (014) reads the paging control register for a
level encoded in A bits 3-6.
TRR <reg>: <internal reg> = A. Notable: TRR STS (01) changes only the low 8 bits;
TRR PID/TRR PIE/TRR IIE request a priority recalc; TRR PCR (03) writes the paging
control register for the level in A bits 3-6.
For pseudo-C, treat TRA/TRR as reads/writes of named CPU system registers (PID, PIE,
PCR, STS, PES, PEA, IIC, IIE, ALD, …). Full per-register semantics are in the cited functions.
9.8 The 147440 idiom = ROP NOOP¶
147440 decodes as a ROP word: RAD=1, arithmetic sub-op = 6 (NOOP), sr=SL(4), dr=0.
It performs no register or status change (§3.2/§3.4). Emitted by assemblers/compilers as a
one-word pad. Treat as /* nop */.
9.9 Other single-purpose instructions seen in handlers¶
| Mnemonic | Opcode | Operation | Source |
|---|---|---|---|
TSET |
140123 | A = mem[T] (alt PT); mem[T] = 0xFFFF (atomic test&set) |
cpu_instr.c:2442-2449 |
RDUS |
140127 | A = mem[T] (alt PT read, no cache) |
:2422-2425 |
LBYT |
142200 | load byte: A = byte(T + X/2), X odd = low byte |
:1619-1634 |
SBYT |
142600 | store byte: writes A's low byte into T + X/2 |
:1646-1662 |
MIX3 |
143200 | X = (A − 1) * 3 |
:1678-1681 |
EXR sr |
140600 | execute the instruction word held in reg[sr] without moving P (EXR-of-EXR sets Z) |
:1951-1971 |
INIT/ENTR/LEAVE/ELEAV |
140134-140137 | stack-frame create/enter/leave (PLANC calling convention) | :1529-1603 |
MOVEW |
143100 | block word move, count in L (≤2048), A:D source / X:T dest | :2474-2586 |
SRB/LRB level |
152402/152602 | store/load 8-word register block (P,X,T,A,D,L,STS,B) at X, alt PT | :2123-2195 |
NLZ/DNZ |
151400/152000 | normalize / denormalize float (scaling in low 8 bits) | float.c:306,349 |
10. UNRESOLVED / partial in the emulator (do not translate blindly)¶
STBTX(143307): no dispatch entry; would execute as illegal instruction. UNRESOLVED (emulator: not in table,cpu_instr.c:3424-3430).CLNREENT(140302),CHREENTPAGES(140303),CLEPU(140304): handlers present but bodies are TODO / not implemented (cpu_instr.c:1416-1508). Behavior UNRESOLVED (emulator: privilege check only, no data effect).GECO(142700): customer instruction, no-op in the emulator (cpu_instr.c:930-944).- ND-110-specific segment/byte-pointer instructions 140500-140517, 140700-140707 map to
unimplemented_instr(print + continue) — UNRESOLVED for data effect. IOT(160000): implemented as illegal instruction (cpu_instr.c:1011-1021).
All other classes in §§1-9 are fully resolved against the emulator source.
Source files cited¶
~/repos/nd100x/src/cpu/cpu.c (effective address, dispatch),
~/repos/nd100x/src/cpu/cpu_instr.c (all instruction bodies + opcode table),
~/repos/nd100x/src/cpu/cpu_disasm.c (mnemonic/field decode, register-letter maps),
~/repos/nd100x/src/cpu/cpu_types.h (register indices, status-bit macros, WriteMode),
~/repos/nd100x/src/cpu/float.c (NLZ/DNZ), ~/repos/nd100x/src/cpu/bcd.c (ADDD/SHDE).