This is a set of 80286 CPU tests produced by Daniel Balsom (gloriouscow) using the ArduinoX86 CPU controller.
- The
v1_real_mode
directory contains real mode opcode tests. - The
v1_unreal_mode
directory will (eventually) contain unreal mode opcode tests. - The
v1_protected_mode
directory will (eventually) contain protected-mode tests.
These tests were produced using a Harris N80C286-12 (L4252050) (C)1986
CPU. The copyright date implies this should be
an E-stepping 286, but it is not known for certain.
1,000 - 5,000 tests are provided per opcode. Opcodes that are trivial (INC reg
, CLI
, etc.) have fewer tests.
The real mode test suite contains 326 instruction forms, containing nearly 1.5 million instruction executions with over 32 million cycle states captured.
This is fewer tests than the previous 8088 test suite, but test coverage is better overall due to improved instruction generation methods.
Each test provides initial and final CPU states including registers and memory. Each test also includes cycle activity for each instruction, including the values of the address and data busses, bus controller signals, miscellaneous pin status and processor T-state.
All tests assume 16MB of RAM is mapped to the processor and writable.
No wait states are incurred during any of the tests. The interrupt and trap flags are not exercised.
Due to lack of queue status pins on the 286, and the added complexity of the 286's two-stage prefetch queue, the prefetch queue is not exercised. All instructions start from a jump to the first opcode or prefix of the instruction, which flushes the queue. The 286 takes some time after a jump to fill the prefetch queue, so most tests will begin with four code fetches.
Previous tests leaned on the MartyPC emulator to help validate test output. Tests were not emitted unless ArduinoX86 and MartyPC agreed on bus activity and final register state. In this manner the CPU and emulator helped verify each other, the CPU showing where MartyPC was inaccurate, and MartyPC catching any errors in hardware test generation.
The test generator for 286 is a new, standalone implementation that does not tie in to any emulator, relying entirely on the hardware to produce tests. The rationale for rewriting the test generator is to support CPUs that MartyPC does not emulate, such as the 286, and eventually, the 386.
Errors can potentially creep in due to mis-clocked cycles or serial protocol errors. A mis-clock is where the CPU either doesn't register a clock edge that the ArduinoX86 delivered, or where noise on the clock line causes the CPU to recognize a clock edge where one was not intended. This can result in missed bus cycles and the readout of incorrect values, which will make their way into the tests. This is not acceptable, of course, which means that error-checking is essential.
The test generator has a host of error-checking for invalid conditions, but the primary error detection method is a simple one: each test is generated at least twice and the results compared. Two operationally identical tests in a row must be generated for the test to be accepted. This eliminates the vast majority of transient clocking errors.
I say operationally identical, because periods where bus lines are floating will be, by the nature of physics, random, so these differences are ignored.
In the event that any error slips through and is discovered at a later date, the hexadecimal string of their hashes will
be added to a revocation_list.txt
file, one per line.
Since a hash uniquely identifies a test, you can load and compare the hash of each test to the hashes in the revocation
list before executing a test.
This way the test suite can be updated for accuracy without requiring large binary updates. Please open issues for any suspected bugs you may run across.
Each test is actually a sequence of two instructions, the instruction under test, and a HALT
opcode 0xF4
. The
initial ram
state includes the opcode byte for the HALT
instruction.
If the test is a flow control operation, or otherwise triggers an exception, then this initial HALT
will not be
executed. Instead, a HALT
will be injected at the first code fetch after the jump.
The rationale for injecting HALT
is to provide a visible signal that an instruction has terminated, since the 286
does not expose queue status pins that allow us to detect instruction boundaries. When the ArduinoX86 CPU server detects
the HALT
bus cycle, it raises the NMI line to the CPU which begins execution of the NMI handler, where we inject the
STOREALL
instruction and read out the final register state. We capture the flags, CS
and IP
pushed to the stack
when calling the NMI handler and use them to patch the registers dumped by STOREALL
. Doing so gives us an accurate
readout of the state of the registers at the end of the instruction.
Since each test terminates via HALT
, the last included cycle state will contain the HALT
bus status that ended cycle
state recording.
If the CPU enters the HALT
state with SP
< 6, the situation is unrecoverable. When the NMI handler attempts to wake
the CPU, the CPU will fail to push the stack frame and will execute a CPU shutdown. This is a special HALT
cycle
with an address of 0x000000
placed on the bus.
To avoid this situation, SP
is not allowed to be initialized to less than 0x0008
for any test - this ensures that
the NMI stack frame can be pushed successfully. If during the course of an instruction SP
becomes < 6, which it can
due to arbitrary ALU operations, a shutdown is unavoidable and the test will be rejected and omitted from the test suite.
Thus the test suite does not have coverage of this particular scenario - however, it is of questionable utility since
your emulated CPU is essentially freezing at this point, requiring a reset.
The general concept of a single step test is to set your emulator state to the initial state provided by each test:
- Set the register values according the values provided in the initial
regs
state. - Set the flags according to the mode being tested.
- Write the bytes specified in the initial
ram
state to memory.
Note
The initial state represents the values provided to the LOADALL
instruction. In real mode, the IOPL
and
NT
bits in the flags register cannot be set, however, they may be set in the initial state as it is a random value.
The simplest way to handle this is to mask off the top four flag bits when executing the real mode tests. Ideally,
your flag-setting function will take into account the current CPU mode and will protect these flags in real mode, as
you will see the same behavior in the tests for POPF
and IRET
.
Then, begin execution at CS:IP as if you have jumped there.
- End execution after the
HALT
instruction. - Compare your emulator's register state to the final
regs
state. - Confirm the values in memory match the final
ram
state. - Optionally, confirm your emulator executed the same cycles and/or bus operations as specified in the
cycles
array.
The 286 supported both real and protected modes, with a third "unreal mode" made possible via the LOADALL
instruction.
Real mode is the CPU's default mode where the CPU's security features are mostly disabled. Typically, only the first
1MB + 64KB of memory was accessible in this mode. In real mode, segment descriptor bases are initialized directly from
the segment register values. The real mode test set will only include the traditional 8088 register file in the initial
state.
Unreal mode is still real mode, however using the LOADALL
instruction, the segment descriptor cache can be initialized
to arbitrary values. By setting the segment descriptor base address appropriately, access to the entire 16MB address
space is possible in unreal mode. Unreal mode tests will include the values of the 286's internal X0
-X9
registers as
well as the initial descriptor cache entries in the initial
state.
Protected mode is a mode in which the 286 enforces security for multitasking system operation, enforcing privilege level and segment access checks. A full description of protected mode is beyond the scope of this README.
Creating tests for protected mode is non-trivial, as memory cannot be randomized (or the vast majority of instructions would immediately triple-fault).
Previous test suites have blindly randomized register and memory state. There is a minor issue in doing so, in regards
to exercising edge cases such as operands of 0 or 0xFFFF
. For example, given a 16-bit ADD
instruction, there's only
a 0.0015% chance that two random 16-bit numbers sum to 0 and set the zero flag. For a test set of 5,000 executions,
this only gives us a ~7% chance that any of the instructions will do so.
Therefore, register state is now generated using a beta distribution (α=0.65, β=0.65) that weights 16-bit values toward
the extremes. Additionally, each register has a small chance to be forced to 0x0000
or 0xFFFF
explicitly.
Memory contents, normally randomized, will be forced to all 0x00
bytes or all 0xFF
bytes at a low probability,
excluding bytes below address 0x1024
.
Immediate 8-bit and 16-bit operands, will also be forced to all-bits-zero or all-bits-one in a similar fashion.
Doing this greatly improves test coverage with fewer instruction runs needed than previous test suites. However, it
isn't perfect - for example, DEC
would need an operand of 1
to set the zero flag. This could be addressed in the
future, perhaps.
If randomly generated, the stack pointer will be odd with a 50% probability. This is an unnatural condition, and so the
stack pointer is specifically forced even at a very high probability. In addition, the stack pointer will be set to
0x0006
at minimum. This is to avoid a processor shutdown when the CPU cannot push the NMI handler stack frame
to the stack.
The value of IP is not forced to any specific values, and is not allowed to exceed 0xFFF8
to allow the 286 to fill its
prefetch queue after a jump.
Note
The value of IP will be the value after the terminating HALT
. If you're wondering why IP is 1 off in all the
tests, that's why.
In real mode, the Interrupt Vector Table (IVT) exists at address 0
.
Instructions are not allowed to begin below address 0x1024
to avoid writing opcode bytes over the IVT.
The IVT is randomized, thus the corresponding handler address can be anywhere in memory, even at odd alignment. This will never cause a CPU shutdown, as tests that do so are rejected.
1-5 random segment override prefixes are prepended to a percentage of instructions, even if they may not do anything. This isn't completely useless - a few bugs have been found where segment overrides had an effect when they should not have. Instructions where segment prefixes are obviously superfluous are excepted from prefix generation.
It is possible for the number of segment prefixes to increase the instruction length beyond the maximum of 10 bytes. An exception interrupt #6 will occur in this case.
The LOCK
prefix is rarely prepended to instructions. It may appear before, after, or between segment override prefixes.
This is useful for verifying proper handling of lockable vs unlockable instructions.
The status of the CPU's LOCK
pin is captured within the included cycle traces. Occasionally, the CPU will assert LOCK
automatically without a LOCK
prefix.
REP
, REPE
, and REPNE
prefixes are randomly prepended to compatible instructions. In this event, CX is masked to 7
bits to produce reasonably sized tests (A string instruction with CX==65535 would be several hundred thousand cycles).
Bytes fetched beyond the terminating HALT
opcode will be random. These random bytes are included in the initial ram
state. It is not critical if your emulator does not fetch all of them.
The 286 test suite is published in a binary format, MOO. This format is a simple and extensible chunked format. Traditionally, SingleStepTests have been published in JSON format, but this format is not always easily parsed by some languages, like C or assembler.
For information on the MOO format, see the MOO repository which contains documentation and Rust and Python code for manipulating MOO files.
If you prefer the traditional JSON format, a script moo2json.py
is available with which you can convert the test suite
from MOO to JSON. The JSON format is slightly different for 286, mostly in the cycles array where things have been
simplified and less string parsing is required. See the "Cycle Format" section below.
Example JSON test:
{
"idx": 0,
"name": "add [bx+0Eh],bl",
"bytes": [0, 95, 14, 244],
"initial": {
"regs": {
"ax": 715,
"bx": 26659,
"cx": 49548,
"dx": 64181,
"cs": 65535,
"ss": 41175,
"ds": 65535,
"es": 43996,
"sp": 39769,
"bp": 65535,
"si": 35262,
"di": 34633,
"ip": 38072,
"flags": 6291
},
"ram": [
[1086632, 0],
[1086633, 95],
[1086634, 14],
[1086635, 244],
[1086636, 161],
[1086637, 214],
[1086638, 248],
[1086639, 46],
[1086640, 107],
[1086641, 166],
[1075233, 222]
],
"queue": []
},
"final": {
"regs": {
"ip": 38076,
"flags": 19
},
"ram": [
[1075233, 1]
],
"queue": []
},
"cycles": [
[13, 1086632, 0, 0, 65535, "CODE", 13, "Ts"],
[12, 1086634, 4, 0, 24320, "PASV", 15, "Tc"],
[13, 1086634, 0, 0, 24320, "CODE", 13, "Ts"],
[12, 1086636, 4, 0, 62478, "PASV", 15, "Tc"],
[13, 1086636, 0, 0, 62478, "CODE", 13, "Ts"],
[12, 1086638, 4, 0, 54945, "PASV", 15, "Tc"],
[13, 1086638, 0, 0, 54945, "CODE", 13, "Ts"],
[12, 16777215, 4, 0, 12024, "PASV", 15, "Tc"],
[14, 1086640, 0, 0, 12024, "PASV", 15, "Ti"],
[13, 1086640, 0, 0, 12024, "CODE", 13, "Ts"],
[12, 1075233, 4, 0, 42603, "PASV", 7, "Tc"],
[13, 1075233, 0, 0, 42603, "MEMR", 5, "Ts"],
[12, 16777215, 4, 0, 56972, "PASV", 7, "Tc"],
[14, 16777215, 0, 0, 56939, "PASV", 7, "Ti"],
[14, 1075233, 0, 0, 56939, "PASV", 7, "Ti"],
[13, 1075233, 0, 0, 57042, "MEMW", 6, "Ts"],
[12, 16777215, 1, 0, 256, "PASV", 7, "Tc"],
[14, 2, 0, 0, 468, "PASV", 7, "Ti"],
[13, 2, 0, 0, 468, "HALT", 4, "Ts"]
],
"hash": "626be5084b331080eb08256c12a62d24afdf2a03"
},
idx
: The numerical index of the test within the test file.name
: A user-readable disassembly of the instruction.bytes
: The raw bytes that make up the instruction.initial
: The register and memory state before instruction execution.final
: Changes to registers and memory after instruction execution.- Registers and memory locations that are unchanged from the initial state are not included in the final state.
- The entire value of
flags
is provided if any flag has changed.
exception
: An optional key that contains exception data if an exception occurred. See 'Exception Format' below.hash
: A SHA1 hash of the originalMOO
test chunk data. It should uniquely identify any test in the suite.
The exception
key is a convenience feature provided so you do not have to attempt exception detection yourself.
If an exception occurred during instruction execution, the exception number will be given along with the address of the
flags register pushed to the stack. This can assist in masking undefined flag values that may exist in the flags
register that might otherwise cause memory validation to fail.
"exception": {
"number": 13,
"flag_address": 461420
},
If you are not interested in writing a cycle-accurate emulator, you can ignore this section.
- Pin bitfield
- Address Bus
- Memory RW status
- IO RW status
- Data Bus
- Bus Status string
- Raw Bus Status value
- T-state string
The first column is a bitfield representing certain chip pin states.
- Bit #0 represents the
ALE
(Address Latch Enable) pin output, which is output by the i82288. This signal is asserted onTs
to instruct the AT's address latches to store the current address. This is necessary since address calculations are pipelined, so onTc
the address of the next bus transaction may be on the address lines.
The i82288's ALE
output is not perfectly synchronized with CPU cycles. In these tests, it is normalized to align such
that ALE
will always read high at Ts
.
-
Bit #1 represents the
BHE
pin output, which is active-low.BHE
is asserted to activate the upper byte of the data bus. If a bus cycle begins at an even address with BHE active, it is a 16-bit transfer. If a bus cycle begins at an odd address withBHE
active, it is an 8-bit transfer, where the high (odd) byte of the data bus is active. If a bus cycle begins at an even address withBHE
inactive, it is an 8-bit transfer where the low (even) byte of the data bus is active. It is important to handle this logic correctly - the inactive half of the bus, if any, should be masked/ignored when validating against the tests. -
Bit #2 represents the
READ
Y pin. The ArduinoX86 arbitrates theREADY
line every bus cycle, so you will see it toggle on and off quite a bit. There are no wait states in the tests, so this is just a curiosity you can ignore. -
Bit #3 represents the
LOCK
pin. The 286 will assertLOCK
for memory transactions when aLOCK
able instruction is prefeixed with theLOCK
prefix, or in some cases, on its own.
The Address Bus
is the value of the 24 address lines, read from the CPU on each cycle. On some cycles the address
lines may float.
Memory RW status
is a bitfield where bit 2 is the Read signal and bit 0 is the Write signal. Bit 1 is unused.
IO RW status
is a bitfield where bit 2 is the Read signal and bit 0 is the Write signal. Bit 1 is unused.
The Data Bus
is the value of the 16 data bus lines, read from the CPU on each cycle. On some cycles, and given the
state of BHE, some data bus lines may float.
The Bus Status string
is a decoded bus status for human-readibility. These will be eight values, INTA, IOR, IOW, MEMR,
MEMW, HALT, CODE, or PASV.
The Raw Bus Status Value
is a bitfield containing the raw bus status, which may be of some interest as there are more
possible states than what are decoded as strings.
- Bit 0 represents the
S0
pin. - Bit 1 represents the
S1
pin. - Bit 2 represents the
M/IO
pin. - Bit 3 represents the
CODE/INTA
pin.
The T-state string
is a convenience string that provides the CPU T-state. This is not a status that is provided
directly by the CPU, but is easily calculated based on bus status. The values will be either Ts
, Tc
, or Ti
.
No queue status is provided. The 286 does not make queue status available.
Unlike the 8088, the 80286 has an UD or Invalid Opcode exception, interrupt #6. Most invalid forms of instructions will generate this exception. Opcodes that will only generate the UD exception are not included in the test suite.
On the 286, nearly any instruction can potentially execute an exception. Any 16-bit instruction with a memory operand
will execute an exception if the address of the operand is 0xFFFF
. When an exception occurs, the test cycle traces
continue through fetching the IVT value for the exception, fetching an injected HALT
opcode at the ISR address, up
until the final HALT
bus state.
The IP pushed to the stack during exception execution is always the IP of the faulting instruction.
Exception execution is noted in both MOO and JSON format tests, with the exception number provided as well as the
address of the flags register pushed to the stack. This is to assist in masking the flag value to handle undefined flags
in instructions such as DIV
.
- 0F: This is the first byte of the 286's extended opcode range. Extended opcodes will have a four hex-digit filename such as
0F04.MOO
.F1 0F 04
: TheSTOREALL
instruction is not included in the real mode test set.0F 05
: TheLOADALL
instruction is not included in real mode test set.
- 54:
PUSH SP
has slightly different behavior than earlier Intel CPUs. The value pushed to the stack is the value of SP before the push. - 6C-6F, A4-A7, AA-AF:
CX
is masked to 7 bits. This provides a reasonable test length, as the full 65535 value inCX
with aREP
prefix could result in several thousand cycles. - 8F, C6, C7: These forms are only valid for a
reg
field of 0. 5% of tests for these opcodes are allowed to have an invalidreg
field to help you test your UD exception handling. - D2,D3:
CL
is not masked. The 286 will internally mask the loop count to 5 bits. - 6C, 6D, E4, E5, EC, ED: All forms of the
IN
instruction should return0xFF
or0xFFFF
on IO read. - F0: The
LOCK
prefix is exercised in this test set, and the status of theLOCK
pin provided. - F1: Traditionally an alias for
LOCK
, on the 286 theF1
opcode relates to the CPU's ICE (In-Circuit Emulation) features. The most practical application of this is theSTOREALL
opcodeF1 0F 04
. As such it is not included as a random prefix. - F4: The
HALT
instruction terminates all instructions in the test suite. TheHALT
instruction is therefore included, and terminates itself. - D4, F6.6, F6.7, F7.6, F7.7 - On the 8088 the return address pushed to the stack on divide exception was the address of the next instruction. On the 286 and later, the address pushed to the stack is the address of the faulting instruction. On the 8088, divisors of
0x80
(for 8-bit division) or0x8000
(for 16-bit division) generate divide exceptions. On the 286, they do not.
Capturing individual cycle states from a CPU running at full speed is non-trivial. To simplify matters, we control the clock to the CPU via an Arduino and read out the state of the CPU on each clock cycle. Deliberatly clocking a CPU via GPIO on a slow microcontroller clocks the CPU at some kilohertz instead of megahertz.
Intel traditionally manufactured CPUs on an HMOS process, producing chips with dynamic logic gates - these gates required minimum cycle times to hold the correct logical state. Clocking them too slowly can cause the CPUs to fail, making this cycle-by-cycle control scheme infeasible.
To satisfy their customers, primarily IBM, who wanted to guarantee availability of their supply chains, Intel entered
second-source agreements with several manufacturers. These included Siemens, Harris, Oki, Fujitsu, and most famously, AMD.
These manufacturers were provided masks and were authorized to produce functionally equivalent CPUs. Some, like
Harris, specialized in CMOS processes.
A CMOS process CPU generally requires less power, but it also can be a fully static design, meaning that it can be clocked very slowly (or not at all) without losing state or malfunctioning. Therefore, a CMOS 80C286 was the ideal choice for generating these tests. It just happens that Harris 80C286 CPUs are the most widely available model. A later model was selected to hopefully be free of the numerous errata present in earlier 286 chips.
In real mode, the IOPL
and NT
flags cannot be set. Although they may be specified in the initial state, you should not
actually set them. So they're not being cleared - they were never actually set in the first place.
It may make more sense to consider the initial state as the state provided to the LOADALL
instruction, rather than the
absolute state of the CPU (because thats what they literally are).
In retrospect, it would have been more clear to mask these flags out of the initial state, but its not worth re-uploading
the entire set.
When executing the real mode test suite, just mask off the top four flags before setting them. Ideally, you would set flags through a function that takes into account the current CPU mode and will protect flags as a real 286 would.
Don't forget the HALT opcode.
The 286 fills its instruction prefetch queue of 6 bytes after a jump. Thus every instruction execution will begin with
several code fetch bus cycles before any opcode is executed. The lack of queue status pins on the 286 prevents the
precise queue status and instruction execution tracking that was possible on the 8088, therefore we use a HALT
instruction after the instruction under test to determine when execution has ended. HALT
is specified to take
two cycles on the 286, so that also adds to the cycle count.
Some instruction executions may generate interrupts or exceptions. Test cycles in this case continue through fetching
the IVT entry for the exception, jumping there, and then executing a HALT
at the corresponding ISR.
Instructions that generate exceptions will push the value of the flags register to the stack. If the flags register contains undefined flag values, this may present a difficulty for emulator authors who have yet to implement undefined flag behavior.
Exceptions are noted in the test files for you, with the exception number that was executed and the address of the flag
word. You can use this address to mask the undefined flags before comparing your final ram
state.
Creating protected mode tests is non-trivial, as memory cannot simply be randomized - valid descriptor tables must be generated in memory to avoid instructions immediately triple-faulting. I cannot say when protected mode tests will appear, all I can do is promise that I'll work on it. Protected mode tests may be generated with the assistance of the MartyPC emulator to dump values from real descriptor tables, once MartyPC has 286 support.