Sixteen parts of the 65xx family in Python, from the MOS 6502 to the WDC W65C816S, advanced one bus cycle at a time and held to the SingleStepTests per-opcode suites and to the manufacturers' data sheets.
English | 日本語 | 简体中文 | 繁體中文(香港)
16 parts, 17,900,000 state cases and 17,870,080 cycle-exact cases compared, 0 unexplained failures, 1,600 tests, 100% statement and branch coverage, no dependencies
from mos65xx import Cpu, SparseMemory
memory = SparseMemory()
for address, value in enumerate(bytes.fromhex("18fbc230a93412")):
memory.write8(0x8000 + address, value)
memory.write8(0xFFFC, 0x00)
memory.write8(0xFFFD, 0x80)
cpu = Cpu("65816", memory)
cpu.reset()
costs = [cpu.step() for _ in range(4)]
print(costs, hex(cpu.a), cpu.emulation, cpu.m8)
print(cpu.steps, cpu.cycles)[2, 2, 3, 3] 0x1234 False False
4 18
The program at $8000 is CLC, XCE, REP #$30 and LDA #$1234. reset() costs 8 cycles and loads the program counter from the vector at $FFFC. CLC and XCE leave emulation mode, REP widens the accumulator and the index registers, and the load then takes a 16-bit operand. Each step() returns the cycles its instruction spent, 10 for the four together, and cycles reads 18 because it also counts the 8 the reset spent.
| Requirement | Version |
|---|---|
| Python | 3.12 or newer |
| git | Any, to clone |
git clone https://github.com/gufranco/mos65xx-python.git
cd mos65xx-python
python3 mos65xx/doctor.pyThe package imports nothing outside the standard library. pip install git+https://github.com/gufranco/mos65xx-python.git installs it with a mos65xx-doctor command that runs the same report.
The doctor checks the machine, builds and resets every part, and reports which conformance suites are fetched. It runs as a file rather than with -m, so it still reports when the package will not import. It ends with one line. Paste its whole output into any bug report.
26 checks, nothing to report
Releases follow Semantic Versioning and are cut from the commit history by semantic-release. Every release is tagged and listed on the releases page.
Cpu(model, memory=None, fill=None, **options) builds one part, powered and not reset. model is required, and leaving it out raises UnknownModelError with every name accepted. The sixteen names and their aliases are in Models. Case, - and _ in a name are ignored. memory is any object with read8(address) and write8(address, value). Without it the part gets a SparseMemory, or a 16 MB Memory filled with fill when fill is given.
| Option | Parts | Effect |
|---|---|---|
seed= |
All | Seeds the scrambled power-on registers. Default UNSET_SEED |
decimal= |
8-bit | Whether a decimal adder is wired. The model sets it, and only the 2a03 sets it false |
table= |
8-bit | The opcode table. The model sets it |
| Call | Effect | Returns |
|---|---|---|
reset(seed=UNSET_SEED) |
Drives RESET. An NMOS part reads its program counter three times and the stack three times, leaving s three lower, as a simulation of the die does, then reads the vector at $FFFC in two cycles. The CMOS parts and the 65816 spend those cycles with nothing on the bus, five on the W65C02S and six on the others. Clears steps, sets the interrupt disable flag, and on the CMOS parts clears the decimal flag. The 65816 also enters emulation mode with 8-bit registers, d, db and pb at 0 |
The part |
step() |
Runs one instruction, then samples the interrupt lines | Cycles spent, interrupt entry included |
run_for(cycles) |
Runs whole instructions until at least cycles have passed. A halted part spends one cycle per call to held_cycle() |
Cycles spent, which can exceed cycles |
run_until(check, limit=None) |
Steps while check(part) is false. Raises RunLimit after limit instructions |
The part |
call(address, limit=None) |
Jumps to address and steps until the matching RTS, or RTL on the 65816, is next. Nested calls are counted |
The part |
held() |
Whether the part is jammed, stopped or waiting | bool |
held_cycle() |
One cycle of a halted part. A jammed NMOS part reads $FFFF. The other parts spend a cycle with no bus access |
Nothing |
irq() |
Takes an interrupt request now unless the interrupt disable flag is set | True if taken |
nmi() |
Takes a non-maskable interrupt now | Nothing on 8-bit parts, True on the 65816 |
abort() |
65816 only. Takes an abort now | True if taken |
status() |
The status register as a byte | int |
set_status(byte) |
Sets every flag from a byte | Nothing |
set_emulation(flag) |
65816 only. Enters or leaves emulation mode, which forces 8-bit registers and a stack in page one | Nothing |
read8(address) / write8(address, value) |
One bus cycle through the part's address lines, traced and counted. Between instructions it is counted as it is made, so the next step() returns only its own instruction's cycles |
The byte read, or nothing |
power_on(seed) |
Scrambles the registers again, as at construction | Nothing |
On a part whose package does not bring a line out, irq() and nmi() raise NoSuchPin. The 65816 and 65802 bring out all three lines.
| Attribute | Meaning |
|---|---|
a, x, y, s, pc |
The registers. On the 65816 a, x, y and s hold 16 bits |
db, pb, d |
65816 only. Data bank, program bank and the direct page register |
n, v, z, c |
The flags with the same meaning on every part, one bool each |
i, d, b, decimal |
8-bit parts. Interrupt disable, the decimal flag, the break flag, and whether a decimal adder is wired |
irq_disable, decimal, m8, x8, emulation |
65816 only. Interrupt disable, the decimal flag, 8-bit accumulator, 8-bit index registers, emulation mode |
irq_line |
The request line as a level. An NMOS part polls it as an instruction's last cycle begins, as a simulation of the die does: a request raised during the last cycle waits for the next instruction, one already polled is taken even if withdrawn, CLI lets a held request in an instruction late, and a taken branch inside its page polls a cycle earlier. The CMOS parts and the 65816 sample it after each instruction, so a request withdrawn before then is not taken |
nmi_line |
The non-maskable line. A low-to-high change interrupts once, and holding it high does not interrupt again. An NMOS part sees a change where it polls irq_line. On an NMOS part a change raised during BRK, before the cycle that pushes the status, replaces the BRK vector with the NMI vector, as WDC and Rockwell print and the die shows, and after a BRK one handler instruction always runs |
abort_line |
65816 only. The abort line. Held during a cycle of an instruction, it lets the instruction finish its bus cycles, puts every register back as the instruction found it, and takes the abort with the opcode's address as the return address. After the modify cycle of a read-modify-write and after the third cycle of RTI the status is kept, and after the second cycle of BRK or COP the program bank becomes 0, as 8.4.1 prints. Latched once per instruction, so a host releases it |
ready_line |
While false, a read spends cycles until it is true again, so a host clears it from on_cycle. The CMOS parts and the 65816 hold writes the same way, and the NMOS parts do not |
cycles |
Cycles since construction. A reset adds to it |
steps |
Instructions since the last reset |
stopped, jammed, waiting |
The three halt states. jammed is NMOS only, waiting is the CMOS parts and the 65816 |
trace |
Set it to a list and every bus cycle is appended as (address, value, kind). kind is read or write on 8-bit parts, and on the 65816 eight pin letters: data, program, vector pull, write, emulation, m, x, memory lock. A 65816 internal cycle records None as its value except the modify cycle in emulation mode, and a halted cycle records (None, None, "--------") |
on_cycle |
Called once per cycle, at its end, after that cycle's bus activity and the work that follows it |
model |
The canonical model name |
package_pins |
The interrupt and ready lines the package brings out, such as ("irq", "nmi", "rdy") |
address_mask |
The address lines the package brings out. A 6507 reads $F234 at $1234 |
reset_cycles |
Cycles a reset spends, 8 or 7 |
memory |
The memory object |
cycle_budget |
65816 only. When set, MVN and MVP move only as many bytes as fit in that many cycles |
from mos65xx import Cpu, Memory
space = Memory(size=0x10000, fill=0xEA)
for address, value in {0xFFFC: 0x00, 0xFFFD: 0x02, 0xFFFE: 0x00, 0xFFFF: 0x30}.items():
space.write8(address, value)
space.write8(0xFFFA, 0x00)
space.write8(0xFFFB, 0x40)
space.write8(0x0200, 0x58)
cpu = Cpu("6502", space).reset()
cpu.irq_line = True
print(cpu.step(), hex(cpu.pc))
cpu.irq_line = False
cpu.nmi_line = True
print(cpu.step(), hex(cpu.pc))
print(cpu.step(), hex(cpu.pc))2 0x201
9 0x4000
2 0x4001
CLI at $0200 clears the interrupt disable flag with the request line held. An NMOS part polls before the flag changes, as its die does, so the step costs 2 cycles and takes nothing, and the request is withdrawn before the next instruction polls it. Raising the non-maskable line interrupts once through $FFFA after the NOP at $0201: 2 cycles for the NOP, 7 for the entry. The third step runs a NOP with the line still high.
| Error | Raised when |
|---|---|
UnknownModelError |
Cpu() gets no name, or a name no model or alias goes by |
NoSuchPin |
irq() or nmi() on an 8-bit part whose package does not bring that line out |
InsideInstruction |
irq() or nmi() from outside a Clock paused inside an instruction or an interrupt entry, or at a boundary where a line's entry is pending. Tick to the instruction boundary, or set irq_line or nmi_line |
RunLimit |
run_until() or call() reaches its limit |
Stopped |
step() on a part that executed STP or jammed |
Waiting |
step() on a part that executed WAI. A subclass of Stopped. irq() and nmi() release it |
ClockClosed |
tick() on a closed Clock |
Truncated |
decode() runs out of bytes before the instruction ends |
UnsupportedError |
An opcode table names an instruction or addressing mode the core has no handler for |
| Name | Content |
|---|---|
MODELS |
The sixteen Model records, keyed by canonical name |
Model |
name, summary, address_bits, data_bits, decimal, pins, reset_cycles, aliases, narrows, revision, address_mask, and build(memory, **options) |
Clock(cpu) |
Runs a part one cycle at a time. See Driving it one cycle at a time |
Memory(size=0x1000000, image=None, seed=UNSET_SEED, fill=None) |
A flat byte array, scrambled from seed, filled with fill, then overlaid with image. Its bytes are data |
SparseMemory(seed=UNSET_SEED) |
24 bits of address space that stores only what was written. Its written bytes are cells |
scramble(size, seed=UNSET_SEED) |
The scrambled bytearray both memories and every part start from |
UNSET_SEED |
The default seed, 0x5A5A5A5A |
Cpu65816(memory, seed=UNSET_SEED) |
The 65816 core class, for type checks |
OPCODES, MODE_SIZE, FLAG_DEPENDENT |
The 65816 opcode table as (mnemonic, mode) pairs, operand bytes per mode, and the two modes whose width follows m or x |
decode(data, offset=0, address=0, m=True, x=True) |
One 65816 Instruction: address, offset, opcode, mnemonic, mode, operand, size, text |
disassemble(data, offset=0, address=0, count=None, m=True, x=True, stop_at_return=False) |
A list of 65816 instructions, tracking REP and SEP. It stops at the last whole instruction |
operand_size(mode, m, x), branch_target(address, size, operand, width), render(mode, operand, address, size, width), apply_flags(instruction, m, x) |
The steps decode and disassemble are built from |
FLAG_C, FLAG_Z, FLAG_I, FLAG_D, FLAG_X, FLAG_M, FLAG_V, FLAG_N |
The 65816 status bits, 0x01 to 0x80 |
NMOS, TABLES |
The 8-bit opcode tables as (mnemonic, mode) pairs: NMOS for the eleven NMOS models, and TABLES keyed 65c02, rockwell and wdc for the three CMOS ones |
decode8(data, offset, address, table=NMOS) |
One 8-bit instruction: address, opcode, mnemonic, mode, operand, size, and the undocumented and unstable properties. Raises Truncated when the bytes run out |
disassemble8(data, offset, address, count=None, table=NMOS, stop_at_return=False) |
8-bit instructions in order. It stops at the last whole instruction |
render8(mode, operand, address, size) |
An 8-bit operand as assembler text, and a branch as the address it reaches |
__version__ |
The installed version |
The three 8-bit functions end in 8 so they never shadow the 65816 ones.
import mos65xx
from mos65xx import MODELS, Cpu, NoSuchPin, RunLimit, SparseMemory, UnknownModelError
for name in ("6502", "6507", "2a03", "65802"):
model = MODELS[name]
print(name, model.address_bits, model.data_bits, model.decimal, model.pins, model.reset_cycles)
print(Cpu("Rockwell65C02").model, Cpu("w65c816s").model)
try:
Cpu("z80")
except UnknownModelError as refused:
print(refused)
try:
Cpu("6507", SparseMemory()).nmi()
except NoSuchPin as refused:
print(refused)
try:
Cpu("6502", fill=0xEA).run_until(lambda part: part.a == 0x99, limit=100)
except RunLimit as refused:
print(refused)6502 16 8 True ('irq', 'nmi', 'rdy') 8
6507 13 8 True ('rdy',) 8
2a03 16 8 False ('irq', 'nmi', 'rdy') 8
65802 16 16 True ('irq', 'nmi', 'rdy') 8
r65c02 65816
z80 is not a model this family covers; it has 2a03, 6502, 6503, 6504, 6505, 6506, 6507, 6512, 6513, 6514, 6515, 65802, 65816, 65c02, r65c02, w65c02
the 6507 has no nmi pin; it brings out rdy
gave up after 100 instructions at $31D0
| Command | Arguments | Exit codes |
|---|---|---|
python3 mos65xx/doctor.py |
None | 0 when every check passes, 1 otherwise |
python3 -m conformance.fetch |
[directory] [--latest]. Default ~/.cache/conformance-suites. --latest takes each upstream head instead of the pin. A copy already there is moved to the pin, and a copy that does not end at the pin stops the run |
0, or 1 when an upstream cannot be reached |
python3 -m conformance.singlestep |
<suite directory> [tests per file] [filter] [--model name]. Default model 65816 |
0 when every case agrees or no suite is there, 1 on a disagreement, 2 on bad arguments |
python3 -m conformance.cycles |
The same as singlestep |
The same as singlestep |
python3 -m conformance.documents |
[--check] [--only substring] |
0 when every digest matches, 1 when one is refused or the arguments are wrong |
python3 -m conformance.quotes |
None | 0 when every quote is found in its document or no document is here, 1 otherwise |
python3 -m conformance.speed |
[--instructions N] [--repeats N]. Defaults 200,000 and 7 |
0 above the floor of 700,000 cycles per second, 1 below, 2 on bad arguments |
python3 -m conformance.netlist |
None. Reads the NETLIST variable |
0 when every case agrees or no simulation is configured, 1 otherwise |
python3 -m conformance.exercisers |
None. Reads the MOS65XX_EXERCISERS variable |
0 when every exerciser stops where its listing says it passed, 1 otherwise, 2 when none ran |
python3 -m conformance.routines |
None. Reads the MOS65XX_TEST_IMAGES variable, one or more folders |
0 when every routine writes its pass flag and every copy matches, 1 otherwise, 2 when none ran |
python3 -m conformance.screens |
None. Reads the MOS65XX_TEST_IMAGES variable |
0 when every program prints its verdict and stops on nothing, 1 otherwise, 2 when none ran |
python3 -m conformance.links |
None | 0, or 1 when a cited address answers 404 or 410 |
python3 -m conformance.watch |
[definition] [--moved] |
0, or 1 when the definitions cannot be read or disagree |
python3 -m conformance.package |
<wheel> |
0 when every module is in the wheel and no test is, 1 otherwise, 2 with no argument |
step() and run_for() report the cycles spent, so a host can hold the part to a crystal. A Ricoh 2A03 runs at 1,789,773 Hz.
import time
from mos65xx import Cpu
HERTZ = 1_789_773
SLICE = 0.02
cpu = Cpu("2a03", fill=0xEA).reset()
per_slice = round(HERTZ * SLICE)
owed = 0
for _ in range(4):
began = time.perf_counter()
owed += per_slice
owed -= cpu.run_for(owed)
print(per_slice, owed, cpu.cycles)
time.sleep(max(0.0, SLICE - (time.perf_counter() - began)))35795 -1 35804
35795 0 71598
35795 -1 107394
35795 0 143188
Memory filled with NOP runs 2 cycles per instruction, and a slice of 35,795 cycles is odd. run_for() cannot cut an instruction in half, so it overshoots by one cycle. Carrying the debt into the next slice keeps the total within one cycle of the clock. A halted part still spends every cycle it is given: a jammed NMOS part keeps reading $FFFF, and a 65816 after STP or WAI produces cycles with no address.
Clock(cpu) stops the part between any two cycles, where a board changes what a later read will answer.
from mos65xx import Clock, Cpu, Memory
space = Memory(image=bytes([0xA5, 0x10]))
space.write8(0x0010, 0x11)
cpu = Cpu("6502", space)
cpu.reset()
cpu.pc = 0x0000
cpu.trace = []
with Clock(cpu) as clock:
clock.tick()
clock.tick()
space.write8(0x0010, 0x99)
clock.run_for(1)
print([(hex(address), hex(value)) for address, value, _ in cpu.trace])[('0x0', '0xa5'), ('0x1', '0x10'), ('0x10', '0x99')]
LDA $10 had fetched its opcode and operand when the byte at $0010 changed, and its third cycle read the new value.
| Call | Effect |
|---|---|
tick() |
Runs the part to the end of its next cycle and returns the clock's cycle count |
run_for(cycles) |
cycles ticks. Returns the clock's cycle count |
close() |
Stops the worker thread and restores the part's on_cycle |
cycles, closed, cpu |
Ticks so far, whether it is closed, the part it drives |
A Clock is a context manager and an iterator that yields one tick at a time. A halted part keeps ticking through held_cycle(). Python cannot suspend a call stack, so the clock runs the part on a worker thread and hands control across two semaphores every cycle. Use step() for throughput and Clock when the question is which cycle something happens in. A clock stopped on an instruction's last cycle leaves the part as step() does, whether it is then closed or not. irq() or nmi() called while a clock is paused at an instruction boundary acts at once, as it does between two steps, and its cycles count in cpu.cycles without being ticks. Paused inside an instruction, or inside an interrupt entry, there is no such place, so the call raises InsideInstruction and changes nothing: tick to the boundary, or set irq_line or nmi_line, which the part samples when the instruction completes. The same holds at a boundary whose lines will start an entry, an nmi_line edge not yet taken or an irq_line held with interrupts enabled: the step is not finished, and the error names the pending line.
The model decides the address lines, the interrupt lines, the decimal adder and the instruction set.
| Build it with | Address bits | Pins | Decimal | Part | Aliases |
|---|---|---|---|---|---|
Cpu("6502") |
16 | IRQ, NMI, RDY | Yes | MOS 6502 | mos6502, nmos6502, 6510, 8500 |
Cpu("6503") |
12 | IRQ, NMI | Yes | MOS 6503 | mos6503 |
Cpu("6504") |
13 | IRQ | Yes | MOS 6504 | mos6504 |
Cpu("6505") |
12 | IRQ, RDY | Yes | MOS 6505 | mos6505 |
Cpu("6506") |
12 | IRQ | Yes | MOS 6506, a second clock output in place of RDY | mos6506 |
Cpu("6507") |
13 | RDY | Yes | MOS 6507 | mos6507 |
Cpu("6512") |
16 | IRQ, NMI, RDY | Yes | MOS 6512, the 6502 on an external two-phase clock | mos6512 |
Cpu("6513") |
12 | IRQ, NMI | Yes | MOS 6513, the 6503 on an external clock | mos6513 |
Cpu("6514") |
13 | IRQ | Yes | MOS 6514, the 6504 on an external clock | mos6514 |
Cpu("6515") |
12 | IRQ, RDY | Yes | MOS 6515, the 6505 on an external clock | mos6515 |
Cpu("2a03") |
16 | IRQ, NMI, RDY | No | Ricoh 2A03 and 2A07 | ricoh2a03, 2a07, ricoh2a07, nes6502, famicom |
Cpu("65c02") |
16 | IRQ, NMI, RDY | Yes | The base CMOS design | synertek65c02, s65c02, cmos6502 |
Cpu("r65c02") |
16 | IRQ, NMI, RDY | Yes | Rockwell R65C02, adding 32 single-bit instructions | rockwell65c02, rockwell |
Cpu("w65c02") |
16 | IRQ, NMI, RDY | Yes | WDC W65C02S, adding STP and WAI |
wdc65c02, w65c02s |
Cpu("65802") |
16 | IRQ, NMI, RDY | Yes | WDC W65C802, the 65816 core with 16 address lines | w65c802, 65c802 |
Cpu("65816") |
24 | IRQ, NMI, RDY | Yes | WDC W65C816S | w65c816, w65c816s, 65c816, 65816s |
The eleven NMOS models share one core, the three 65C02 models share a second, and the 65816 and 65802 share a third. A narrower package reaches less: the 65802 reads $7E0012 at $0012, and a 6507 reads $F234 at $1234. On the 2A03 the decimal flag can be set and changes nothing.
from mos65xx import Cpu, SparseMemory
memory = SparseMemory()
memory.write8(0x0012, 0x5A)
memory.write8(0x7E0012, 0xA5)
for name in ("65816", "65802"):
print(name, hex(Cpu(name, memory).read8(0x7E0012)))
for name in ("6502", "2a03"):
cpu = Cpu(name, SparseMemory())
cpu.a, cpu.c, cpu.d = 0x19, False, True
cpu.add_with_carry(0x01)
print(name, hex(cpu.a))65816 0xa5
65802 0x5a
6502 0x20
2a03 0x1a
All 256 opcodes run on every 8-bit part. MOS documented a hundred and fifty one of them. The other hundred and five run as the suites record them, JAM included, and conformance/instruction-set.json holds the documented split as Appendix B prints it.
disassemble() reads bytes with no part to run them in.
from mos65xx import NMOS, TABLES, disassemble, disassemble8, render8
for instruction in disassemble(bytes.fromhex("c230a9341260"), address=0x018000):
print(f"{instruction.address:06X} {instruction.size} {instruction.text}")
code = bytes.fromhex("a9ff8f3412")
for table in (NMOS, TABLES["wdc"]):
for found in disassemble8(code, 0, 0xC000, table=table):
operand = render8(found.mode, found.operand, found.address, found.size)
print(f"{found.address:04X} {found.mnemonic} {operand} {found.undocumented}")018000 2 rep #$30
018002 3 lda #$1234
018005 1 rts
C000 lda #$FF False
C002 sax $1234 True
C000 lda #$FF False
C002 bbs0 $34,$C017 False
On the 65816 the width of an immediate operand depends on flags set earlier. disassemble starts with m and x set and follows every REP and SEP, so after REP #$30 the load takes two operand bytes. The same 8-bit bytes decode as the undocumented SAX on an NMOS part and as BBS0 on a W65C02S.
Registers and memory power up holding a reproducible scrambled pattern, so a program that reads a byte nothing wrote gets a byte that is not zero. Registers have no option that clears them. A memory is cleared only when built with fill.
from mos65xx import Cpu, Memory, SparseMemory
print(hex(SparseMemory().read8(0x123456)))
print(Memory(size=0x1000).data == bytearray(0x1000))
powered = Cpu("65816", Memory(size=0x1000000))
print(hex(powered.pc), powered.cycles)0x1d
False
0xd8ef 0
SparseMemory derives an unwritten byte from its address and seed, so a test that touches a dozen bytes stores a dozen. Both memories and every part take a seed, so a run against another implementation repeats exactly. Memory takes an image, which is what a board holds at power on.
Every core is compared with the SingleStepTests suite recorded for its part, 10,000 cases per opcode across six suites: 65816, 6502, nes6502, synertek65c02, rockwell65c02 and wdc65c02. The state comparison checks registers, flags and every byte of memory each case touched: 17,900,000 cases, no unexplained failures. The cycle comparison checks every bus cycle in order, address by address and read against write, and on the 65816 the eight pins as well: 17,870,080 cases, no unexplained failures.
python3 -m conformance.fetch ~/.cache/conformance-suites
python3 -m conformance.singlestep ~/.cache/conformance-suites/6502/6502/v1 --model 6502
python3 -m conformance.cycles ~/.cache/conformance-suites/6502/6502/v1 --model 6502Both totals are derived from conformance/suites.json, and a test holds this page to them. The wdc65c02 suite ships the files for WAI and STP empty. The cycle total leaves out 29,920 CMOS decimal cases whose extra cycle is recorded at a constant address no register produces. Where this core follows another source against the suite, both runners print how many 65816 cases they treated differently. None is left out. Each is compared in everything but what the readings disagree on: the data bank register in 19,924 emulation BRK and COP cases, the address of one stack write in forty-three JSR (a,x) cases with the stack pointer at the bottom of page one, and the address of the pointer's second byte in 265 emulation (d,x) cases whose pointer ends a page, where this core follows gilyon's test program.
conformance/hardware.json holds every fact taken from a manufacturer's page, with the sentence and the page it came from, and tests hold the code to them. conformance/addressing-cycles.json and conformance/instruction-set.json hold Appendix A of the hardware manual and Appendix B of the programming manual as printed. conformance/divergences.json records 23 places where two sources disagree, what this package follows, and what would settle each. python3 -m conformance.quotes finds every quoted sentence in its document.
The suites are pinned by commit. A weekly job runs them at each upstream head and opens a pull request when the newer suite passes or an issue when it fails. OPEN-QUESTIONS.md lists 12 questions the documents and the suites do not settle, each with the measurement on a real part that would close it.
Two whole test programs run here as well, on the bare processor: Klaus Dormann's 6502 functional test passes as the 6502, and his 65C02 extended opcodes test passes as the r65c02 and the w65c02. Each one stops by looping on one address, and his listing says which address means passed. conformance/exercisers.json records where each stopped and pins the commit they come from; python3 -m conformance.exercisers runs them from a checkout named in MOS65XX_EXERCISERS. conformance/testroms.json lists every other test program for these parts and the member each one waits on.
Test programs written for the machines these parts went into run too. python3 -m conformance.routines takes the processor routine out of six factory and service test images and runs it to the flag it writes: all six pass, recorded in conformance/routines.json. python3 -m conformance.screens runs thirty whole test programs from reset, with conformance/board.py standing in for the rest of the machine, and reads back the text they print: Peter Lemon's 23 and Shay Green's six pass, and gilyon's reaches Success, stopping on nothing, recorded in conformance/screens.json. Both find an image by its SHA-256 in the folders MOS65XX_TEST_IMAGES names.
| Need | Where |
|---|---|
| Every gate and the order to run them | CONTRIBUTING.md |
| The same material for a coding agent | AGENTS.md |
| The standard this repository shares with the rest of the family | FAMILY.md |
| Bug reports | GitHub Issues |
| Security reports | SECURITY.md |
uvx ruff@0.16.3 format --check .
uvx ruff@0.16.3 check .
uvx --python 3.12 --with types-setuptools==84.0.0.20260812 mypy==1.14.1
python3 mos65xx/mos6502.test.pyEach module has a test file beside it, named after it, and each runs on its own as above. Coverage is a hard gate at 100% statement and branch.
Every document below is copyrighted by its publisher and none is in this repository. conformance/documents.json carries each one's full SHA-256, byte count and address. python3 -m conformance.documents downloads all twelve into docs/, which git ignores, and refuses any file whose digest does not match. --check verifies what is already there.
| Document | Date | Pages | SHA-256 |
|---|---|---|---|
| MOS Technology, Inc., MCS6500 Microcomputer Family Hardware Manual | 1976-01 | 182 | 81ea570c9d68deff64d67365bdf24534df93a8c62121e53036aeac6a557cea23 |
| MOS Technology, Inc., MCS6500 Microcomputer Family Programming Manual | 1976-01 | 262 | a2d54dd8b6557c7f2f275b4ddaa9180b5754274a762df122d79392f03264b483 |
| MOS Technology, Inc., MOS 6500 Microprocessors | 1985-11 | 12 | ccd72376d1e5db1fb13777de538417f4799ab38e1f746452f9695465c422dcd7 |
| MOS Technology, Inc., MOS 6500 Microprocessors | 1980-03 | 12 | a564c4de593ea178868a43eaf7e00fe7d3460016d2a7880d20eb756f2bf2d030 |
| Synertek, Inc., SY6500 Microprocessors | Undated | 6 | f20b50961df60bb0e1980f271ad49b4b788c46a143127ccc58e8d3ce84d99193 |
| Synertek, Inc., SY6500 Hardware Manual | Undated | 178 | 9cd0991dcbb4a46bf63747528282ad175ed15e1957d08bd755ef14b978b67a1c |
| Rockwell International, R65C00 Microprocessors: R65C02, R65C102 and R65C112 | 1987-06 | 16 | 63deee76c8d0d4faee7bb8f9c50ff21662c73f2e82093ebacfb284f527faf134 |
| The Western Design Center, Inc., W65C02S 8-bit Microprocessor Data Sheet | 2022-04-08 | 32 | a6af3ca9da45c8a03504c94c7399cb6b08f15ce14185dd3e1e880433cdaa8cf5 |
| The Western Design Center, Inc., W65C816S 8/16-bit Microprocessor Data Sheet | 2004-06-14 | 62 | 60f21a3da0331273c6b92963ba7660408ab1ba3cf9dca2e37b1e55e7950ff313 |
| The Western Design Center, Inc., W65C816S 8/16-bit Microprocessor Data Sheet | 1994-07 | 72 | 823c2f286a97102fc3695596f7c03e26e2f01cff0f45023dc3bb71688e689bc7 |
| The Western Design Center, Inc., W65C816S Datasheet | 2024-03-13 | 55 | b9177e1b045d2c8a801d1b23619abb4e5b29b88868fa491df7296dbc0b13447e |
| The Western Design Center, Inc., David Eyes and Ron Lichty, Programming the 65816, including the 6502, 65C02 and 65802 | 2007 | 469 | dbccafcc47d6e53436bdc1c0049b3c54ffacfc9034a4041a7d92b6e6c5d72bb5 |
Ricoh published no data sheet for the 2A03, so the MOS documents cover the core it is built from. None of the three W65C816S revisions mentions the W65C802, so the 65816 documents cover its core and its package is undocumented.
The suites come from SingleStepTests, pinned by commit in conformance/suites.json. The publisher names no generating implementation for any of them, so they rank below a manufacturer's statement.
CITATION.cff is updated with each release by the script that stamps the package version.
MIT. See LICENSE.