The three coprocessors Seta made for the Super Nintendo, each running the program you supply rather than a description of it.
3 parts, 0 commands described by hand, both waits measured on the shipped microcode, 0 disagreements, 642 tests, 100% statement and branch coverage, no dependencies
from snesst import Chip
chip = Chip("st010")
chip.write(0x000000, 0x00)
for at, value in enumerate((0x00, 0x01, 0x00, 0x02)):
chip.write(0x680000 + at, value)
chip.write(0x680020, 0x01)
chip.write(0x680021, 0x80)
chip.read(0x680010) | (chip.read(0x680011) << 8)
# 0x9300The first write wakes the part. The four after it are a point at (0x0100, 0x0200) in the shared memory, then a command number and a start bit in the two
registers just past the end of it. The answer comes back out of the same memory.
git clone --recurse-submodules https://github.com/gufranco/snes-st-python.git
cd snes-st-pythonPython 3.12 or newer, and the submodule. Nothing else.
The submodule sits at the repository root as
nec-upd7725-96050-python/,
named after itself rather than buried under a generic folder, because it is the
processor both of these parts are built on. Without it nothing here can run.
The microcode is a separate matter and is not carried here. Where to put a copy you already own is under the microcode you supply.
Everything a caller touches. Nothing else is public.
| Name | What it is |
|---|---|
Chip(model, **options) |
A part of that model, running its own microcode |
Chip(model, image=...) |
The same, with the bytes handed straight in and no directory searched |
MODELS |
Every part this package covers, by the name it goes by |
Model |
One entry of that catalogue: its name, its aliases and what it is |
available() |
Every part there is an image for on this machine |
why_not() |
Why the backend cannot run, or nothing when it can |
read(address), write(address, value) |
The two accesses a cartridge makes |
handshake() |
What the console does to wake the part |
reset() |
Back to a part that has not been woken, handed back for chaining |
step(count) |
Run the processor for a number of its own instructions |
UnknownModelError, NoFirmware, NeverFinished |
What a caller can catch |
Unrecognised, Corrupt, WrongShape |
What a supplied image can be refused for |
Chip takes the model first, which is the argument every member of the family
takes first. The name is the kind rather than the chip, so a traceback says what
sort of thing it was rather than which of two parts happened to raise.
from snesst import Chip
Chip("seta011").model
# 'st011'Every part is reached the same way, by the name it is known as. The third one needs an image nobody can carry, so the line below builds the two whose images this machine happens to hold:
from snesst import Chip
print(Chip("st010").part, Chip("st011").part)
# st010 st011Chip("st018") and Chip("setast018") reach the third, which is an ARM rather
than a signal processor and is snesst.ST018 underneath. The factory decides
which arrangement a name means, so a caller who knows the part does not have to
know which silicon is under it.
A name no part answers to is refused rather than quietly building the default:
from snesst import Chip, UnknownModelError
try:
Chip("st012")
except UnknownModelError as refused:
print(str(refused).split(";")[0])
# st012 is not a part this package covers| Name | Also answers to | What it does |
|---|---|---|
st010 |
st-010, seta010, setast010 |
Eight commands for a racing cartridge |
st011 |
st-011, seta011, setast011 |
A shogi opponent |
st018 |
st-018, seta018, setast018 |
A shogi opponent, on an ARM |
The first two are NEC uPD96050 signal processors and share one class. The third shares a vendor and a prefix with them and shares no silicon: it is a 32 bit ARM on the cartridge's own crystal, reached through three addresses and one byte at a time rather than through four kilobytes of shared memory, so it has a class of its own.
from snesst import ST018
sorted(one for one in dir(ST018) if not one.startswith("_"))
# ['drive', 'model', 'part', 'port', 'processor', 'read', 'reset', 'status', 'step', 'write']This chip has no port. It shares four kilobytes of battery-backed memory with the console: every command reads its arguments out of fixed addresses in that memory and writes its answers back into other fixed addresses in the same memory.
Underneath, both ST parts are a NEC uPD96050 with a program masked into it. What a command computes is that program. Working out what each one does and writing it down produces something that can be checked and can never be finished, and for these two it produces something worse: the tables this chip works from cannot be restated as the formulas that made them. Each agrees with school mathematics to within a unit or two and none agrees exactly, which is what a table computed by an iterative routine on the machine that would use it looks like.
So carrying them means carrying the chip's content, and deriving them means being slightly wrong everywhere.
Run the program. Neither problem survives it: nothing needs deriving, and nothing of the chip's content is carried.
The ST011 arrives with that change. It plays shogi, so its behaviour was never a set of commands anybody could write down; it is the player masked into it. A part that plays shogi and a part that computes a bearing are the same arrangement once the program is run, so both are here.
The cost is stated plainly: without an image this package refuses. It does not fall back to a guess, because an answer that did not come from the part is worse than no answer.
This used to carry a hand-written implementation of the ST010's eight commands and fifteen hundred lines of the tables they worked from. Its own opening said none of those tables could be restated as a formula. All of it is gone, along with the corpus recorded from another implementation that existed to check it.
Every image is identified before a byte of it is executed. SHA-256 decides; the other values are there so you can cross-check against a database that keys on them.
| Part | Bytes | CRC32 | SHA-256 |
|---|---|---|---|
st010 |
53,248 | 8d136190 |
55c697e864562445621cdf8a7bf6e84ae91361e393d382a3704e9aa55559041e |
st011 |
53,248 | 750c6012 |
651b82a1e26c4fa8dd549e91e7f923012ed2ca54c1d9fd858655ab30679c2f0e |
A copy you already own goes in firmware/ in this project, or in the firmware/
of the project this one sits inside when it is checked out as a submodule, or in
any directory named by SNES_ST_FIRMWARE_DIR. That variable is read first and
may name more than one directory at once, separated the way the operating system
separates a path. UPD7725_FIRMWARE_DIR is read after it and still works: this
member and snes-dsp-python shared
that one name until somebody wanted to point them at two different sets. Nothing
is downloaded.
A caller who already holds the bytes hands them straight over as
Chip("st010", image=...), and then no directory is searched at all and no
variable is read.
Confirm one you hold:
shasum -a 256 firmware/st0010.bin # macOS
sha256sum firmware/st0010.bin # Linux
certutil -hashfile firmware\st0010.bin SHA256 # WindowsA file that does not match is refused rather than run.
The model this replaced treated one write as a switch. A write below the shared window made the chip start listening, and until it arrived the two registers past the end of memory could not be set at all.
On the part there is no switch. The window below the shared memory is the processor's own data port, and the window above it is the processor's scratch memory, which is the four kilobytes the console shares. That single fact explains the write the model could not account for: the microcode raises its attention bit on its very first instruction and waits for the console to take a word off the data port. Until that happens it never reaches the loop that watches for a command, so a part spoken to without it answers nothing and reads as broken.
A console does that at power-on without being told, and so does this. Past it, each part sits in a wait of its own, measured on each rather than assumed.
| Part | Where its program waits |
|---|---|
| ST010 | words 3, 4 and 5, testing the top bit of the word holding the command and the start byte |
| ST011 | word 2 |
Measured rather than asserted, on whichever images are on this machine:
from snesst import Chip, available
for name in sorted(available()):
print(name, Chip(name).core.registers.pc)A machine holding no microcode still checks everything this package can get wrong, because the part-specific knowledge is no longer in the code.
| Layer | What is checked | Needs an image |
|---|---|---|
| The processor | Every instruction, in nec-upd7725-96050-python |
No |
| The decode | The wake write, the two registers past memory, the shared window, driven by a program of zeroes | No |
| Identity | That both parts name an image with a deciding digest, so a supplied file is confirmed rather than trusted | No |
| The catalogue | Both parts, every name they answer to, and which image each runs | No |
| The parts | That each reaches its own wait, stays there, and answers a command | Yes |
That last one is the only check that needs an image, and it reports as skipped rather than as passed when there is none.
Open questions are listed with the measurement that would close each one:
OPEN-QUESTIONS.md. Where two sources part, both are kept
in conformance/divergences.json with what would
settle it.
python -m coverage erase
for file in $(find st010 conformance -name '*.test.py' | sort); do
python -m coverage run -a "$file"
done
python -m coverage reportpython3 snesst/doctor.py says what is actually on this machine: both parts, which image each wants, where each one's program stops waiting, and the state of the processor underneath. 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.
AGENTS.md is the document for an agent working here. FAMILY.md is the standard this repository shares with the rest of the family, kept identical in every member.
snesst/
__init__.py the package, and the part chosen at construction
models.py which parts exist, what they answer to, which image each runs
chip.py loading an image, the handshake, and driving the part
microcode.test.py the checks that need a real image, kept out of the gate
version.py rewritten by the release job and by nothing else
nec-upd7725-96050-python/ the processor both of these are, as a submodule at the root
Each module has its tests beside it as <module>.test.py, so a module and the
cases that pin its behaviour are read together.
for f in snesst/*.test.py; do python3 "$f"; done| Area | File | What it pins |
|---|---|---|
| The catalogue | snesst/models.test.py |
Both parts, their names, their images, and that each image is declared with a digest |
| The part | snesst/chip.test.py |
Loading, the handshake, the decode, the shared memory, refusing |
| The microcode | snesst/microcode.test.py |
That each part reaches its own wait and answers a command. Needs an image |
Coverage is enforced at 100% of statements and branches by
pyproject.toml, so a new branch without a test fails the
build rather than quietly lowering the number.
| Command | Description |
|---|---|
ruff format . |
Format |
ruff check . |
Lint |
python3 -m coverage run -a <file> |
Run one test file under coverage |
python3 -m coverage report |
Coverage, which fails below 100% |
python3 snesst/microcode.test.py -v |
Run the checks that need an image |
pnpm run format:check |
Check that every JSON file is formatted, which CI also does |
| Convention | Source |
|---|---|
| Commit format | Conventional Commits |
| Formatting and lint | ruff, pinned in .github/workflows/ci.yml |
| Versioning | semantic-release, from the commit history |
| Tests | Beside the module, named <module>.test.py |
This project follows Semantic Versioning. Every release is tagged. See releases for the changelog and upgrade notes.
Why will it not work without a firmware image?
Because what these parts do is the program masked into them, and that program belongs to whoever made the part. A package that answered without one would be answering from a description somebody wrote.
Where do I get the microcode?
Not from here, and this will not tell you. Dump it from hardware you own. The digests above let you confirm that what you have is what the part expects.
Why is the ST011 here now when it was refused before?
It plays shogi. Its behaviour was never a set of commands that could be written down, which is exactly why it was refused by name while this package described things rather than running them. Running the program removes the distinction: both parts are a processor and a mask ROM, and both are reached the same way.
This repository carries no documents and no microcode. Every claim is traced to something published elsewhere, listed here so a reader can fetch the same file and check the same page.
Seta published nothing about either part. The top rung of the authority ladder is
empty here and conformance/hardware.json says so
rather than promoting the rung below it.
| Source | Used for |
|---|---|
| nec-upd7725-96050-python | The processor itself: its data sheet, its record, its divergences and its corpus |
| snes-driver-python | Reading a cartridge's own code to find what it says to its coprocessor |
| The microcode a reader supplies | Every answer this package gives. Confirmed by digest before a byte of it runs |
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.