The two coprocessors Seta made for the Super Nintendo, running the microcode you supply rather than a description of it.
Quick start | Why there is no model | The microcode you supply | The handshake | What is checked without one | Issues
2 parts · 1 processor underneath both · 4 KB of memory shared with the console · 0 commands described by hand · 60 tests · 100% statement and branch coverage · every image confirmed by SHA-256 before a byte of it runs
from st010 import St010
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.
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.
| Tool | Version | Install |
|---|---|---|
| Python | 3.12, 3.13 or 3.14 | python.org |
| Git | any | git-scm.com |
git clone --recurse-submodules https://github.com/gufranco/snes-st010-python.git
cd snes-st010-pythonThe submodule sits at the repository root as
nec-upd7725-python/, named
after itself rather than buried under a generic folder, because it carries the
NEC uPD96050 both of these parts are built on and anybody browsing this should
see that immediately.
A copy of the microcode you already own goes in one of these, and the first one that has it wins:
- any directory named by
UPD7725_FIRMWARE_DIR, several separated the way your system separates a path - the
firmware/directory of the project this one sits inside, which is what a parent project uses when it carries this as a submodule - this project's own
firmware/directory
Nothing is downloaded and nothing is shipped.
python3 -c "import st010; print(sorted(st010.available()) or st010.why_not())"
# ['st010', 'st011']Without an image that prints the reason instead, naming where to put one.
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 |
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 |
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-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.
st010/
__init__.py the package, and the part chosen at construction
models.py which parts exist, what they answer to, which image each runs
silicon.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-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 st010/*.test.py; do python3 "$f"; done| Area | File | What it pins |
|---|---|---|
| The catalogue | st010/models.test.py |
Both parts, their names, their images, and that each image is declared with a digest |
| The part | st010/silicon.test.py |
Loading, the handshake, the decode, the shared memory, refusing |
| The microcode | st010/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 st010/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.