Overview
MB8 is an 8-bit microcomputer in the spirit of the ZX Spectrum and Commodore 64, initially inspired by CHIP-8. It ships with a tiny CP/M-like operating system layer and a minimal assembly-first toolchain.
What’s inside
- 8-bit CPU with a compact ISA and pseudo-instructions for convenience.
- Memory-mapped devices (RAM, ROM, GPU TTY, keyboard, disk) wired through a simple bus.
- A small kernel plus user-space programs, all written in assembly.
Running the project
- Build all assembly artifacts (kernel, user programs, tests):
make all
- Run the VM, passing the kernel entrypoint first and then any user-space programs:
cargo run -- run ./kernel/main.bin ./user/sh.bin ./user/hw.bin ./user/ls.bin ./user/exit.bin ./user/help.bin
The kernel image is loaded at 0xE000, user programs are passed as extra binaries, and the OS provides basic CP/M-like services via syscalls.
Memory model
The MB8 VM exposes a single 64 KiB address space. All reads and writes go through the bus (crates/mb8/src/dev/bus.rs), which forwards them to RAM, ROM, or an MMIO device based on the address range.
Layout
| Range | Size | Description |
|---|---|---|
0x0000 – 0xBFFF | 48 KiB | RAM |
0xC000 – 0xDFFF | 8 KiB | Reserved MMIO (not wired yet) |
0xE000 – 0xEFFF | 4 KiB | ROM |
0xF000 – 0xF0FF | 256 B | GPU registers |
0xF100 – 0xF1FF | 256 B | Keyboard registers |
0xF200 – 0xF3FF | 512 B | Disk registers and buffer |
0xF400 – 0xFFFF | 3072 B | Reserved MMIO (not wired yet) |
The bus rejects the reserved regions with unimplemented!().
Bus
- CPU memory accesses always call into the bus, which in turn calls the matching device
read/write. - Devices own their buffers; the bus itself does not store data.
RAM (crates/mb8/src/dev/ram.rs)
- Plain byte-addressable memory. Writes update the backing array; reads return what was last written.
RAM_SIZE = 0xC000. The stack grows downward (STACK_TOP = 0xBFFF,STACK_BOTTOM = 0xBF00).
ROM (crates/mb8/src/dev/rom.rs)
- Backing store for program code (
ROM_SIZE = 0x1000). - The device currently accepts writes from the bus, but programs should not rely on mutating ROM; this may be blocked in the future. ROM is meant to hold the kernel/boot image.
GPU (crates/mb8/src/dev/gpu.rs)
- Registers live at
0xF000(offsets relative to that base):0x0000— mode register.0x00= off,0x01= TTY.0x0001— TTY data register. When mode is TTY, each write pushes a character to the screen and advances the cursor.
- Reading
0x0000returns the current mode. Other reads are currently unimplemented.
Keyboard (crates/mb8/src/dev/keyboard.rs)
- Registers at
0xF100(offsets relative to that base):0x00—STATUS. Returns1when keys are queued, otherwise0.0x01—DATA. Reading pops the next key code from the queue; returns0when empty.
- Writes are ignored.
Disk (crates/mb8/src/dev/disk.rs)
- Registers at
0xF200(offsets relative to that base):0x0000—BLOCKnumber to operate on.0x0001—CMD(0x00no-op,0x01read,0x02write).0x0002–0x0102— 256-byte disk buffer used for reads/writes.
CMDoperations move data between the internal image and the buffer; buffer reads/writes go directly to the 256-byte window.
Register set
Small and simple; each VM context (judge or bot) gets its own copies.
| register | mask | description | size |
|---|---|---|---|
| R0 - R7 | 0x00 - 0x07 | General-purpose registers | 8 bits |
| SP | 0x0D | Stack pointer; PUSH / POP move it upward | 8 bits |
| PC | 0x0E | Program counter; steps by 2 bytes per instruction | 12 bits |
| F | 0x0F | Flags register (Z/N/C) | 8 bits |
Notes:
Fis overwritten by arithmetic/logic/shift ops. Jumps read it, but most other ops leave it untouched.- Context switches keep register sets separate: judge and each bot maintain their own registers (
PC,SP,R0-R7). - Masks
0x08 - 0x0Care reserved for future expansion.
Flags
These live in the F register and are rewritten by arithmetic/logic/shift instructions:
| flag | mask | description | set by |
|---|---|---|---|
| Z | 0x01 | Result is zero. | ADD, SUB, AND, OR, XOR, SHL, SHR (and pseudo-instructions that expand to them) |
| N | 0x02 | Copies bit 7 (sign) of the 8-bit result. | ADD, SUB, AND, OR, XOR, SHL, SHR |
| C | 0x04 | Set when an 8-bit result wraps: carry on ADD/SHL/SHR, borrow on SUB. | ADD, SUB, SHL, SHR |
Notes:
- Instructions not listed leave flags unchanged.
- Pseudo-instructions (
INC,DEC,CMP,CMPI, shifts) inherit flag behavior from the underlying ops. - Flags 0x08, 0x10, 0x20, 0x40, 0x80 are reserved for future use.
Instruction format
Every opcode is 16 bits wide (0xABCD):
A— instruction group.B— sub-opcode or register nibble.C— usually a register or the upper 4 bits of an address.D— usually a register or the lower 4 bits of an address.
Example, ADD R0, R1 encodes as 0x1101:
0001 0001 0000 0001
Jump/load/store instructions treat XXX in 0xYXXX as a 12-bit address, covering the full 4 KiB memory bank.
Instruction Set
- System instructions
- Register-register instructions
- Register-immediate instructions
- Jump instructions
- Stack instructions
- Memory instructions
System instructions
NOP
Syntax:
NOP
Args: None
Encoding:
0000 0000 0000 0000
Hex: 0x0000
Flags: None
Description: Do nothing for one instruction cycle.
HALT
Syntax:
HALT
Args: None
Encoding:
0000 0001 0000 0000
Hex: 0x0100
Flags: None
Description: Stop the VM. Execution does not resume until a reset happens.
SYS
Syntax:
SYS
Args: None
Encoding:
0000 0010 0000 0000
Hex: 0x0200
Flags: None
Description: Enter the system call handler. Callers place the syscall ID in R0 and jump to 0xE500 (see system calls doc) to execute OS services.
Register-register instructions
MOV
Syntax:
MOV rD rS
Operation:
rD = rS
Args:
- rD — destination register.
- rS — source register.
Encoding:
0001 0000 DDDD SSSS
Hex: 0x10DS
Flags: None
Description: Copy the value from rS into rD.
ADD
Syntax:
ADD rD rS
Operation:
rD = rD + rS
Args:
- rD — destination register.
- rS — source register.
Encoding:
0001 0001 DDDD SSSS
Hex: 0x11DS
Flags: Updates Z, N, C.
Description: Add rS to rD.
SUB
Syntax:
SUB rD rS
Operation:
rD = rD - rS
Args:
- rD — destination register.
- rS — source register.
Encoding:
0001 0010 DDDD SSSS
Hex: 0x12DS
Flags: Updates Z, N, C.
Description: Subtract rS from rD.
AND
Syntax:
AND rD rS
Operation:
rD = rD & rS
Encoding:
0001 0011 DDDD SSSS
Hex: 0x13DS
Flags: Updates Z, N.
Description: Bitwise AND.
OR
Syntax:
OR rD rS
Operation:
rD = rD | rS
Encoding:
0001 0100 DDDD SSSS
Hex: 0x14DS
Flags: Updates Z, N.
Description: Bitwise OR.
XOR
Syntax:
XOR rD rS
Operation:
rD = rD ^ rS
Encoding:
0001 0101 DDDD SSSS
Hex: 0x15DS
Flags: Updates Z, N.
Description: Bitwise XOR.
SHR
Syntax:
SHR rD rS
Operation:
rD = rD >> rS
Encoding:
0001 0110 DDDD SSSS
Hex: 0x16DS
Flags: Updates Z, N; C is set only when the shifted result overflows 8 bits.
Description: Logical right shift by the amount in rS.
SHL
Syntax:
SHL rD rS
Operation:
rD = rD << rS
Encoding:
0001 0111 DDDD SSSS
Hex: 0x17DS
Flags: Updates Z, N, C.
Description: Logical left shift by the amount in rS.
CMP
Syntax:
CMP rD rS
Operation:
flags = rD - rS
Encoding:
0001 1000 DDDD SSSS
Hex: 0x18DS
Flags: Updates Z, N, C.
Description: Compare two registers and set flags as if subtracting rS from rD. Register values are not modified.
Register-immediate instructions
LDI
Syntax:
LDI rD imm8
Operation:
rD = imm8
Args:
- rD — destination register.
- imm8 — unsigned 8-bit immediate value.
Encoding:
0010 DDDD XXXX XXXX
Hex: 0x20DX
Flags: None.
Description: Load an 8-bit immediate into rD.
Jump instructions
JMP
Syntax:
JMP rH rL
Operation:
PC = (rH << 8) | rL
Args:
- rH — register containing the high byte of the absolute address.
- rL — register containing the low byte of the absolute address.
Encoding:
0011 0000 HHHH LLLL
Hex: 0x30HL
Flags: None.
Description: Absolute jump using two registers to form a 16-bit address.
JR
Syntax:
JR off8
Operation:
PC = PC + sign_extend(off8)
Args:
- off8 — signed 8-bit offset.
Encoding:
0011 0001 OOOO OOOO
Hex: 0x31OO
Flags: None.
Description: Relative jump that always branches by the signed offset.
JZR
Syntax:
JZR off8
Operation:
if Z == 1 { PC = PC + sign_extend(off8) }
Encoding:
0011 0010 OOOO OOOO
Hex: 0x32OO
Flags: Reads Z.
Description: Relative jump taken only when the zero flag is set.
JNZR
Syntax:
JNZR off8
Operation:
if Z == 0 { PC = PC + sign_extend(off8) }
Encoding:
0011 0011 OOOO OOOO
Hex: 0x33OO
Flags: Reads Z.
Description: Relative jump taken only when the zero flag is clear.
JCR
Syntax:
JCR off8
Operation:
if C == 1 { PC = PC + sign_extend(off8) }
Encoding:
0011 0100 OOOO OOOO
Hex: 0x34OO
Flags: Reads C.
Description: Relative jump taken only when the carry flag is set.
JNCR
Syntax:
JNCR off8
Operation:
if C == 0 { PC = PC + sign_extend(off8) }
Encoding:
0011 0101 OOOO OOOO
Hex: 0x35OO
Flags: Reads C.
Description: Relative jump taken only when the carry flag is clear.
Stack instructions
CALL
Syntax:
CALL rH rL
Operation:
push(PC)
PC = (rH << 8) | rL
Args:
- rH — high byte register.
- rL — low byte register.
Encoding:
0100 0000 HHHH LLLL
Hex: 0x40HL
Flags: None.
Description: Push the current PC onto the stack and jump to the absolute address formed by rH/rL.
RET
Syntax:
RET
Args: None
Encoding:
0100 0001 0000 0000
Hex: 0x4100
Flags: None.
Description: Pop the return address from the stack and jump to it.
PUSH
Syntax:
PUSH rS
Args:
- rS — register to push.
Encoding:
0100 0010 0000 SSSS
Hex: 0x420S
Flags: None.
Description: Decrement SP, store rS on the stack.
POP
Syntax:
POP rD
Args:
- rD — destination register.
Encoding:
0100 0011 0000 DDDD
Hex: 0x430D
Flags: None.
Description: Load a byte from the stack to rD and increment SP.
Memory instructions
LD
Syntax:
LD rD rH rL
Operation:
rD = MEM[(rH << 8) | rL]
Args:
- rD — destination register.
- rH/rL — registers holding the high/low bytes of the source address.
Encoding:
0101 DDDD HHHH LLLL
Hex: 0x5DHL
Flags: None.
Description: Read one byte from RAM at the 16-bit address formed by rH/rL and place it in rD.
ST
Syntax:
ST rS rH rL
Operation:
MEM[(rH << 8) | rL] = rS
Args:
- rS — source register.
- rH/rL — registers holding the high/low bytes of the destination address.
Encoding:
0110 SSSS HHHH LLLL
Hex: 0x6SHL
Flags: None.
Description: Write one byte from rS to RAM at the 16-bit address composed from rH/rL.
Pseudo-instructions
Assembler-only helpers from asm/ext.asm. They rewrite into core opcodes and often use R7 plus the stack as scratch.
- LDI (16-bit)
- CALL (abs)
- JMP (abs)
- JR (abs)
- JZR (abs)
- JNZR (abs)
- ZERO
- INC
- DEC
- INC16
- NOT
- CMPI
- SHRI
- SHLI
- SWAP
- MUL
LDI (16-bit)
Syntax:
LDI rH rL imm16
Expands to:
LDI rH (imm16 >> 8)
LDI rL (imm16 & 0xFF)
Scratch: none
Flags: none
Description: Load a 16-bit immediate into two registers.
CALL (abs)
Syntax:
CALL addr16
Expands to:
LDI R6 (addr16 >> 8)
LDI R7 (addr16 & 0xFF)
CALL R6 R7
Scratch: uses R6, R7
Flags: none
Description: Absolute subroutine call to a 16-bit address.
JMP (abs)
Syntax:
JMP addr16
Expands to:
LDI R6 (addr16 >> 8)
LDI R7 (addr16 & 0xFF)
JMP R6 R7
Scratch: uses R6, R7
Flags: none
Description: Absolute jump to a 16-bit address.
JR (abs)
Syntax:
JR label
Expands to: relative JR with the computed offset.
Scratch: none
Flags: none
Description: Jump to a label using a computed relative offset (assembler checks the range).
JZR (abs)
Syntax:
JZR label
Expands to: relative JZR with the computed offset.
Scratch: none
Flags: reads Z
Description: Jump to a label when zero flag is set, using a computed offset.
JNZR (abs)
Syntax:
JNZR label
Expands to: relative JNZR with the computed offset.
Scratch: none
Flags: reads Z
Description: Jump to a label when zero flag is clear, using a computed offset.
ZERO
Syntax:
ZERO rD
Expands to:
LDI rD 0
Scratch: none
Flags: none
Description: Clear a register.
INC
Syntax:
INC rD
Expands to:
PUSH R7
LDI R7 1
ADD rD R7
POP R7
Scratch: uses R7, stack
Flags: from ADD (Z/N/C)
Description: Increment a register by one.
DEC
Syntax:
DEC rD
Expands to:
PUSH R7
LDI R7 1
SUB rD R7
POP R7
Scratch: uses R7, stack
Flags: from SUB (Z/N/C)
Description: Decrement a register by one.
INC16
Syntax:
INC16 rH rL
Expands to:
CMPI rL 0xFF
JZR inc_hi
INC rL
JR end
inc_hi:
LDI rL 0
INC rH
end:
NOP
Scratch: stack via INC
Flags: from CMPI, INC (Z/N/C)
Description: Increment a 16-bit register pair in-place.
NOT
Syntax:
NOT rD
Expands to:
PUSH R7
LDI R7 0xFF
XOR rD R7
POP R7
Scratch: uses R7, stack
Flags: from XOR (Z/N, clears C)
Description: Bitwise invert a register.
CMP
Syntax:
CMP rA rB
Expands to:
PUSH rA
SUB rA rB
POP rA
Scratch: stack
Flags: from SUB (Z/N/C)
Description: Compare two registers; flags reflect rA - rB, operands restored.
CMPI
Syntax:
CMPI rD imm
Expands to:
PUSH R7
LDI R7 imm
SUB R7 rD
POP R7
Scratch: uses R7, stack
Flags: from SUB (Z/N/C)
Description: Compare a register with an immediate; flags reflect imm - rD.
SHRI
Syntax:
SHRI rD imm
Expands to:
PUSH R7
LDI R7 imm
SHR rD R7
POP R7
Scratch: uses R7, stack
Flags: from SHR (Z/N/C)
Description: Shift right by an immediate count.
SHLI
Syntax:
SHLI rD imm
Expands to:
PUSH R7
LDI R7 imm
SHL rD R7
POP R7
Scratch: uses R7, stack
Flags: from SHL (Z/N/C)
Description: Shift left by an immediate count.
SWAP
Syntax:
SWAP rA rB
Expands to:
PUSH rA
MOV rA rB
POP rB
Scratch: stack
Flags: none
Description: Exchange the values of two registers.
MUL
Syntax:
MUL rD rA rB
Expands to:
ZERO rD
PUSH rB
iter:
ADD rD rA
DEC rB
CMPI rB 0
JNZR iter
POP rB
Scratch: uses rB, stack
Flags: from ADD, DEC, CMPI (Z/N/C)
Description: Unsigned multiply by repeated addition; destroys rB during the loop, restores it after.
Standard Library
Helper macros from asm/std.asm. Include them after cpu.asm/ext.asm to get simple data routines.
MEMCPY
- Syntax:
MEMCPY i len srchi srclo dsthi dstlo - Inputs:
iloop counter (start at 0),lenstop value,srchi:srclosource pointer,dsthi:dstlodestination pointer. - Behavior: Copies bytes from source to destination until
i == len. Both pointers are incremented withINC16. Restoresieach iteration via push/pop. - Scratch: uses stack to save
i; flags fromCMP,INC,INC16.
STRCMP
- Syntax:
STRCMP i j srchi srclo dsthi dstlo - Inputs:
srchi:srclofirst string pointer,dsthi:dstlosecond string pointer. - Outputs:
ireceives0when strings match,1otherwise. - Behavior: Walks both zero-terminated strings byte-by-byte. Returns early on mismatch, or when a
0x00terminator is reached on both sides. - Scratch: uses
i,j, flags fromCMP,CMPI,JZR/JNZR, and increments addresses withINC16.
System Calls
System calls live at 0xE500 (kernel/syscalls.asm). To invoke one, load the call ID into R0 and CALL 0xE500. Inputs and outputs travel through the registers listed below; all other registers are caller-saved.
-
0x01 — SYS_GPU_MODE
Input:R1mode byte (0x00off,0x01TTY). Writes the GPU mode register at0xF000. -
0x02 — SYS_WRITE
Input:R1character byte. Sends it to the GPU TTY data register at0xF001. -
0x03 — SYS_WRITELN
Input:R1:R2address of a zero-terminated string. Streams characters to the TTY data register until0x00. -
0x04 — SYS_WAIT_FOR_KEY
Blocks until the keyboard status register (0xF100) is non-zero. No outputs. -
0x05 — SYS_READ_KEY
Output:R0key code popped from the keyboard data register (0xF101). Returns0if the queue was empty. -
0x06 — SYS_DISK_SET_BLOCK
Input:R1block index. Stores it in the disk block register at0xF200for later operations. -
0x07 — SYS_DISK_READ_BLOCK
Uses the previously selected block and copies it into the disk buffer window (0xF202–0xF302). -
0x08 — SYS_DISK_WRITE_BLOCK
Flushes the current disk buffer window into the previously selected block. -
0x09 — SYS_FS_LIST
Input:R1:R2destination buffer. Copies the directory block (block0) from disk into RAM viaMEMCPY. -
0x0A — SYS_FS_FIND
Input:R1:R2filename pointer.
Output:R0status (0success,1not found),R1block index,R2file size. -
0x0B — SYS_FS_READ
Input:R1:R2filename pointer,R3:R4destination buffer.
Output:R0status (0success,1not found). On success it loads the file into the buffer using the disk buffer window. -
0x0C — SYS_FS_WRITE
Currently unimplemented placeholder. -
0x0D — SYS_FS_DELETE
Currently unimplemented placeholder. -
0x0E — SYS_EXEC
Input:R1:R2filename pointer. Loads the file into RAM at0x1000(user entry) and jumps to it.
Output:R0status (0success,1not found). -
0x0F — SYS_EXIT
No inputs. Returns control to the kernel entrypoint at0xE000(used by user programs to quit).
Assembler syntax
We assemble with customasm.
Writing a program for the VM
- Always include
../asm/cpu.asmfirst (seeuser/sh.asm). It defines the memory banks so your ROM segment assembles with a base address of0x1000. - When the program is launched, that ROM image is copied into RAM starting at
0x1000and execution begins at your entry label.
Includes
- Always include
asm/cpu.asmto get the core ISA and register definitions. - Optionally include
asm/ext.asmto unlock pseudo-instructions likeINC,JMP addr,CMPI, etc.
#include "../asm/cpu.asm"
#include "../asm/ext.asm" ; optional
Banks and layout
- Kernel: uses
#bank rom(4 KiB at0xE000) for executable code — seekernel/main.asm. - User programs: include
cpu.asmand you automatically get the RAM bank defined there; you typically do not write your own#bankdirectives. #addr <hex>— set the write pointer inside the active bank. Handy to place data at a specific RAM offset.
Example RAM data:
#addr 0x0100 ; place data in general RAM (RAM bank is already active)
SPRITE:
#d8 0b1111_0000
#d8 0b1000_1000
Example ROM code:
#bank rom ; only in the kernel image
start:
LDI R0 0x42
HALT
Data and literals
#d8emits bytes (comma-separated or one-per-line).- Labels mark addresses (
label:). You can jump to labels or use them for data pointers. - Constants use
NAME = value. - Literals: decimal, hex (
0xFF), binary (0b1010_1010), or single-character strings ("A"stores ASCII).
Comments
- Everything after
;on a line is ignored.
Building and running
- Build:
customasm file.asm→ producesfile.bin. - Run:
cargo run -- run file.bin(add--bot other.binto load a bot alongside the judge).
Examples
User-space programs now live under user/. A good starting point is the shell at user/sh.asm: the kernel loads it into RAM at 0x1000 and jumps to it after boot. Build it with make user and run via cargo run -- run ./kernel/main.bin ./user/sh.bin ./user/hw.bin ./user/ls.bin ./user/exit.bin ./user/help.bin.