Skip to content

Commit d0355bc

Browse files
committed
feat(models): cover the eight NMOS packages the data sheet lists
The 6500 data sheet names ten packages sharing one die. Two were modelled. The other eight differ only in how far an address reaches and which interrupt lines the package brings out, both of which the per-model pages state exhaustively. A line the package does not bring out is not a line a system can assert, so pulling one now raises instead of quietly taking the interrupt. That makes the 6507 having no interrupt pins a behaviour rather than a sentence in its summary.
1 parent 2a054a7 commit d0355bc

8 files changed

Lines changed: 340 additions & 25 deletions

File tree

README.md

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -183,29 +183,46 @@ describe("w65c802").address_bits
183183
cpu = Cpu(SparseMemory(), model="65802")
184184
```
185185

186-
| Model | Address bits | Decimal | Notes |
187-
|:------|:------------:|:-------:|:------|
188-
| `6502` | 16 | yes | MOS 6502. Aliases: `mos6502`, `nmos6502`, `6510`, `8500` |
189-
| `6507` | 13 | yes | The same die in a smaller package. Aliases: `mos6507` |
190-
| `2a03` | 16 | **no** | Ricoh 2A03 and 2A07. Aliases: `ricoh2a03`, `2a07`, `nes6502`, `famicom` |
191-
| `65c02` | 16 | yes | The base CMOS design. Aliases: `synertek65c02`, `cmos6502` |
192-
| `r65c02` | 16 | yes | Rockwell R65C02, adding thirty two single-bit instructions. Aliases: `rockwell65c02` |
193-
| `w65c02` | 16 | yes | WDC W65C02S, adding stop and wait. Aliases: `wdc65c02`, `w65c02s` |
194-
| `65802` | 16 | yes | WDC W65C802, the sixteen bit core in a 6502 pin out. Aliases: `w65c802`, `65c802` |
195-
| `65816` | 24 | yes | WDC W65C816S. Aliases: `w65c816`, `w65c816s`, `65c816`, `65816s` |
186+
| Model | Address bits | Pins | Decimal | Notes |
187+
|:------|:------------:|:-----|:-------:|:------|
188+
| `6502` | 16 | IRQ, NMI, RDY | yes | MOS 6502. Aliases: `mos6502`, `nmos6502`, `6510`, `8500` |
189+
| `6503` | 12 | IRQ, NMI | yes | Four kilobytes, on-chip clock. Aliases: `mos6503` |
190+
| `6504` | 13 | IRQ | yes | Eight kilobytes, no non-maskable pin. Aliases: `mos6504` |
191+
| `6505` | 12 | IRQ, RDY | yes | Four kilobytes, ready line, no non-maskable pin. Aliases: `mos6505` |
192+
| `6506` | 12 | IRQ | yes | Four kilobytes, second clock output instead of ready. Aliases: `mos6506` |
193+
| `6507` | 13 | RDY | yes | The same die in a smaller package, no interrupt pins at all. Aliases: `mos6507` |
194+
| `6512` | 16 | IRQ, NMI, RDY | yes | The 6502 with the clock oscillator left off the die. Aliases: `mos6512` |
195+
| `6513` | 12 | IRQ, NMI | yes | The 6503 on an external clock. Aliases: `mos6513` |
196+
| `6514` | 13 | IRQ | yes | The 6504 on an external clock. Aliases: `mos6514` |
197+
| `6515` | 12 | IRQ, RDY | yes | The 6505 on an external clock. Aliases: `mos6515` |
198+
| `2a03` | 16 | IRQ, NMI, RDY | **no** | Ricoh 2A03 and 2A07. Aliases: `ricoh2a03`, `2a07`, `nes6502`, `famicom` |
199+
| `65c02` | 16 | IRQ, NMI, RDY | yes | The base CMOS design. Aliases: `synertek65c02`, `cmos6502` |
200+
| `r65c02` | 16 | IRQ, NMI, RDY | yes | Rockwell R65C02, adding thirty two single-bit instructions. Aliases: `rockwell65c02` |
201+
| `w65c02` | 16 | IRQ, NMI, RDY | yes | WDC W65C02S, adding stop and wait. Aliases: `wdc65c02`, `w65c02s` |
202+
| `65802` | 16 | IRQ, NMI, RDY | yes | WDC W65C802, the sixteen bit core in a 6502 pin out. Aliases: `w65c802`, `65c802` |
203+
| `65816` | 24 | IRQ, NMI, RDY | yes | WDC W65C816S. Aliases: `w65c816`, `w65c816s`, `65c816`, `65816s` |
196204

197205
Three of those differences are the kind that produce a bug rather than a compile error.
198206

199207
The address bus is not cosmetic. The 65802 has sixteen address lines, so bank bits never leave the chip and a read of `$7E0012` lands on `$0012`. The 6507 has thirteen, so everything above eight kilobytes is a mirror of something below it.
200208

209+
Neither are the pins. The ten NMOS packages share one die and one instruction set, and what separates most of them is how far an address reaches and which interrupt lines the package brought out. A line that is not on the package is not a line a system can assert, so pulling one here raises rather than quietly taking the interrupt:
210+
211+
```python
212+
cpu = Cpu(SparseMemory(), model="6507")
213+
cpu.irq()
214+
215+
# NoSuchPin: the 6507 has no irq pin; it brings out rdy
216+
```
217+
201218
Decimal mode is a property of the part. The Ricoh variant in the Famicom has the decimal adder left unwired, so the flag can be set and changes nothing. Code that sets it and expects decimal arithmetic is wrong, and the part will not say so.
202219

203220
And the undocumented instructions are not optional. A hundred and fifty one of the 6502's opcodes were never documented, programs used them anyway, and a core that treats them as undefined is wrong for the machines that shipped.
204221

205222
Revisions are separate models, including the ones that only fixed a bug, because a machine has whichever revision it was built with. The three CMOS parts differ in exactly two opcode columns and two opcodes: a bit-clear instruction on a part that does not have it is a no-operation, and nothing reports that the bit was never cleared.
206223

207224
> [!NOTE]
208-
> A model is only listed once a conformance suite backs it. A model with no suite behind it would make its fidelity a claim rather than a measurement.
225+
> A model is only listed once something measures it. Six are held to a suite of their own. The other ten name the part they narrow and are held to that part's suite plus a check that the narrowing, fewer address lines or fewer pins, is the only difference. Either way the fidelity is a measurement rather than a claim.
209226
210227
## What "nothing starts clean" means
211228

conformance/hardware.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@
6060
"readVia": "docs/manufacturer/w65c816s-2024.pdf, 55 pages",
6161
"covers": "The current revision. Its caveats table carries the same values as the 2004 one, renumbered from Table 8-1 to Table 7-1, which is what makes those claims safe to cite without pinning a revision.",
6262
"pinnedIn": "docs/documents.json, manufacturer/w65c816s-2024.pdf"
63+
},
64+
"mos6500_datasheet": {
65+
"publisher": "Commodore Semiconductor Group, formerly MOS Technology, Inc.",
66+
"title": "6500 Microprocessors data sheet",
67+
"revision": "November 1985, with the March 1980 printing beside it",
68+
"readOn": "2026-08-21",
69+
"readVia": "docs/manufacturer/mos-6500-mpu-nov-1985.pdf, 12 pages",
70+
"covers": "Which parts the NMOS family has. A page per package giving its addressable range and the interrupt lines it brings out, which between them are the whole of what a program can tell apart.",
71+
"pinnedIn": "docs/documents.json, manufacturer/mos-6500-mpu-nov-1985.pdf"
6372
}
6473
},
6574
"facts": {
@@ -285,6 +294,17 @@
285294
"section": "Table 7-1 Caveats, which the 2004 revision numbers Table 8-1",
286295
"quote": "A.ABS,X,ASL,LSR,ROL with no Page Crossing: 7 cycles, 6 cycles, 6 cycles, 7 cycles.",
287296
"note": "Every cell of the comparison table is the same in the 2004 and 2024 revisions, twenty years apart. Only the section number moved. A claim taken from that table therefore does not depend on which revision a reader happens to hold, which is worth knowing because three revisions of this data sheet are pinned here."
297+
},
298+
"nmosFamilyPackages": {
299+
"document": "mos6500_datasheet",
300+
"section": "Features of 6502 through Features of 6515",
301+
"quote": "Features of 6504: 8K Addressable Bytes of Memory (A0-A12). On-the-chip Clock. IRQ Interrupt. 8 Bit Bidirectional Data Bus.",
302+
"note": "Ten packages, one die. The list of features under each is exhaustive, so a line absent from it is a line the package does not bring out: the 6504 has no non-maskable pin and the 6507 has neither interrupt pin. Six of the ten have an on-chip oscillator and four take their clock on pins, which is the only difference between 6502 and 6512, between 6503 and 6513, and so on, and it is not something a program can see.",
303+
"whatIsImplemented": {
304+
"addressLines": "12 on the 6503, 6505, 6506, 6513 and 6515; 13 on the 6504, 6507 and 6514; 16 on the 6502 and 6512",
305+
"interruptPins": "Both on the 6502, 6503, 6512 and 6513. The request line alone on the 6504, 6505, 6506, 6514 and 6515. Neither on the 6507.",
306+
"whatHappensWhenOneIsMissing": "Pulling it raises, because a line the package does not bring out is not a line a system can assert"
307+
}
288308
}
289309
}
290310
}

conformance/hardware.test.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
ROOT = Path(__file__).resolve().parent.parent
1717
sys.path.insert(0, str(ROOT))
1818

19-
from mos65xx import Memory, mos6502, wdc65816 # noqa: E402
19+
from mos65xx import Memory, models, mos6502, wdc65816 # noqa: E402
2020

2121
HARDWARE = Path(__file__).resolve().parent / "hardware.json"
2222

@@ -270,6 +270,39 @@ def test_which_is_what_the_earliest_revision_says_and_the_later_ones_omit(self)
270270

271271
self.assertIn("lands above 0001FF rather than wrapping", recorded["note"])
272272

273+
def packages(self) -> list[str]:
274+
"""The ten the data sheet lists, read from what it is recorded as covering."""
275+
manifest = json.loads((ROOT / "docs" / "documents.json").read_text())
276+
found = [
277+
entry["covers"]
278+
for entry in manifest["documents"]
279+
if "mos-6500-mpu-nov" in entry["file"]
280+
]
281+
282+
return [str(name) for name in found[0]]
283+
284+
def test_the_data_sheet_lists_ten_packages(self) -> None:
285+
self.assertEqual(len(self.packages()), 10)
286+
287+
def test_the_family_table_matches_the_parts_this_package_builds(self) -> None:
288+
recorded = fact("nmosFamilyPackages")["whatIsImplemented"]["interruptPins"]
289+
both = [
290+
name for name in self.packages() if {"irq", "nmi"} <= set(models.describe(name).pins)
291+
]
292+
293+
self.assertEqual(
294+
(both, "6502, 6503, 6512 and 6513" in recorded),
295+
(["6502", "6503", "6512", "6513"], True),
296+
)
297+
298+
def test_and_the_one_with_neither_line_is_the_one_the_table_names(self) -> None:
299+
recorded = fact("nmosFamilyPackages")["whatIsImplemented"]["interruptPins"]
300+
neither = [
301+
name for name in self.packages() if not {"irq", "nmi"} & set(models.describe(name).pins)
302+
]
303+
304+
self.assertEqual((neither, "Neither on the 6507" in recorded), (["6507"], True))
305+
273306
def test_the_comparison_table_is_the_same_in_both_revisions_that_carry_it(self) -> None:
274307
recorded = fact("caveatsTableIsStableAcrossRevisions")
275308

mos65xx/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from . import mos65c02, mos6502, opcodes65c02, opcodes6502
2929
from .memory import Memory, SparseMemory, scramble
30-
from .models import MODELS, UnknownModelError, describe
30+
from .models import MODELS, NoSuchPin, UnknownModelError, describe
3131
from .opcodes65816 import (
3232
FLAG_DEPENDENT,
3333
MODE_SIZE,
@@ -86,6 +86,7 @@ def Cpu(memory: Any, model: str = DEFAULT_MODEL, **options: Any) -> Any: # noqa
8686
"Cpu",
8787
"Cpu65816",
8888
"Memory",
89+
"NoSuchPin",
8990
"SparseMemory",
9091
"StepLimit",
9192
"Stopped",

mos65xx/models.py

Lines changed: 134 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,21 @@
1212
A model with no suite behind it does not belong in this table, because then its
1313
fidelity would be a claim rather than a measurement.
1414
15-
Two of these have no suite of their own, and each says which part it narrows.
16-
Neither is a different processor: the 6507 is a 6502 with three address lines
17-
left inside the package, and the 65802 is a 65816 in a forty pin part whose bank
18-
registers reach nothing outside the first bank. What holds them is the suite of
19-
the part they narrow plus a check that the narrowing is the only difference, so
20-
the claim is still measured; it is measured against a sibling rather than against
21-
a recording of its own.
15+
Ten of these have no suite of their own, and each says which part it narrows.
16+
None is a different processor. Nine are the 6502 die in a smaller package, with
17+
address lines left inside and, on most of them, one or both interrupt pins gone;
18+
the 65802 is a 65816 in a forty pin part whose bank registers reach nothing
19+
outside the first bank. What holds them is the suite of the part they narrow
20+
plus a check that the narrowing is the only difference, so the claim is still
21+
measured; it is measured against a sibling rather than against a recording of
22+
its own.
23+
24+
A pin the package does not bring out is not a pin a system can assert, so
25+
pulling one here raises rather than quietly taking the interrupt. That is the
26+
whole of what separates the 6503, 6504, 6505 and 6506 from one another: they
27+
have the same core, the same instructions and the same timing, and they differ
28+
in how far an address reaches and which of the two interrupt lines survived the
29+
pin count.
2230
"""
2331

2432
from __future__ import annotations
@@ -31,6 +39,16 @@ class UnknownModelError(Exception):
3139
pass
3240

3341

42+
class NoSuchPin(Exception):
43+
"""Raised when a caller pulls a line the package does not bring out.
44+
45+
The narrower parts of the family are the same die in a smaller package, and
46+
the lines that did not fit are simply not there. A system cannot assert one,
47+
so a model that quietly accepted the request would be describing a part
48+
nobody could build.
49+
"""
50+
51+
3452
class Model:
3553
"""One part of the family: what it is, what it reaches, and how to build it."""
3654

@@ -45,6 +63,7 @@ def __init__(
4563
aliases: Iterable[str] = (),
4664
revision: str | None = None,
4765
narrows: str | None = None,
66+
pins: Iterable[str] = ("irq", "nmi", "rdy"),
4867
) -> None:
4968
self.name = name
5069
self.summary = summary
@@ -55,6 +74,7 @@ def __init__(
5574
self.aliases = tuple(aliases)
5675
self.revision = revision
5776
self.narrows = narrows
77+
self.pins = tuple(pins)
5878

5979
@property
6080
def address_mask(self) -> int:
@@ -74,6 +94,7 @@ def _build_6502(model: Model, memory: Any, **options: Any) -> Any:
7494
cpu = Cpu6502(memory, decimal=model.decimal, **options)
7595
cpu.model = model.name
7696
cpu.address_mask = model.address_mask
97+
cpu.package_pins = model.pins
7798
return cpu
7899

79100

@@ -87,6 +108,7 @@ def _build_65c02(model: Model, memory: Any, **options: Any) -> Any:
87108
cpu = Cpu65c02(memory, table=TABLES[model.revision], decimal=model.decimal, **options)
88109
cpu.model = model.name
89110
cpu.address_mask = model.address_mask
111+
cpu.package_pins = model.pins
90112
return cpu
91113

92114

@@ -96,6 +118,7 @@ def _build_65816(model: Model, memory: Any, **options: Any) -> Any:
96118
cpu = Cpu65816(memory, **options)
97119
cpu.model = model.name
98120
cpu.address_mask = model.address_mask
121+
cpu.package_pins = model.pins
99122
return cpu
100123

101124

@@ -125,8 +148,112 @@ def _build_65816(model: Model, memory: Any, **options: Any) -> Any:
125148
decimal=True,
126149
core=_build_6502,
127150
narrows="6502",
151+
pins=("rdy",),
128152
aliases=("mos6507",),
129153
),
154+
Model(
155+
name="6503",
156+
summary=(
157+
"MOS 6503. Twelve address lines and both interrupt pins, in a twenty eight pin "
158+
"package. Four kilobytes, and everything above that is a mirror."
159+
),
160+
address_bits=12,
161+
data_bits=8,
162+
decimal=True,
163+
core=_build_6502,
164+
narrows="6502",
165+
pins=("irq", "nmi"),
166+
aliases=("mos6503",),
167+
),
168+
Model(
169+
name="6504",
170+
summary=(
171+
"MOS 6504. Thirteen address lines and the request pin only, so a program on it "
172+
"cannot be interrupted by anything it cannot mask."
173+
),
174+
address_bits=13,
175+
data_bits=8,
176+
decimal=True,
177+
core=_build_6502,
178+
narrows="6502",
179+
pins=("irq",),
180+
aliases=("mos6504",),
181+
),
182+
Model(
183+
name="6505",
184+
summary=(
185+
"MOS 6505. Twelve address lines, the request pin and the ready line, with no "
186+
"non-maskable pin brought out."
187+
),
188+
address_bits=12,
189+
data_bits=8,
190+
decimal=True,
191+
core=_build_6502,
192+
narrows="6502",
193+
pins=("irq", "rdy"),
194+
aliases=("mos6505",),
195+
),
196+
Model(
197+
name="6506",
198+
summary=(
199+
"MOS 6506. Twelve address lines and the request pin, with the ready line spent on "
200+
"a second clock output instead."
201+
),
202+
address_bits=12,
203+
data_bits=8,
204+
decimal=True,
205+
core=_build_6502,
206+
narrows="6502",
207+
pins=("irq",),
208+
aliases=("mos6506",),
209+
),
210+
Model(
211+
name="6512",
212+
summary=(
213+
"MOS 6512. The 6502 with the clock oscillator left off the die, so the two phases "
214+
"come in on pins. Nothing a program can see differs."
215+
),
216+
address_bits=16,
217+
data_bits=8,
218+
decimal=True,
219+
core=_build_6502,
220+
narrows="6502",
221+
pins=("irq", "nmi", "rdy"),
222+
aliases=("mos6512",),
223+
),
224+
Model(
225+
name="6513",
226+
summary="MOS 6513. The 6503 driven by an external clock rather than its own.",
227+
address_bits=12,
228+
data_bits=8,
229+
decimal=True,
230+
core=_build_6502,
231+
narrows="6502",
232+
pins=("irq", "nmi"),
233+
aliases=("mos6513",),
234+
),
235+
Model(
236+
name="6514",
237+
summary="MOS 6514. The 6504 driven by an external clock rather than its own.",
238+
address_bits=13,
239+
data_bits=8,
240+
decimal=True,
241+
core=_build_6502,
242+
narrows="6502",
243+
pins=("irq",),
244+
aliases=("mos6514",),
245+
),
246+
Model(
247+
name="6515",
248+
summary="MOS 6515. The 6505 driven by an external clock rather than its own.",
249+
address_bits=12,
250+
data_bits=8,
251+
decimal=True,
252+
core=_build_6502,
253+
narrows="6502",
254+
pins=("irq", "rdy"),
255+
aliases=("mos6515",),
256+
),
130257
Model(
131258
name="2a03",
132259
summary=(

0 commit comments

Comments
 (0)