A uPD7725 you can drive from a clock, held to NEC's own data book for every flag each ALU operation touches and to a per-encoding corpus for the whole processor state after every instruction.
2 parts, 1,120 encodings walked field by field, 22,240 instructions compared, 0 disagreements, 911 tests, 100% statement and branch coverage, no dependencies
from upd7725 import Cpu
cpu = Cpu("upd7725")
cpu.reset()
cpu.stores.load_program(bytes([0xC0, 0x0A, 0x81]))
cpu.step()
print(f"{cpu.registers.word('a'):04X}")002A
One instruction, loading 002A into the first accumulator. A word here is
twenty four bits, so a program image is three bytes to the instruction.
pip install git+https://github.com/gufranco/nec-upd7725-96050-python.gitPython 3.12 or newer. Nothing else.
Everything a caller touches. Nothing else is public.
| Call | Does | Returns |
|---|---|---|
Cpu(model="upd96050", memory=None, **options) |
Builds a part, powered and not yet reset. Stores of its own if none are given | a Cpu |
cpu.reset() |
Drives RESET. Costs the four cycles NEC names as the minimum the pin must be held | the Cpu |
cpu.step() |
Runs one instruction | cycles it cost, always one |
cpu.run_for(cycles) |
Runs whole instructions until at least that many cycles have passed | cycles actually spent |
cpu.run_until(check, limit=None) |
Steps while check(cpu) is false. limit bounds the instructions and raises RunLimit |
the Cpu |
cpu.held() |
Whether the part has stopped advancing the program | bool, always false: this part has no halt |
cpu.irq() |
Offers the interrupt line and acts on it now. A call to 100H when the enable bit is set | True if taken |
identify(image) |
Names an image from its digest, with no machine to run it in | an Identity |
disassemble(words, address) |
Reads program words with no machine to run them in | Instruction objects with .text |
MODELS |
Every model this package covers, by the name it goes by | a mapping |
| Pin or attribute | Is |
|---|---|
cpu.irq_line |
The request line as a level. Edge sensitive: the transition takes the interrupt, and holding it afterwards does not take it again. cpu.lower_irq() drops it |
cpu.cycles / cpu.steps |
Cycles since construction, across resets; instructions since the last reset |
cpu.registers |
a and b with their flag sets, k, l, m, n, tr, trb, dr, si, so, the pointers pc, rp, dp, sp, the stack, and sr, the status word the host reads |
cpu.flags_a / cpu.flags_b |
One accumulator's six bits each: s1, s0, c, z, ov1, ov0 |
cpu.stores |
program, table and scratch, each as long as the register that addresses it |
cpu.on_cycle |
Called once per cycle, after that cycle's work |
Options: seed= fixes the undefined state, fill= and sources= decide what an unwritten word answers.
A part arrives powered, not reset, because no board hands over one that has reset itself. Every register holds rubbish derived from the seed, the program counter included, so stepping it executes rubbish from a rubbish address. Call reset() to get a machine that runs a program.
A part runs at whatever its crystal says. step() reports what an instruction cost, so a host can hold the part to a real clock.
import time
from upd7725 import Cpu
HERTZ = 8_192_000
SLICE = 0.02
cpu = Cpu("upd7725")
cpu.reset()
per_slice = round(HERTZ * SLICE)
owed = 0
for _ in range(5):
began = time.perf_counter()
owed += per_slice
owed -= cpu.run_for(owed)
time.sleep(max(0.0, SLICE - (time.perf_counter() - began)))One instruction is one cycle on this part, so run_for() lands exactly on its budget rather than overshooting. Carrying the difference anyway is what keeps the loop identical to the ones in the sibling packages, where an instruction spans several cycles and overshoot is unavoidable.
Clock stops the part between any two cycles, which is where a board changes what a read will answer.
from upd7725 import Clock, Cpu
cpu = Cpu("upd7725")
cpu.reset()
with Clock(cpu) as clock:
clock.tick()
clock.run_for(6)
print(cpu.registers.pc)7
On this part a cycle boundary and an instruction boundary are the same place, because NEC states it executes an instruction in one external clock cycle. So Clock buys the same interface as the sibling packages rather than finer resolution, and step() is the right call when a caller wants speed.
It is not free. An instruction is an ordinary call stack and Python cannot suspend one, so the clock runs the part on a thread and lets it block where the cycle is spent, which is what full-system emulators do.
One instruction set, two sizes. Every store is exactly as long as the register that addresses it, which is why the two differ in capacity and in nothing else this package can measure.
| Build it with | Program store | Table | Scratch | Stack |
|---|---|---|---|---|
Cpu("upd7725") |
2048 x 24 bits | 1024 x 16 | 256 x 16 | 4 |
Cpu("upd96050") |
16384 x 24 bits | 2048 x 16 | 2048 x 16 | 8 |
The larger part is the one to be careful with. NEC published a data sheet and a data book for the smaller one; no document for the larger was located, so its four figures rest on secondary sources, it carries verified: false in the record, and the gap is written up in OPEN-QUESTIONS.md.
Each answers to the numbers NEC sold it under. Case and separators do not matter.
| Build it with | Also answers to |
|---|---|
Cpu("upd7725") |
7725, upd77c25, 77c25, necupd7725 |
Cpu("upd96050") |
96050, upd96050gf, necupd96050 |
A part number nothing here implements is refused rather than resolved to something close, so Cpu("upd7720") raises UnknownModelError instead of handing back a part missing instructions the caller asked for.
A program somebody else wrote has nothing but its words, so reading and running are separate halves.
from upd7725 import disassemble
for found in disassemble([0xC00A81, 0x910040, 0x400000], 0x100):
print(f"{found.address:04X} {found.text}")0100 ld $002A,a
0101 jnza $0010
0102 rt
Words rather than bytes, because the program store is addressed by word and a caller holding an image has already had to decide how three bytes become one. Every one of the sixteen million words is a valid instruction of one of four forms, so nothing here raises and there is no undefined case to report.
Registers and stores hold a reproducible scrambled pattern. There is no parameter that clears the registers and there will not be one: a part that has been powered and not reset holds rubbish on real silicon, and a model that starts at zero turns a missing reset into a passing test.
from upd7725 import Cpu
powered = Cpu("upd7725")
print(hex(powered.registers.pc), powered.cycles)
print(hex(Cpu("upd7725", seed=7).registers.pc))
print(Cpu("upd7725", seed=7).registers.pc == Cpu("upd7725", seed=7).registers.pc)0x4eb 0
0x438
True
Rubbish derived from the seed, the same every time, and not zero. The part has spent nothing because nothing has driven RESET yet.
Every encoding is walked field by field and every instruction is compared against a recorded corpus that states the whole processor state after each one, not merely the answer: 22,240 instructions, no disagreements. Two independent implementations are stepped against that one corpus.
python3 -m conformance.instructions
python3 -m conformance.differential --from 70000000 --cases 4000
python conformance/alu_flags.test.pyThe first runs with nothing else on your disk. It is the gate.
Where a document and the corpus disagree, both are kept. conformance/hardware.json holds every fact taken from a document with the sentence it came from and the page. conformance/divergences.json holds every place two sources part, with what would settle it.
The flags are the part worth naming. The 1987 data sheet is Advance Product Information and states no flag rules, so they rested on the corpus alone until NEC's 1989 data book turned up: its Table 6 gives, for all sixteen ALU operations, which flags are affected, which are reset, which are held and which NEC declines to define. This model agrees with it on every cell, and conformance/alu_flags.test.py drives all sixteen against a run.
Four questions remain where being faithful is a claim rather than a measurement, and each names the measurement that would close it: OPEN-QUESTIONS.md. One cannot be closed by anyone here: no NEC document for the larger part has been found, so everything asserted about it rests on secondary sources, and secondary sources for this family are emulators.
python -m coverage erase
for file in $(find upd7725 conformance -name '*.test.py' | sort); do
python -m coverage run -a "$file"
done
python -m coverage reportpython3 upd7725/doctor.py says what is actually on this machine: the parts, what makes each one different, and whether the firmware this repository cannot carry is here. It is run as a file rather than with -m so that it still runs when the package itself will not import, which is the case it exists for. Its report is what an issue asks for, because a report is only as good as what it says about the machine that produced it.
Tests sit beside the module they cover, named <module>.test.py. Coverage is 100% of statements and branches, enforced. Types are mypy at strict. Commits follow Conventional Commits, and releases are cut by semantic-release.
AGENTS.md is the document for an agent working here. FAMILY.md is the standard this repository shares with zilog-z80-python and mos65xx-python, kept identical in all three.
upd7725/
core.py the processor
clock.py driving it one cycle at a time
models.py the two parts, by name and alias
memory.py the three stores, each at its own width
registers.py the register file, and the status word
flags.py one accumulator's six bits
ports.py the two addresses the host sees, and the handshake
opcodes.py naming the fields of a word, without running it
conformance/
pinned.json which corpus, from which reference, at which commit
instructions.py every encoding, walked field by field
differential.py model against reference, on cases the corpus does not cover
alu_flags.test.py every ALU operation against the flag matrix NEC printed
hardware.json what NEC printed, fact by fact
divergences.json where sources part
This repository carries no documents. Every claim is traced to something published elsewhere, listed here so a reader can fetch the same file and check the same page. Each row gives the page count and the first sixteen characters of the file's SHA-256, because vendor links move and a link that has rotted into a different scan is easy to follow without noticing. Compute the full digest with shasum -a 256 <file>.
Every manufacturer document below is copyrighted and not redistributable, which is why none is in this repository. Individual sentences are quoted in conformance/hardware.json with the page they are printed on.
| Document | Date | Pages | SHA-256 | Redistributable |
|---|---|---|---|---|
| NEC Electronics, uPD77C25/uPD77P25 Digital Signal Processor Data Sheet, Advance Product Information | 1987-08 | 36 | d043be18d5cd21d9… |
No |
| NEC Electronics, Digital Signal Processor and Speech Processor Products Data Book, document 50052 | 1989 | 388 | 2f0190523de99938… |
No |
Fetching them is a command rather than an exercise. conformance/documents.json carries the full digest, the byte count and a fetchable address for each, and conformance/documents.py brings both down into docs/, which git ignores, and refuses anything whose digest does not match.
python3 -m conformance.documents # fetch and verify every digest
python3 -m conformance.documents --check # verify what is already hereThe data book is the fuller of the two. The 1987 sheet is Advance Product Information and states no flag rules; the data book's Table 6 gives, for all sixteen ALU operations, which flags are affected, which are reset, which are held and which NEC declines to define. It numbers pages per section, as 2-33, so a fact read from it names the section.
Both are photographs of printed books, so each is read twice: once from the page images and once from the text layer the file carries. Neither is reliable alone. The 1987 layer prints lhe for the and an a-bit data pointer where the page says 8-bit, and the image read misses a faint line outright. A page recorded beside a quote is one both readings agree on, or one confirmed by reading that page directly.
No document for the uPD96050 was located. Everything asserted about that part rests on secondary sources, it carries verified: false in the record, and the gap is written up in OPEN-QUESTIONS.md.
| Source | Used for |
|---|---|
| ares-emulator/ares | The reference the corpus was recorded from. Commit in conformance/pinned.json |
CITATION.cff is kept in step with the released version by the same script that stamps the package, so the version it names is the version that shipped. GitHub renders it as a Cite this repository button.