Skip to content

# 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
Opcode | Mnemonic | Format | Operands | Semantics | Flags
--- | --- | --- | --- | --- | ---
0x00 | HALT | Z | none | halted ← true | none
0x01 | RET | Z | none | PC ← M[SP]; SP ← SP+1 | none
0x02 | ADD | R | Rd, Ra, Rb | rX ← rY + rZ | add
0x03 | ADC | R | Rd, Ra, Rb | rX ← rY + rZ + CF | add
0x04 | SUB | R | Rd, Ra, Rb | rX ← rY − rZ | sub
0x05 | SBC | R | Rd, Ra, Rb | rX ← rY − rZ − CF | sub
0x06 | AND | R | Rd, Ra, Rb | rX ← rY & rZ | log
0x07 | OR | R | Rd, Ra, Rb | rX ← rY | rZ | log
0x08 | XOR | R | Rd, Ra, Rb | rX ← rY ^ rZ | log
0x09 | NOT | R | Rd, Ra | rX ← ~rY | log
0x0A | NEG | R | Rd, Ra | rX ← 0 − rY | sub
0x0B | MOV | R | Rd, Ra | rX ← rY | none
0x0C | CMP | R | Ra, Rb | rX − rY, result discarded | sub
0x0D | TEST | R | Ra, Rb | rX & rY, result discarded | log
0x0E | PUSH | R | Rs | SP ← SP−1; M[SP] ← rX | none
0x0F | POP | R | Rd | rX ← M[SP]; SP ← SP+1 | none
0x10 | JMPR | R | Ra | PC ← rX | none
0x11 | CALLR | R | Ra | SP ← SP−1; M[SP] ← PC; PC ← rX | none
0x12 | MOV | R | Rd, SP | rX ← SP | none
0x13 | MOV | R | SP, Rs | SP ← rX | none
0x14 | SHL | I | Rd, Ra, #n | rX ← rY << n (n≥16 ⇒ 0) | shf
0x15 | SHR | I | Rd, Ra, #n | rX ← rY >> n logical (n≥16 ⇒ 0) | shf
0x16 | SAR | I | Rd, Ra, #n | rX ← rY >> min(n,15) arithmetic | shf
0x17 | ADDI | I | Rd, Ra, #imm5 | rX ← rY + sx5(imm5) | add
0x18 | CMPI | I | Ra, #imm5 | rX − sx5(imm5), discarded | sub
0x19 | LD | I | Rd, [Ra + #off] | rX ← M[rY + sx5(off)] | none
0x1A | ST | I | Rs, [Ra + #off] | M[rY + sx5(off)] ← rX | none
0x1B | LDL | M | Rd, #imm8 | rX ← imm8 (high byte cleared) | none
0x1C | LDH | M | Rd, #imm8 | rX ← (imm8 << 8) | (rX & 0xFF) | none
0x1D | BR<cc> | B | label | if cond(cc) then PC ← PC + sx7(simm7) | none
0x1E | JMP | J | label | PC ← PC + sx11(simm11) | none
0x1F | CALL | J | label | SP ← SP−1; M[SP] ← PC; PC ← PC + sx11 | none

## Condition codes

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

Condition codes: all 16
Code | Mnemonic | Aliases | Taken when
--- | --- | --- | ---
0 | BR | BRAL | always
1 | BRNV | none | never
2 | BREQ | BRZ | ZF=1: equal / zero
3 | BRNE | BRNZ | ZF=0: not equal
4 | BRMI | BRS | SF=1: negative
5 | BRPL | BRNS | SF=0: non-negative
6 | BRCS | BRLO | CF=1: borrow / unsigned <
7 | BRCC | BRHS, BRNC | CF=0: unsigned ≥
8 | BRVS | BRO | OF=1: signed overflow
9 | BRVC | BRNO | OF=0: no signed overflow
10 | BRHI | none | CF=0 ∧ ZF=0: unsigned >
11 | BRLS | none | CF=1 ∨ ZF=1: unsigned ≤
12 | BRGE | none | SF=OF: signed ≥
13 | BRLT | none | SF≠OF: signed <
14 | BRGT | none | ZF=0 ∧ SF=OF: signed >
15 | BRLE | none | ZF=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
Step | PC | Word | Instruction | R0 | R1 | R3 | R4 | Flags after
--- | --- | --- | --- | --- | --- | --- | --- | ---
1 | 0000 | DB00 | LDL R3, #0x00 | 0000 | 0000 | 0000 | 0000 | Z=0 S=0 C=0 O=0 (unchanged)
2 | 0001 | E302 | LDH R3, #0x02 | 0000 | 0000 | 0200 | 0000 | Z=0 S=0 C=0 O=0 (unchanged)
3 | 0002 | D800 | LDL R0, #0x00 | 0000 | 0000 | 0200 | 0000 | Z=0 S=0 C=0 O=0 (unchanged)
4 | 0003 | D901 | LDL R1, #0x01 | 0000 | 0001 | 0200 | 0000 | Z=0 S=0 C=0 O=0 (unchanged)
5 | 0004 | DC0C | LDL R4, #0x0C | 0000 | 0001 | 0200 | 000C | Z=0 S=0 C=0 O=0 (unchanged)
6 | 0005 | D060 | ST R0, [R3 + #0] | 0000 | 0001 | 0200 | 000C | Z=0 S=0 C=0 O=0 (unchanged)
7 | 0006 | BB61 | ADDI R3, R3, #1 | 0000 | 0001 | 0201 | 000C | Z=0 S=0 C=0 O=0
8 | 0007 | 1204 | ADD R2, R0, R1 | 0000 | 0001 | 0201 | 000C | Z=0 S=0 C=0 O=0
9 | 0008 | 5820 | MOV R0, R1 | 0001 | 0001 | 0201 | 000C | Z=0 S=0 C=0 O=0 (unchanged)
10 | 0009 | 5940 | MOV R1, R2 | 0001 | 0001 | 0201 | 000C | Z=0 S=0 C=0 O=0 (unchanged)
11 | 000A | BC9F | ADDI R4, R4, #-1 | 0001 | 0001 | 0201 | 000B | Z=0 S=0 C=1 O=0
12 | 000B | E9F9 | BRNE .-7 | 0001 | 0001 | 0201 | 000B | Z=0 S=0 C=1 O=0 (unchanged)
13 | 0005 | D060 | ST R0, [R3 + #0] | 0001 | 0001 | 0201 | 000B | Z=0 S=0 C=1 O=0 (unchanged)
14 | 0006 | BB61 | ADDI R3, R3, #1 | 0001 | 0001 | 0202 | 000B | Z=0 S=0 C=0 O=0
15 | 0007 | 1204 | ADD R2, R0, R1 | 0001 | 0001 | 0202 | 000B | Z=0 S=0 C=0 O=0
16 | 0008 | 5820 | MOV R0, R1 | 0001 | 0001 | 0202 | 000B | Z=0 S=0 C=0 O=0 (unchanged)
17 | 0009 | 5940 | MOV R1, R2 | 0001 | 0002 | 0202 | 000B | Z=0 S=0 C=0 O=0 (unchanged)
18 | 000A | BC9F | ADDI R4, R4, #-1 | 0001 | 0002 | 0202 | 000A | Z=0 S=0 C=1 O=0
19 | 000B | E9F9 | BRNE .-7 | 0001 | 0002 | 0202 | 000A | Z=0 S=0 C=1 O=0 (unchanged)
20 | 0005 | D060 | ST R0, [R3 + #0] | 0001 | 0002 | 0202 | 000A | Z=0 S=0 C=1 O=0 (unchanged)
21 | 0006 | BB61 | ADDI R3, R3, #1 | 0001 | 0002 | 0203 | 000A | Z=0 S=0 C=0 O=0
22 | 0007 | 1204 | ADD R2, R0, R1 | 0001 | 0002 | 0203 | 000A | Z=0 S=0 C=0 O=0
23 | 0008 | 5820 | MOV R0, R1 | 0002 | 0002 | 0203 | 000A | Z=0 S=0 C=0 O=0 (unchanged)
24 | 0009 | 5940 | MOV R1, R2 | 0002 | 0003 | 0203 | 000A | Z=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