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.
| 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.
| 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.
| 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.
cpu-cli from the site's source with:
cargo build --release --bin cpu-cliSource and licence terms