80286
A Hardware-Generated CPU Test Suite for the Intel 80286
Install / Use
/learn @SingleStepTests/80286README
80286
This is a set of emulator CPU tests for the Intel 80286 CPU produced by Daniel Balsom using the ArduinoX86 Arduino-to-CPU interface board.
Current Version: 1.1.0
- The
v1_real_modedirectory contains real mode opcode tests. - The
v1_unreal_modedirectory will (eventually) contain unreal mode opcode tests. - The
v1_protected_modedirectory will (eventually) contain protected-mode tests.
About the 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.
Accuracy
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.
Test Methodology
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.
CPU Shutdown
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.
Using the Tests
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
regsstate. - Set the flags according to the mode being tested.
- Write the bytes specified in the initial
ramstate to memory.
Then, begin execution at CS:IP as if you have jumped there.
- End execution after the
HALTinstruction. - Compare your emulator's register state to the final
regsstate. - Confirm the values in memory match the final
ramstate. - Optionally, confirm your emulator executed the same cycles and/or bus operations as specified in the
cyclesarray.
Processor Modes and Test Subsets
The 286 supported both real and protected modes, with a third "unreal mode" made possible via the LOADALL instruction.
Real Mode
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
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
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).
Randomization
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.
Stack Pointer
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.
Instruction Pointer
The value of IP is not forced to any specific values, and is not allowed to exceed 0xFFF8 to allow the 286 to fil
