Overview
A CPU, and only a CPU
SPK286 implements the Intel 80286 as RTL. The repository ends at the CPU boundary: execution core, microcode and its compiler, prefetch, segmentation and protection, interrupts, task switching, the Bus Interface Unit and the timing policy. Machine designs, whether on an FPGA platform or in custom hardware, consume it as a component.
Project-owned microcode
Human-readable .mc source compiled by a deterministic
Python tool into a synchronous ROM. Microcode requests functional
operations; it is not a second x86 decoder.
Full protected mode
Descriptors, privilege checks, call, interrupt, trap and task gates, task switching, IOPL, exception replacement, double fault and shutdown.
Hardware-derived oracle
Checked against the SingleStepTests/80286 captures from a physical Harris N80C286-12, plus directed benches in two simulators and mutation audits.
SPK286 is functionally complete for the documented 80286 and its two best-known undocumented instructions. It does not yet claim historical cycle accuracy: execution is dependency-driven, and the historical schedule is a provisional, separable layer.
Logical design
Core architecture
Every piece of state has one owner. Architectural registers, segment caches, protected-mode state, the stack and operand services and the BIU each own what they hold; microcode asks them for operations and waits for real dependencies.
Code fetches and every data, I/O, descriptor, TSS and interrupt acknowledge cycle are issued through the BIU. Data access never flushes the prefetch queue.
Instruction stream
- One outstanding code fetch; the fetch cursor may run ahead of IP by the queue capacity plus one response.
- Refill needs two free queue slots; bytes are queued low byte first.
- A redirect flushes the queue and discards the response of any code read still in flight.
- The instruction start CS:IP and TF are captured at opcode consumption and become the saved IP for faults.
- Instruction length is qualified without side effects against the ten-byte limit.
Operands and stack
- ModR/M and displacement are captured once; later queue bytes cannot alter the executing instruction.
- Each byte or word access is checked against the cached limit and rights before any bus cycle.
- 16-bit offsets wrap; physical addresses are 24 bits.
- PUSH SP pushes the original SP; POP SP loads the popped value.
- A faulting POP to memory keeps its SP increment, as observed on physical parts.
Module map
| Module | Role |
|---|---|
spk286 | Top level: connects every service and exposes the bus and debug ports. |
spk286_fetch | One outstanding code fetch and one response slot; drains stale responses after a redirect. |
spk286_prefetch_queue | Six-byte instruction queue with simultaneous consume/refill. |
spk286_instruction_context | Instruction byte and prefix lifetime (segment override, LOCK, REP). |
spk286_decoder and *_decode | Instruction-form decode into a microcode entry plus captured metadata. |
spk286_encoding_preview | Side-effect-free instruction-length qualification. |
spk286_microsequencer | Generic micro-PC sequencing over the generated synchronous ROM. |
spk286_micro_executor | Executes micro-operations through the owning services. |
spk286_registers, spk286_segments | General registers, IP and FLAGS; visible selectors and hidden segment caches. |
spk286_modrm, spk286_effective_address, spk286_operand | ModR/M metadata, the single EA table and the shared byte/word operand service. |
spk286_stack, spk286_stack_range | Stack addressing, SP commit and PUSHA/POPA preflight. |
spk286_alu, _shift, _muldiv, _adjust, _condition | Arithmetic, shift/rotate, iterative multiply/divide, decimal adjust and condition predicates. |
spk286_events | Event arbitration, delivery-failure classification and saved context. |
spk286_protected_state, _selector, _descriptor | MSW, CPL, GDTR, IDTR, LDTR and TR; selector bounds and descriptor parsing. |
spk286_tss_*, spk286_task_* | Fixed 80286 TSS image, descriptor resolution, task segment caches and the atomic task switch. |
spk286_loadall_service | Serialized LOADALL image acquisition. |
spk286_biu | Sole bus arbiter for code, data, I/O, INTA and LOCK. |
spk286_timing_policy, spk286_timing_trace | Reset-time timing policy selection and non-intrusive timing observation. |
External interface
A normalized 80286 bus
The bus is a synchronous transaction interface, not a pin-accurate model. A native wrapper can translate it to 80286 pin polarities and the full S0/S1/M-IO/COD-INTA encoding.
| Signal | Dir | Meaning |
|---|---|---|
clk | in | Synchronous core clock. |
reset | in | Active-high synchronous reset; inhibits bus output while asserted. |
ready | in | Completes the active bus cycle. |
address[23:0] | out | Physical address. |
data_in[15:0] | in | Read data, sampled on the READY edge of an active read. |
data_out[15:0], data_oe | out | Write data on the selected lanes and its drive enable. |
bus_valid, bus_write | out | Active cycle and direction. |
bus_bhe_n | out | Active-low upper byte enable. |
bus_status[3:0] | out | Cycle type (see below). |
bus_lock | out | Explicit LOCK, implicit XCHG, and the INTA/first-push chain. |
intr, nmi | in | INTR is level sensitive; NMI is a rising edge. Synchronous to clk. |
hold_request, hlda | in / out | HOLD request and grant at a free cycle boundary. |
execute_enable | in | Pauses or resumes semantic execution without repeating bus cycles. |
timing_policy_request[1:0] | in | Timing policy, sampled at reset. |
halted, shutdown | out | HLT and shutdown states. |
Bus status encoding
- After reset CS=F000 with hidden base FF0000 and IP=FFF0, so the first fetch is at
FFFFF0. - Address, status, BHE and write data hold while
bus_valid && !ready. - An odd word is two ordered cycles (N on D15:8, then N+1 on D7:0), each waiting for its own READY, with bus ownership kept between them.
- At a free boundary an accepted data request wins over code; once visible, a cycle keeps the bus until READY.
- HOLD is granted only at a free boundary and never splits an odd word, a locked read-modify-write or the INTA chain.
- Interrupt acknowledge is two READY-qualified INTA cycles; the vector is D7:0 of the second, and LOCK links both with the first stack push.
- Halt and shutdown are indicated with status 4 and address 2 or 0, without a READY-qualified transfer.
The top level also exposes setup ports (register, FLAGS, IP and
segment writes plus a coordinated redirect) used by test harnesses,
and read-only debug_* ports for registers, segment
caches, protected-mode state, micro-PC, events, bus activity and
timing counters.
Control store
Readable microcode, deterministic ROM
Source is authoritative
Instructions are written in microcode/source/*.mc as
named functional operations. The compiler checks operands and
temporary lifetimes, resolves entries and labels, and emits one ROM
word per statement. Timing annotations, padding and arbitrary
branches are rejected by the language.
microcode_format 21
# Undocumented 80286 SALC: AL = CF ? FFh : 00h; preserve AH and all FLAGS.
instruction LOAD_CARRY_BYTE:
READ_FLAGS -> TMP0
CARRY_FILL8 TMP0
TMP0 -> DST8
END
Execution contract
The decoder consumes the opcode and loads the generated entry with captured metadata. The microsequencer owns micro-PC and active state and knows nothing about x86. The executor completes an operation only when its real dependency is satisfied — a queue byte, an operand response, BIU completion — and otherwise the sequencer holds.
Reset and redirects clear microstate; pauses hold it. An invalid
ROM address or word latches microcode_fault.
Reproducible artifacts
- No timestamps or absolute paths in the output.
- Generated files are committed and never edited by hand.
- A stale artifact fails every build.
- Frozen images of earlier formats keep extensions append-only.
- Each micro-PC maps back to its source line.
Generated artifacts and compiler commands
File in microcode/generated/ | Content |
|---|---|
spk286_microcode_pkg.sv | Dimensions, operation constants and entry constants. |
spk286_microcode_rom.sv | Portable combinational ROM with validity output. |
spk286_microcode_sync_rom.sv | Synchronous ROM inferred as block memory; used by the core. |
spk286_microcode.lst | Symbolic listing with source locations. |
spk286_microcode_symbols.json | Entries, labels and per-PC source mapping. |
spk286_microcode_validation.json | Format and encoding versions, source hashes, usage and status. |
make microcode # regenerate
make microcode-check # fail if any artifact is stale
make microcode-test # compiler and frozen-format tests
make microcode-audit # independent ROM reconstruction and round-trip
python3 -m tools.microcode.compile_microcode --lookup MOV_R16_IMM16.writeback
python3 -m tools.microcode.compile_microcode --pc 0x1
Instruction set
The documented 80286, and then some
| Area | Status | Notes |
|---|---|---|
| Real-mode instructions | Complete | Every documented form; all 307 opcode forms checked against hardware captures. |
| Protected mode | Implemented | System instructions, segment loads, gates, task switching, IOPL, #DF and shutdown. |
LOADALL (0F 05) |
Implemented | Fixed 102-byte image at 000800h, atomic commit, #GP(0) at nonzero CPL. |
SALC (D6) |
Implemented | AL = CF ? FFh : 00h; AH and FLAGS preserved. |
| Coprocessor (80287) | Not attached | ESC and WAIT follow the MSW no-NPX rules. |
| 80386 behavior | Excluded | No granularity, 32-bit fields or operand-size prefixes, by design. |
Real mode
- Data transfer, byte and word ALU, TEST, INC/DEC/NEG/NOT.
- Shifts and rotates, including immediate counts.
- MUL/IMUL/DIV/IDIV and immediate IMUL; decimal and ASCII adjust.
- String instructions with REP/REPE/REPNE, restartable per iteration and interruptible between iterations.
- Port and string I/O, PUSHA/POPA, ENTER/LEAVE, BOUND.
- Near and far transfers, LOOP/JCXZ, INT/INTO/IRET, HLT, segment override and LOCK prefixes.
- Byte IDIV overflow follows the physical 80286 captures; invalid opcodes raise #UD.
Protected mode
- LMSW, SMSW, LGDT, SGDT, LIDT, SIDT, LLDT, SLDT, LTR, STR, VERR, VERW, LAR, LSL, ARPL, CLTS — checks before any side effect.
- Segment loads with selector, type, privilege, presence and limit checks, accessed-bit write-back and expand-down segments.
- Far CALL/JMP/RET, conforming and non-conforming, through call gates with parameter copying and TSS stacks.
- Interrupt and trap gates with error codes and outer-level stacks; same- and outer-level IRET.
- Task switching through TSS descriptors and task gates, busy bits and NT back-link.
- IOPL checks on CLI/STI/IN/OUT/INS/OUTS; POPF/IRET FLAGS privileges.
LOADALL image layout (102 bytes at 000800h)
| Offset | Size | Field |
|---|---|---|
| 00 | 6 | Reserved |
| 06 | 2 | MSW |
| 08 | 8 | Reserved |
| 10 | 2 | TR selector |
| 12 | 2 | FLAGS |
| 14 | 2 | IP |
| 16 | 2 | LDT selector |
| 18 / 1A / 1C / 1E | 2 each | DS / SS / CS / ES selectors |
| 20 | 16 | DI, SI, BP, SP, BX, DX, CX, AX |
| 30 / 36 / 3C / 42 | 6 each | ES / CS / SS / DS hidden caches |
| 48 | 6 | GDTR (base low, base high, limit) |
| 4E | 6 | LDT hidden cache |
| 54 | 6 | IDTR |
| 5A | 6 | TSS hidden cache |
Each hidden cache record is base low word, base high byte, access
byte and limit word. 0F 04 and the other 0F
holes remain invalid.
Interrupts and exceptions
Intel priority at every boundary
At an instruction boundary, instruction faults come first, then the TF single-step trap, then NMI, then INTR.
| Request | Vector | Saved IP | Notes |
|---|---|---|---|
| Instruction fault | 0, 5, 6, 7, 10–13 | Instruction start, prefixes included | Never retires; #DF (8) on a fault during delivery. |
| Single step (TF) | 1 | Next / target IP | TF is sampled at instruction start. |
| NMI | 2 | Next instruction | One pending edge; blocked until IRET. |
| INTR | INTA vector | Next instruction | Needs IF=1 and no STI/SS shadow. |
| INT3 / INT n / INTO | 3 / n / 4 | Next instruction | Retires when entry completes; INT clears TF without trapping. |
- Real-mode entry pushes FLAGS, CS and IP, clears IF and TF, reads the IVT, then commits CS:IP and flushes the queue in one step.
- No event is admitted inside a frame or a physical operand word.
- A frame push that cannot complete leads to double fault or shutdown, never to unbounded recursion.
- STI creates a one-instruction shadow only when IF goes from 0 to 1; MOV SS and POP SS inhibit interrupts for one instruction.
Timing model
Function first, timing as a separate layer
The number of micro-operations an instruction uses does not define its historical cycle count, and no idle or padding micro-operations exist. Timing is applied by an explicit policy, and real dependencies are honored in every mode.
Baseline
timing_policy_request = 0. Normalized, dependency-driven functional execution.
Max
timing_policy_request = 2. Dependency-driven, functionally identical to the baseline.
Profiled variant
spk286_profiled holds retirement until an Intel-derived
per-instruction budget expires, counted in host-supplied virtual CPU
clocks.
Observation without intrusion
spk286_timing_trace counts cycles, BIU busy time, READY
waits, prefetch requests and responses, queue-empty cycles, redirects,
operand waits and events. Unsupported policy requests fall back to 0
and set debug_timing_policy_error. The profiled variant is
a generated drop-in copy of the top level; with
historical_enable=0 it behaves as the functional core.
The long-term modes are Accurate (validated historical timing), Fast and Max. Native Ts/Tc bus timing, prefetch overlap and named 6–25 MHz profiles are not yet validated against hardware, and a profile is not called accurate because a nominal MHz or aggregate benchmark matches.
Verification
Expected values never come from the RTL
Tests are part of the specification. Every meaningful RTL change goes through the same ladder before it is committed, and expectations derive from Intel documentation, physical 80286 evidence and independent reference models.
- Focused testpython3 -m unittest
- Lintmake lint
- Directed benchestest_units.sh both
- Dual simulatorVerilator + Icarus
- SingleSteprun_singlestep
- Synthesismake synth-check
- Regressionmake regression
SingleStepTests/80286 as the functional oracle
The corpus was captured from a physical Harris N80C286-12 in real
mode (pinned commit 37c73ca, MIT licence). Each MOO test
is normalized, loaded into the RTL, run to one target retirement and
compared on final registers and the whole sparse memory image,
including neighbouring bytes. Undefined FLAGS bits are masked with
the corpus masks.
MOO test -> normalized state -> RTL initialization -> one target retirement
-> final registers and sparse RAM -> masked comparison
Every admitted case runs, including prefixed and exception-terminated
ones. The 307 documented real-mode forms account for 1,386,996 cases;
the rest cover SALC and the no-NPX 9B/D8
captures. Three cases are revoked upstream, and one record without a
captured write strobe is reported as not evaluable — never as a pass.
Directed benches
Each bench in sim/unit runs under both Verilator and
Icarus Verilog. Many are driven by vectors from independent Python
reference models in tools/, so the expected values do
not come from the RTL under test.
Checking the checkers
Mutation audits inject isolated RTL faults into temporary copies and
require the benches to reject each one. make verify-failure
proves both simulators report a wrong expectation, and the MOO
parser is cross-checked against the upstream reference parser.
Make targets
| Target | Purpose |
|---|---|
make test | Microcode checks, lint, every directed bench in both simulators, Python tests and smoke runs. |
make regression | test plus Yosys synthesis of the whole core. |
make singlestep-fetch | Fetch the pinned SingleStepTests/80286 working set into .cache. |
make singlestep | Run every case of every supported opcode file. |
make singlestep-audit | Cross-check the MOO parser and comparators against upstream and injected faults. |
make mutation-audit | Require the directed benches to reject injected RTL faults. |
make profiled-check | Freshness, lint and dual-simulator oracles of the profiled variant. |
make full-check | All of the above. |
FPGA resources
Cyclone V footprint
Yosys 0.33 synth_intel_alm estimate for Cyclone V, the
reference FPGA target. The figures are Yosys cells, not Quartus ALMs; Quartus packs
and optimizes differently. Regenerate them with
make resource-report.
| Integrated core | LUTs | Arithmetic | Registers | M10K | MLAB | DSP |
|---|---|---|---|---|---|---|
spk286 (flattened) | 9,247 | 2,651 | 4,793 | 1 | 0 | 0 |
Largest modules by LUTs
Hierarchy and all debug ports kept, so per-module figures are an upper bound. Multiply and divide are iterative and use no DSP blocks; the single M10K block holds the microcode ROM.
Full per-module table
| Module | LUTs | Arith. | Regs | M10K |
|---|---|---|---|---|
spk286_micro_executor | 3,185 | 884 | 902 | 0 |
spk286 | 1,471 | 496 | 16 | 0 |
spk286_protected_state | 1,273 | 88 | 540 | 0 |
spk286_task_segment_service | 669 | 65 | 734 | 0 |
spk286_task_switch_service | 516 | 296 | 652 | 0 |
spk286_segments | 472 | 32 | 256 | 0 |
spk286_muldiv | 456 | 299 | 202 | 0 |
spk286_registers | 426 | 16 | 160 | 0 |
spk286_tss_service | 318 | 142 | 402 | 0 |
spk286_events | 266 | 0 | 129 | 0 |
spk286_decoder | 229 | 3 | 0 | 0 |
spk286_descriptor (x8) | 200 | 0 | 0 | 0 |
spk286_adjust | 147 | 100 | 84 | 0 |
spk286_biu | 141 | 24 | 90 | 0 |
spk286_fetch | 139 | 91 | 98 | 0 |
spk286_loadall_service | 123 | 6 | 651 | 0 |
spk286_prefetch_queue | 111 | 16 | 58 | 0 |
spk286_alu | 108 | 18 | 0 | 0 |
spk286_operand | 104 | 0 | 2 | 0 |
spk286_effective_address | 100 | 80 | 0 | 0 |
spk286_control_decode | 88 | 0 | 0 | 0 |
spk286_timing_trace | 77 | 832 | 837 | 0 |
spk286_microsequencer | 75 | 45 | 23 | 0 |
spk286_shift | 75 | 6 | 57 | 0 |
spk286_alu_decode | 42 | 0 | 0 | 0 |
spk286_operand_decode | 32 | 0 | 49 | 0 |
spk286_stack | 32 | 33 | 0 | 0 |
spk286_instruction_context | 19 | 4 | 12 | 0 |
spk286_flags_decode | 15 | 0 | 0 | 0 |
spk286_selector (x2) | 12 | 86 | 0 | 0 |
spk286_condition | 4 | 0 | 0 | 0 |
spk286_modrm | 3 | 0 | 0 | 0 |
spk286_stack_range | 2 | 34 | 0 | 0 |
spk286_timing_policy | 1 | 0 | 4 | 0 |
spk286_microcode_sync_rom | 0 | 0 | 0 | 1 |
Total spk286 (hierarchical) | 10,931 | 3,696 | 5,958 | 1 |
spk286_timing_trace only holds debug counters. The
profiled variant is not covered because Yosys 0.33 cannot parse the
module-scope package import in rtl/timing.
Known limitations
What still needs evidence
Where Intel documentation, emulators and physical parts disagree or stay silent, SPK286 implements a documented policy and records the question as open instead of picking an answer quietly.
Functional
- Explicit LOCK privilege rules and the protected zero-count INS/OUTS boundary.
- LAR/LSL/VERR/VERW truth tables for absent and conforming descriptors.
- NT after a JMP task switch keeps the incoming value; Intel table 8-2 and some emulators disagree.
- The multi-word boundary and restart matrix is only partly covered.
- Competing-event ordering needs a wider independent matrix.
Policies pending physical evidence
- NMI blocking is released at IRET start, including a faulting IRET.
- The STI shadow exists only on a 0-to-1 IF transition.
- A faulting instruction does not also produce a single-step trap.
- Event recognition order for zero-count REP instructions.
- LOADALL stepping-specific details and images that clear PE.
No directed bench yet covers an owed TF, NMI or INTR event at an instruction boundary competing with a fetch fault of the next instruction. Real-mode fetch wraps at FFFF, so the case only arises at a protected-mode CS limit.
Scope and usage
Integrating the core
Inside SPK286
- CPU top level, decoder, microsequencer and datapath.
- Microcode source, compiler and generated ROM.
- Prefetch, segmentation, protection and task switching.
- Interrupts, exceptions and the BIU.
- Timing policy and CPU-only verification.
Left to the machine
- PIC, PIT, DMA, 8042, RTC/CMOS and ISA.
- Video, audio and storage.
- BIOS packaging and PC/AT memory map.
- Board top levels, memory controllers and platform integration.
Instantiation
Instantiate spk286 from rtl/spk286.sv with
the sources listed in scripts/rtl_sources.sh: the
generated microcode package and ROMs first, then rtl/*.sv.
rtl/timed/spk286_profiled.sv is a drop-in variant that
adds the optional historical instruction-interval schedule.
Requirements: Verilator 5.020 or later, Icarus Verilog 12, Yosys,
GNU make and Python 3 with the standard library only. On Ubuntu or
WSL, make bootstrap installs them and make env
checks them.
make regression # microcode, lint, directed benches, Python tests, synthesis
make singlestep-fetch # fetch the pinned SingleStepTests/80286 corpus
make singlestep # run every supported SingleStep opcode file
make mutation-audit # check that the benches reject injected RTL faults
make full-check # everything, plus the profiled variant checks
Sources
References and credits
| Source | Use |
|---|---|
| Intel iAPX 286 / 80286 and 80287 Programmer's Reference Manuals (210498-003, -005) | Primary architectural reference. |
| Intel 80286 Hardware Reference Manual (210760-002) | Bus, priorities, INTA, HOLD, halt and shutdown. |
| Intel 80286 B-2/B-3 step information (1984) | INT/TF and INTA LOCK corrections. |
| Intel "Undocumented iAPX 286 Test Instruction" | LOADALL image layout. |
| SingleStepTests/80286 (MIT) | Hardware-derived test corpus, fetched at build time and not redistributed. |
| dbalsom/moo (MIT) | MOO 1.1 format, implemented independently. |
| 86Box, PCem | Behavioral cross-references only; no code copied. |
| MCL86, 80x86, z386, ao486 | Architectural ideas only; no code copied. |
SPK286 is directed by spark2k06 (aitorgomez.net) and developed with AI coding agents, which wrote the RTL, microcode, tools and tests. spark2k06 set the architecture and scope, defined the verification strategy, ran the testing and drove the adjustments needed to make the core work.
No external CPU source code is included: emulators and other cores are treated as evidence, not authority, and expected behavior derives from Intel documentation and physical 80286 evidence. That rule, the hardware-captured test corpus and independent reference models are what keep AI-written RTL honest.
Source code and the full technical reference:
github.com/spark2k06/SPK286
· docs/SPK286.md.
SPK286 is released under the MIT License, so it can be integrated into designs under any license, including GPL machine cores. The SingleStepTests/80286 corpus (MIT) is fetched at build time and not redistributed.