PM-16 Micro-CPU

A 16-bit CPU you can assemble for, single-step, and watch execute: registers, flags, memory and the memory-mapped console all visible at once.

Rendered when the site was built, by the same emulator the browser runs. No JavaScript required. With JavaScript on, this is replaced by the interactive workstation.

The instruction set

The top five bits are the opcode, and the opcode alone selects the format. Flag column: none writes no flags · add unsigned carry-out · sub carry means borrow · log clears CF and OF · shf CF is the last bit shifted out.

PM-16 instruction set: all 32 opcodes
OpcodeMnemonicFormatOperandsSemanticsFlags
0x00HALTZnonehalted ← truenone
0x01RETZnonePC ← M[SP]; SP ← SP+1none
0x02ADDRRd, Ra, RbrX ← rY + rZadd
0x03ADCRRd, Ra, RbrX ← rY + rZ + CFadd
0x04SUBRRd, Ra, RbrX ← rY − rZsub
0x05SBCRRd, Ra, RbrX ← rY − rZ − CFsub
0x06ANDRRd, Ra, RbrX ← rY & rZlog
0x07ORRRd, Ra, RbrX ← rY | rZlog
0x08XORRRd, Ra, RbrX ← rY ^ rZlog
0x09NOTRRd, RarX ← ~rYlog
0x0ANEGRRd, RarX ← 0 − rYsub
0x0BMOVRRd, RarX ← rYnone
0x0CCMPRRa, RbrX − rY, result discardedsub
0x0DTESTRRa, RbrX & rY, result discardedlog
0x0EPUSHRRsSP ← SP−1; M[SP] ← rXnone
0x0FPOPRRdrX ← M[SP]; SP ← SP+1none
0x10JMPRRRaPC ← rXnone
0x11CALLRRRaSP ← SP−1; M[SP] ← PC; PC ← rXnone
0x12MOVRRd, SPrX ← SPnone
0x13MOVRSP, RsSP ← rXnone
0x14SHLIRd, Ra, #nrX ← rY << n (n≥16 ⇒ 0)shf
0x15SHRIRd, Ra, #nrX ← rY >> n logical (n≥16 ⇒ 0)shf
0x16SARIRd, Ra, #nrX ← rY >> min(n,15) arithmeticshf
0x17ADDIIRd, Ra, #imm5rX ← rY + sx5(imm5)add
0x18CMPIIRa, #imm5rX − sx5(imm5), discardedsub
0x19LDIRd, [Ra + #off]rX ← M[rY + sx5(off)]none
0x1ASTIRs, [Ra + #off]M[rY + sx5(off)] ← rXnone
0x1BLDLMRd, #imm8rX ← imm8 (high byte cleared)none
0x1CLDHMRd, #imm8rX ← (imm8 << 8) | (rX & 0xFF)none
0x1DBR<cc>Blabelif cond(cc) then PC ← PC + sx7(simm7)none
0x1EJMPJlabelPC ← PC + sx11(simm11)none
0x1FCALLJlabelSP ← SP−1; M[SP] ← PC; PC ← PC + sx11none

Condition codes

One opcode, sixteen predicates over the flags already computed. Every code is defined; there is no illegal encoding.

Condition codes: all 16
CodeMnemonicAliasesTaken when
0BRBRALalways
1BRNVnonenever
2BREQBRZZF=1: equal / zero
3BRNEBRNZZF=0: not equal
4BRMIBRSSF=1: negative
5BRPLBRNSSF=0: non-negative
6BRCSBRLOCF=1: borrow / unsigned <
7BRCCBRHS, BRNCCF=0: unsigned ≥
8BRVSBROOF=1: signed overflow
9BRVCBRNOOF=0: no signed overflow
10BRHInoneCF=0 ∧ ZF=0: unsigned >
11BRLSnoneCF=1 ∨ ZF=1: unsigned ≤
12BRGEnoneSF=OF: signed ≥
13BRLTnoneSF≠OF: signed <
14BRGTnoneZF=0 ∧ SF=OF: signed >
15BRLEnoneZF=1 ∨ SF≠OF: signed ≤

Programmer's model

Registers
R0–R7, fully general; none is hardwired to zero
Special
PC · SP · IR, each 16 bits
Flags
ZF (zero) · SF (sign) · CF (carry/borrow) · OF (signed overflow)
Memory
65,536 words of 16 bits; one address is one word, so there is no alignment rule and no endianness inside the machine
RAM
0x0000–0xEFFF, shared by code, data and stack
Reserved
0xF000–0xF7FF, held for a future framebuffer
Console MMIO
0xF800–0xFFFF: 32 rows × 64 columns, cell (row, col) at 0xF800 + row×64 + col
Stack
full-descending; SP resets to 0xF000 and the first PUSH writes 0xEFFF
Entry point
always 0x0000

Worked example: Fibonacci

The first sample program writes the first twelve Fibonacci numbers to memory and halts after exactly 90 steps.

; fib.asm: the first 12 Fibonacci numbers into 0x0200..0x020B
        .equ  COUNT, 12
        .equ  DEST,  0x0200

        .org  0x0000
start:  SET   R3, #DEST         ; R3 = write pointer
        LDL   R0, #0            ; R0 = a
        LDL   R1, #1            ; R1 = b
        LDL   R4, #COUNT        ; R4 = counter
loop:   ST    R0, [R3]          ; mem[p] = a
        ADDI  R3, R3, #1        ; p = p + 1
        ADD   R2, R0, R1        ; t = a + b
        MOV   R0, R1            ; a = b
        MOV   R1, R2            ; b = t
        ADDI  R4, R4, #-1       ; counter = counter - 1
        BRNE  loop              ; ZF from the ADDI
        HALT

Assembled

segment @ 0000: 13 words
  0000  DB00  LDL R3, #0x00
  0001  E302  LDH R3, #0x02
  0002  D800  LDL R0, #0x00
  0003  D901  LDL R1, #0x01
  0004  DC0C  LDL R4, #0x0C
  0005  D060  ST R0, [R3 + #0]
  0006  BB61  ADDI R3, R3, #1
  0007  1204  ADD R2, R0, R1
  0008  5820  MOV R0, R1
  0009  5940  MOV R1, R2
  000A  BC9F  ADDI R4, R4, #-1
  000B  E9F9  BRNE .-7
  000C  0000  HALT

Trace

The first 24 of 90 steps, two full turns of the loop. Flags after is dimmed where the instruction writes no flags at all, which is most of them: only the sixteen ALU instructions touch the flags, which is what lets you compare, then load, then branch.

fib.asm: the first 24 of 90 steps
StepPCWordInstructionR0R1R3R4Flags after
10000DB00LDL R3, #0x000000000000000000Z=0 S=0 C=0 O=0 (unchanged)
20001E302LDH R3, #0x020000000002000000Z=0 S=0 C=0 O=0 (unchanged)
30002D800LDL R0, #0x000000000002000000Z=0 S=0 C=0 O=0 (unchanged)
40003D901LDL R1, #0x010000000102000000Z=0 S=0 C=0 O=0 (unchanged)
50004DC0CLDL R4, #0x0C000000010200000CZ=0 S=0 C=0 O=0 (unchanged)
60005D060ST R0, [R3 + #0]000000010200000CZ=0 S=0 C=0 O=0 (unchanged)
70006BB61ADDI R3, R3, #1000000010201000CZ=0 S=0 C=0 O=0
800071204ADD R2, R0, R1000000010201000CZ=0 S=0 C=0 O=0
900085820MOV R0, R1000100010201000CZ=0 S=0 C=0 O=0 (unchanged)
1000095940MOV R1, R2000100010201000CZ=0 S=0 C=0 O=0 (unchanged)
11000ABC9FADDI R4, R4, #-1000100010201000BZ=0 S=0 C=1 O=0
12000BE9F9BRNE .-7000100010201000BZ=0 S=0 C=1 O=0 (unchanged)
130005D060ST R0, [R3 + #0]000100010201000BZ=0 S=0 C=1 O=0 (unchanged)
140006BB61ADDI R3, R3, #1000100010202000BZ=0 S=0 C=0 O=0
1500071204ADD R2, R0, R1000100010202000BZ=0 S=0 C=0 O=0
1600085820MOV R0, R1000100010202000BZ=0 S=0 C=0 O=0 (unchanged)
1700095940MOV R1, R2000100020202000BZ=0 S=0 C=0 O=0 (unchanged)
18000ABC9FADDI R4, R4, #-1000100020202000AZ=0 S=0 C=1 O=0
19000BE9F9BRNE .-7000100020202000AZ=0 S=0 C=1 O=0 (unchanged)
200005D060ST R0, [R3 + #0]000100020202000AZ=0 S=0 C=1 O=0 (unchanged)
210006BB61ADDI R3, R3, #1000100020203000AZ=0 S=0 C=0 O=0
2200071204ADD R2, R0, R1000100020203000AZ=0 S=0 C=0 O=0
2300085820MOV R0, R1000200020203000AZ=0 S=0 C=0 O=0 (unchanged)
2400095940MOV R1, R2000200030203000AZ=0 S=0 C=0 O=0 (unchanged)

At halt

The last ADDI R4, R4, #-1 computed 0x0001 + 0xFFFF, which carried, so the machine halts with CF set even though the loop counted down to zero. ADDI is an addition, so its carry is an add-carry, the opposite polarity from SUB and CMPI. There is no SUBI; that is deliberate, and it is the first thing to try predicting.

result: halted after 90 steps
PC=000D  SP=F000  IR=0000  Z=1 S=0 C=1 O=0  steps=90  HALTED
R0=0090  R1=00E9  R2=00E9  R3=020C
R4=0000  R5=0000  R6=0000  R7=0000
min SP: F000
memory writes: 12
digest: 31965eb81e41eb20d062558b4e6f4d5968c59c83f4300cc2e174e617e521c8d3
segments:
  0000: 13 words
console row 0: ||

Everything on this page is a 16-bit word underneath. The Base Converter reads one of them in binary, octal, decimal or hex, and the Binary Value & Endianness Inspector takes a whole buffer of them as signed and unsigned integers, either way round.

About this lab3 paragraphs

The machine decomposes each instruction into its parts (the fetched word, the decoded fields, the result, the flag write), so a condition flag stops being folklore.

Thirty-two instructions, eight registers, four flags, one addressing mode; the whole instruction set prints on one page and it is below, along with a worked trace, so this page is useful with JavaScript off.

It runs offline via cpu-cli.

Use it locally This lab has a native command line twin. Build cpu-cli from the site's source with:
cargo build --release --bin cpu-cli
Source and licence terms