The CPU is a single threaded 8MHz RISC chip. It uses the FoxVision16 architecture.
| ID | Register | Size | Read/Write | Description |
|---|---|---|---|---|
| 0x0 | X | 16-bit | Yes | General-purpose register #1 |
| 0x1 | Y | 16-bit | Yes | General-purpose register #2 |
| 0x2 | PC | 16-bit | No | Program counter (only modified internally by CPU control-flow logic; not directly accessible) |
| 0x3 | STATUS | 8-bit | Limited | CPU flags register (written by CPU operations like CMP, DIV, HLT, CLR, and by POP STATUS) |
| 0x4 | SP | 16-bit | Yes | Stack Pointer. Points to the top of the stack in memory. Modified by PUSH/POP instructions and may be read/written directly for low-level control. |
| 0x5 | CYC | 16-bit | Read-only | Global cycle counter. Increments by 1 every CPU cycle (wrapping at 0xFFFF → 0x0000). Represents elapsed CPU cycles since reset and is used for timing and synchronisation. |
| 0x6 | EM | 16-bit | Yes | Extension Mode control register. Defaults to 0x0000. Writing 0x0001 enables extension mode; writing 0x0000 disables it. Used to toggle extended CPU features and unlock up to 32K words of ROM address space. |
| Bit | Name | Meaning |
|---|---|---|
| 0 | Equality / Result flag | 0x0 = false, 0x1 = true |
| 1 | Less-than flag | 0x0 = false, 0x1 = true |
| 2 | Greater-than flag | 0x0 = false, 0x1 = true |
| 3 | Not-equal flag | 0x0 = false, 0x1 = true |
| 4 | Active register | 0x0 = X register, 0x1 = Y register |
| 5 | Illegal division flag | 0x0 = OK, 0x1 = divide-by-zero occurred |
| 6 | Halt flag | 0x0 = continue execution, 0x1 = halt after cycle |
| 7 | Reserved | Implementation-defined (must not be relied upon) |
JPZ and JNZ evaluate bit 0. EQU writes bit 0 using equality. LEQ preserves legacy flow by writing bit 0 using the less-than result while also updating less-than/greater-than/not-equal bits.
CMP (V1.5) writes all comparison bits using X vs Y and writes bit 0 using equality.
All instructions fetch 48 bits (3 words). None operand encoded ones such as NOP and SRA know how many operands they should take zero and one respectively whereas MOI instructions encode the operand count.
- Word 0: Opcode
- Word 1: Operand 1
- Word 2: Operand 2
Some instructions may ignore one or more operand words (e.g. HLT), but all three words are still fetched.
The on-disk ROM format used by the assembler and emulator is a small wrapper around a sequence of 16-bit words (the machine words used by the CPU). The following rules describe the exact layout produced by the assembler and consumed by the emulator implementation in this repository:
- Header: an ASCII identifier written first and read by the emulator to identify the file as a Fox Vision ROM. Two header variants are recognised:
- Legacy image header (default): the literal string
.VISOFOX16(10 bytes). Legacy images use the simple 10-byte header only. - Extended-mode image header: the literal string
.VFOX16EXT(10 bytes) followed by an extended header block. When the emulator observes the.VFOX16EXTheader it SHOULD treat the file as an extended-mode container and parse the extended header fields described below.
- Legacy image header (default): the literal string
Extended-mode ROM images are containers that include additional metadata immediately after the 10-byte magic. The extended header uses network (big-endian) byte ordering for multi-byte fields. The canonical layout is:
| Field | Size (bytes) | Meaning |
|---|---|---|
| Magic | 10 | Container magic (.VFOX16EXT) |
| ROM format version | 1 | Format/version byte (major version number) |
| Mapper | 2 | ROM container mapping selector (0 = ROM4K, 1 = ROM32K) |
| ROM start | 2 | 16-bit load address (big-endian) where the payload should be placed in memory |
| ROM size | 2 | 16-bit payload length in words (big-endian) |
Total extended header length: 16 bytes. After these fields the payload follows (the ROM words in big-endian file order). The emulator and other loaders SHOULD validate ROM size against the actual payload length and may reject mismatches.
Semantics:
ROM format version: allows future changes to the container structure. Tools SHOULD support version 1 and may reject unknown higher versions unless explicitly configured to be permissive.Mapping: selects the ROM container size policy only.0means ROM4K and1means ROM32K. This field does not replace the runtimeEMregister.ROM start: allows images to specify a non-zero load address (useful for relocation or alternative memory layouts). Loaders MAY ignore this field and place the payload at address 0, but SHOULD respect it when present and supported.ROM size: the number of 16-bit words in the payload. Loaders SHOULD use this to avoid reading beyond the expected payload and to pre-allocate memory structures.
Compatibility note: existing tools in this repository currently write a 10-byte magic only. Adopting the extended container header is a backwards-compatible evolution: legacy loaders expecting only the 10-byte magic should continue to work with legacy images, while updated loaders that inspect the post-magic bytes can enable the extended file-size limit when the .VFOX16EXT magic is present and the extended header parses successfully.
- Payload: a contiguous sequence of 16-bit words representing the program and data. Words are written in big-endian byte order in the file (most-significant byte first). The assembler uses big-endian encoding for portability; the emulator decodes words assuming big-endian file order.
- Footer: the generator appends two final words to every ROM payload: a
NOPword followed by aHLTword to provide a safe termination sequence for simple ROMs. These are written as 16-bit opcode words after the program payload. - Padding: if the payload byte-count after the 10-byte header is odd, the loader/payload decoder pads a single trailing zero byte when decoding to word values. This ensures the final ROM word is well-formed.
Size limits and enforcement (current implementation):
- The assembler generator enforces a strict-format check when requested via the assembler's
--strict-formatoption. In that mode the assembler enforces a maximum payload size of 2,048 words (4K words) excluding the 10-byte header. This preserves compatibility with legacy ROM size expectations. - The emulator's runtime loader accepts ROMs up to 0x1000 words (4,096 words) when copying into the machine memory area; ROMs larger than this will be rejected by the emulator. The repository's configuration and extension-mode design allow larger ROM sizes conceptually (extension mode may expand ROM space up to 32K words), but the current emulator enforces the limits above.
Notes for tool authors and integrators:
- The file format is intentionally minimal and self-describing via the 10-byte header. Tools should preserve the header when producing ROM images.
- When writing ROMs from tools running on little-endian hosts, ensure big-endian word ordering is used in the file (the assembler in this repo already handles this).
- The generator appends a terminating
NOP/HLTpair; toolchains that need precise control over footer words should emit their own termination sequence before finalising the payload.
The opcode word is split into two 8-bit fields:
- High byte (bits 15–8): Opcode ID
- Low byte (bits 7–0): Operand interpretation control
The low byte defines how the two operand words are interpreted:
- Bits 0–1: Operand count
- Bits 2–3: Operand 1 type
- Bits 4–5: Operand 2 type
- Bits 6–7: Reserved
00= Register01= Immediate10= Direct Memory Address11= Indirect Memory Address
0000 00000000 0000-NOP- Waste clock cycle0000 00000000 0001-LFM- Load 2 byte value from memory in active register (Legacy mode only)0000 00000000 0010-WTM- Write to memory the value of the active register (Legacy mode only)0000 00000000 0011-SRA- Set register active (X - 0, Y - 1) (Legacy mode only)0000 00000000 0100-AXY- Add X and Y and store result in active register (Legacy mode only)0000 00000000 0101-SXY- Subtract X from Y and store result in active register (Legacy mode only)0000 00000000 0110-MXY- Multiply X by Y and store result in active register (Legacy mode only)0000 00000000 0111-DXY- Divide X by Y and store result in active register (Legacy mode only)0000 00000000 1000-EQU- Check if X and Y registers are equal (Legacy mode only)0000 00000000 1001-LEQ- Check if X register is less than Y register (Legacy mode only)0000 00000000 1010-JPZ- Jump if zero to 2 byte wide address (Legacy mode only)0000 00000000 1011-JNZ- Jump if not zero to 2 byte wide address (Legacy mode only)0000 00000000 1100-JMP- Jump to 2 byte wide address0000 00000000 1101-CLR- Clear all Status register bits (set to zero) (Legacy mode only)0000 00000000 1110-HLT- Halt program execution (quit/power-off)0000 00000000 1111-BSL- Bitshift left value in active register (Legacy mode only)0000 00000001 0000-BSR- Bitshift right value in active register (Legacy mode only)0000 00000001 0001-AND- AND bitwise value in active register by value in non-active register (Legacy mode only)0000 00000001 0010-ORA- OR bitwise value in active register by value in non-active register (Legacy mode only)0000 00000001 0011-XOR- XOR bitwise value in active register by value in non-active register (Legacy mode only)0000 00000001 0100-DWR- Direct write sets the given 16 bit value to the active register (Legacy mode only)
0000 00000001 0101-ILM- Indirect load from memory - load address stored in active register (Legacy mode only)0000 00000001 0110-IWR- Indirect write register to memory - write value in active register to address stored in inactive register (Legacy mode only)
0000 00000001 0111-INC- Increase the value in the active register by one (Legacy mode only)0000 00000001 1000-DEC- Decrease the value in the active register by one (Legacy mode only)
V1.3 controller support is deprecated. To use controllers, ROMs must run in extended mode (EM = 0x0001) and read controller state through configured ports.
NOTE: V1.10 (EM=1 mode) introduces ports. When enabled, the system exposes eight generic 16-bit I/O ports (0x0000–0x0007). Ports are simple bidirectional data endpoints with no fixed semantic meaning in the ISA. Device behaviour is defined externally by the emulator/system configuration.
In a typical configuration, controller devices may be mapped by the emulator to PORT0 (0x0000) and PORT1 (0x0001), each exposing a 16-bit input state. This mapping is not part of the CPU specification and is fully implementation-defined.
Device state (including controllers) is maintained continuously by the emulator as a live snapshot. The CPU does not receive input events and does not rely on buffering, latching, or consumption semantics.
Each IN instruction reads the current state of the connected device at the time of execution.
Frame timing (including VBlank) is a system-level synchronisation mechanism used for rendering and scheduling. It is not part of the I/O system and is not transmitted through ports or memory-mapped registers.
Ports do not latch, queue, clear, or acknowledge input. They always return the most recent device state.
Controller state is a level-based snapshot exposed by a configured port device:
1= Button is currently held0= Button is not held
State is continuously updated by the emulator from host input events.
Bit layout:
| Bit | Button |
|---|---|
| 0 | Up |
| 1 | Down |
| 2 | Left |
| 3 | Right |
| 4 | A |
| 5 | B |
| 6 | Start |
| 7 | Select |
Examples:
00000000= No buttons held00010000= A held01000000= Start held00001001= Up + Right held
- Programs read controller state through the configured port device.
- Input is polled; it is not event-driven.
- No explicit clearing or acknowledgment is required.
Multi-operand Instructions (MOIs) use multiple operands to simplify writing assembly and reduce the need to set active registers.
MOI register operands support X, Y, and STATUS (source-only for status reads).
0000 00000001 1001-MOVSRCDST0000 00000001 1010-STRSRCDST0000 00000001 1011-LODSRCDST
Extended comparison and jump instructions (ECJI) are instructions which modernise comparison and jump logic to reduce assembly instructions and make better use of redundant space in the status register.
0000 00000001 1100-CMP- CompareXandY, update Status comparison bits0000 00000001 1101-JEQ- Jump if equal (Status bit 0)0000 00000001 1110-JNE- Jump if not equal (Status bit 3)0000 00000001 1111-JLT- Jump if less-than (Status bit 1)0000 00000010 0000-JGT- Jump if greater-than (Status bit 2)0000 00000010 0001-JLE- Jump if less-than or equal0000 00000010 0010-JGE- Jump if greater-than or equal
V1.6 adds arithmetic and bitwise MOIs using the form OP SRC DST.
SRCcan be an immediate 16-bit value or a register operand (X/Y)SRCcan be an immediate 16-bit value or a register operand (X/Y/STATUS)DSTmust be a register operand (X/Y)- Result is written to
DST - Arithmetic wraps to 16 bits
Instruction list:
0000 00000010 0011-ADDSRCDST-DST = DST + SRC0000 00000010 0100-SUBSRCDST-DST = DST - SRC0000 00000010 0101-MULSRCDST-DST = DST * SRC0000 00000010 0110-DIVSRCDST-DST = DST / SRC(divide by zero sets illegal division flag and writes0)0000 00000010 0111-ANDSRCDST-DST = DST & SRC0000 00000010 1000-ORSRCDST-DST = DST | SRC0000 00000010 1001-XORSRCDST-DST = DST ^ SRC0000 00000010 1010-SHLSRCDST-DST = DST << SRC0000 00000010 1011-SHRSRCDST-DST = DST >> SRC
Legacy one-word ALU instructions remain available:
AXY/SXY/MXY/DXYAND/ORA/XOR(active/inactive register form)BSL/BSR
When these legacy mnemonics are written with two operands (for example AND X Y), the assembler emits the V1.6 MOI opcode.
0000 00000010 1100-PUSH SRC- Push registerSRC(X/Y/SP/STATUS) onto the stack0000 00000010 1101-POP DST- Pop the top of stack into registerDST(X/Y/SP/STATUS)
PUSH/POP consume one operand word (register id) and no longer depend on the active-register bit.
V1.8 introduces a global cycle counter and a blocking timing instruction for deterministic execution delays.
- A new read-only 16-bit register
CYCis introduced CYCincrements by 1 every CPU cycleCYCwraps from0xFFFF → 0x0000- Represents total elapsed CPU cycles since reset
- Used for timing, scheduling, and frame control
0000 00000010 1110-WAITSRC- Stall execution until cycle delay has elapsed
Where:
SRCis a 16-bit immediate or register value
Behaviour:
- CPU captures current
CYCvalue at execution start - CPU enters WAIT state (execution stalls)
- Program counter and registers remain frozen
- No instructions are fetched or executed during WAIT state
CYCcontinues to increment normally- Execution resumes when:
(CYC - start) >= SRC
- WAIT is a blocking CPU state, not a no-op loop
- It does not consume instruction flow while stalled
- Designed for deterministic timing (animation, frame pacing, delays)
- Can be used as a lightweight timing primitive in place of interrupts
Fox Vision exposes a VBlank-style synchronisation point once per rendered frame.
0000 00000010 1111-VBLANK- Stall until the next frame VBlank signal
Behaviour:
- The emulator raises a VBlank signal once per display refresh tick
VBLANKblocks execution until the next signal is observed- Program counter and registers remain frozen while stalled
CYCcontinues to advance normally while the CPU is blocked- This is intended for frame pacing, VRAM updates, and animation loops
Typical usage:
- Render or update game state
- Call
VBLANK - Repeat on the next frame
Fox Vision introduces a machine extension mode control register.
- The default value is
0x0000for legacy mode - Setting the register to
0x0001enables extension mode and V1.10 features at runtime - The ROM container's
.VFOX16EXTheader does not by itself enable V1.10 features; it only selects the ROM4K/ROM32K mapping used for file-size handling
When extension mode is enabled (0x0001):
SRAis disabled- Legacy extension debug opcodes (EDO) are unavailable
- Memory-mapped I/O is removed except for VRAM
- Port I/O becomes available
Programs that use port I/O should initialize the runtime mode register before their first IN or OUT instruction by writing 0x0001 to EM:
MOV %1 EM
Compilers that target V1.10 extended mode are expected to emit this initialization at the start of the program image.
Ports are not memory-mapped.
- Eight ports are exposed
- Each port is 16 bits wide
- Port numbers are encoded as 16-bit immediate values in the range
0x0000to0x0007 - Port
0x0000through0x0007are the only valid IN/OUT targets
Port I/O instructions use an immediate operand for the port number:
0000 00000011 0000-INPORT DST- Read the 16-bit value from portPORTinto registerDST0000 00000011 0001-OUTSRC PORT- Write the 16-bit value from registerSRCto portPORT
Note: These are instructions which are only used for testing the virtual machine. They allow console I/O, printing memory, etc. They are only available in legacy mode (0x0000).
Note: All extension debug instructions start with 11 so the first instruction is 1100 0000 0000 0000.
1100 00000000 0000-DBG_LGC- Log a character to the console1100 00000000 0001-DBG_MEM- Log the memory in hex to the console1100 00000000 0010-DBG_INP- Prompt the user for input which is then converted to an unsigned uint16
Compatibility aliases for older sources are also accepted by the assembler:
DGB_MEMmaps toDBG_MEMDGB_INPmaps toDBG_INP
Note: Unknown displays ? when outputting and unknown reading in is converted to 40
#=0A-Z=1-26-=270-9=28-37- Newline =
38 - Space =
39
first 16 bits: opcode second 16 bits: addresses/values (if applicable) third 16 bits: addresses/values (if applicable)
Fox Vision supports a display size of 100x100 with four bits used to represent each colour (colours are predefined) and retrieves this data from RAM 60 times a second to display it.
The total memory size for an uncompressed frame is:
4bits * (100 * 100) = 40000 bits (5000 bytes or 5kb approx)
VRAM starts at address FFFF and descends the next 5000 bytes.
FFFF corresponds to top-left corner, moving right then down.
The device has a total of 65,536 ushort (64K words) of addressable space.
This memory is broken up into several sections.
0x0000 - 0x0FFFROM (4K words)0xEC78 - 0xFFFFVRAM- Remaining space available for RAM and VRAM
- ROM may expand to use up to 32K words of address space
- VRAM remains fixed at 5000 bytes (
0xEC78 - 0xFFFF) - Memory-mapped I/O is removed except for VRAM
- Remaining space available for RAM and VRAM
The machine supports 8 port devices, these are not defined by the CPU ISA as they are platform devices.
They are all use bidirectional single lane to send/retrieve data and use same port type VF16P which delivers power and offers single lane for data transfers.
Controller state is a level-based snapshot exposed by a configured port device:
1= Button is currently held0= Button is not held
State is continuously updated by the emulator from host input events.
Bit layout:
| Bit | Button |
|---|---|
| 0 | Up |
| 1 | Down |
| 2 | Left |
| 3 | Right |
| 4 | A |
| 5 | B |
| 6 | Start |
| 7 | Select |