System overview
This wiki documents how TI-84 Plus OS 2.55MP uses the Z80, banked memory, calculator hardware, and its internal software subsystems. It keeps direct ROM evidence, emulator observations, public hardware behavior, and unresolved inferences distinct.
The target is a validated 1 MiB Flash image whose OS identifies itself as 2.55MP. Conventions and methodology defines the address notation and confidence flags used throughout the book.
Machine and OS
The TI-84 Plus is a Z80 machine that can only see 64 KiB at once. The target has
1 MiB of Flash and eight RAM selector values. Community hardware reports assign
eight independent 16 KiB RAM blocks to early units. They assign 48 KiB to later
units, with selectors 82–87 sharing one block. No physical result is recorded
for the calculator used by this project. A four-slot paging scheme and a
system-call (bcall) mechanism expose code and data beyond the current address
space. The OS is a single-tasking monitor. Its boot/kernel core occupies Flash
page 0, other OS routines span banked Flash pages, and fixed RAM windows hold
system state. See RAM pages for the revision evidence.
Four mechanisms connect most user-facing OS behavior:
| Mechanism | Role |
|---|---|
| Paging and bcalls | Reach code and data outside the current 64 KiB address space. |
| Floating-point engine | Store real and complex values in the OP1–OP6 registers and perform arithmetic. |
| Variable Allocation Table (VAT) | Catalog named reals, lists, matrices, strings, programs, and AppVars. |
| Tokenizer and parser | Store TI-BASIC as one- and two-byte tokens and execute the resulting token stream. |
Around those sit the I/O subsystems: the Flash command path and boot write APIs; the IM1 interrupt dispatcher (interrupts.md); the standard timers, RTC, and low-power state machine (clock-timers-power.md); the MD5 round accelerator; the LCD driver; the keypad scanner; and the link port.
Subsystem index
Each row maps a documentation page to the subsystem it covers.
| Page | Subsystem |
|---|---|
| Memory map | Address space, ports, and RAM layout |
| Flash memory | Flash geometry, protection, command sequences, boot write APIs, archive traces, and emulator differences |
| Paging | Paired and independent Flash/RAM mapping, extended selectors, boot transition, and forced overlays |
| Bus timing and wait states | CPU-speed-selected Flash, RAM, LCD, and timer wait-state registers |
| ASIC status, identity, protection, and GPIO | ASIC status and identity, battery comparison, protection mode, and GPIO |
| The bcall mechanism | rst 28h system calls and the jump table |
| Interrupts | IM1 entry, USB and legacy routing, masks, status, acknowledgement, priority, and wake |
| Clock, timers, and power | Clock domains, programmable timer API, RTC, APD cadence, and power-off |
| MD5 accelerator and boot API | MD5-assist ports, boot digest API, round descriptors, and Rabin hash transformation |
| Variables and the VAT | Variable Allocation Table and object types |
| Floating-point engine | BCD floating-point format and OP registers |
| Tokenizer and TI-BASIC tokens | Token tables, parser, and interpreter |
| Display and LCD | LCD ports and screen buffers |
| Keyboard and link port | Keyboard and link overview |
| Keypad and ON-key hardware | Matrix electrical behavior, scan timing, debounce, repeat, ON interrupts, and wake |
| Subsystem map | Bcall API surface and the system through-line |
| Boot, contexts, and errors | Boot, the context system, _JError, and onSP |
| Memory management | RAM heap, VAT, userMem, Flash archive, and garbage collection |
| Flash page map | Contents of each of the 64 Flash pages |
| RAM pages | RAM page selectors, page 83, and restore rules |
| Open questions and roadmap | Prioritized future work |
The sidebar groups subsystem deep dives beneath their parent pages. The Glossary defines TI-specific terms, and the bcall index is the alphabetical system-call reference.
Subsystem map (bcall API surface)
This page groups the roughly 600 named bcall entry points by subsystem. The result shows the public API used by both application code and the OS itself.
| Subsystem | ~bcalls | Representative entry points |
|---|---|---|
| Floating-point / numeric | ~230 (+~120 more in “misc”) | _FPAdd,_Times2,_DivHLBy10,_Intgr,_Trunc,_DToR,_RToD,_Min,_Max,_SqRoot |
| Display / LCD | ~41 | _PutMap,_PutC,_PutS,_DispHL,_NewLine,_ClrLCDFull,_VPutS,_GrBufCpy |
| Variables / VAT | ~37 | _FindSym,_ChkFindSym,_CreateReal,_CreateStrng,_CreateAppVar,_DelVar,_InsertMem,_Arc_Unarc |
| String / convert | ~18 | _ExpToHex,_OP1ExpToDec,_CreateStrng,_StrCopy,_Get_Tok_Strng |
| Parser / TI-BASIC | ~18 | _IsA2ByteTok,_GetTokLen,_BinOPExec,_ParseInp |
| Link / I-O | ~15 | _SendAByte,_RecAByteIO,_SendVarCmd,_Rec1stByte,link_xfer_op |
| System / power | ~15 | _AppInit,_PutAway,_RandInit,_ApdSetup,_Chk_Batt_Low,_SetExSpeed,_JForceCmd |
| Boot cryptography | 5 | _MD5Init, _MD5Update, _MD5Final, _SigModR, _TransformHash |
| List / Matrix | ~13 | _CreateRList,_CreateCList,_CreateRMat,_ErrDimMismatch,dim/element ops |
| Keyboard | ~5 | _GetCSC,_GetKey,_KeyToString |
| Menu / UI | ~5 | _DispMenuTitle,_CursorOn,_CursorOff,_RunIndicOn,_RunIndicOff |
(Counts approximate — keyword buckets overlap; ~170 “misc” are mostly more math/int helpers.)
Reading the map
This is a calculator: roughly two-thirds of the API is numeric, and everything else is comparatively small glue. The architecture flows:
flowchart TD
KP([keypad]) --> GK["_GetKey"] --> P[parser] --> TD[token dispatch]
TD --> VAT["VAT · _FindSym<br/>variables"]
TD --> FP["FP engine · OP1..6<br/>arithmetic / transcendentals"]
TD --> DISP["display · _PutMap<br/>homescreen / graph"]
VAT --> R["result in OP1<br/>shown via _DispOP1A / _PutS"]
FP --> R
DISP --> R
Cross-cutting services used by all of the above: the bcall mechanism, interrupt dispatch, clock/timers/APD/power, MD5 and boot signature arithmetic, error handling (_JError + TIError codes), and the system flags (SystemFlags @ flags).
Execution through-line
- Interrupt keeps time, scans the keypad into
kbdScanCode, runs APD. _GetKeyturns scan codes into key codes (TIKeyCode), driving menus and the homescreen.- The parser reads tokenized input/programs, dispatching each
TIToken. - Number tokens → FP engine (OP1–OP6, BCD); name tokens → VAT (
_FindSym). - Results land in
OP1and are rendered by the display subsystem. - bcall + paging is the substrate that lets steps 3–5 live on different flash pages; errors unwind via
_JError/onSP.
See the subsystem pages linked above and in the sidebar for detail.
Conventions and evidence
This page defines the notation, evidence labels, and naming conventions used throughout the wiki.
Suggested reading order
- System overview introduces the machine, OS, and evidence model.
- Subsystem map shows the major services and their dependencies.
- Memory map, Paging, The bcall mechanism, and Interrupts cover the shared architecture.
- Continue with a core subsystem such as Floating point, Variables and the VAT, Tokenizer and TI-BASIC tokens, or Display and LCD, followed by its linked deep dives.
- Glossary for any unfamiliar term.
Address notation
pp:addr— Flash pagepp(00–3F) and logical addressaddr. Banked pages run in the0x4000–0x7FFFwindow, so_PutSat01:5C39means page01, address0x5C39.ram:addr— page 0 (the always-mapped kernel) and the RAM window; Ghidra keeps page 0 in itsramspace, soram:229E≡00:229E.- Ghidra’s overlay space writes flash addresses as
page_pp:addr(e.g.page_38:4000); the wiki normalizes these to the shortpp:addrform, sopage_38:4000is written38:4000. - A bare
0x….(no page) is a RAM data address or an unpaged value (e.g.flags0x89F0, the bcall-ID ranges0x4xxx/0x8xxx, a page number like0x3B). - bcall ID ≠ address. A bcall has an ID (the 2-byte word after
rst 28h, such as_FindSym=0x42F4) and a body address (00:0E65). The ID indexes the jump table; it is not where the code lives.
Confidence flags
Every non-obvious claim is tagged:
| Flag | Meaning |
|---|---|
| [confirmed] | Directly observed in this ROM’s disassembly, decompiler, raw bytes, generated database, or a labeled execution trace. |
| [standard] | Matches the publicly-documented TI-83+/84+ architecture and is consistent with the disassembly, but not every byte was traced. |
| [hypothesis] | Inferred / not yet verified — treat with caution. |
Function naming
_CamelCase— an official TI bcall/equate name (fromti83plus.inc, the full 2007 TI-83 Plus SDK equates file, or the TI SDK), e.g._FindSym,_FPAdd. High confidence.snake_case— a name inferred from a routine’s behavior, including its callees and RAM or port accesses, such asfindsym_scanorfp_normalize. Any individual low-level helper name remains a best-effort interpretation.
The rebuilt Ghidra project keeps each kind of name in a separate checked registry:
tools/symbols/names.txtcontains function entries. Its importer disassembles the entry and creates a function.tools/symbols/labels.txtcontains ROM data and internal code-entry labels. Rows markedentryseed and preserve disassembly without creating an overlapping function.tools/symbols/ram.txtcontains RAM symbols, including official SDK equates and carefully named inferred state.tools/symbols/ports.txtcontains I/O-port symbols.tools/symbols/poffsets.txtcontains reviewed base-plus-offset references. These make an operand such asmathprintArenaState + 0x0Drender as a structure member without inventing a second global name for the field address.
tools/symbols/ty_regions.txt applies the C layouts built by BuildTypes.java. The prose can therefore use expressions such as table_value_cache.band[1].value[row] once it introduces the typed base and its concrete address. A physical boundary, trace target, or byte-level proof still keeps its concrete address. [confirmed]
Math notation
Formulas are written in LaTeX and rendered by KaTeX (offline, client-side): $…$ for inline math and $$…$$ for display. Algorithms render as pseudocode blocks and data/control-flow diagrams as Mermaid.
Evidence and reproducibility
- The Ghidra database is rebuilt from the ROM by
tools/build.sh(a 15-stage reproducible pipeline around Ghidra’s headless analyzer). It loads all 64 flash pages (page 0 + overlays at4000), then resolves routines, applies function and data symbols, and installs the checked C layouts and offset references. - Local ROM trust boundary.
tools/ti84re/rom/assemble_local_rom.py --checkvalidates the exact ignored base-ROM and AppVar hashes without writing output. Its reusabletools/ti84re/rom/assembly.pylibrary decodes each TI variable container, verifies its checksum, type, name, flags, duplicate length fields, internal 16 KiB size, and payload hash, then requires the assembled-ROM hash. This proves which bytes the analysis uses; it does not prove that the files were captured from a physical calculator. The pinned base already contains theD84PBE1.8Xvpage-3Fpayload byte for byte. OnlyD84PBE2.8Xv, installed at page2F, changes the base image (8,615 bytes). [confirmed] - bcall table resolution. The main jump table page was found by scoring all 64 flash pages: for each candidate, count how many of the known bcall IDs produce a valid
(addr, page)entry. Page 0x3B scored highest for the0x4xxxtable — more known bcall IDs resolve to a valid(addr, page)entry there than on any other page — and is confirmed by the documented RST shortcuts (all six matched) and by every entry resolving and live-confirming once 0x3B is applied.0x8xxxbcall IDs index 87 populated retail boot-table entries on page3F; several USB entries target page2F. The localrom.binis assembled from the patched base plus the retailD84PBE1.8XvandD84PBE2.8Xvpayloads.tools/symbols/bcalls8x_targets.txtcontains the 83 byte-resolved bodies with public SDK names; the remaining four entries have project-inferred names. The resolver rejects these targets when page3Fhas a BootFree prefix. - Decompiler caveats. Ghidra’s Z80 decompiler can mis-render
SET b,(IY+d)flag operations,CALL cross_page_jump(ram:2B09) trampolines, and register-passed arguments on banked pages. Raw disassembly and ROM bytes are authoritative for these cases.
See the repository README.md for the exact build pipeline and tooling.
Build and evidence provenance
Reverse-engineering results are meaningful only when their ROM, execution environment, include file, and analysis tools are identifiable. This repository uses SHA-256 identities rather than filenames as the primary provenance boundary.
Two local OS 2.55MP images
The repository recognizes two complete-image identities. Pages 0x00–0x2E
and 0x30–0x3E are identical. Their pages 0x2F and 0x3F provide
different boot support. [confirmed]
| Image | SHA-256 | Page 0x2F | Page 0x3F |
|---|---|---|---|
| Canonical retail analysis image | 7d9a7d96d89fc552ebee6afdbdd011fdc6047be9c16d308245dff07eb1f7bd6d | D84PBE2 USB boot page | Retail boot 1.03 |
| BootFree runtime-trace image | dbb47afae091ab36f9abe74e32083013fbeff3d7e0516bbf5d1abf4ee57adc09 | Patched-base page | BootFree 11.259 |
The canonical image starts from ti84plus_patched.rom, whose SHA-256 is
90472848b5f56902287fd5d8b455e62d60e9ab054647c9a03c1c91a67fc1a95a.
D84PBE2.8Xv supplies page 0x2F; D84PBE1.8Xv supplies page 0x3F, although
that decoded retail page is byte-identical to the base image’s page 0x3F.
The exact AppVar and decoded-page identities are pinned in
tools/ti84re/rom/signatures.py. [confirmed]
The BootFree image matches the patched base on pages 0x00–0x3E. Only page
0x3F differs, with SHA-256
b3ae75aa81231de15e5931746d79834863132d5e4dca01010e3a8e24aabd3003.
Its acquisition artifact is not pinned, so this is a page-identity statement,
not a physical-capture provenance claim. [confirmed]
Use the retail image for boot-table, USB boot, reset, recovery, certificate,
and page-0x3F claims. A BootFree trace identifies execution on unchanged page
bytes, but the replacement boot can change the program’s initial state. Such a
trace supports an OS-path claim only when the required state and dependent
pages are also recorded. It cannot establish retail boot behavior.
BootFree callable surface and reset path
tools/data/boot-page-comparison.csv compares all 87 populated 0x8xxx
table entries. Every target address differs. BootFree maps 38 entries to other
bodies, 45 entries to a bare RET, and four entries to small
constant-return stubs. The six retail entries whose bodies live on USB boot
page 0x2F all map to BootFree’s bare-RET stub. [confirmed]
The reset paths also differ before any OS code runs: [confirmed]
| Step | Retail boot 1.03 | BootFree 11.259 |
|---|---|---|
| Reset stub | Writes ports 0x04, 0x06, and 0x0E, then jumps to 0x812C | Maps page 0x3F through ports 0x06 and 0x07, then jumps to 0x812C |
| Installed-OS test | Scans the keypad; DEL and STAT select recovery; otherwise tests byte 0x0038 and marker 0xA55A at 0x0056 | Tests only marker 0xA55A at 0x0056; it does not scan a recovery key |
| Missing or rejected OS | Enters serial or USB-assisted recovery and can receive an OS | Displays No OS Loaded and halts |
| Boot services | Certificate, validation, serial receive, USB receive, installer display, and error paths | Smaller Flash/certificate utility set; signature, receive, USB, and most installer-display entries are stubs |
The retail reset and installed-OS branches are at 3F:4000–3F:400C and
3F:420B–3F:4308. The corresponding BootFree branches are at
3F:4000–3F:4006 and 3F:412C–3F:41FC. [confirmed]
The CSV classifies every populated boot-table slot, not every internal helper entry in either page. A complete internal-routine comparison still requires matched function-entry recovery for both images. [confirmed]
Generate a manifest
tools/ti84re/rom/provenance.py records the complete ROM identity, target model,
hardware or emulator environment, ASIC revision, emulator profile, OS version,
boot-page classification, component page ranges, the 2007 include-file
identity, Ghidra version, Git revision, dirty-tree state, and a digest over the
top-level analysis scripts.
nix develop -c python3 -m ti84re.rom.provenance manifest \
--rom tools/rom.bin --model 'TI-84 Plus' --environment emulator \
--asic unknown --emulator-profile 'TilEm x4' \
--output /tmp/ti84p-provenance.json
The generated manifest reports an unknown component map for an unrecognized ROM rather than silently assigning it a known OS identity. [confirmed]
Reject stale results
Single-ROM CSV result tables use a rom_sha256 column. Dual-ROM tables record
both identities, and tables derived from external source or hardware record an
evidence identity instead. JSON reports use either a rom_sha256 field or a
rom.sha256 object. Verify a single-ROM artifact against the current manifest
before reuse:
nix develop -c python3 -m ti84re.rom.provenance verify \
--manifest /tmp/ti84p-provenance.json \
tools/data/launch-boundary-results.csv \
tools/data/resident-launch-snapshot.csv
The command rejects missing, mixed, or mismatched identities. Raw TLMT traces do not embed a ROM hash, so they require a JSON provenance sidecar; verify the sidecar rather than treating the trace filename as evidence. [confirmed]
Audit the Ghidra database
tools/ghidra/DatabaseHealth.java makes database coverage and cleanup debt
machine-readable. Run it against the existing project without analysis or
writes:
nix develop -c ghidra-analyzeHeadless "$PWD" ti84 \
-process -noanalysis -readOnly -scriptPath "$PWD/tools/ghidra" \
-postScript DatabaseHealth.java tools/data/database-health.json
The checked report identifies the BootFree runtime-trace image by its complete ROM hash. It records 64 loaded Flash pages, 27,995 instructions, 2,413 functions, and 94.081086 percent of instructions inside functions. The listing has no overlapping instructions. It also lists each of the 163 unresolved inline cross-page jumps and 45 primary symbols that have neither an instruction nor typed storage at their address. [confirmed]
The 989,125 undefined Flash bytes are addresses with no defined Ghidra code or data unit. That number measures database coverage; it does not imply that those bytes are unused or safe to overwrite. Likewise, an unresolved jump is a specific analysis task, not evidence that the ROM’s control flow is invalid. [confirmed]
The health report is deterministic for a given database, script revision, and
Ghidra version. Its rom_sha256 field can be checked with
tools/ti84re/rom/provenance.py verify; use a separately rebuilt retail project when
auditing retail boot and USB pages. [confirmed]
Community assembly archive snapshot
The community-source audit uses a 2026-08-24 mirror of the ticalc.org TI-83/84 Plus assembly archive. The archive was cloned outside this repository. This repository contains hashes and analysis tools, not the contributed programs. [confirmed]
tools/data/community-archive-inventory.csv records every mirrored ZIP path,
its SHA-256, compressed and expanded sizes, member count, source-member count,
and the number of paths with byte-identical archive contents. The snapshot has
2,770 ZIP paths, 2,649 unique archive identities, 19,323 members, and 4,241
members with recognized assembly or implementation-source suffixes. Of the
ZIPs, 897 contain at least one such member. [confirmed]
Run the same inventory and safe extraction process from the development environment:
nix develop -c python3 -m ti84re.community.archive \
"$COMMUNITY_ARCHIVE/mirror/pub/83plus/asm" \
--archive-csv tools/data/community-archive-inventory.csv \
--member-csv /tmp/community-archive-members.csv \
--extract-to "$COMMUNITY_ARCHIVE/extracted"
The extractor validates paths before writing, keeps each ZIP in a separate
directory, preserves duplicate member revisions under an explicit namespace,
and stores archived symbolic links as inert metadata. It uses the system
unzip only for compression methods that Python cannot decode, after rejecting
links and duplicate paths and before checking every resulting file against the
ZIP size and CRC records. The audit does not execute contributed host or
calculator binaries. [confirmed]
An archive description or readme is a community claim. Source establishes the instructions present in that member, but does not prove that the distributed calculator variable was assembled from it or that the code works on physical hardware. Cite both archive and member hashes for a source finding, and retain the wiki’s existing hardware or ROM evidence boundary. [confirmed]
Evidence limit
A matching hash proves byte identity, not how the image was obtained. The environment, ASIC, and emulator-profile fields keep physical and emulator evidence separate. They do not turn emulator behavior into hardware evidence. Git and script-tree identities make an analysis run reproducible; they do not by themselves validate the analysis conclusion.
Glossary
This glossary defines the TI-specific terms and key RAM symbols used throughout the wiki.
Core concepts
| Term | Meaning |
|---|---|
| bcall | “branch call” — the OS system-call mechanism: rst 28h + a 2-byte ID, dispatched through a jump table to a routine on any flash page. See The bcall Mechanism. |
| bjump | OS-internal cross-page jump: CALL cross_page_jump.dw addr.db page (a tail-jump). The sibling of bcall for the OS’s own use. |
| RST shortcut | A 1-byte rst NN vector that fast-paths a hot routine (rst 10h=_FindSym, rst 30h=_FPAdd, rst 28h=the bcall dispatcher). |
| context | The active “mode” (homescreen, Y= editor, graph, an app…). A block of handler vectors at cxMain (0x858D); the main loop runs the current context’s handlers. See Boot, Contexts & Errors. |
| paging / banking | The Z80 sees 64 KiB; ports 6/7 swap which 16 KiB flash/RAM page is visible in the two middle slots. See Paging. |
| APD | Auto Power Down — the standard-timer-driven idle shutoff. See Clock, timers, and power. |
| RTC | Real-time clock — a 32-bit seconds counter with an epoch of 1 January 1997, exposed through ports 0x40–0x48. |
| programmable timer | One of three independent source/mode/counter blocks at ports 0x30–0x38; distinct from the two standard interrupt timers. |
| MathPrint | The 2D “pretty-print” rendering of expressions; on this OS the engine is on page 0x39. |
Floating point
| Term | Meaning |
|---|---|
| BCD | Binary-Coded Decimal — numbers stored as decimal digits (2 per byte), the format of all TI floats. |
TIFloat | The 9-byte float: 1 type/sign byte, 1 biased exponent, 7 bytes = 14 BCD mantissa digits. See Floating-Point Engine. |
OP1–OP6 | The six 11-byte floating-point accumulator registers in RAM at 0x8478+. OP1 is the primary accumulator; binary ops use OP1+OP2, result in OP1. |
| FPS | Floating-Point Stack — a software stack (pointer at 0x9824) for spilling OP registers during nested evaluation. |
| guard digits | The 2 extra mantissa bytes past the 9-byte number (OP1EXT/OP2EXT), used for rounding during math. |
Variables and memory
| Term | Meaning |
|---|---|
| VAT | Variable Allocation Table — the RAM catalog of every named object, growing down from symTable (0xFE66). See Variables & the VAT. |
| object type | The 1-byte type tag of a variable (RealObj=0, ListObj=1, ProgObj=5, AppVarObj=0x15…), modeled as the TIVarType enum. |
| archive | Variables relocated to Flash to save RAM; the VAT entry’s page byte then points into Flash. See Variables, archive & unarchive and Flash memory. |
| Flash page | A 16 KiB ASIC paging unit selected through port 0x06. It is not necessarily an erase sector; ordinary sectors span four pages. See Flash memory. |
| Flash sector | The smallest physical region restored to 0xFF by one sector-erase operation. The one-megabyte top-boot chip uses 64 KiB ordinary sectors and 32/8/8/16 KiB sectors at the top. |
| garbage collection | Compacting the Flash archive in physical sector units. archive_gc_collect at 3C:7733 copies live records, erases reclaimed sectors, and journals its phase in the inactive half of page 3E. See Variables, archive and unarchive. |
| RAM heap | The dynamic region from userMem (0x9D95) up to the VAT; managed by _InsertMem/_DelMem. See Memory Management. |
Registers and RAM symbols
| Symbol | Addr | Meaning |
|---|---|---|
IY | (reg) | Held at flags (0x89F0) almost everywhere, so (IY+off) indexes the SystemFlags bitfield. |
flags | 0x89F0 | The IY-indexed system flag area (SystemFlags struct). |
OP1 | 0x8478 | Primary FP accumulator. |
FPS | 0x9824 | Floating-point stack pointer. |
onSP | 0x85BC | SP saved at context/parse start; _JError unwinds to it (try/catch). |
symTable | 0xFE66 | Top of RAM; the VAT grows down from here. |
kbdScanCode | 0x843F | Last keypad scan code (filled by the ISR, read by _GetCSC). |
plotSScreen | 0x9340 | The 768-byte graph/display buffer (96×64). |
parsePtr / parseEnd | 0x965D / 0x965F | The TI-BASIC parser’s token-stream cursor. |
Conventions
- Addresses: written
pp:addrwhereppis the flash page (00–3F) — e.g.3D:6745. Page 0 (the always-mapped kernel) is also writtenram:addrsince Ghidra keeps it in theramspace. A bare0x….with no page is a RAM/data address. See Conventions. - bcall IDs vs addresses: a bcall has both an ID (the 2-byte value after
rst 28h, e.g._FlashToRam=5017h) and a body address (3D:6745). The ID is not an address. - Confidence flags:
[confirmed](seen in disassembly),[standard](matches documented TI-83+/84+ behavior),[hypothesis](inferred). See Conventions. - Function names: official TI bcalls are
_CamelCase(_FindSym); RE-inferred names aresnake_case(findsym_scan).
Memory map
The Z80 sees a flat 64 KiB logical space divided into four 16 KiB windows.
Port 0x04 selects paired or independent mapping; ports 0x05–0x07 and
their extensions select the physical Flash or RAM pages. See
Paging for the complete mapper and RAM pages for
RAM page 83 and restore rules.
Logical address space (what the Z80 sees)
| Range | Slot | Contents | Notes |
|---|---|---|---|
0000-3FFF | Window 0 | Flash page 0 (fixed) | Boot/kernel: RST vectors, dispatcher, FP/VAT core. Never swapped. [confirmed] |
4000-7FFF | Window A | Port 0x06 in independent mode; even half of the port-0x06 pair in paired mode | Paged bcall targets run here after the dispatcher maps their page. [confirmed] |
8000-BFFF | Window B | Port 0x07 in independent mode; odd half of the port-0x06 pair in paired mode | Normally RAM page 81; boot executes page 3F here in paired mode. [confirmed] |
C000-FFFF | Window C | Port 0x05 RAM in independent mode; port 0x07 in paired mode | Normally RAM page 80; the stack lives near the top. [confirmed] |
In this OS the system RAM variables all live at 8000+, so the static RE model treats 8000-FFFF as one RAM block (see tools/ghidra/BuildTI84Full.java).
Flash layout (physical, 1 MiB = 64 × 16 KiB pages)
| Page(s) | Role | Evidence |
|---|---|---|
00 | Boot/kernel core, mapped at 0000 | RST vectors, bcall_dispatcher, FP/VAT/mem routines [confirmed] |
01 | OS routines (display, homescreen text, menus) | _PutC,_PutS,_ClrLCDFull,_NewLine resolve here [confirmed] |
06 | OS routines (key input, parser-ish) | _GetKey→06:491E [confirmed] |
2F | USB boot support page | validated local D84PBE2.8Xv supplies this page; retail page 3F maps _AttemptUSBOSReceive→2F:4145, _ReceiveOS_USB→2F:48CA, _InitUSB→2F:52A4, _KillUSB→2F:5961 [confirmed] |
3B | bcall jump table | highest-scoring page for the 0x4xxx bcall ID table; first entry _JErrorNo→00:2799 [confirmed] |
3C | Link code, archive GC, and OS version string ("2.55MP") | page starts 32 2E 35 35 4D 50; collector entry 3C:7733 [confirmed] |
3E | Two 8 KiB certificate sectors; the inactive half also carries the transactional GC journal | _GetCertificateStart (8057) and the GC command trace [confirmed] |
3F | Retail boot page | the patched base and validated local D84PBE1.8Xv contain the same page byte for byte; it starts 3E 07 D3 04 3E 7F D3 06 3E 03 D3 0E C3 2C 81, contains boot version string 1.03, and hosts the 0x8xxx boot bcall table [confirmed] |
Pages 01-3F are loaded in Ghidra as overlays page_01 … page_3F (each at 4000). Goto e.g. 01:5b4c for _PutC.
The assembled tools/rom.bin is the Ghidra build input. tools/ti84re/rom/assemble_local_rom.py
starts with ti84plus_patched.rom, validates the complete TI AppVar containers,
installs D84PBE2.8Xv as page 2F, and installs D84PBE1.8Xv as page 3F.
The first installation changes 8,615 bytes; the second changes none because the
base already has that exact page. The required SHA-256 identities are
90472848b5f56902287fd5d8b455e62d60e9ab054647c9a03c1c91a67fc1a95a
for the base and
7d9a7d96d89fc552ebee6afdbdd011fdc6047be9c16d308245dff07eb1f7bd6d
for the result. These checks establish reproducible analysis inputs, not a
physical-capture history. The page-2F and page-3F bodies above decode
directly from rom.bin. The resolver detects a BootFree input and omits retail
targets when the retail pages are absent.
Key named and typed RAM regions
| Addr | Name | Type | Purpose |
|---|---|---|---|
0x8478-0x84B9 | OP1–OP6 | TIFloat slot (9B body + 2B …EXT guard, 11B-spaced) | Floating-point accumulators [confirmed] |
0x89F0 | flags | SystemFlags (74B) | IY-indexed system flag bitfield [confirmed] |
0x844B/0x844C | curRow/curCol | byte | Homescreen text cursor (16 cols) [confirmed] |
0x8447 | contrast | byte | LCD contrast [confirmed] |
0x843F–0x8446 | kbdScanCode through keyExtend | 8 bytes | scan mailbox, release filter, repeat state, and cooked-key workspace; see Keypad and ON-key hardware [confirmed] |
0x8448–0x844A | apdSubTimer/apdTimer/curTime | 3 bytes | APD low/high countdown and cursor timer [confirmed] |
0x8259–0x82A1 | MD5 state | 73 bytes, with gaps | working words, bit length, compact length prefix, and digest; see MD5 accelerator and boot API [confirmed] |
0x83A5–0x83E4 | MD5Buffer | 64 bytes | partial message block or transformed-hash output [confirmed] |
0x9C0C–0x9C12 | timer API state | 7 bytes | programmable timer-1 state, durations, and expiry count [confirmed] |
0x9340 | plotSScreen | byte[768] | Graph/display buffer (96×64/8) [confirmed] |
0x86EC | saveSScreen | byte[768] | Saved screen buffer [confirmed] |
0x9824 | FPS | — | Floating-point stack pointer [standard] |
0x85BC | onSP | — | SP saved by ON-interrupt [confirmed] |
IY is held at flags (0x89F0) almost everywhere, so (IY+off) accesses index SystemFlags fields (appFlags, kbdFlags, …).
Principal input/output ports [standard]
A curated selection of the ports most relevant to the memory map and paging; the kernel touches many more (timer/crystal, USB-assist, and ASIC-control ports).
| Port | Name | Purpose |
|---|---|---|
00 | link | Active-high pull-low controls on write and physical high-line levels on read; see Two-wire link port hardware |
01 | keypad | Active-low matrix group select/read; see Keypad and ON-key hardware |
02 | hwStatus | Battery comparator, LCD-ready, Flash-lock, and family status; see ASIC status, identity, protection, and GPIO |
03 | intMask | Legacy interrupt enable/acknowledgement and low-power-on-HALT control; see Interrupts (IM1) |
04 | intStatus / memMapMode | Read = legacy pending state, ON level, and programmable completion; write = mapping mode, standard-timer rate, and battery selector; see Interrupts (IM1) |
05 | mapBankC | RAM selector for window C in independent mode |
06 | mapBankA | Flash/RAM selector for window A in independent mode or the A/B pair in paired mode |
07 | mapBankB | Flash/RAM selector for window B in independent mode or window C in paired mode |
08–0D | usb/link assist | 84+ hardware byte-assist control/status/data/FIFO ports; see USB ASIC and link assist |
0E/0F | mapBankAHigh/mapBankBHigh | High two Flash-page bits for ports 0x06/0x07; no page effect on this 64-page TI-84 Plus |
10/11 | lcdCmd/lcdData | LCD controller |
18–1F | MD5 assist | Six serial operand registers, rotate/mode control, and four result bytes; see MD5 accelerator and boot API |
20 | cpuSpeed | 0=6 MHz, 1=15 MHz (set in ISR) |
15 | asicIdentity | Public ASIC/RAM/USB revision value; this ROM has no immediate or statically resolved literal-C access; see ASIC status, identity, protection, and GPIO |
21 | flashGroup/ramExec | Protected writable Flash grouping and RAM-execution mode; the boot writes zero at 3F:41DC, and the kernel reads the low bits for model-specific page bounds; see ASIC status, identity, protection, and GPIO |
22–26 | execution bounds | Protected Flash-page and RAM-chunk bounds; see Execution protection |
27/28 | forced RAM overlays | 64-byte-granularity page-80/81 subranges; OS 2.55MP writes only zero, and paired-mode hardware behavior remains open |
2D | crystalControl | Quartz and programmable-timer behavior in low power |
29–2C | speedDelay | Speed-selected LCD instruction delays and Flash/RAM wait-state gates; see Bus timing and wait states |
2E | memoryDelay | Per-access Flash/RAM one-T-state additions; see Bus timing and wait states |
2F | lcdTimerAdjust | LCD-ready timing and programmable mode-3 prescaler; see Bus timing and wait states |
30–38 | programmable timers | Three source/mode/counter triplets; see Clock, timers, and power |
39/3A | gpioConfig/gpioData | Battery-comparison and USB GPIO configuration/data; exact electrical signals remain open; see ASIC status, identity, protection, and GPIO |
40–48 | RTC | Control, staged set value, and current 32-bit seconds count |
4D | usbLineState | USB line-state gate sampled by _GetVarCmdUSB (id 50FB; Ghidra alias link_xfer_op); bits 5/6 gate the ram:2E0B bjump to 35:4280 |
55/56 | usbIntStatus/usbLineEvents | USB interrupt state / line events (84+) — polled before the separate legacy controller; both read-only (port 0x56 is an event bitmap, not a write mask) |
Paging
The ASIC maps four 16 KiB logical windows into Flash and RAM. Port 0x04
selects one of two mapping modes, ports 0x05–0x07 hold the page selectors,
and ports 0x0E, 0x0F, 0x27, and 0x28 modify the selected pages or small
subranges.
This page distinguishes behavior executed by OS 2.55MP from public hardware
descriptions and emulator implementations. The ROM never gives ports 0x27
or 0x28 a nonzero value, so their physical behavior remains unconfirmed.
Evidence and scope
| Evidence | What it establishes | Confidence |
|---|---|---|
ROM bytes at 00:0000–029C, 3F:4000–4210, and the paged-RAM helpers | exact selector writes, boot transitions, and OS restore values | [confirmed] |
| Resolved TilEm boot and homescreen traces | executed page transitions and logical-to-physical page resolution | [confirmed] |
TilEm x4_io.c and x4_memory.c | one emulator’s paired mode, selector masks, forced overlays, and protection order | [standard] |
Wabbitemu 83psehw.c and core.c | an independent implementation, including extended Flash pages and different overlay rules | [standard] |
| Guarded Wabbitemu mapper run | initialized-core reset, selector readback, fixed-page handoff, paired mapping, and overlay routing | [standard] |
MAME 0.287 ti85.cpp and ti85_m.cpp | a third implementation’s bank arithmetic, reset latch, mapped I/O, and backing ranges | [standard] |
| Guarded MAME mapper run | fresh-reset latch qualifiers, selector masks, safe RAM banks, absent overlay ports, and read/write/fetch routing | [standard] |
| Public port descriptions | intended family-wide contracts for ports 0x04–0x07, 0x0E, 0x0F, 0x27, and 0x28 | [standard] |
The target ROM runs on a TI-84 Plus with 64 Flash pages and eight RAM page
selectors. Family members with more Flash need the extended selector bits in
ports 0x0E and 0x0F.
Page selection and execution permission are separate. The mapper can expose a RAM page in a CPU window while the execution-protection logic rejects an opcode fetch from the corresponding physical chunk. See Execution protection. [standard]
OS and analysis use
The bcall dispatcher maps a paged body through port 0x06, executes it in
window A, then restores the previous page. A bcall whose body resides on fixed
page 0 executes below 0x4000 without changing bank A. The page-set helper at
ram:181C implements the dispatcher write. [confirmed]
cross_page_jump at ram:2B09 consumes an inline address-and-page payload and
transfers control across Flash pages. Banked routines are position-fixed for
window A because their logical addresses remain in 0x4000–0x7FFF regardless
of the selected physical page. [confirmed]
Ghidra models physical Flash pages 1–63 as overlay blocks based at 0x4000.
This makes every banked body statically visible while preserving its logical
address. Runtime traces still need the active port-0x06 value to identify the
physical page. [confirmed]
The four logical windows
The Z80 supplies a 16-bit address. The top two address bits choose a logical window; the mapper supplies the physical page.
| Logical range | Window | Base selector in independent mode | Normal OS use |
|---|---|---|---|
0x0000–0x3FFF | 0 | fixed Flash page 00 | reset vectors, interrupts, and kernel code |
0x4000–0x7FFF | A | port 0x06, extended by 0x0E for Flash | paged Flash code or temporary banked RAM |
0x8000–0xBFFF | B | port 0x07, extended by 0x0F for Flash | normally RAM page 81 |
0xC000–0xFFFF | C | port 0x05 | normally RAM page 80 |
Window 0 stays fixed in both modes. [standard] The fixed page and the normal RAM values match the OS trace. [confirmed]
A physical page can appear in more than one window. During boot, Flash page
3F is visible through both A and B for part of the transition. Logical
addresses in those windows then alias the same physical bytes at different
offsets. [confirmed]
Mapper view. Window 0 remains fixed. The boot example is [confirmed]; the complete paired and independent register contract is [standard].
Mapping modes — port 0x04 bit 0
Writing port 0x04 changes the memory mode and the standard-timer rate. Its
bit 0 controls the mapper. Reading the same port returns interrupt and key
status, not the last mode byte. See Clock, timers, and power
for the other bits. [standard]
Independent mode — bit 0 clear
Each banked window has its own selector.
| Window | Source | Result |
|---|---|---|
A, 0x4000–0x7FFF | ports 0x06 and 0x0E | one Flash or RAM page |
B, 0x8000–0xBFFF | ports 0x07 and 0x0F | one Flash or RAM page |
C, 0xC000–0xFFFF | port 0x05 | one RAM page |
The OS normally writes 0x06 to port 0x04, selecting independent mode and
the slowest standard-timer rate. Its bcall dispatcher maps a target Flash page
through port 0x06. Paged-RAM helpers use ports 0x05 and 0x07 as a pair.
[confirmed]
Paired mode — bit 0 set
Port 0x06 supplies an adjacent page pair, and port 0x07 moves to window C.
Port 0x05 does not select a visible window in this mode. [standard]
| Window | Source | Page rule |
|---|---|---|
A, 0x4000–0x7FFF | port 0x06 | selected physical page with bit 0 cleared |
B, 0x8000–0xBFFF | port 0x06 | the adjacent page with bit 0 set |
C, 0xC000–0xFFFF | port 0x07 | the page selected by port 0x07 |
For example, the TI-84 Plus reset selector 0x3F produces Flash page 3E in
window A and page 3F in window B. The boot trace begins at logical 0x8000,
which therefore executes retail_boot_reset_stub at 3F:4000. [confirmed]
Changing bit 0 reinterprets all three banked windows at once. Code that changes the mode must execute from fixed page 0 or from a physical page visible at the next logical PC under both mappings. The boot transition uses the latter method. [confirmed]
Selector encoding
Ports 0x06 and 0x07
Bit 7 chooses the memory type. On the TI-84 Plus, these selector rules match
the trace, TilEm, and Wabbitemu: [confirmed] for executed OS values; [standard]
for the complete register contract. MAME matches the OS-used Flash values and
RAM selectors 0x80–0x86, but does not wrap higher RAM values.
| Selector | Physical page |
|---|---|
| bit 7 clear | Flash page, low six bits on this 64-page calculator |
| bit 7 set | RAM page `0x80 |
The hardware-facing 0x80–0x87 RAM notation distinguishes RAM selectors
from Flash page numbers. It does not mean that RAM has 128 pages. See
RAM pages for physical aliasing and OS use.
Port 0x05
Port 0x05 always selects RAM. On the TI-84 Plus, its low three bits select
RAM page 0x80 | (value & 7). The normal value 0x00 therefore maps RAM page
80 into window C in independent mode. [confirmed]
TilEm stores four low bits for port readback but uses only three in its page calculation. Wabbitemu reduces the low seven bits by the model’s RAM page count. MAME stores the low three bits. The arithmetic in all three selects the same eight page numbers, although MAME’s backing range omits the last page as described below. [standard]
Extended Flash bits — ports 0x0E and 0x0F
Public descriptions and Wabbitemu model these registers as two high Flash-page
bits. Port 0x0E extends port 0x06; port 0x0F extends port 0x07: [standard]
$$ P_A = ((p_6 \bmod 128) + 128(p_{0E} \bmod 4)) \bmod N_F $$
$$ P_B = ((p_7 \bmod 128) + 128(p_{0F} \bmod 4)) \bmod N_F $$
Here $N_F$ is the number of physical Flash pages. The high registers apply only when bit 7 of the corresponding low selector is clear. A RAM selector ignores them. [standard]
For the 64-page TI-84 Plus, every contribution from ports 0x0E and 0x0F
is a multiple of 128 and disappears after the page mask. The boot ROM still
writes 3 to each register while selecting its highest boot page, then clears
both during normal mapper initialization. [confirmed]
TilEm’s TI-84 Plus mapper stores and reads the low two bits but does not feed
them into its 64-page calculation. Wabbitemu uses the family-wide formula and
masks the result by the configured Flash size. These implementations agree for
this target. MAME does not map ports 0x0E or 0x0F; writes reach no handler.
Its TI-84 Plus port-0x06 and port-0x07 handlers instead truncate every
Flash selector below 0x80 to six bits. [standard]
Emulator mapper comparison
The source-level comparison below describes the pinned implementations, not the ASIC. Agreement is useful corroboration of an intended rule; disagreement is a test target rather than a vote. [standard]
| Detail | TilEm | Wabbitemu | MAME 0.287 | jsTIfied 20170706a |
|---|---|---|---|---|
| Mapper ports | 0x04–0x07, 0x0E, 0x0F, 0x27, 0x28 | same | only 0x04–0x07 | 0x04–0x07, 0x0E, 0x0F, 0x27, 0x28 |
| Declared driver status | usable mapper | usable mapper | MACHINE_NOT_WORKING | browser emulator source model |
port 0x05 write | stores low four bits; maps low three | reduces low seven bits by RAM-page count | stores low three bits | selects window C on TI-84 Plus |
| TI-84 Plus Flash selector | low six bits | extended formula, then Flash-size mask | low six bits for values below 0x80 | low selector plus ports 0x0E/0x0F extensions |
| RAM selector | low three bits | low bits masked by RAM-page count | raw value 0x80–0xFF becomes the bank number | 0x80 flag plus low three-bit page |
| paired A | port-0x06 page with bit 0 clear | same | same | even member selected from port 0x06 |
| paired B | port-0x06 page with bit 0 set | see expression bug below | port-0x06 page with bit 0 set | adjacent odd member |
| paired C | port-0x07 page | same | same | port-0x07 page |
paired reads from 0x05–0x07 | stored register values | active C/A/B page values | stored register values | stored selector state |
| forced-RAM overlays | both modes | independent mode only | absent | implemented |
Wabbitemu’s reads are therefore not register snapshots in paired mode. Port
0x06 reads visible A, port 0x07 reads visible B, and port 0x05 reads the
physical page visible in C without a RAM-type bit. A routine that writes an
even page to port 0x06 and reads port 0x07 receives that duplicated even
page under the pinned implementation. TilEm and MAME instead return their
stored selector bytes from ports 0x05–0x07. [standard]
Wabbitemu’s paired-B expression
Wabbitemu’s update_bootmap_pages intends to construct the second member of
the pair, but the pinned source assigns it with: [standard]
page = normal_page | (!flash_version == 1);
Every supported Plus-family initializer gives flash_version a nonzero value.
C operator precedence therefore evaluates !flash_version first, producing
zero, and then compares zero with one, again producing zero. The expression is
effectively page | 0. An odd port-0x06 selection still produces the usual
even/odd pair because A clears its low bit and B preserves the already-set bit.
An even selection duplicates the even page into both A and B. TilEm and MAME
instead produce the adjacent odd B page. [standard]
This is a Wabbitemu implementation result, not evidence that the ASIC duplicates even pages. [hypothesis] for physical behavior until a hardware test exercises an even selector in paired mode.
MAME’s raw RAM banks and short backing range
For TI-84 Plus Flash values below 0x80, MAME stores value & 0x3F. For RAM
values at or above 0x80, it stores the complete byte and passes that byte
directly to the address-map bank. It does not reduce the selector modulo eight.
Port 0x05 is the exception because its handler stores value & 7.
[standard]
The TI-84 Plus banked map provides Flash at offsets 0x000000–0x0FFFFF and
RAM at 0x200000–0x21BFFF. The latter is seven, not eight, 16 KiB pages.
Selectors 0x80–0x86 reach RAM; selector 0x87, port-0x05 = 7, and every
higher raw RAM selector land outside the mapped backing range. This boundary
follows directly from the MAME address map and is an emulator defect, not a
claim that TI-84 Plus RAM page 87 is absent. [standard]
Reset entry and fixed-page handoff
The three emulators do not begin at the same logical address: [standard]
| Implementation | Reset PC | Initial visible pages 0/A/B/C | Fixed-page handoff |
|---|---|---|---|
| TilEm | 0x8000 | Flash 00/3E/3F/3F | none; page 0 is already fixed |
| Wabbitemu | 0x0000 | Flash 3F/00/00, RAM 80 | first qualifying opcode fetch in A, or B while paired, changes fixed page 3F to 00 |
| MAME 0.287 | 0x0000 | Flash 3F/00/01/00 | a read from A, or from B while paired, clears the boot latch before returning the byte |
MAME’s reset initializes selectors 0x05–0x07 to zero, port 0x04 to one,
and m_booting to true. Its fixed window consequently starts on page 3F,
while paired A/B are pages 00/01 and C is Flash page 00. The handoff is
implemented in read handlers, despite a comment saying it should apply only to
opcode fetches. [standard]
The pinned MAME source also swaps model constants in two machine-start
functions: the ti83pse start assigns TI84PSE, and ti84pse assigns
TI83PSE. The ti84p start correctly assigns TI84P, so the six-bit selector
mask described here does execute for this article’s target. The swapped names
still make cross-model inferences from this driver unsafe without checking the
actual machine configuration and m_model branch. MAME registers the TI-84
Plus driver with MACHINE_NOT_WORKING; this comparison treats its source as an
implementation oracle, not a fidelity endorsement. [standard]
Boot mapping transition
The retail boot page contains retail_boot_reset_stub at 3F:4000. Under
TilEm’s reset mapping it executes at logical 0x8000: [confirmed]
3F:4000 LD A,0x07
3F:4002 OUT (0x04),A ; paired mode
3F:4004 LD A,0x7F
3F:4006 OUT (0x06),A ; A=page 3E, B=page 3F
3F:4008 LD A,0x03
3F:400A OUT (0x0E),A
3F:400C JP 0x812C ; continue on page 3F in window B
Page 0 also contains a restart path at 00:0000 → 00:028C. It tests port
0x02 bit 7, writes either 0x1F or 0x03 to port 0x0E, writes 0x7F to
port 0x06, selects paired mode, and jumps to the same logical 0x812C.
Both values written to 0x0E have low two bits equal to three. [confirmed]
boot_os_entry at 3F:412C changes to independent mode without paging out its
next instruction: [confirmed]
3F:412C IM 1
; stack/RAM probe omitted
3F:4142 LD A,0x03
3F:4144 OUT (0x0F),A
3F:4146 LD A,0x7F
3F:4148 OUT (0x07),A ; C=page 3F while still paired
3F:414A LD A,0x06
3F:414C OUT (0x04),A ; independent: A=3F, B=3F, C=RAM 80
3F:414E JP 0x4151 ; page 3F remains visible in window A
Boot then maps 0x81 through port 0x07, probes writable RAM at 0xC000 and
0x8000, and programs the execution-protection registers. At 3F:4208 it
clears ports 0x0E, 0x0F, and 0x05, writes page 0x3F to port 0x06,
and later maps RAM through port 0x07. [confirmed]
The resolved trace records the complete transition:
OUT (0x04) <- 07 paired mode
OUT (0x06) <- 7F A=page 3E, B=page 3F
OUT (0x0E) <- 03
OUT (0x0F) <- 03
OUT (0x07) <- 7F C=page 3F
OUT (0x04) <- 06 independent mode
OUT (0x07) <- 81 B=RAM page 81
OUT (0x0E) <- 00
OUT (0x0F) <- 00
OUT (0x05) <- 00 C=RAM page 80
OUT (0x06) <- 3F A=Flash page 3F
OUT (0x07) <- 80 B=RAM page 80
This sequence shows why a mode change cannot be modeled as three independent
port assignments. The meaning of the already-written 0x06 and 0x07
selectors changes when port 0x04 bit 0 changes.
Why RAM helpers clear port 0x0F
Several OS helpers clear port 0x0F immediately before selecting RAM through
port 0x07. Representative sites include ram:0B78, 05:5B66, 2F:45A9,
36:74F8, 37:44CA, and 38:7782. They then compute a page pair:
[confirmed]
XOR A
OUT (0x0F),A
LD A,B
SLA A
OUT (0x05),A ; even RAM page in window C
INC A
OR 0x80
OUT (0x07),A ; odd RAM page in window B
Bit 7 of the port-0x07 value is set, so the extended Flash bits do not
participate in the resulting RAM page under the public contract, TilEm, or
Wabbitemu. MAME has no extended selectors to clear. The clear may be defensive
state normalization or compatibility with another ASIC revision. The ROM does
not establish that it is required. [hypothesis]
Forced RAM subranges — ports 0x27 and 0x28
The public descriptions and TilEm implement two 64-byte-granularity overlays. For a byte value $n$: [standard]
| Port | Forced logical range | Physical page |
|---|---|---|
0x27 | 0x10000 - 64n through 0xFFFF | RAM page 80 |
0x28 | 0x8000 through 0x8000 + 64n - 1 | RAM page 81 |
A zero value disables the corresponding overlay. The maximum byte value,
0xFF, leaves one 64-byte block of its 16 KiB window outside the forced range.
OS 2.55MP writes zero to both ports in mode_default_init at 37:6D3A and
37:6D3E. It also clears port 0x27 before RAM-bank tests at 37:72D3 and
3F:45F5. No immediate or dynamic write in the analyzed ROM gives either port
a nonzero value. These writes confirm that the OS disables the feature, but
they do not confirm the nonzero mapping formula. [confirmed]
Emulator disagreement
The three emulator implementations disagree at the point most useful for a hardware test: [standard]
| Detail | TilEm | Wabbitemu | MAME 0.287 |
|---|---|---|---|
| Overlay active in paired mode | yes | no; checks !boot_mapped | no overlay model |
Port 0x28 range | complete formula above | complete formula above in independent mode | port unmapped |
Port 0x27 range | complete formula above | also requires the logical address to be at least 0xFB64 | port unmapped |
| Read, write, and instruction fetch | all resolve through the overlay | data reads/writes use the overlay; execution checks retain some underlying-bank logic | underlying bank only |
WikiTI’s historical description says these ports have no effect in paired mode, which agrees with Wabbitemu and disagrees with TilEm. None of these software sources proves the ASIC behavior. Whether the overlays operate in physical paired mode remains a hypothesis. [hypothesis]
Native Wabbitemu mapper edges
A guarded initialized-core run invokes the eight registered mapper handlers
and the pinned core’s memory paths directly. Reset reads are 0x08 from port
0x04 and zero from ports 0x05–0x07, 0x0E, 0x0F, 0x27, and
0x28. The 0x08 is interrupt status, not the mapping mode. The visible
reset windows are Flash 3F/00/00 and RAM 80, with independent mode
active and hasChangedPage0 clear. [standard]
A data read at 0x4000 leaves fixed page 3F and the handoff flag unchanged.
Executing a NOP from the same address changes fixed page 3F to 00, sets
hasChangedPage0, and advances the PC to 0x4001. This confirms that
Wabbitemu’s fixed-page handoff is an opcode-fetch effect. [standard]
Writing 0xFF to ports 0x0E and 0x0F reads back 0x03. With those high
fields set, a raw Flash selector of 0x7F remains stored internally but ports
0x06 and 0x07 read the visible 64-page result 0x3F. RAM selectors
0xFF and 0xFE remain stored while the visible windows read 0x87 and
0x86. Port 0x05 = 0xFF maps and reads RAM page 7 as 0x07. [standard]
The paired-mode case writes C/A/B selectors 0x05, 0x02, and 0x83.
Ports 0x05–0x07 then read 0x03, 0x02, and 0x02; the visible windows
are Flash page 2, duplicate Flash page 2, and RAM page 3. Port 0x04 still
reads interrupt status 0x08. This exercises the paired-B expression through
the registered port handlers. [standard]
Directly seeded backing bytes isolate the two forced ranges. In independent
mode with 0x28 = 1 and 0x27 = 0xFF, reads at 0x8000, 0x803F, and
0x8040 return markers 0xB0 and 0xB1 from RAM page 1, then underlying
Flash marker 0xA2. Reads at 0xFB63 and 0xFB64 return marker 0xC3 from
underlying RAM page 5 and marker 0xD4 from forced RAM page 0. Low-level
writes change RAM pages 1 and 0 while leaving the two underlying windows
unchanged. A NOP in forced RAM over an underlying Flash HALT executes the NOP.
[standard]
Switching only to paired mode changes those five reads to underlying markers
0xE0, 0xE1, 0xE2, 0xF3, and 0xF4. Low-level writes modify the
underlying Flash pages while both forced RAM markers remain unchanged. The
same NOP/HALT discriminator executes the underlying HALT. These writes call
Wabbitemu’s low-level mapper function; they test address routing, not Flash
command acceptance. The run establishes emulator behavior only. [standard]
Native MAME mapper edges
A guarded MAME 0.287 run uses five fresh processes so each handoff case starts
with m_booting set. The untouched process reports PC = 0x0000, port reads
08 00 00 00 for 0x04–0x07, and visible Flash prefixes 3E 07, DB 02,
44 6F, and DB 02 in windows 0, A, B, and C. These prefixes identify pages
3F, 00, 01, and 00 in the exact OS 2.55MP image. [standard]
Reading A through Lua’s CPU program space changes the window-0 prefix from
page 3F’s 3E 07 to page 00’s DB 02. This MAME Lua access carries read
side effects; it is not a side-effect-free debugger peek. Three tiny programs
in RAM then perform actual Z80 data reads in separate processes. An independent
B read returns page-02 byte 0E and leaves page 3F fixed. An A read returns
page-01 byte 44 and fixes page 00. A paired B read returns page-03 byte
02 and also fixes page 00. Each program reaches the expected HALT at
0xC008. [standard]
The selector cases exercise only mapped backing. Flash write 0x41 reads back
01 and exposes page 01; 0x7F reads back 3F and exposes page 3F. RAM
selectors 0x80, 0x85, and 0x86 reach seeded pages 0, 5, and 6. Port
0x05 = 0xFE reads back 06 and reaches RAM page 6. In paired mode, port
0x06 = 0x02 exposes adjacent Flash pages 02 and 03, while port
0x07 = 0x83 maps RAM page 3 into C. The probe omits selector 0x87; the
pinned address map places that bank beyond its seven mapped RAM pages.
[standard]
Ports 0x0E, 0x0F, 0x27, and 0x28 return zero before and after
patterned writes. With underlying RAM pages 2 and 3 in B and C, writes at
0x8000 and 0xFB64 change those pages while seeded candidate overlay bytes
in RAM pages 1 and 0 remain unchanged. A fetched discriminator executes marker
22 from underlying RAM page 2 rather than marker 11 from candidate overlay
page 1. The run therefore covers MAME read, write, and instruction-fetch
routing without treating the missing overlay as ASIC evidence. [standard]
Interaction with execution protection
Ports 0x21–0x26 control Flash and RAM execution permissions. They do not
select pages; see Execution protection for their
write gate, equations, and boundary discrepancies.
TilEm first resolves a port-0x27 or port-0x28 overlay, then applies the
execution rule for the resulting physical RAM page. A fetch forced from a
Flash-backed window into RAM is therefore checked as RAM in TilEm. [standard]
Wabbitemu’s fetch check chooses its Flash-versus-RAM branch from the underlying window before adjusting a RAM address for the overlay. A forced RAM range over an underlying Flash page can therefore follow different protection logic from TilEm. This is an emulator fidelity difference, not evidence for either ASIC ordering. [standard]
The analyzed OS leaves both overlays disabled before normal execution, so this difference does not affect the traced boot and homescreen paths. [confirmed]
Safe mapper use
- Preserve every selector that the routine changes. Ports
0x05,0x06,0x07,0x0E,0x0F,0x27, and0x28have readable state in the public contract, TilEm, and Wabbitemu. MAME does not implement the latter four. [standard] - Do not use
IN A,(0x04)to save the memory mode. Reads return interrupt status. Code entered under TI-OS can rely on its documented normal independent mode or must receive the mode from its caller. [standard] - Disable interrupts around a temporary mapping unless the interrupt path can
run with that mapping. The OS page-
83helpers preserve interrupt state for this reason. [confirmed] - When changing paired mode, ensure the next instruction remains mapped. Fixed page 0 is the least state-dependent place to perform the transition. [standard]
- On family members with more than 128 Flash pages, preserve the matching high
selector with each low Flash selector. Restoring only port
0x06or0x07can restore the wrong physical page. [standard] - Restore normal TI-84 Plus RAM windows with port
0x07 = 0x81and port0x05 = 0x00when the caller follows the ordinary OS convention. For a general library, restore the values read on entry instead. See RAM pages.
Reproducing the mapping
tools/ti84re/hardware/memory_mapper.py contains explicit documented, tilem, wabbitemu,
and mame profiles. tools/ti84re/hardware/describe_memory_mapping.py applies writes and
reads, compares profiles, and can emit JSON. List the pinned coverage first:
nix develop -c python3 -m ti84re.hardware.describe_memory_mapping profiles
This reproduces the final TilEm boot state shown above:
nix develop -c python3 -m ti84re.hardware.describe_memory_mapping \
map --profile tilem \
--write 0x0e=3 --write 6=0x7f \
--write 0x0f=3 --write 7=0x7f --write 4=6 \
--write 7=0x81 --write 0x0e=0 --write 0x0f=0 \
--write 5=0 --write 6=0x3f --write 7=0x80
An even paired selector exposes Wabbitemu’s duplicated B page while also showing MAME’s ignored high-selector and overlay writes:
nix develop -c python3 -m ti84re.hardware.describe_memory_mapping compare \
--write 4=1 --write 6=2 \
--write 0x0e=3 --write 0x27=0xff --write 0x28=1
To reproduce MAME’s fixed-page read latch and machine-read the result:
nix develop -c python3 -m ti84re.hardware.describe_memory_mapping --json \
map --profile mame --read 0x4000
The trace resolver uses the same library. To show the executed boot writes:
nix develop -c python3 -m ti84re.trace.resolve /tmp/boot.trace \
--initial-mapping ti84p-reset --page-switches \
--io-ports 04-07,0e-0f,27-28
Static whole-ROM port scans are candidate generators because data can decode as instructions. Add context and verify control flow before treating a hit as code:
nix develop -c python3 -m ti84re.rom.analyze_io \
--before 8 --after 8 0x0e-0x0f,0x27-0x28
tools/ti84re/emulators/wabbitemu/mapper_probe.py derives the native edge expectations from
the same mapper profile. tools/ti84re/emulators/wabbitemu/run_mapper_edge_probe.py requires
the exact OS 2.55MP ROM and writes a hash-complete JSON manifest.
tools/ti84re/emulators/mame/mapper.py derives the corresponding MAME oracle from the reusable
profile and pinned ROM prefixes. The guarded CLI runs every latch case from a
fresh machine:
mame_mapper_parent=$(mktemp -d /tmp/ti84-mame-mapper.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_mapper_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_mapper_parent/run" --json
Open physical tests
- Write nonzero values to ports
0x27and0x28in independent mode and test both range boundaries with reads and writes. - Repeat the boundary tests in paired mode to distinguish TilEm from Wabbitemu and the historical WikiTI claim.
- Execute controlled code on each side of an overlay boundary while varying
ports
0x21–0x26; record whether protection follows the underlying window or the forced RAM page. - Test port
0x27below0xFB64to determine whether Wabbitemu’s additional cutoff models hardware or is an emulator-specific restriction. - Read ports
0x0Eand0x0Fafter writing values with upper bits set on TA2, TA3, and larger-Flash family members. - Select an even Flash page through port
0x06in paired mode and verify that window B exposes the adjacent odd page rather than Wabbitemu’s duplicate. - Select RAM page
87through ports0x05,0x06, and0x07; this confirms the physical page independently of MAME’s seven-page backing-map defect.
Sources
| Source | Use |
|---|---|
OS 2.55MP rom.bin, especially 00:0000–029C, 3F:4000–4210, 37:44AE, and 37:6D33 | executed selector sequences and reset values |
TilEm x4_io.c and x4_memory.c | mapping modes, 64-page masks, overlays, and protection order |
Wabbitemu 83psehw.c and core.c | extended selectors, paired mode, overlays, and independent comparison |
MAME 0.287 ti85.cpp and ti85_m.cpp | mapped ports, bank backing, selector writes, reset mapping, and read-latch behavior |
jsTIfied deployed 20170706a artifact and readable mirror | fourth source implementation of selector, paired-mode, and overlay routing; the deployed artifact is pinned by tools/ti84re/emulators/jstified.py |
WikiTI port 0x04, 0x0E, 0x0F, 0x27, and 0x28 | historical public register descriptions checked against ROM and emulators |
Bus timing and wait states
The TI-84 Plus ASIC inserts programmable T-states into Flash, RAM, and LCD
transactions. CPU-speed mode selects one of four LCD-delay registers, which
also gates the memory waits in port 0x2E; port 0x2F separately controls the
high-speed LCD-ready interval and a documented programmable-timer prescaler.
OS 2.55MP programs this block once during retail boot. The ROM bytes and trace pin the register values. Public measurements, TilEm, and Wabbitemu supply the detailed timing contract; MAME provides an explicit omission comparison.
Evidence and limits
| Evidence | What it establishes | Confidence |
|---|---|---|
boot_bus_timing_init at 3F:41BD–41D3 | exact OS values for ports 0x29–0x2C, 0x2E, and 0x2F | [confirmed] |
| Resolved cold-boot trace | all six writes execute in order before normal CPU speed is selected | [confirmed] |
| Whole-ROM immediate-port scan | no second control-flow-verified write to these registers in the analyzed ROM | [confirmed] |
| TilEm and Wabbitemu source | independent decode of speed selection, LCD instruction delays, and memory wait bits | [standard] |
| Native Wabbitemu execution | reset state, speed masks and frequencies, all seven delay latches, wait-gate selection, and port-0x2D side effects | [standard] |
| MAME 0.287 source | binary CPU-speed selection and absence of the delay-register block | [standard] |
| Guarded MAME ASIC-control run | raw speed readback, measured 6:15 instruction throughput, absent delay ports, and soft-reset retention | [standard] |
| Public hardware tests | intended bit meanings, LCD failure thresholds, and mode-3 timer divisor | [standard] |
The trace proves what the ROM writes and which CPU-speed values execute. It does not measure the ASIC bus electrically. Emulator-added clock counts are reported as emulator behavior, not physical measurements.
Register block
| Port | Role | Selected by |
|---|---|---|
0x20 | CPU-speed mode | software; low two bits form mode 0–3 |
0x29 | LCD instruction delay and memory-wait gates for speed mode 0 | port 0x20 & 3 = 0 |
0x2A | same controls for speed mode 1 | port 0x20 & 3 = 1 |
0x2B | same controls for speed mode 2 | port 0x20 & 3 = 2 |
0x2C | same controls for speed mode 3 | port 0x20 & 3 = 3 |
0x2D | quartz and low-power control | independent block; see Clock, timers, and power |
0x2E | one-T-state Flash and RAM access selectors | gated by bits 0–1 of the active 0x29–0x2C register |
0x2F | high-speed LCD-ready interval and documented mode-3 timer prescaler | field selected by port 0x20 |
TilEm reads back the last byte written to ports 0x29–0x2C, 0x2E, and
0x2F. Wabbitemu registers one generic latch handler across the complete
0x29–0x2F range. Its port 0x2D therefore stores a raw byte instead of
implementing the separate low-power contract. MAME maps only port 0x20 from
this block; its I/O map has no handler for the six delay-register writes.
[standard]
Selection pipeline
Let $s = p_{20} \bmod 4$. The active speed-dependent register is [standard]
$$ D_s = p_{29+s} $$
That one byte controls two independent effects:
- bits 2–7 select the T-states added to each LCD-port instruction;
- bit 0 enables the Flash bits in port
0x2E, and bit 1 enables the RAM bits.
Timing model. The register behavior is [standard], and the diagram’s widths are conceptual. OS 2.55MP’s register bytes and executed speed values are [confirmed].
Changing port 0x20 changes the active byte immediately in TilEm and
Wabbitemu. The OS does not need to rewrite ports 0x29–0x2C when it moves
between 6 and 15 MHz. Both emulators recompute memory delays on an accepted
speed write. [standard]
Under the public TI-84 Plus contract and TilEm, speed value 0 selects nominal
6 MHz and values 1–3 select nominal 15 MHz. Wabbitemu’s default TI-84 Plus
context clamps writes 2–3 to mode 1. Its external extraSpeed option instead
assigns 20 and 25 MHz to modes 2 and 3. MAME stores the raw byte and selects
15 MHz for every nonzero value. These are software policies, not evidence for
extra physical TI-84 Plus clocks. OS 2.55MP uses only values 0 and 1 in the
captured workflows. See
Clock, timers, and power for frequency and
ASIC-revision caveats. [confirmed] for executed values; [standard] for public
and emulator behavior.
A guarded initialized-core run writes 0xFC–0xFF. The reset
timer_version = 0 context reads back modes 0/1/1/1 at 6, 15, 15, and
15 MHz. Directly setting timer_version = 1 produces modes 0/1/2/3 at 6,
15, 20, and 25 MHz. The direct setting represents Wabbitemu front-end state;
it is not a calculator port transition. [standard]
A guarded MAME run writes 00, 01, 02, 03, and FF to port 0x20 and
reads the same five raw bytes back. A 50-T-state RAM counter advances 12,000
times during five 20 ms frames after write zero and 30,000 times after write
one. The exact 2.5 ratio dynamically distinguishes 6 MHz from 15 MHz. A MAME
soft reset retains raw speed 0x03; this is driver behavior, not a calculator
reset claim. [standard]
Boot configuration
boot_bus_timing_init at 3F:41BD writes the complete block after the retail
boot RAM probes and link-assist initialization: [confirmed]
3F:41BD LD A,0x17
3F:41BF OUT (0x29),A
3F:41C1 LD A,0x27
3F:41C3 OUT (0x2A),A
3F:41C5 LD A,0x2F
3F:41C7 OUT (0x2B),A
3F:41C9 LD A,0x3B
3F:41CB OUT (0x2C),A
3F:41CD LD A,0x45
3F:41CF OUT (0x2E),A
3F:41D1 LD A,0x4B
3F:41D3 OUT (0x2F),A
The resolved trace executes the writes at clocks 1,747,536 through 1,747,628.
Port 0x20 still reads zero at 3F:653E. The OS later writes zero at
ram:0DD5 and selects speed mode 1 at ram:0C72. [confirmed]
TilEm resets its internal registers to the older values 0x14, 0x27,
0x2F, 0x3B, 0x44, and 0x4A. The retail boot writes above replace every
differing value before normal OS operation. Emulator reset defaults therefore
must not be mistaken for TI-84 Plus OS policy. [standard]
MAME accepts the later port-0x20 speed write but drops all six boot writes to
ports 0x29–0x2C, 0x2E, and 0x2F. It therefore runs the ROM at the
selected base clock without the programmable LCD or memory additions described
below. A native patterned-write run reads zero from every port 0x29–0x2F
both before and after the writes. [standard]
LCD instruction delay — ports 0x29–0x2C
TilEm and Wabbitemu add [standard]
$$ T_{\mathrm{LCD}} = D_s >> 2 $$
T-states to each Z80 IN or OUT instruction targeting LCD ports 0x10–0x13.
The two low bits do not contribute to this count; they gate memory waits.
The OS bytes decode as follows:
| Speed mode | Active port | OS byte | Added LCD T-states | Low-bit gates |
|---|---|---|---|---|
| 0 | 0x29 | 0x17 | 5 | Flash and RAM |
| 1 | 0x2A | 0x27 | 9 | Flash and RAM |
| 2 | 0x2B | 0x2F | 11 | Flash and RAM |
| 3 | 0x2C | 0x3B | 14 | Flash and RAM |
At nominal 6 MHz, five T-states are about 0.833 µs. At nominal 15 MHz, nine T-states are 0.6 µs. These are additions to the Z80 I/O instruction, not the complete interval between two LCD transfers. [standard]
Published hardware tests report that values below 0x0C can make LCD writes
stop responding, while read behavior has a different lower boundary. The
exact threshold and failure mode should be remeasured by controller and ASIC
revision. [standard] for the published observation; [hypothesis] for
cross-revision behavior.
Memory waits — port 0x2E
Each selected bit adds one T-state to one memory-access class. Bits 0–2 apply only when active register $D_s$ has bit 0 set. Bits 4–6 apply only when $D_s$ has bit 1 set. [standard]
Port-0x2E bit | Memory | Access class | Emulator placement |
|---|---|---|---|
| 0 | Flash | opcode/M1 fetch | each fetched opcode or prefix byte |
| 1 | Flash | non-opcode read | operands, data, and stack reads |
| 2 | Flash | attempted write | every CPU write routed to Flash |
| 3 | — | unused by the documented delay block | stored on readback |
| 4 | RAM | opcode/M1 fetch | each fetched opcode or prefix byte |
| 5 | RAM | non-opcode read | operands, data, and stack reads |
| 6 | RAM | write | every CPU write routed to RAM |
| 7 | — | unused by the documented delay block | stored on readback |
The boot value 0x45 sets bits 6, 2, and 0. Because all four active-register
bytes have gate bits 0 and 1 set, the OS policy in every speed mode is:
[confirmed] for register values; [standard] for the access decode.
| Access | Flash addition | RAM addition |
|---|---|---|
| opcode/M1 fetch | 1 T-state | 0 |
| non-opcode read | 0 | 0 |
| write | 1 T-state | 1 T-state |
A CB-, ED-, DD-, or FD-prefixed Z80 instruction performs two M1 fetches. Both TilEm and Wabbitemu apply the opcode wait to the prefix and following opcode. A repeated DD or FD prefix adds another M1 fetch in both cores. [standard] for the Z80 bus cycle and pinned emulator source paths.
Indexed CB instructions expose a model difference. The Z80 fetches DD or FD
and CB with M1 signaling, then reads the displacement and final opcode without
M1. TilEm follows this split in z80main.h:674 and z80ddfd.h:301.
Wabbitemu routes the final opcode through CPU_opcode_fetch at core/core.c:832,
then decrements R at line 836. Its wait model therefore counts three opcode
waits while its visible refresh count remains two. The hash-guarded
ti84re.emulators.describe_prefix_fetch_models CLI reproduces this result from the pinned
source trees. The exact assembled HWPFX program also reproduces the split in
the pinned Wabbitemu runtime: its indexed-CB row adds 30 timer ticks, compared
with 25 for the three ordinary one-prefix rows and 29 for repeated DD.
[confirmed] for the emulator run; physical ASIC placement remains open.
The delay follows the physical page selected by the mapper in TilEm. An opcode executed from banked RAM uses the RAM M1 bit; the same logical address backed by Flash uses the Flash bit. See Paging for physical-page resolution. [standard]
One T-state is about 0.167 µs at nominal 6 MHz and 0.067 µs at nominal 15 MHz. The register therefore preserves a cycle margin, not a fixed wall-time margin, when CPU speed changes. [standard]
LCD-ready interval — port 0x2F
The LCD instruction delay above slows the I/O instruction itself. Port 0x2F
controls a second mechanism: at high speed, the ASIC deasserts port-0x02 bit
1 for a longer programmable interval after an LCD transaction. The OS
lcd_wait helper polls that bit before accessing the controller. See
LCD controller and display bus.
[standard]
Speed mode selects one field: [standard]
| Speed mode | Field | Width |
|---|---|---|
| 0 | no high-speed ready hold | — |
| 1 | bits 0–1 | 2 bits |
| 2 | bits 2–4 | 3 bits |
| 3 | bits 5–7 | 3 bits |
For a selected field $f$, TilEm and Wabbitemu use [standard]
$$ T_{ready} = 48 + 64f $$
The boot value 0x4B produces:
| Speed mode | Field value | Ready hold | Nominal interpretation |
|---|---|---|---|
| 0 | — | none | CPU and per-access delay provide the low-speed spacing |
| 1 | 3 | 240 T-states | 16 µs at 15 MHz |
| 2 | 2 | 176 T-states | mode not used by the traced OS path |
| 3 | 2 | 176 T-states | mode not used by the traced OS path |
TilEm restarts this ready timer on every modeled access to ports 0x10–0x13,
including reads. Wabbitemu derives readiness from the last successful LCD
write. This disagreement matters for read-heavy code and requires a physical
test. [standard] for emulator behavior; [hypothesis] for ASIC read behavior.
The guarded Wabbitemu initialized-core run sets speed mode 1 and field 3, then
writes the LCD at T-state 2,000. Port 0x02 reads 0xE1 at T-state 2,240 and
0xE3 at 2,241. An accepted status read at 2,241 leaves the write timestamp at
2,000 and leaves port 0x02 at 0xE3. The comparison is strict rather than
inclusive: readiness requires elapsed time greater than 240 T-states in this
implementation. [standard]
Programmable-timer mode-3 divisor
Public documentation assigns a second role to the selected port-0x2F field.
For programmable-timer sources in the 0xC0 family, the field selects divisor
$f+1$; speed mode 0 applies no divisor. [standard]
With the OS byte 0x4B, the documented divisors are 1, 4, 3, and 3 for speed
modes 0–3. TilEm treats the 0xC0 family like its ordinary CPU-clock modes,
and Wabbitemu’s timer-source update does not use port 0x2F. The prescaler is
therefore absent from both compared emulator paths. [standard]
OS 2.55MP’s timer API can select 0xC0-family sources. The prepared
HWTMR probe counts
source-0xE0 expiries against a crystal reference in CPU-speed modes 0–3. Its
exact assembled image completes in pinned Wabbitemu and measures a prescaler
near one, matching that emulator’s source implementation. No result from a
physical calculator has been recorded. [confirmed] for the probe and emulator
run; [hypothesis] for the physical divisor.
Emulator comparison
| Behavior | TilEm | Wabbitemu | MAME 0.287 | jsTIfied 20170706a |
|---|---|---|---|---|
Port-0x20 write | low two bits select modes 0–3; nonzero runs at 15 MHz | default TI-84 Plus state clamps modes 2–3 to mode 1; external extraSpeed enables 20/25 MHz | stores the raw byte; zero selects 6 MHz and any nonzero value selects 15 MHz | selects the browser emulator’s CPU-speed state |
Active 0x29–0x2C register | indexed by port 0x20 & 3 | indexed by the accepted CPU-speed mode | registers absent | delay values are stored |
| LCD instruction addition | active byte shifted right by two | same | absent | LCD uses its own busy interval |
Memory gates and 0x2E bits | all six access classes | all six access classes | absent | no source-equivalent per-access wait insertion identified |
Port 0x2D | low-power control outside this block | raw fifth delay latch; no timer or low-power transition | absent | stored control state |
| High-speed ready start | every LCD-port read or write | last successful LCD write | programmable interval absent | LCD readiness uses randomized controller timing |
| LCD controller rejection | ready bit and controller model | also has a separate fixed 60-T-state controller-access guard | T6A04 device behavior without the ASIC delay block | controller transfers use the jsTIfied LCD timer |
| Mode-3 timer prescaler | not modeled | not modeled in the compared timer path | not modeled | not modeled in the timer-source decoder |
The matching memory and LCD-instruction decode corroborates the public bit layout. MAME cannot corroborate that decode because it omits the block. The readiness, speed, and timer differences remain emulator policy, not proof of physical timing.
The same guarded Wabbitemu run dynamically checks three linked cases. Port
0x2A = 0x27 adds nine T-states to a status read. With port 0x2E = 0x45, the
enabled additions are Flash opcode fetch, Flash write, and RAM write. A write
of speed value 3 reads back mode 1 in the reset TI-84 Plus context because
timer_version = 0 disables the external extra-speed modes. [standard]
A dedicated speed run reads all seven reset latches as zero and verifies raw
byte readback across ports 0x29–0x2F. With active-register values
0x00/0x01/0x02/0x03 and port 0x2E = 0x77, modes 0–3 produce wait masks
0x00/0x07/0x38/0x3F: none, all Flash classes, all RAM classes, and all six
classes.
Writing 0x5A to port 0x2D changes only that latch. The active wait mask,
CPU frequency, timer version, programmable-timer state, LCD-active state,
HALT, interrupt line, and T-state count remain unchanged. These observations
describe Wabbitemu’s registered handler, not physical low-power behavior.
[standard]
Reproducing the decode
tools/ti84re/hardware/bus_timing.py is a pure register decoder.
tools/ti84re/hardware/describe_bus_timing.py prints all four speed modes from the boot values:
nix develop -c python3 -m ti84re.hardware.describe_bus_timing
The default documented profile reports:
documented (WikiTI pages retrieved 2026-08-09): speed-mode=1 clock=15MHz port20=01/01
port2e=0x45 port2f=0x4B
mode port value MHz LCD-I/O Flash +1T RAM +1T LCD-ready doc-div
0 0x29 0x17 6 5T M1,write write 0T /1
1 0x2A 0x27 15 9T M1,write write 240T /4
2 0x2B 0x2F 15 11T M1,write write 176T /3
3 0x2C 0x3B 15 14T M1,write write 176T /3
doc-div is the public port-0x2F divisor decode. It is not an emulator claim.
Compare the four pinned implementations, including ignored MAME writes and
jsTIfied’s stored-delay model, with:
nix develop -c python3 -m ti84re.hardware.describe_bus_timing --compare
nix develop -c python3 -m ti84re.hardware.describe_bus_timing --compare --json
Pass --extra-speeds to model Wabbitemu’s external 20/25 MHz option. The flag
does not describe the default TI-84 Plus configuration.
Repeated --write PORT=VALUE options make altered settings explicit. JSON is
available for scripts:
nix develop -c python3 -m ti84re.hardware.describe_bus_timing \
--write 0x20=0 --write 0x29=0x17 --write 0x2e=0x45 --json
Run the native boundary checks with the pinned Wabbitemu adapter:
wabbit_lcd_parent=$(mktemp -d /tmp/ti84-wabbit-lcd.XXXXXX)
python3 -m ti84re.emulators.wabbitemu.run_lcd_edge_probe \
--rom tools/rom.bin \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$wabbit_lcd_parent/run" --json
wabbit_speed_parent=$(mktemp -d /tmp/ti84-wabbit-speed.XXXXXX)
python3 -m ti84re.emulators.wabbitemu.run_speed_edge_probe \
--rom tools/rom.bin \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$wabbit_speed_parent/run" --json
mame_asic_parent=$(mktemp -d /tmp/ti84-mame-asic.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_asic_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_asic_parent/run" --json
The manifest labels the run as initialized-core emulator evidence and records
both input hashes. tools/ti84re/emulators/wabbitemu/lcd_probe.py derives its pointer, latch,
ready-hold, delay, wait-bit, and speed-clamp expectations from the reusable LCD
and bus-timing libraries.
tools/ti84re/emulators/wabbitemu/speed_probe.py derives speed, latch, and wait-mask
expectations from tools/ti84re/hardware/bus_timing.py. Its guarded CLI records both input
hashes and labels the direct timer_version = 1 configuration explicitly.
tools/ti84re/emulators/mame/asic.py reuses the MAME timing profile for raw readback, binary
clock selection, and the absent delay block.
The executed initialization can be recovered from a full boot trace:
nix develop -c python3 -m ti84re.trace.resolve /tmp/boot.trace \
--initial-mapping ti84p-reset --io-ports 20,29-2f
Open physical tests
The read-only ASIC register snapshot
captures ports 0x20, 0x29–0x2C, 0x2E, and 0x2F before a mutating
timing test. It does not measure any delay. No physical snapshot is recorded.
[confirmed] for the probe bytes; [hypothesis] for pending readback values.
The guarded memory-bus timing probe prepares paired timer-2 measurements for all six access classes. Its counted loops separate fixed-Flash opcode fetches, Flash data reads, safe Flash reset writes, RAM opcode fetches, RAM non-opcode reads, and idempotent RAM writes. It restores the entry timing byte and an initially idle timer 2. No exported physical result has been recorded. [confirmed] for the probe bytes and decoder; [hypothesis] for pending measurements.
The guarded prefix-M1 timing probe prepares paired RAM-M1 measurements for unprefixed, CB, ED, DD, repeated-DD, and indexed-CB shapes. The indexed-CB row distinguishes TilEm and documented Z80 M1 placement from Wabbitemu’s extra wait. The exact image completes through the cleanup boundary in pinned Wabbitemu and selects its three-wait model. No physical result has been recorded. [confirmed] for the probe bytes, decoder, and emulator run; [hypothesis] for pending measurements.
The guarded programmable-timer probe
prepares the port-0x2F mode-3 measurement. It also distinguishes crystal
divisor, counter-zero, and expiry-status models while timers 1 and 2 are idle.
The exact image completes through cleanup in pinned Wabbitemu. No exported
physical result has been recorded. [confirmed] for the probe bytes, decoder,
and emulator run; [hypothesis] for pending measurements.
- Run all three prepared timing matrices on TA2 and TA3 units.
- Repeat the loop test with active-register gate bits 0 and 1 cleared to verify
whether they disable all corresponding
0x2Eeffects. - Measure the interval from LCD writes and reads to port-
0x02bit 1 becoming ready. This distinguishes TilEm’s every-access restart from Wabbitemu’s write-based model. - Find the lowest reliable ports-
0x29–0x2Cvalues for each LCD controller revision without assuming the published0x0Cthreshold is universal. - Compare the
HWTMRport-0x2Fresults across CPU-speed modes and ASIC revisions. - Compare nominal and measured T-state wall times on TA2 and TA3 ASICs.
Sources
| Source | Use |
|---|---|
OS 2.55MP boot_bus_timing_init and resolved boot trace | boot register values, write order, and later CPU-speed transitions |
WikiTI ports 0x29, 0x2A, 0x2B, and 0x2C | speed selection, gate bits, LCD instruction delay, and published failure thresholds |
WikiTI port 0x2E | six memory-access classes and prefix observation |
WikiTI port 0x2F | LCD-ready intervals and mode-3 timer prescaler |
TilEm x4_io.c, x4_memory.c, and x4_init.c | delay decode, cycle placement, ready timer, and reset defaults |
Wabbitemu 83psehw.c and core.c | independent delay decode, cycle placement, and readiness comparison |
MAME 0.287 ti85.cpp and ti85_m.cpp | mapped I/O ports, raw speed readback, binary clock selection, and absent delay block |
jsTIfied deployed 20170706a artifact and readable mirror | fourth CPU-speed, stored-delay, LCD-ready, and timer-source comparison |
ASIC status, identity, protection, and GPIO
TI-84 Plus OS 2.55MP — status, battery comparison, protection mode, and GPIO.
Ports 0x02, 0x15, 0x21, 0x39, and 0x3A expose several unrelated
ASIC controls. The ROM establishes how TI-OS uses four of them. Port 0x15
comes only from public tables and emulator configuration because this ROM does
not read it through an immediate or statically resolved literal-C access.
Evidence boundaries
The sources answer different questions. Emulator behavior is useful for constructing tests, but it does not establish electrical behavior on a physical calculator.
| Source | What it establishes |
|---|---|
| Retail OS 2.55MP and boot 1.03 bytes | Port operations, masks, branch conditions, bcall targets, and return values [confirmed] |
| Resolved TilEm boot and archive traces | Values observed on port 0x02, the boot writes to ports 0x21 and 0x39, and the absence of GPIO-data accesses in those scenarios [confirmed] |
| WikiTI port pages | Public bit names, port-0x15 identity values, and port-0x21 size tables [standard] |
TilEm commit f56ad63 | One executable model for battery comparison, Flash grouping, and RAM execution masks [standard] for the implementation; [hypothesis] for physical equivalence |
Wabbitemu commit 48c2dc0 | An independent status and protection model, including implementation defects described below [standard] for the implementation; [hypothesis] for physical equivalence |
| MAME 0.287 | A third implementation with fixed status and identity values, incompatible port-0x21 masking, and no GPIO ports [standard] |
Guarded Wabbitemu --asic-edge-probe run | Initialized-core status, identity, protected-write, internal-field, readback, and GPIO-map observations [standard] |
Guarded Wabbitemu --protection-port-probe run | Shared write gate and internal-field behavior for ports 0x22–0x26 [standard] |
| Guarded MAME ASIC-control run | Raw Flash-gate status, identity, speed, port-0x21, missing protection/GPIO ranges, USB constants, and soft-reset retention [standard] |
The static I/O scanner reports candidates from a linear disassembly. Data can
decode as instructions, so a candidate needs control-flow or trace evidence.
The credible code for these ports is concentrated in page 00, pages 2F,
33, 35–37, 3C, 3D, and the retail boot page 3F. [confirmed]
Complete ROM I/O candidate audit
Unlisted immediate ports
A full linear scan of the 1 MiB retail ROM finds 35 aligned immediate-port candidates whose 21 apparent port values are absent from the project port map. This count removes inline bcall and bjump descriptors. None establishes an I/O operation. [confirmed]
| Candidate locations | Apparent ports and directions | Classification |
|---|---|---|
01:4304, 01:446A, 01:446E, 01:4C5A, 01:556D, 01:6E55, 01:6E95, 01:7CD6 | 0x49 OUT; 0x4E IN/OUT; 0x5E IN ×2; 0x6E IN; 0x70 OUT; 0xFF OUT | table-shaped data ×8 |
03:630B, 03:6323, 03:634F, 03:6367, 03:656F | 0xFE IN ×2/OUT ×2; 0x65 IN | table-shaped data ×5 |
03:6DE1 | 0x9C IN | operand overlap in LD HL,0x9CDB at 03:6DE0 |
07:4076 | 0xD1 OUT | table-shaped data |
33:4010 | 0x6B OUT | table-shaped data |
34:6CF5, 34:6CF7, 34:73AB, 34:73AD | 0x6D OUT ×2; 0x73 IN ×2 | table-shaped data ×4 |
37:6A9C, 37:6B14 | 0x6B IN ×2 | table-shaped data ×2 |
38:6A00 | 0xDC IN | table-shaped data |
3A:7D81, 3A:7FED | 0x5E IN; 0xDB IN | table-shaped data ×2 |
3B:47B9, 3B:4F45, 3B:52AE, 3B:535C, 3B:5467 | 0x6F OUT; 0x51 OUT; 0x6E IN; 0x5D IN; 0x6D IN | table-shaped data ×5 |
3F:40FC, 3F:4111, 3F:56F7, 3F:671B, 3F:67F7 | 0x5E OUT; 0x63 IN; 0xD1 IN; 0xE7 OUT; 0xE6 IN | table-shaped data ×5 |
The rebuilt Ghidra database gives the 34 table candidates no containing
function and no xrefs. A page-local direct CALL/JP scan also finds no
target to any candidate. Ghidra places 03:6DE1 inside
editbuf_clr_hibit, but the owning instruction starts at 03:6DE0; bytes
DB 9C are the little-endian operand of LD HL,0x9CDB. [confirmed]
A reset/idle TLMT trace executes 1,753,851 instructions and reaches none of the 35 locations. This trace result covers one emulator scenario. The byte and control-flow classifications, rather than trace absence, establish that these linear candidates do not add ports to the ROM inventory. [confirmed]
Register and block I/O
A separate raw-byte scan finds every ED-prefixed register and block-I/O
opcode pair. It does not depend on recovering the value of C across calls or
control-flow joins. The exact retail ROM contains 37 pairs and no ED prefix
at a 16 KiB page boundary. [confirmed]
| Raw pairs | Classification | Evidence |
|---|---|---|
37:58A9 (INI), 37:5944 (OUTI) | resolved instructions ×2 | Straight-line loads select RTC ports 0x48 and 0x44; both instructions belong to page-37 functions. |
04:4178, 04:4182, 04:6F5B, 05:40E7, 05:428C, 05:46E5, 05:7159, 05:715F, 38:57AC, 38:57D7, 38:57F5, 38:589F, 38:75AF, 39:73B7, 3C:4EFE, 3C:53E2, 3C:783B, 3C:7F99, 3F:540E, 3F:5C92, 3F:63E0, 3F:6C1A, 3F:6C2A, 3F:6C37, 3F:6C54, 3F:6C70, 3F:6C90 | operand overlaps ×27 | Each ED xx pair straddles a little-endian operand inside an owning CALL, JP, or LD instruction. |
01:428C, 07:4465, 38:40C4, 38:48B4, 39:7268, 3B:4F15, 3F:408D, 3F:567B | reviewed data ×8 | Rebuilt Ghidra has no containing function or xref, and the page-local direct-target scan finds no target. |
The 27 operand sites include ED 41 inside JP Z,0x41ED at 04:4177,
ED 70 inside CALL 0x70ED at 04:6F5A, and ED 40 inside
LD HL,0x40ED at 39:73B6. The scanner resolves the two aligned
instructions from their preceding literal loads and DEC C. No other raw pair
is an I/O instruction, so this ROM has no hidden computed-C access to an
unlisted, status, GPIO, or USB port. [confirmed]
tools/ti84re/rom/io_coverage.py contains the reusable scanner and review manifest.
The manifest pins retail ROM SHA-256
7d9a7d96d89fc552ebee6afdbdd011fdc6047be9c16d308245dff07eb1f7bd6d.
The CLI fails on a different ROM, a missing or duplicate review, a stale
location, or changed instruction bytes. --trace uses the constant-memory
point counter instead of constructing an object for every instruction:
nix develop -c python3 -m ti84re.rom.describe_io_coverage
nix develop -c python3 -m ti84re.rom.describe_io_coverage --json
nix develop -c python3 -m ti84re.rom.describe_io_coverage \
--trace /tmp/trace-benchmark.tlmt
The complete direct result is 34 reviewed-data entries and one
operand-overlap. The indirect result is 27 operand-overlap, eight
reviewed-data, and two resolved-instruction entries. Both manifests have
zero unresolved candidates. [confirmed]
Port 0x02 status
Port 0x02 combines transient hardware state with model-family bits. The boot
and archive traces return 0xE1, 0xE3, and 0xE7. [confirmed]
| Bit | ROM use or public meaning | Evidence |
|---|---|---|
| 0 | Battery comparator result. _Chk_Batt_Low tests it at 00:0D20; _Chk_Batt_Level tests it at 33:4EA3 and 33:4EE8. | [confirmed] for the tested bit; [standard] for the electrical “battery good” polarity |
| 1 | LCD-ready state. LCD wait loops continue while this bit is zero. | ROM wait helpers and the dynamic 0xE1 → 0xE3 transition [confirmed] |
| 2 | Flash-unlocked state. The archive trace changes 0xE3 to 0xE7 after the port-0x14 unlock sequence. No direct status consumer in this ROM tests it. | [confirmed] for the observed state; [hypothesis] for any OS use outside the audited direct reads |
| 3 | No meaning established. No direct status consumer in this ROM tests it. | [hypothesis] |
| 4 | No meaning established. No direct status consumer in this ROM tests it. | [hypothesis] |
| 5 | Publicly documented as USB-capable. Both emulators set it for their TI-84 Plus model, but this ROM does not test it directly. | [standard] |
| 6 | Publicly documented as link-assist available. Both emulators set it for their TI-84 Plus model, but this ROM selects assist code through bit 7. | [standard] for the published field; [confirmed] for the ROM gate |
| 7 | Advanced-family/model gate. ram:1837 tests this bit before several TI-84 Plus-only paths. The certificate accessor at 3D:5247 selects the App-trial table at offset 0x1E50 when the bit is set and the alternate table at 0x1F18 when clear. | [confirmed] for the branches and table use; [standard] for the family label |
| Value | Bit-level interpretation |
|---|---|
0xE1 | Comparator high, LCD wait active, Flash locked, and bits 5–7 set [confirmed] |
0xE3 | Comparator high, LCD ready, Flash locked, and bits 5–7 set [confirmed] |
0xE7 | Comparator high, LCD ready, Flash unlocked, and bits 5–7 set [confirmed] |
_NZIf83Plus model probe [confirmed]
_NZIf83Plus = 0x50E0, body ram:1837, preserves BC and the caller’s A
while returning its result only in the flags. It reads port 0x02, masks bit
7, and XORs with 0x80. Bit 7 set therefore returns Z; bit 7 clear returns NZ.
The historical name should not be read as “NZ on every TI-83 Plus-family
calculator.” OS 2.55MP uses the flag to distinguish the advanced TI-84 Plus
path from the older family path.
A controlled trace enters with A = 0xA5, returns with the same A, and
records Z set on the TI-84 Plus model. The reduced result is in
tools/data/community-manual-bcall-traces.csv. [confirmed] under TilEm.
Complete direct-consumer audit
The 1 MiB ROM contains exactly 55 raw DB 02 byte pairs, the opcode and
operand for IN A,(0x02). Every linear-disassembly candidate reaches one of
three conservative A-register consumers within two following instructions:
[confirmed]
| Consumer mask | Sites | ROM role and anchors |
|---|---|---|
0x01 — bit 0 | 8 | battery comparisons, including _Chk_Batt_Low at 00:0D20 and _Chk_Batt_Level at 33:4E9F and 33:4EE6 |
0x02 — bit 1 | 3 | LCD-ready waits at 00:0CC4, 00:0CDC, and 3F:744F |
0x80 — bit 7 | 44 | family-specific paging, keypad, link-assist, Flash, and boot paths; ram:1839 is the shared model probe |
The 33:4E9F battery path inserts LD C,0 before BIT 0,A. Every other
candidate tests A in the next instruction. The decoder crosses only
instructions that preserve A; calls, branches, arithmetic, and unknown
instructions produce an unclassified result. This ROM produces zero
unclassified candidates. [confirmed]
No status read selects bits 2–6. The complete raw register/block-I/O census
finds no port-0x02 access beyond these 55 immediate reads. This result does
not depend on propagating C across a call or control-flow merge. [confirmed]
The assist routines therefore use bit 7 as their model-family gate. Public bit 6 remains a hardware capability field, but OS 2.55MP does not consult it before the link-assist port accesses described in USB ASIC and link assist.
TilEm computes bits 0–2 from its battery value, LCD wait timer, and Flash lock. Wabbitemu does the same except that its TI-84 Plus battery result is fixed high. Their agreement supports the interpretation but does not replace a voltage or timing measurement. [standard]
The certificate-tail use gives bit 7 a concrete persistent-data consequence.
The model-selected clear, write, and query paths call 3D:5247; the helper
calls ram:1837, selects offset 0x1E50 for the TI-84 Plus trace values above,
and selects 0x1F18 when bit 7 is clear. Wabbitemu independently makes the
same family split, setting bit 7 for its TI-84 Plus models and clearing it for
its TI-83 Plus family. [confirmed] for the ROM branch and resolved TI-84 Plus
values; [standard] for the emulator family mapping.
MAME returns 0xC3 | (m_flash_unlocked << 2), truncated to one byte. Its
comparator and LCD-ready bits are fixed high. The normal gate values zero and
one therefore return 0xC3 and 0xC7. The handler does not normalize other
bytes: writes 02, 3F, 40, and FF return CB, FF, C3, and FF.
Port 0x14 remains write-only and reads zero. Bit 5 is fixed low for the
normal locked status, so this TI-84 Plus driver reports link assist but not USB
capability. These are emulator inconsistencies, not evidence for a different
ASIC status layout. [standard]
Battery comparison
Port 0x04 has two roles on this hardware: its low bits configure mapping and
standard-timer behavior, while bits 6–7 select a battery-comparison setting.
The battery routines keep the low configuration at 0x06 and write 0x46,
0x86, or 0xC6 to change the upper selector. [confirmed] for the writes;
[standard] for the selector interpretation.
_Chk_Batt_Low
The bcall _Chk_Batt_Low = 0x50B3 resolves to 00:0D07. A larger entry at
00:0D04 first runs a RAM-access delay, then performs this sequence: [confirmed]
00:0D07 IN A,(0x3A)
00:0D09 OR 0x80
00:0D0B OUT (0x3A),A
00:0D0D LD A,0x06
00:0D0F OUT (0x04),A
00:0D11 LD A,0x86
00:0D13 OUT (0x04),A
; delay
00:0D20 IN A,(0x02)
00:0D22 BIT 0,A
The routine sets (IY+0x18) bit 5 before the comparison and clears it when
port 0x02 bit 0 is zero. It restores port 0x04 to 0x06, writes 0xF0 to
port 0x39, pulses port-0x3A bit 4, clears port-0x3A bit 7, then returns
with Z reflecting (IY+0x18) bit 5. [confirmed]
| Bcall | ID | Body | Port-0x04 comparison value |
|---|---|---|---|
_Chk_Batt_Low_B | 80F0 | 3F:6171 | 0x86 |
_Chk_Batt_Low_B2 | 80F3 | 3F:6163 | 0x46 |
Both boot entries set port-0x3A bit 7, sample port-0x02 bit 0, restore
port 0x04 = 0x06, clear GPIO bit 7, and return Z according to the saved
comparison flag. The public flag file names (IY+0x18) as traceFlags, so the
bit’s wider ownership remains unknown. [confirmed]
_Chk_Batt_Level
The main bcall table resolves _Chk_Batt_Level = 0x5221 to 33:4E9B.
The routine returns a value from 0 through 4 in A: [confirmed]
| Result | Path |
|---|---|
0 | The initial port-0x02 bit-0 test is low, so the routine returns before enabling the GPIO sequence. |
4 | The comparison after port 0x04 = 0xC6 is high. |
3 | The 0xC6 comparison is low and the 0x86 comparison is high. |
2 | The first two comparisons are low and the 0x46 comparison is high. |
1 | All three comparisons are low. |
It sets (IY+0x18) bit 5 before the first comparison and clears that bit before
the final 0x46 test. Results 1 and 2 therefore leave the bit clear.
The routine restores 0x04 = 0x06, pulses GPIO bit 4, clears GPIO bit 7, and
returns C in A. [confirmed]
TilEm maps the two high selector bits to the following thresholds. Its source
labels the table FIXME: measure actual levels, so these voltages are emulator
parameters rather than measured ASIC facts. [standard]
Port-0x04 value | Selector | TilEm threshold |
|---|---|---|
0x06 | 0 | 3.3 V |
0x46 | 1 | 3.9 V |
0x86 | 2 | 3.6 V |
0xC6 | 3 | 4.3 V |
This mapping conflicts with the ROM’s comparison order. Whenever the later
0x46 comparison succeeds at 3.9 V or above, the earlier 0x86 comparison at
3.6 V has already returned level 3. Result 2 is therefore unreachable in that
emulator model. Physical measurements must establish the actual selector
ordering, thresholds, hysteresis, and load conditions. [confirmed] for the
code/model comparison; [hypothesis] for the unmeasured electrical behavior.
Pinned TilEm comparator sweep
A guarded direct-core run sets TilEm’s battery field from 3.0 through 4.5 V in
0.1 V steps. For each value, it writes all four selectors through port 0x04
and reads port-0x02 bit 0. The observed comparator mask uses bit order
0x06, 0x46, 0x86, 0xC6: [standard]
| Modeled voltage | Comparator mask | ROM bcall result |
|---|---|---|
| below 3.3 V | 0x0 | 0 |
| 3.3–3.5 V | 0x1 | 1 |
| 3.6–3.8 V | 0x5 | 3 |
| 3.9–4.2 V | 0x7 | 3 |
| 4.3 V and above | 0xF | 4 |
The native mask transitions match the four source constants. The reusable
model then applies the byte-verified decision tree at 33:4E9B–4EDA.
The combination reaches levels 0, 1, 3, and 4, but not level 2. The probe
binary has SHA-256
47008d660c7ea3e88c07df3d41d5c3e34c51d49850a806d5d2e37d5ca6214029.
This run validates TilEm’s implementation; it does not measure a calculator’s
battery rail. [confirmed] for the ROM tree; [standard] for the emulator run.
Port 0x15 identity
WikiTI publishes the following read values. They are [standard] because this OS image does not use the port through an immediate or statically resolved literal-C I/O instruction.
| Value | Public ASIC reference | USB driver family | Reported RAM |
|---|---|---|---|
0x33 | 83PL2M/TA2 | none | external 128 KiB |
0x44 | 83PLUSB/TA2 | old | 128 KiB |
0x45 | 84PLUSB/TA3 | new | 128 KiB |
0x55 | 84PLC/TA1 | new | 48 KiB |
Wabbitemu returns these values according to its selected model and RAM
revision. TilEm’s TI-84 Plus model returns fixed 0x45 with a ??? source
comment. Neither implementation verifies what a particular physical unit
returns. [standard]
MAME’s shared TI-83 Plus Silver Edition/TI-84 Plus I/O map returns fixed
0x33 from port 0x15 for every machine using that map. The TI-84 Plus
configuration therefore identifies itself as the public 83PL2M/TA2 row.
MAME’s MACHINE_NOT_WORKING declaration and shared handler make this a driver
defect, not an alternate identity claim. [standard]
The complete-ROM scan finds no immediate IN or OUT instruction for port
0x15. The conservative C-register resolver also finds no access after a
straight-line literal load into C or BC. Computed register-C accesses that
cross calls or control-flow edges remain outside that static proof. [confirmed]
Port 0x21 Flash grouping and RAM execution mode
Port 0x21 is a writable protected register, not a read-only ASIC identity.
The retail boot page loads A=0, executes the protected-byte sequence, and
writes the value at 3F:41DC. [confirmed]
3F:41D5 LD A,0x00
3F:41D7 NOP
3F:41D8 NOP
3F:41D9 IM 1
3F:41DB DI
3F:41DC OUT (0x21),A
3F:41DE DI
TilEm stores writes only while Flash is unlocked and exposes value & 0x33
on reads. Wabbitemu also marks the port protected and stores the same two
fields. Its read handler shifts the stored mode right by four a second time,
so it loses bits 4–5. [standard]
MAME accepts writes without the protected-byte gate, stores value & 0x0F,
and returns that nibble. It can therefore expose undocumented bits 2–3 while
discarding the RAM execution field in bits 4–5. MAME does not implement the
execution-protection ports controlled by that field. A native locked write of
0x33 reads back 0x03; opening the port-0x14 gate does not change results
for writes 0x30, 0x03, 0x33, or 0xFF. [standard]
Complete immediate-I/O audit
The ROM contains 11 raw DB 21 pairs and three raw D3 21 pairs. Ten reads
are instructions, and every one immediately executes AND 0x03. The boot
write at 3F:41DC is the only OUT (0x21),A instruction. [confirmed]
| Read sites | Consumer |
|---|---|
00:02AE, 00:1831, 00:2B32, 00:2B5B | AND 0x03 |
2F:4DD5, 2F:511D, 36:5E90 | AND 0x03 |
3C:6BA8, 3C:7F0C, 3D:7392 | AND 0x03 |
The remaining three raw pairs overlap other instructions: [confirmed]
| Raw pair | Owning instruction | Why it is not I/O |
|---|---|---|
06:5A10 — DB 21 | 06:5A0D: LD (IX-1),0xDB | The DB byte is the stored immediate; 21 begins the following LD HL instruction. |
05:6C96 — D3 21 | 05:6C95: JR Z,05:6C6A | The D3 byte is the relative displacement; 21 begins the following LD HL instruction. |
3C:5B91 — D3 21 | 3C:5B90: JR 3C:5B65 | The D3 byte is the relative displacement; 21 begins the following LD HL instruction. |
The rebuilt Ghidra database confirms instruction ownership for these
boundaries. The raw scanner reports zero unclassified pairs and zero decoded
instructions without a matching opcode pair. The conservative literal-C
resolver finds no additional port-0x21 access. [confirmed]
Bits 0–1: Flash group
The OS repeatedly reads port 0x21 & 3 to distinguish the 1 MiB TI-84 Plus
configuration from larger family members. The archive App scan at 3D:726E
selects top page 0x29 when the field is zero and 0x69 for the remaining
advanced-family branch. [confirmed]
| Field | Public size | Highest boot page |
|---|---|---|
| 0 | 1 MiB | 0x3F |
| 1 | 2 MiB | 0x7F |
| 2 | 4 MiB | 0xFF |
| 3 | 8 MiB | 0x1FF |
WikiTI supplies these configured sizes. TilEm also uses the field as a Flash-sector protection override group. The physical relation between the programmed field and chip capacity has not been tested here. [standard] for the table and model; [hypothesis] for unmeasured hardware.
Bits 4–5: RAM execution mode
WikiTI describes this field as RAM chip size. The boot writes mode 0 even on a 128 KiB TI-84 Plus. “RAM execution mode” therefore describes the observed use more precisely than treating it as detected capacity. [confirmed] for the boot value; [standard] for the public size labels.
TilEm converts the field to one of four repeating masks. Wabbitemu’s intended
page shortcut collapses to zero for modes 1–3, so it does not implement the
same arithmetic. Ports 0x25 and 0x26 add inclusive 1 KiB bounds. See
Execution protection for the
equations, complete page coverage, and physical tests. [standard]
Ports 0x39 and 0x3A GPIO
The ROM treats port 0x39 as GPIO direction or configuration and port 0x3A
as GPIO data. It normally updates 0x3A with read-modify-write sequences so
unrelated bits retain their values. [confirmed]
The direction polarity is not established by the ROM alone. WikiTI’s port
0x39 page says both that clearing and setting a bit designate output, which
is self-contradictory. Its port-0x3A page recommends 0xE0, but this boot
writes 0xF0 at 3F:4214, and the archive trace later reads back 0xF0 from
port 0x39. Those public direction claims remain [hypothesis] pending physical
tests.
Complete immediate-I/O audit
Raw-byte coverage separates GPIO code from accidental opcode pairs: [confirmed]
| Port and direction | Raw pairs | Reviewed instructions | Other raw pairs |
|---|---|---|---|
IN (0x39) | 14 | 13 | 02:5142 is table-shaped data with no function or xrefs. |
OUT (0x39) | 16 | 16 | none |
IN (0x3A) | 21 | 19 | 06:5A8D and 3C:7365 overlap operands. |
OUT (0x3A) | 17 | 17 | none |
The two DB 3A overlaps begin on an 0xDB operand byte. At 06:5A8C, the
owner is CP 0xDB; at 3C:7364, the owner is a relative JR whose
displacement is 0xDB. In both cases, 0x3A begins the following LD A,(nn)
instruction. [confirmed]
Of the 13 port-0x39 reads, every one begins an adjacent read-modify-write.
The 16 writes comprise those 13 updates and three direct 0xF0 writes at
ram:0D39, 37:6D10, and 3F:4214. Port 0x3A has 17 adjacent
read-modify-write sequences. Its remaining two reads test bit 3 at 2F:521B
and 35:402C. The page-35 body duplicates the page-2F USB implementation.
[confirmed]
TilEm lacks a meaningful TI-84 Plus port-0x3A model, Wabbitemu lacks port
0x39, and MAME maps neither port. Emulator execution can check control flow
around these instructions, but it cannot validate the paired GPIO state or
electrical effect. The sequences below are byte- and control-flow-validated
against the ROM; their physical signal interpretation remains unmeasured.
[confirmed] for the instructions; [hypothesis] for electrical behavior.
Battery GPIO sequence
_Chk_Batt_Level provides the clearest GPIO sequence: [confirmed]
- Set port-
0x3Abit 7. - Run the port-
0x04comparator tests. - Set port-
0x39bit 4. - Set port-
0x3Abit 4, delay, and clear it. - Clear port-
0x3Abit 7.
The shorter _Chk_Batt_Low path writes 0xF0 directly to port 0x39 before
the bit-4 pulse. The ROM confirms that bits 7 and 4 participate in battery
testing. It does not expose the electrical signal names. [confirmed]
USB GPIO sequence
The boot USB code on page 2F and the parallel OS code on page 35 use the
low GPIO bits: [confirmed]
| Location | Operation |
|---|---|
2F:5330 | Clear port-0x3A bit 1, then set port-0x39 bit 1. |
2F:5353 | Clear port-0x39 bit 1. |
2F:538C | Set the low data bits to binary 100, then set port-0x39 bits 0–2. |
2F:53AB | Clear port-0x3A bits 0–2, then set port-0x39 bits 0–2. |
2F:53D5 and 2F:593B | Clear port-0x39 bits 0–2 during cleanup. |
2F:521B | Test port-0x3A bit 3 while selecting a USB state. |
These operations tie GPIO bits 0–3 to USB setup and state selection. The exact charge-pump, PHY, or cable signals remain [hypothesis]. See USB ASIC and link assist for the controller transaction.
Dynamic and emulator limits
The boot trace writes 0xF0 to port 0x39 but does not access port 0x3A.
The archive scenario reads and rewrites 0xF0 through port 0x39; it also does
not reach port 0x3A. Those traces cover startup and archive work, not battery
level or connected USB workflows. [confirmed]
TilEm’s TI-84 Plus model returns fixed 0xF0 from port 0x39 and has no
meaningful write or port-0x3A model. Wabbitemu stores port 0x3A as a latch
but does not register port 0x39. Both emulators model a color-calculator
backlight side effect for port-0x3A bit 5; that does not validate TI-84 Plus
GPIO wiring. [standard]
MAME maps neither port 0x39 nor port 0x3A. Battery and USB code can execute
through the driver, but those GPIO reads and writes reach no device handler.
[standard]
Native Wabbitemu confirmation. The guarded initialized-core probe reads port 0x02 as 0xE3 while the in-memory Flash gate is locked and as 0xE7 after directly opening that gate. With Wabbitemu’s TI-84 Plus model, port 0x15 reads 0x44 at RAM revision 0 and 0x55 at RAM revision 2. [standard]
Port 0x21 is active and protected. A write of 0x33 while Flash remains locked is rejected, leaving both internal fields and readback zero. After the probe directly opens the in-memory gate, writing 0x30 stores internal RAM execution mode 3 but reads back 0x00. Writing 0x03 stores Flash group 3 and reads 0x03. Writing 0x33 stores both internal fields while still reading 0x03. This run exercises the protected device handler, not the retail ROM’s port-0x14 unlock sequence. [standard]
A separate initialized-core run verifies the same locked-write rejection for
every port from 0x22 through 0x26. It also checks the Flash-bound low-byte
handlers, port-0x24 high-field clearing, and the 16-bit RAM-bound wrap. See
Execution protection
for the complete value matrix and evidence limits. [standard]
The same initialized core has no active device at port 0x39; a read is rejected and produces the device layer’s 0xFF fallback. Port 0x3A is active, starts at zero, and reads back complete 0xA5 and 0x5A writes. The run advances zero T-states and does not assign electrical direction or signal meaning to either GPIO port. [standard]
Native MAME confirmation. The guarded run reads startup values C3, 00,
33, 00, and 00 from ports 0x02, 0x14, 0x15, 0x20, and 0x21.
The raw gate sweep produces C3 C7 CB FF C3 FF for writes
00 01 02 3F 40 FF. Port 0x14 reads zero after every write. [standard]
Ports 0x22–0x2F and 0x39–0x3A return zero before and after patterned
writes. A 50-T-state counter continues executing from RAM while port 0x21
reads 0x03 and ports 0x22–0x26 retain no written byte. The five-frame
counter records 12,000 iterations at the zero speed value and 30,000 at the
nonzero value. This checks one RAM execution path, not the missing physical
boundary rules. [standard]
A scheduled MAME soft reset begins at PC = 0x0000 but retains a gate value of
one, raw speed 0x03, and the port-0x21 nibble from write 0xAB. The reads
are consequently 0xC7, 0x03, and 0x0B after reset. The driver reset
routine does not restore these fields. This is MAME reset behavior and does
not establish calculator warm-reset retention. [standard]
Emulator comparison
The four pinned implementations disagree on several control groups not already established by ROM use. Their values are test oracles for the software, not physical ASIC measurements.
| Area | TilEm f56ad63 | Wabbitemu 48c2dc0 | MAME 0.287 | jsTIfied 20170706a |
|---|---|---|---|---|
Port 0x02 | dynamic comparator, LCD-ready, and Flash lock; family bits 5–7 set | same layout, with the TI-84 Plus comparator fixed high | `0xC3 | (raw gate << 2)`, truncated to a byte |
Port 0x15 | fixed 0x45 | model and RAM-revision dependent | fixed 0x33 | model-dependent identity value |
Port 0x21 accepted readback | value & 0x33, subject to Flash unlock | only bits 0–1 survive its read defect | value & 0x0F, without protected-write gating | stored while Flash-unlocked and used for page-level execution groups |
| GPIO | port 0x39 fixed at 0xF0; no meaningful TI-84 Plus port 0x3A | port 0x3A latch; port 0x39 absent | both ports absent | software latches without physical GPIO modeling |
| Driver status | usable model with unmeasured battery thresholds | usable model with implementation-specific defects | TI-84 Plus marked MACHINE_NOT_WORKING | browser emulator source model |
Reusable analysis tools
tools/ti84re/hardware/asic_control.py decodes port-0x02 values, generic immediate-port
consumers and raw-opcode coverage, the public port-0x15 table, port-0x21
modes, TilEm’s battery selector, and adjacent GPIO read-modify-write sequences.
Its raw audit distinguishes aligned instructions, operand overlaps, reviewed
data, and unclassified pairs. tools/ti84re/hardware/describe_asic_control.py exposes those
operations as text or JSON. tools/ti84re/emulators/wabbitemu/asic_probe.py validates
native results against the reusable source model.
tools/ti84re/rom/io_coverage.py and tools/ti84re/rom/describe_io_coverage.py separately
reconcile every direct candidate for ports absent from the project port map.
tools/ti84re/emulators/wabbitemu/run_asic_edge_probe.py
guards the exact ROM and native binary identities and writes a JSON manifest.
tools/ti84re/emulators/wabbitemu/protection_port_probe.py applies the adjacent boundary-port
model from tools/ti84re/hardware/execution_protection.py; its guarded CLI records the same
two identities. tools/ti84re/emulators/mame/asic.py combines the ASIC and bus-timing profiles
with a typed native report. tools/ti84re/emulators/mame/run_asic_probe.py guards the MAME,
ROM, and Lua identities and retains the soft-reset output.
tools/ti84re/hardware/battery.py formalizes the ROM result tree and threshold
regions. tools/ti84re/hardware/describe_battery.py exposes voltage and raw-sample
queries as text or JSON. tools/ti84re/emulators/tilem/battery.py validates a typed native
comparator sweep against the same model.
[confirmed] for the ROM-analysis tools; [standard] for the emulator oracle.
nix develop -c python3 -m ti84re.hardware.describe_asic_control
nix develop -c python3 -m ti84re.hardware.describe_asic_control --status 0xE7 --port21 0x20
nix develop -c python3 -m ti84re.hardware.describe_asic_control --implementations --json
nix develop -c python3 -m ti84re.hardware.describe_asic_control \
--scan-status-consumers --json
nix develop -c python3 -m ti84re.hardware.describe_asic_control \
--scan-port21-consumers --scan-gpio --json
nix develop -c python3 -m ti84re.hardware.describe_asic_control \
--audit-port 0x21 --audit-port 0x39 --audit-port 0x3A
nix develop -c python3 -m ti84re.rom.analyze_io \
0x02 0x15 0x21 0x39 0x3A --summary
nix develop -c python3 -m ti84re.rom.disassemble 0x33 \
--start 0x4E9B --end 0x4F02
nix develop -c python3 -m ti84re.hardware.describe_battery --json
nix develop -c python3 -m ti84re.hardware.describe_battery --voltage 3.6
nix develop -c python3 -m ti84re.hardware.describe_battery --samples 1010
asic_probe_parent=$(mktemp -d /tmp/ti84-asic-probe.XXXXXX)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_asic_edge_probe \
--rom tools/rom.bin \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$asic_probe_parent/run" --json
protected_port_parent=$(mktemp -d /tmp/ti84-protected-port.XXXXXX)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_protection_port_probe \
--rom tools/rom.bin \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$protected_port_parent/run" --json
mame_asic_parent=$(mktemp -d /tmp/ti84-mame-asic.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_asic_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_asic_parent/run" --json
The I/O and GPIO scans generate candidates. A report becomes evidence for code only after raw-byte and control-flow review or a resolved execution trace.
Open physical tests
The read-only ASIC register snapshot
captures ports 0x15, 0x21, 0x39, and 0x3A without changing GPIO
configuration or data. It provides a baseline, not an electrical direction
test. No physical snapshot is recorded. [confirmed] for the probe bytes;
[hypothesis] for pending readback values.
- Run the restoring battery-level probe across an upward and downward controlled-supply sweep. It records 16 retail bcall results per point and verifies port/GPIO/flag cleanup. Run the higher-risk raw battery-selector probe after it at each voltage point. The second result records all four comparator bits so the sweep can assign individual selector thresholds and hysteresis.
- Read port
0x15on known TA1, TA2, and TA3 units and compare the result with package markings and installed RAM. - Program each port-
0x21field while Flash is unlocked. Test Flash protection groups and the execution ranges described in Execution protection. Do not rely on emulator reset behavior. - Measure port-
0x39direction polarity and port-0x3Aelectrical state with battery and USB paths active. Preserve the OS configuration and avoid driving externally forced pins against the ASIC.
Sources
| Source | Use |
|---|---|
| Retail OS 2.55MP and boot 1.03 ROM bytes | Status branches, battery routines, port-0x21 boot setup, and GPIO operations |
WikiTI port 0x02 | Public status-bit names |
WikiTI port 0x15 | Public ASIC identity table |
WikiTI port 0x21 | Public Flash/RAM size tables and execution-page description |
WikiTI ports 0x39 and 0x3A | Historical GPIO interpretation, with the contradictions identified above |
TilEm x4_io.c at f56ad63 | Battery table, status read, identity constant, protection mode, and fixed GPIO read |
Wabbitemu 83psehw.c at 48c2dc0 | Independent port models and the port-0x21 read defect |
MAME 0.287 ti85.cpp and ti85_m.cpp | shared I/O map, fixed status and identity reads, port-0x21 mask, missing GPIO, and driver status |
jsTIfied deployed 20170706a artifact and readable mirror | fourth status, identity, protected-write, execution-group, and software-GPIO model |
Execution protection
TI-84 Plus OS 2.55MP — Flash-page and RAM-chunk fetch controls.
Ports 0x21–0x26 define where the ASIC permits instruction fetches. The
retail boot writes five protection registers before it changes the normal
memory map. Emulator source supplies reproducible models for the resulting
checks, but several boundary details still require physical measurement.
Evidence boundaries
| Evidence | What it establishes | Confidence |
|---|---|---|
Retail boot page 3F | protected writes, register values, and _SetFlashLowerBound behavior | [confirmed] |
| Complete-ROM static I/O scan | one write each to ports 0x21, 0x22, 0x25, and 0x26; two writes to port 0x23; no resolved port-0x24 access | [confirmed] |
| TilEm x4, Wabbitemu, and jsTIfied source | three executable software models, including their disagreements | [standard] |
| Guarded TilEm boundary traces | fetch, return, warning, and reset sequences at pages 07, 08, 29, and 2A | [confirmed] for the pinned emulator run |
| Guarded Wabbitemu boundary runs | fetch, return, marker, and instrumented reset sequences at pages 07, 08, 09, 29, and 2A | [confirmed] for the pinned emulator run |
| Guarded RAM execution runs | chunk-edge and mode disagreements under pinned TilEm and Wabbitemu | [confirmed] for the pinned emulator runs |
| Guarded Wabbitemu protected-port run | registered-port gate, readback, high-field handling, and 16-bit RAM-bound storage | [standard] |
| WikiTI port pages | public inclusive-bound descriptions and the larger-device port-0x24 extension | [standard] |
| Physical TA2/TA3 behavior | lower-edge, reset, violation, and overlay details | [hypothesis] until measured |
The emulator equations below are source-verified descriptions of those programs. Agreement between an emulator and a public table does not promote an unmeasured ASIC behavior to [confirmed].
Registers and boot values
The boot sequence establishes these values. [confirmed]
| Port | Boot value | Modeled role |
|---|---|---|
0x21 | 0x00 | bits 4–5 select the repeating RAM address mask; bits 0–1 also select the Flash protection group |
0x22 | 0x08 | lower Flash no-execute page |
0x23 | 0x29 | upper Flash no-execute page |
0x25 | 0x10 | lower executable RAM chunk in 1 KiB units |
0x26 | 0x20 | upper executable RAM chunk in 1 KiB units |
boot_execution_protection_init at 3F:41D5 performs the five writes through
3F:4206: [confirmed]
3F:41D5 ld a,0x00
3F:41D7 nop
3F:41D8 nop
3F:41D9 im 1
3F:41DB di
3F:41DC out (0x21),a
3F:41DE di
3F:41DF ld a,0x08
3F:41E1 nop
3F:41E2 nop
3F:41E3 im 1
3F:41E5 di
3F:41E6 out (0x22),a
3F:41E8 di
3F:41E9 ld a,0x29
3F:41EB nop
3F:41EC nop
3F:41ED im 1
3F:41EF di
3F:41F0 out (0x23),a
3F:41F2 di
3F:41F3 ld a,0x10
3F:41F5 nop
3F:41F6 nop
3F:41F7 im 1
3F:41F9 di
3F:41FA out (0x25),a
3F:41FC di
3F:41FD ld a,0x20
3F:41FF nop
3F:4200 nop
3F:4201 im 1
3F:4203 di
3F:4204 out (0x26),a
3F:4206 di
Each output is preceded by fetched bytes 00 00 ED 56 F3 D3. TilEm advances
its protected-write recognizer only while these bytes come from physical Flash
0xB0000–0xBFFFF or 0xF0000–0xFFFFF. Wabbitemu does not recognize the
six bytes. It accepts a port-0x14 lock change whenever the output instruction
executes on one of its privileged pages. Both implementations then accept
writes to ports 0x21–0x26 only while Flash is unlocked. [standard]
The ROM proves that it emits the required byte sequence from page 3F. It does
not by itself prove the complete privileged-page set or the response to an
invalid sequence. [confirmed] for the bytes; [standard] for the differing
emulator gates.
Flash instruction fetches
TilEm x4 applies the following test after it resolves a logical address to a physical Flash page $p$: [standard]
$$ \mathit{denied}_{\mathrm{TilEm}} = (\mathtt{port22} \le p \le \mathtt{port23}) $$
The boot values therefore deny pages 0x08–0x29, inclusive. Pages
0x00–0x07 and 0x2A–0x3F remain executable. Reversing the bounds makes
the interval empty in this model. [standard]
Wabbitemu implements a different lower edge: [standard]
return bank->page <= flash_lower || bank->page > flash_upper;
Its forbidden interval is (port22, port23]. With the boot values, Wabbitemu
allows page 0x08, while TilEm denies it. Both deny page 0x09 and page
0x29. WikiTI describes the lower bound as inclusive, which agrees with TilEm,
but the physical page-0x08 result remains unmeasured. [standard] for the
published contract and source comparison; [hypothesis] for hardware.
Boot execution ranges. The written bounds are [confirmed], and the emulator predicates are [standard]. The physical page-08 boundary remains [hypothesis]. The RAM panel introduces the mode-dependent chunk model detailed below.
Both emulator paths apply this rule to opcode fetches. Ordinary Flash data reads use a separate path. The locked certificate-page read censor is also a separate mechanism. [standard]
jsTIfied stores ports 0x22 and 0x23 only while its Flash gate is open and
builds page-level run_lock entries. A denied fetch sets its halted/reset state
to 2. Ports 0x25 and 0x26 are stored but do not participate in the fetch
predicate, so jsTIfied cannot test the documented 1 KiB RAM-bound behavior.
Its port-0x21 handler instead rebuilds coarser RAM-page execution groups.
These are properties of the pinned JavaScript source, not ASIC evidence.
[standard]
WikiTI also says page 0x00 always remains executable and that a forbidden
fetch resets the calculator. Wabbitemu always permits page 0 and resets the CPU
on a violation when no debugger callback is installed. TilEm’s interval test
can deny page 0 when port 0x22 = 0. Its opcode-read handler raises an
execution exception, and its Z80 loop performs a full calculator reset after
the fetched opcode completes. These custom-bound and post-violation behaviors
remain physical test cases. [standard]
Guarded TilEm boundary trace
A controlled fixture tests both sides of the boot interval rather than
inferring runtime behavior from source alone. Each derived ROM changes only six
erased bytes at target pp:7FF0 to this marker routine:
ld a,pp
ld (0x8478),a
ret
The 75-byte assembly program at ram:9D95 first reads those six bytes as data
and compares them with its embedded signature. It then seeds 0x8478, maps
the target page, and executes CALL 0x7FF0 at ram:9DBD. A mismatch returns
without attempting the fetch. The fixture builder requires the exact complete
ROM hash, verifies that the patched span was FF FF FF FF FF FF, and writes a
new ROM copy rather than modifying tools/rom.bin.
The pinned headless TilEm run produced these control-flow sequences. Clock
deltas are relative to the CALL; absolute clocks include UI launch timing.
[confirmed]
| Page | Recorded sequence after ram:9DBD | TilEm warning count | Outcome |
|---|---|---|---|
07 | 07:7FF0 at +8, 07:7FF2 at +23, return ram:9DC0 at +47 | 0 | returned |
08 | attempted 08:7FF0 at +8, reset entry at +15; no 08:7FF2 or return | 1 | violation reset |
29 | attempted 29:7FF0 at +8, reset entry at +15; no 29:7FF2 or return | 1 | violation reset |
2A | 2A:7FF0 at +8, 2A:7FF2 at +23, return ram:9DC0 at +47 | 0 | returned |
TilEm records the denied target’s first opcode-fetch address before its main
loop services the pending execution exception. It does not advance to the
marker store at pp:7FF2; the next record is logical 0x8000, followed by the
retail reset stub’s mapping writes and boot continuation. The first post-reset
record is resolved with stale pre-reset mapper state because TLMT has no
explicit internal-reset event, so the classifier uses the logical 0x8000
transition and the subsequent reset-stub sequence.
The machine-code and trace identities were: [confirmed]
| Page | Probe SHA-256 | Trace SHA-256 |
|---|---|---|
07 | 87c11964b6cf67624b2eff46e1a962c56f1684dd48db931a5cb68e08c1b84b4e | 250cc9d2b8b3c85f5edb6391e847993e27e6c308c4a70d62dd5cfc8168af8e68 |
08 | ddd023d522d301315c0f4929f348499faca08c708e96c1333bf85e32505f9534 | f9c1f142430aafc47b514ef220a707be01de02678e6cd22fcb1f6e5fb024eeac |
29 | f671bdb62e6bad19f33402eb919e70631cf7cc8f00b9f7f52114d052f86cea78 | ee3dac7ec1843c2a82ee321c0a3a16c95bc5898d3c70fb97296127dbf2020007 |
2A | d5f72f96562ef5e96f4ddaa12954548d210650d9ca6bec365f75f1bb6f3bad1b | b9db26bc7ef69d97907118d0124213603632d9e2f3d9ebb56680b87d8644636d |
This dynamically confirms the inclusive 08–29 interval and reset policy
implemented by this TilEm build. It does not decide the physical page-08
boundary or validate Wabbitemu’s lower-exclusive model. [confirmed] for TilEm;
[hypothesis] for the physical lower edge and violation response.
TilEm reset and exception scope
TilEm’s tilem_calc_reset resets the Z80, LCD controller, link port, keypad,
Flash command state, MD5 accelerator, programmable timers, and model-specific
hardware state in sequence. The TI-84 Plus callback then selects this starting
state: [standard]
| Group | Reset value |
|---|---|
Z80 register pairs except PC | 0xFFFF |
PC, R bit 7 | 0x8000, 0x80 |
IFF1, IFF2, IM, interrupt requests, HALT | zero |
| Mapper windows | page 00, certificate page 3E, boot page 3F, boot page 3F |
| CPU speed | 6 MHz |
Protection ports 0x21–0x23, 0x25, 0x26 | 00, 08, 29, 10, 20 |
| Flash command gate, state, and busy flag | locked, array-read mode, idle |
| LCD controller | inactive, contrast 32, 8-bit mode, increment 7, row stride 16 |
| Link output and assist, keypad, MD5, programmable timers | cleared or reset defaults |
The reset retains all memory arrays, including LCD backing memory. It also
retains Flash program-address, data, toggle, override-group, and emulation
fields. External link-emulator state remains. The TI-84 Plus callback leaves
port 0x05, ports 0x09–0x0F, the three RTC fields, and LCD_WAIT
unchanged. Scheduler and debugger state also survives: the Z80 clock, access
timestamps, emulation flags, dynamic timers, and breakpoints. [standard]
A guarded direct-core run seeds eight reset groups and nine retained groups.
All 17 match the source model. It records PC=0x8000, SP=0xFFFF, mapper
windows 00/3E/3F/3F, and a retained dynamic timer with 4,321 clocks
remaining. Direct internal seeding tests TilEm implementation state, not a
physical reset. [confirmed]
TilEm’s forbidden-fetch path does not suppress the opcode. The M1 read raises
TILEM_EXC_FLASH_EXEC, returns the byte, and the Z80 executes the complete
instruction. The main loop checks the exception afterward and calls
tilem_calc_reset. The direct-core fixture maps Flash page 08 at 0x4000
and RAM page 0x40 at 0x8000. Its forbidden instruction is
LD (0x8000),A with A=0x5A. The run stops on the exception after reset with
PC=0x8000 and AF=SP=0xFFFF, while physical RAM byte 0x100000 contains
0x5A. [confirmed]
The boundary fixture above begins with LD A,pp; reset follows that first
instruction, before its marker-store instruction. Its unchanged marker proves
only that the second opcode did not execute. It does not prove that TilEm
suppresses the forbidden opcode itself.
The direct-core binary SHA-256 is
ab0a862b1fbb7f8a09a075fbd0ec61ebb0bab84d12d2a9c2a650813476cc7e5a.
The builder requires clean TilEm commit
f56ad637d0524ee841dd381be6ecbaf5b8975600 and Git tree
58316afe35d69e69353f0f743698144153051d4a.
Guarded Wabbitemu boundary run
The pinned Wabbitemu core executed the same six-byte ROM markers and 75-byte
RAM probes. The native adapter first cold-boots the fixture ROM. It waits until
the retail boot establishes 0x08, 0x29, 0x4000, and 0x83FF as the Flash
and RAM bounds. It also requires mode 0 and relocked Flash. Every run reached
this state after
134,845 instructions and 1,746,999 T-states at 3F:4223. [confirmed] for the
pinned emulator run.
The adapter then maps physical RAM page 1 at 0x8000, copies the probe to
ram:9D95, verifies the complete copy through the logical mapping, and sets
PC=0x9D95. This is a direct emulator-core injection, not an OS variable or UI
launch. An execution-violation callback counts the event and calls the same
CPU_reset function used by Wabbitemu’s callback-free path. [confirmed] for the
harness behavior.
The native core produced these sequences: [confirmed]
| Page | Sequence from ram:9DBD | Probe instructions | Marker | Outcome |
|---|---|---|---|---|
07 | 07:7FF0, 07:7FF2, return ram:9DC0 | 54 | 07 | returned |
08 | 08:7FF0, 08:7FF2, return ram:9DC0 | 54 | 08 | returned |
09 | attempted 09:7FF0; no 09:7FF2 or return; one reset | 52 | A0 | violation reset |
29 | attempted 29:7FF0; no 29:7FF2 or return; one reset | 52 | A0 | violation reset |
2A | 2A:7FF0, 2A:7FF2, return ram:9DC0 | 54 | 2A | returned |
The probe seeds the marker with A0 immediately before the call. The denied
pages therefore show that the marker store at pp:7FF2 did not execute. Page
08 returns while page 09 resets, dynamically distinguishing Wabbitemu’s
lower-exclusive interval from TilEm’s inclusive interval. This establishes the
pinned emulator’s behavior only. The physical lower edge and violation state
remain unmeasured. [confirmed] for Wabbitemu; [hypothesis] for hardware.
The native binary SHA-256 is
07d56ac311cc6726d95f0e76987ce34af8814d07bcf1528f6b25375c083489f2.
It was built from pinned Wabbitemu commit 48c2dc0e6d1d87bb5cf9611efbeb0d048b19c422
and source-tree SHA-256
a8a4f97fc7952770bed317b4a477f80345894da38d14fad8f0bf0ee60aae71ba.
The derived fixture identities were: [confirmed]
| Page | Fixture ROM SHA-256 | Probe SHA-256 |
|---|---|---|
07 | ed2372b459cddd89deea6a27d00cd6f757d612c4f63db4feaf134665ad2e78cf | 87c11964b6cf67624b2eff46e1a962c56f1684dd48db931a5cb68e08c1b84b4e |
08 | b0d32c8f3af1f87c8fce8f7966ab45d588a8ed42ed9ce7708de38b4d7dc57934 | ddd023d522d301315c0f4929f348499faca08c708e96c1333bf85e32505f9534 |
09 | 7f2443e3aecceaa8c1ad60e0de4e2316caad3d17802ec3a719567a05e25a244c | f121bae475d56947bec80090bb3047fab478cc86db4dec897e4161f78df14584 |
29 | 1590ddf2681c3636e119df3759909c43b62a49a9dbc74f5a4f00d6500ae9017d | f671bdb62e6bad19f33402eb919e70631cf7cc8f00b9f7f52114d052f86cea78 |
2A | 1ee90aef8e9795ef56b668ae36560ad6a4c99938055cd0e5763b930b0f585d2a | d5f72f96562ef5e96f4ddaa12954548d210650d9ca6bec365f75f1bb6f3bad1b |
Wabbitemu reset scope
Wabbitemu’s CPU_reset is a CPU and mapper reset, not a complete hardware
reinitialization. The function writes only these groups for the TI-84 Plus
model: [standard]
| Group | Reset value |
|---|---|
PC, SP | 0x0000 |
| Interrupt mode | 1 |
Interrupt, EI block, IFF1, IFF2, HALT, and I/O flags | cleared |
| Prefix state | zero |
Ports 0x27 and 0x28 remap counts | zero |
| RAM execution bounds | 0x0000–0x03FF |
| Mapper windows | boot page 3F, Flash page 00, Flash page 00, RAM page 00 |
| Boot-map and page-0-change flags | cleared |
Legacy protected_page[4] array and selected group | zero |
The function retains the other directly seeded state. This includes general
and alternate CPU registers, I, R, bus state, RAM contents, the Flash
command state, Flash lock and bounds, protection mode and selectors, timer
frequency and T-states, delay registers, MD5 state, standard and programmable
timers, RTC, keypad and ON state, raw link, link assist, USB, GPIO, and the LCD
object. [standard]
The frontend calc_reset calls CPU_reset and then the LCD reset callback.
The LCD callback clears display memory and its queue, disables output, zeros
the coordinates and last-read latch, selects 8-bit words, and sets contrast
32. It retains the LCD access timestamp and port-0x2F delay field. No other
peripheral reset follows. [standard]
A guarded initialized-core run seeds 14 component groups before calling
CPU_reset. All 14 retain their seeded values. The reset mapping is
3F/00/00/00, the RAM bounds are 0x0000–0x03FF, and the frontend-equivalent
call produces the LCD state above while retaining last_tstate = 654321 and
lcd_delay = 61. Direct state seeding isolates field retention; it does not
reproduce a physical warm or cold reset. [confirmed] for the pinned Wabbitemu
run.
An execution violation calls CPU_reset inside CPU_opcode_fetch, then
continues the same CPU_step. A seeded FLASH_PROGRAM violation ends the
command state, fetches boot bytes 3E 07, executes LD A,0x07, and finishes
at PC=0x0002 after seven T-states. A seeded FLASH_ERROR violation fetches
opcode 0x3E, returns status byte 0xE0 for the immediate read, clears the
error flag, executes LD A,0xE0, and also finishes at PC=0x0002. The command
step remains FLASH_ERROR in the second case. This is an emulator control-flow
quirk, not a physical reset model. [confirmed]
The reset-retention manifest guards OS 2.55MP ROM SHA-256
7d9a7d96d89fc552ebee6afdbdd011fdc6047be9c16d308245dff07eb1f7bd6d
and native-binary SHA-256
386be74e738f2a0f9ad17f12bae4cd44994b5a73835ab10d488c7b8232afd87e.
RAM instruction fetches
TilEm x4 reduces a physical RAM byte offset $a$ to a 1 KiB chunk address. For
port-0x21 mode $t = 0,1,2,3$, it computes: [standard]
$$ \begin{aligned} M_t &= (\mathtt{0x8000} \ll t) - \mathtt{0x400} \\ m &= a \mathbin{\&} M_t \\ \mathit{allowed} &= \mathtt{port25}\cdot\mathtt{0x400} \le m \le \mathtt{port26}\cdot\mathtt{0x400} \end{aligned} $$
The mask clears the low ten address bits, so the comparisons operate at 1 KiB
granularity. The upper comparison is inclusive. A port-0x26 value of 0x20
therefore includes the complete 0x8000–0x83FF chunk whenever $M_t$ can
produce 0x8000. [standard]
Coverage with the boot bounds
The table covers the TI-84 Plus’s eight physical RAM pages. Page notation uses
the ordinary selector spelling 0x80–0x87. “Chunk 0” means the first 1 KiB
at page offset 0x0000–0x03FF. [standard]
| Mode | TilEm mask | Repetition | Fully executable pages | Partly executable pages |
|---|---|---|---|---|
| 0 | 0x7C00 | 32 KiB | 0x81, 0x83, 0x85, 0x87 | none |
| 1 | 0xFC00 | 64 KiB | 0x81, 0x85 | chunk 0 of 0x82 and 0x86 |
| 2 | 0x1FC00 | 128 KiB | 0x81 | chunk 0 of 0x82 |
| 3 | 0x3FC00 | 256 KiB | 0x81 | chunk 0 of 0x82 |
Modes 2 and 3 have the same coverage within the first 128 KiB. Their masks
diverge only when an address reaches the next 128 KiB. Reversing ports 0x25
and 0x26 denies every RAM chunk in TilEm because the inclusive interval is
empty. [standard]
Wabbitemu comparison
Wabbitemu’s source comments list the fully executable page pattern implied by the four modes. Its executable predicate contains this expression: [standard]
if (bank->page & (2 >> (prot_mode + 1)))
return TRUE;
For mode 0, the shifted value is 1, so every odd RAM page returns early. For
modes 1–3, the shifted value is zero. Those modes fall through to one global
address comparison instead of applying a repeating mask. The default bounds
produce this actual coverage: [standard]
| Mode | Wabbitemu fully executable pages | Partly executable pages |
|---|---|---|
| 0 | 0x81, 0x83, 0x85, 0x87 | chunk 0 of 0x82 |
| 1 | 0x81 | chunk 0 of 0x82 |
| 2 | 0x81 | chunk 0 of 0x82 |
| 3 | 0x81 | chunk 0 of 0x82 |
The extra mode-0 chunk comes from Wabbitemu’s inclusive global range check, not its page shortcut. Its mode-2 and mode-3 coverage happens to match TilEm within 128 KiB under these bounds. The source arithmetic still differs for other RAM sizes and bound values. These implementation results are not ASIC evidence.
Wabbitemu stores ram_lower and ram_upper as 16-bit unsigned fields. Its
port handlers multiply the 8-bit port value by 0x400 before assigning those
fields. Values 0x40–0xFF therefore wrap modulo 0x10000. For example,
writing 0x40 to both ports produces the implemented interval
0x0000–0x03FF, not 0x10000–0x103FF. TilEm retains the wider products.
[standard] for the emulator sources; [hypothesis] for physical high-value
behavior.
Guarded RAM execution runs
Two guarded runners exercise the predicates through opcode fetches. Both use a
six-byte target routine that stores a case marker at 0x8478 and returns. The
source program reads back all six target bytes before it seeds 0x8478 with
0xA0 and calls the target. A returned case records its case marker. A denied
case retains 0xA0, omits the target store at logical target +2, and records
one reset. [confirmed] for the pinned emulator runs.
The Wabbitemu adapter cold-boots the exact ROM through the retail protection
sequence. It then configures the requested RAM fields and injects the guarded
source at physical RAM page 1, ram:9D95, plus the target routine. This is a
direct emulator-core injection. Every default-bound case reached the baseline
at 3F:4223 after 134,845 instructions and 1,746,999 T-states. Returned cases
executed 47 injected instructions; denied cases reset on instruction 45.
[confirmed]
The TilEm runner changes only the immediate byte at 3F:41D6 when it selects
a nonzero mode. Mode 1 changes that byte from 0x00 to 0x10; the derived ROM
SHA-256 is
47b38fa0fd747529dea85d4fe54d24bafdadeee29c8ade82014f4452ef52699f.
The OS launches a self-installing assembly program through the normal variable
and UI path. The program writes the marker through data accesses before the
guarded call. [confirmed]
The runtime comparison produced these boundary results: [confirmed]
| Mode | Physical target | TilEm | Wabbitemu | Predicate detail |
|---|---|---|---|---|
| 0 | page 0x82, offset 0x03F0 | violation reset | returned | Wabbitemu includes page-2 chunk 0 through its global range; TilEm’s mode-0 mask maps it below the lower bound |
| 0 | page 0x82, offset 0x0400 | violation reset | violation reset | first target in chunk 1 |
| 1 | page 0x82, offset 0x03F0 | returned | returned | target lies wholly inside chunk 0 |
| 1 | page 0x82, offset 0x0400 | violation reset | violation reset | first target in chunk 1 |
| 1 | page 0x85, offset 0x3FF0 | returned | violation reset | TilEm repeats the full-page window after 64 KiB; Wabbitemu uses one global range |
| 1 | page 0x86, offset 0x03F0 | returned | violation reset | TilEm repeats the page-2 upper chunk after 64 KiB; Wabbitemu uses one global range |
The TilEm target fetch occurs seven clocks after each CALL. Allowed marker
routines return 44 clocks after the call. Denied targets reset seven clocks
after the attempted fetch. TilEm completes the first target opcode during that
interval. Wabbitemu’s callback records the violation and invokes the same
CPU_reset function as its callback-free path. Timing between the two runners
is not compared because the Wabbitemu harness reports instruction counts, not
a TLMT clock trace.
The other Wabbitemu cases cover all four modes under the boot bounds: [confirmed]
| Mode | Returned targets | Violation-reset targets |
|---|---|---|
| 0 | page 0x81 offset 0x3FF0; page 0x82 offset 0x03F0; page 0x83 offset 0x3FF0 | page 0x82 offset 0x0400; page 0x84 offset 0x0000 |
| 1 | page 0x81 offset 0x3FF0; page 0x82 offset 0x03F0 | page 0x82 offset 0x0400; page 0x85 offset 0x3FF0; page 0x86 offset 0x03F0 |
| 2 | page 0x81 offset 0x3FF0; page 0x82 offset 0x03F0 | page 0x82 offset 0x0400; page 0x83 offset 0x0000 |
| 3 | page 0x81 offset 0x3FF0; page 0x82 offset 0x03F0 | page 0x82 offset 0x0400; page 0x83 offset 0x0000 |
A separate Wabbitemu run configured both chunk ports to 0x40. The native
report recorded the wrapped bounds 0x0000–0x03FF. Mode-0 page 0x80,
offset 0x0000, returned; page 0x82, offset 0x0000, reset. The first result
comes from the wrapped global interval. The second lies outside it and has no
odd-page shortcut. [confirmed] for Wabbitemu; [hypothesis] for hardware.
The pinned identities for the cross-emulator cases were:
| Mode and target | TilEm probe SHA-256 | TilEm trace SHA-256 | Wabbitemu probe SHA-256 |
|---|---|---|---|
mode 0, 0x82+0x03F0 | a0853c1ea1f900a7b8b4c26d1091e5696265b993a214d20836c182743ae330c3 | 0bde946b277f0c3fe7c6040931ea1df6c265aa11d4fad6394cfeea5955dfe18b | 783d757f767b0d89df7c68881413e0cb47a6652da2c94829f90972e3eb2a64cb |
mode 0, 0x82+0x0400 | 9f82e2df6960cc6e0658c1db4b19e755bd544105be73fa476f8b17e34527a116 | 66853fc88e6a934577f1e70916df012f77e02ea7ab7e77b2caa4a9cda0a5e602 | d068ef192978d9fbddada76d6f55320263315ad2b3344e64751cc33f9aa58d5f |
mode 1, 0x82+0x03F0 | 7ef4086cf9fe4e938215cf3592435d13fcd0874a239e58fbdc50c78719531ff2 | 31601907572c2060adaa76d2031a138e225a5e73bdc8f84c50505178e1e871ee | f0843119d9a19ab5f5578f61160a8cb5ce723d12ed2b3ea13a5d9cdfc8857ce7 |
mode 1, 0x82+0x0400 | 1531839a1d11895ad14ddada9974da4c307eb1ba09b5b660b3f1858bf2659a7f | 8a9ded0bb3479a86587579ae647a4c2604689f3f0dea9beba96b7fb1117415a9 | d2ba9523c63f7645f61cfad45677b9afc3bc47a64b251f2d8f3daafadc8525b0 |
mode 1, 0x85+0x3FF0 | 4996653aca01db9c7ce67d7a367810cf4a07ee42fa65991920232a81d6b3074c | a76f11d993e1b5cf6e19feca1af4321637670a9f1c7f5232159096ea2ba0839f | 857a38aa6ccf163ebac779c775d812e9ad9df844c870a9bbc42fdad7932da959 |
mode 1, 0x86+0x03F0 | 8851278b8f3b54b7a7e7a0ff206b03f98bbec6528a3d705598a054dcf5f501a0 | e976f06992278db20f1b00c6faf2caf4140d7635de1af180f6491d103e0719ce | 3c989491f4031cfac972cd72af55824d6a1ca8f384315ac3fbbd5ed0ad15a3c0 |
The TilEm assembly source SHA-256 is
eafb257ff0190bfa8417269c981ab0ff94508e92a31b716c01814b0afe4bb2ca.
The Wabbitemu assembly source SHA-256 is
e21fe4374eec887b4877ad27ccb77dd458dcb38c24e48cb167ebb1438fc8d43c.
These runs establish emulator behavior only. Physical mode repetition,
high-value wrapping, chunk endpoints, and reset state remain open measurements.
Port 0x24 larger-device extension
WikiTI assigns two high Flash-bound bits to port 0x24: bit 0 extends port
0x22, and bit 1 extends port 0x23. TilEm’s color xc model uses exactly
those two bits when it compares pages. Its TI-84 Plus x4 model has no
port-0x24 case. [standard]
The complete OS 2.55MP ROM scan finds no direct port-0x24 instruction and no
access resolved from a nearby literal load into C or BC. Computed accesses
across calls or control-flow joins remain outside this static proof.
[confirmed]
Port 0x24 is therefore a family extension, not part of the confirmed retail
TI-84 Plus initialization path. Wabbitemu registers it on the TI-84 Plus-family
device, but its two high-bit assignment expressions lack parentheses around
the masked bit before shifting. Both expressions evaluate to zero for an
8-bit bus value under C operator precedence. [standard]
Native protected-register confirmation
The guarded initialized-core probe finds active, protected handlers at every
port from 0x22 through 0x26. The reset core reads
0x10/0x30/0x00/0x00/0x00. These are Wabbitemu initialization values, not the
retail boot values in the table above. All five writes are rejected while the
in-memory Flash lock remains closed. Reads remain active. [standard]
The probe then opens the emulator’s lock directly and seeds the internal Flash
bounds as 0x01A5 and 0x02B6. Writes of 0xCC and 0xDD to ports 0x22
and 0x23 produce 0x01CC and 0x02DD, confirming that both low-byte
handlers preserve their seeded high bytes. Writing 0xFF to port 0x24
reads back 0xFF but changes the internal bounds to 0x00CC and 0x00DD.
The native result matches Wabbitemu’s precedence defect. It does not test a
physical larger-device extension. [standard]
The same run writes four edge values to both RAM-bound ports: [standard]
| Written byte | Port-0x25 read | Internal lower field | Port-0x26 read | Internal upper field |
|---|---|---|---|---|
0x3F | 0x3F | 0xFC00 | 0x3F | 0xFFFF |
0x40 | 0x00 | 0x0000 | 0x00 | 0x03FF |
0x41 | 0x01 | 0x0400 | 0x01 | 0x07FF |
0xFF | 0x3F | 0xFC00 | 0x3F | 0xFFFF |
The manifest records the exact ROM and native-binary hashes. Opening the lock and seeding high fields are direct emulator-core operations. This mode does not execute the ROM’s protected-byte sequence or attempt an opcode fetch.
_SetFlashLowerBound
The official bcall name does not match the port written by its body.
_SetFlashLowerBound = 80CF maps to 3F:4784, which writes A to port
0x23 — the upper end of the modeled forbidden interval: [confirmed]
3F:4784 nop
3F:4785 nop
3F:4786 im 1
3F:4788 di
3F:4789 out (0x23),a
3F:478B di
3F:478C ret
Flash must already be unlocked for either emulator to accept the write. The
routine preserves A and leaves interrupts disabled. [confirmed] for the
routine; [standard] for the modeled write gate.
Mapping and forced overlays
Execution protection runs after logical-to-physical page resolution. TilEm
applies ports 0x27 and 0x28 first, then chooses its Flash or RAM predicate
from the resulting physical page. [standard]
Wabbitemu chooses its Flash-versus-RAM branch from the underlying bank. Its RAM path also evaluates the page shortcut before replacing the global address for a forced overlay. The two emulators can therefore disagree when an overlay forces RAM over a Flash-backed window or substitutes RAM page 0 or 1. See Paging. [standard]
The guarded Wabbitemu mapper run places a NOP in forced RAM over an underlying Flash HALT. Independent mode executes the NOP; paired mode disables the overlay and executes the HALT. This confirms the fetched-byte routing through the initialized core. Both underlying Flash pages are permitted by the boot protection bounds, so the run does not distinguish which protection predicate Wabbitemu evaluated. That ordering remains established by pinned source. [standard]
The normal OS boot and homescreen traces leave both overlays disabled, so this difference does not affect those executed paths. [confirmed]
MAME 0.287 omission
MAME’s TI-84 Plus I/O map does not register ports 0x22–0x28. Its opcode
fetch path reads the mapped Flash or RAM without an execution-protection
predicate. Port 0x14 records an unlock value, but the paging and Flash-write
paths do not consult it. [standard]
A guarded native run writes patterns to ports 0x22–0x28; all seven ports
still read zero. With port 0x21 = 0x33, writes CC DD AA 10 20 to
0x22–0x26 also read back as five zero bytes while a 50-T-state loop
continues executing from RAM page 0. This covers one allowed-by-absence fetch
path. It does not emulate any boundary or violation response. [standard]
MAME therefore cannot test any boundary or violation described on this page.
The driver is marked MACHINE_NOT_WORKING, so this omission is a driver limit,
not evidence that the physical ASIC lacks execution protection. See
Flash emulator comparison for the resulting
Flash-command behavior. [standard]
Reproducing the models
tools/ti84re/hardware/execution_protection.py contains side-effect-free predicates and RAM
coverage enumeration. The focused CLI prints both emulator results:
$ python3 -m ti84re.hardware.describe_execution_protection flash
Flash bounds 0x08-0x29
page 0x07: TilEm=allow Wabbitemu=allow
page 0x08: TilEm=deny Wabbitemu=allow
$ python3 -m ti84re.hardware.describe_execution_protection ram --compare-wabbitemu
RAM chunks 0x10-0x20
mode 0 TilEm-mask=0x7C00
page 0x80: TilEm=- Wabbitemu=-
page 0x81: TilEm=all Wabbitemu=all
Use --json for machine-readable output. Custom --lower, --upper,
--mode, and --ram-pages values expose boundary cases without modifying an
emulator.
The guarded trace runner builds all four boundary fixtures in a fresh output directory, runs them, and rejects a classification that disagrees with the TilEm predicate:
probe_parent=$(mktemp -d)
nix develop -c python3 -m ti84re.hardware.run_execution_protection_probe \
--tilem "$TILEM" --output-dir "$probe_parent/run" --json
tools/ti84re/hardware/execution_protection_fixture.py holds the exact-ROM patching,
machine-code validation, reusable assembler entry point, packaging, and trace
classifier. The CLI retains each derived ROM, program pair, log, trace, and a
hash-complete manifest.json in the requested directory.
The direct reset probe supplies the missing single-opcode control. Its build
and run commands are under “Reset and execution exception” in the repository’s
tools/notes/emulator-probes.md. The shared tools/ti84re/emulators/tilem/core.py library validates
the source tree and runs the binary. tools/ti84re/emulators/tilem/reset.py parses the native
report and checks the reset and retention vectors against the source model.
The Wabbitemu CLI uses the same fixture library and adds page 09 to
distinguish the two lower-edge predicates:
wabbit_probe_parent=$(mktemp -d)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_execution_probe \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$wabbit_probe_parent/run" --json
It refuses an existing output directory. Each native report must contain the boot-register snapshot, exact injection mapping, call and target visit counts, marker value, reset count, fixture hashes, and native-binary hash.
The RAM runners accept repeatable MODE:PHYSICAL_PAGE:PAGE_OFFSET targets.
Their defaults cover the cross-emulator disagreements and all Wabbitemu modes:
tilem_ram_parent=$(mktemp -d)
nix develop -c python3 -m ti84re.emulators.tilem.run_ram_execution_probe \
--tilem "$TILEM" --output-dir "$tilem_ram_parent/run" --json
wabbit_ram_parent=$(mktemp -d)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_ram_execution_probe \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$wabbit_ram_parent/run" --json
The Wabbitemu CLI also accepts custom --lower-chunk and --upper-chunk
values. The 0x40 wrap case is reproducible with:
wrap_parent=$(mktemp -d)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_ram_execution_probe \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$wrap_parent/run" \
--lower-chunk 0x40 --upper-chunk 0x40 \
--target 0:0:0 --target 0:2:0 --json
protected_port_parent=$(mktemp -d /tmp/ti84-protected-port.XXXXXX)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_protection_port_probe \
--rom tools/rom.bin \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$protected_port_parent/run" --json
The boot bytes can be recovered independently:
$ python3 -m ti84re.rom.disassemble 0x3f --start 0x41d5 --end 0x4206
$ python3 -m ti84re.rom.analyze_io 0x21 0x22 0x23 0x24 0x25 0x26 --summary
Flash write-disable bcall [confirmed]
_FlashWriteDisable = 0x4F3C, body 3C:66D5, controls Flash programming,
not the execution-protection bounds described above. After four padding
NOPs, it saves AF, clears A, executes DI, selects IM 1, writes
0x00 to port 0x14, executes another DI, restores AF, and returns. The
intervening OR A makes its JP NZ,ram:0000 unreachable on this path.
The body restores the caller’s AF but deliberately leaves maskable
interrupts disabled. Callers that require interrupts must re-enable them after
the bcall; they must not infer interrupt restoration from the saved AF.
A controlled trace reaches the body once, observes OUT (0x14),A with
A = 0x00, returns to the fixture, and then executes an explicit EI. The
reduced result is in tools/data/community-bcall-semantics.csv. [confirmed]
under TilEm; the physical Flash gate was not written on hardware in this test.
Community execution techniques
Crabcake’s original source uses two model-specific methods. On a 6 MHz
TI-83 Plus it uses a Flash-unlock exploit, locates a protected
OUT (0x16),A routine on Flash page 0x1F, and calls it with 0x00 or 0x07.
Separate exploit code changes the port-0x05 mapping. On the
TI-83+SE/TI-84+/SE family it does not rewrite ports 0x25 or 0x26. With
interrupts disabled, it swaps all 0x4000 bytes between physical RAM pages
0x80 and 0x83, then maps page 0x83 into bank C. Code above 0xC000
remains at the same CPU address while residing on an executable odd physical
page. The Crabcake release source contains both paths. [confirmed]
The TI-84-family cleanup restores port 0x05 to an assumed normal value,
unconditionally enables interrupts, and has no error handler around the swap.
These operations are explicit in the release source. [confirmed] The release
does not distinguish the later 48 KiB alias model. That hardware remains
unsupported without a physical probe.
zStart 1.3.013 implements its Execute >C000 option as a persistent policy.
Its menu toggle first writes the assumed retail values 0x10 and 0x20 to
ports 0x25 and 0x26, regardless of the new option state. If the option is
enabled, later configuration and ON-script paths call unlockC000, which
writes 0x00 and 0xFF after unlocking Flash writes. This is not a per-launch
save/restore wrapper. The zStart release source confirms this control flow.
[confirmed]
The Swords 2 source release contains FULLRENE.8xv, a 229-byte AppVar payload
with the Axiom DE C0 signature. Both embedded command bodies contain this
sequence: [confirmed]
LD A,0x10
JR NC,+1
XOR A
OUT (0x25),A
The incoming carry therefore selects 0x10 or 0x00 for port 0x25, and
neither command body writes port 0x26. One command immediately calls
_FlashWriteDisable and returns. The other restores port 0x06 from the stack
before the same bcall and return. [confirmed] The artifact does not establish a
hardware model matrix or behavior after an OS error. Those details remain
[hypothesis].
tools/data/execution-protection-observations.csv records the emulator and
physical-probe classifications in the preceding sections. It does not classify
Crabcake, zStart, or Fullrene.
Resolved findings and open hardware tests
- The boot writes
00,08,29,10, and20to ports0x21,0x22,0x23,0x25, and0x26through protected byte sequences. [confirmed] _SetFlashLowerBoundwrites port0x23, despite its official name. [confirmed]- TilEm denies the inclusive Flash interval. Four guarded TilEm traces execute
pages
07and2Aand reset on attempted fetches from pages08and29. Wabbitemu allows its programmed lower page: guarded native runs execute pages07,08, and2Aand reset on attempted fetches from pages09and29. [standard] for the source comparison; [confirmed] for the pinned emulator runs. - TilEm applies a repeating RAM mask and inclusive 1 KiB chunk bounds.
Wabbitemu’s modes 1–3 omit the intended page shortcut and its 16-bit fields
wrap high chunk values. Guarded runs exercise the mode-0 extra chunk, both
mode-1 repetition disagreements, the common chunk edge, all four Wabbitemu
modes, and the
0x40wrap case. [standard] for source behavior; [confirmed] for the pinned emulator runs. - The retail ROM has no statically resolved port-
0x24access. [confirmed] - jsTIfied implements page-level Flash and RAM execution groups, but its stored
ports
0x25and0x26do not affect instruction fetches. [standard] - A guarded initialized-core Wabbitemu run verifies the common protected-write
gate across ports
0x22–0x26, the port-0x24high-field clearing defect, and 16-bit RAM-bound wrap at0x40and above. [standard] - A guarded initialized-core Wabbitemu run confirms forced-RAM fetch routing in independent mode and underlying-window fetch routing in paired mode. It does not dynamically distinguish the Flash-versus-RAM protection predicate. [standard]
- Wabbitemu’s low-level reset retains most peripheral and Flash state. Its
frontend adds only an LCD reset. A guarded initialized-core run verifies all
14 seeded retention groups and shows that an execution violation continues
the interrupted
CPU_stepthrough one boot instruction. [standard] for the source model; [confirmed] for the pinned run. - TilEm’s full reset reinitializes eight CPU, peripheral, and ASIC groups while retaining memory plus selected Flash, link, RTC, scheduler, and debugger fields. A forbidden opcode completes before that reset. A guarded direct-core run verifies the reset inventory and a surviving RAM-store side effect. [standard] for the source model; [confirmed] for the pinned run.
Physical tests must determine whether page 0x08 executes, what exception or
reset state follows a violation, and whether lower-greater-than-upper disables
each protection range. The read-only physical fetch
suite prepares the
retail-mode Flash edge and RAM chunk tests. Tests should also sweep all 1 KiB
RAM boundaries in all four modes, repeat them with ports 0x27 and 0x28
active, and record the register state after warm and cold resets. Until then,
emulator agreement is only a test oracle for emulator behavior.
Sources
| Source | Use |
|---|---|
OS 2.55MP and boot 1.03 ROM, especially boot_execution_protection_init and _SetFlashLowerBound | protected writes and bcall body |
TilEm calcs.c, z80.c, x4_init.c, and x4_memory.c | full reset sequence, TI-84 Plus reset fields, Flash and RAM fetch predicates, and post-opcode exception handling |
TilEm x4 I/O model at f56ad63 | protected register writes and mask updates |
Headless TilEm fork at 8da54573ac49fe271fa22c60924b4c6a7cb9639f | boundary execution traces; binary SHA-256 1c1f7dbe04fe074c2b9aca1657d0eb5ac5cfd1f7cbd480725eb7fb39b8126f33, x4_memory.c SHA-256 ddaa1e45330e3e4ad49486bd5c3675a0a0dff01bfda4d01817ba3387e309ac89 |
TilEm xc memory model at f56ad63 | port-0x24 high-bound bits |
Wabbitemu core.c at 48c2dc0 | Flash and RAM fetch predicates, CPU_reset, and execution-violation control flow |
Wabbitemu calc.c and lcd.c | frontend reset scope and LCD reset fields |
Wabbitemu device.c at 48c2dc0 | global protected-port write gate |
Wabbitemu 83psehw.c at 48c2dc0 | port handlers and port-0x24 implementation |
MAME ti85.cpp and ti85_m.cpp at mame0287 | absent execution-protection ports and unused Flash-unlock state |
jsTIfied deployed 20170706a artifact and readable mirror | protected writes, page-level run_lock, violation reset, and unused stored RAM-bound ports |
WikiTI port 0x22, 0x23, 0x24, 0x25, and 0x26 | public register descriptions, treated as secondary evidence |
Crabcake release archive, SHA-256 84f6660c86f715e09e03637b19df47abe46b86906ed34791bc4281959186f71e | 6 MHz protected-port path and TI-84-family page-swap path |
zStart 1.3.013 release archive, SHA-256 7a1b7c69c85030b412bb6ea11ae71ac608b9882a9de3ab7dbef1faf69519c5e9 | persistent Execute >C000 configuration and ON-script restoration |
Swords 2 source release, archive SHA-256 830878e3449221664b85eb3996992ad0f8b46b7e57183c337930b7e78e5a3397; FULLRENE.8xv SHA-256 327ea2ce2a603febc46490d9758cffc12c9fc926fc1d773a0d53a5ccdf5d4ec3 | original Fullrene Axiom command bodies |
The bcall mechanism
bcall is how the OS spans 1 MiB with a 64 KiB CPU: a routine on any page calls a routine on any other page without knowing where it physically lives.
The call site
RST 28h ; opcode 0xEF
.dw <bcall_id> ; 2-byte little-endian ID immediately after
rst 28h is a 1-byte Z80 call 0028h. So the return address pushed on the stack points at the 2-byte ID. The dispatcher reads the ID through the return address, then fixes the return to skip those 2 bytes — i.e. execution resumes at call_site + 3. [confirmed] (modeled in Ghidra by setting each rst 28h’s fall-through to +3 and typing the ID as a word.)
The dispatcher — bcall_dispatcher @ ram:2a2f [confirmed]
From the decompiler:
- Read the 2-byte ID
dwfrom the caller’s return address. - Decode the ID’s high bits:
bit15/bit14select the address class; the low bits form the table offset. - Bank the bcall table page into slot A (via the helper at
ram:181c, which setsport_mapBankA). - Read the 3-byte table entry: target address (2) + target page (1).
- Bank the target page into slot A (
port_mapBankA = page), save the previous page. callthe target. On return, restore the previous page and resume the caller at+3.
The jump table — flash page 0x3B [confirmed]
- Located at the start of physical flash page
0x3B(file offset0x3B*0x4000 = 0xEC000). - 3-byte entries:
addr_lo, addr_hi, page. IDs step by 3 from0x4000, so entry for ID X is at table offsetX-0x4000. - Resolution method (
tools/Python): scored all 64 pages by how many named IDs produced a valid(addr∈4000..7FFF or page-0, page<0x40)entry; page0x3Bscored highest (the page-selection heuristic uses a conservative validity filter chosen only to pick the table). Once0x3Bis selected and applied, 645 entries resolve and are live-confirmed. Of those IDs, 623 also appear in the included SDK equates and 22 are project-inferred additions. - Validation: known bcalls land exactly where expected —
_PutS→01:5C39,_GetKey→06:491E,_ClrLCDFull→01:60E4,_GetCSC→00:04B2,_CreateReal→00:10B8.
tools/symbols/bcall_targets.txt holds 645 resolved main-table bcall rows.
The retail boot table has 87 populated entries. tools/symbols/bcalls8x_targets.txt
holds the 83 rows with official SDK names; four populated slots have only
project-inferred names in the bcall index.
tools/ti84re/rom/resolve_bcalls.py emits the official-name rows only when page 3F has
the retail prefix and page 2F contains the companion USB payload; its
BootFree guard otherwise leaves only diagnostic comments.
tools/ghidra/ApplyBcalls.java disassembles and names the confirmed bodies.
tools/ghidra/BcallEvidenceStudy.java then provides a read-only listing, reference,
and decompiler dump for a selected set of IDs. For example:
nix develop -c ghidra-analyzeHeadless "$PWD" ti84 \
-process -noanalysis -readOnly -scriptPath tools/ghidra \
-postScript BcallEvidenceStudy.java tools/symbols/bcall_targets.txt \
/tmp/bcall-evidence.txt 4030 4ED6 50C8
Jump-table ID ranges
The dispatcher (bcall_dispatcher) decodes the ID’s top two bits to pick the table page: bit 15 set → page-byte 0x7F (masked & 0x3F → page 0x3F); bit 14 set → 0x7B (→ page 0x3B); with neither bit set it falls through to lookup_bcall_table_page (ram:2ADA). The two tables real bcall IDs use:
0x4xxx–0x7FFF(bit 14 set): the main table on flash page0x3B, entry at offsetID − 0x4000(645 live-confirmed bcalls: 623 IDs also present in the included SDK equates and 22 project-inferred additions).0x8xxx(bit 15 set): the retail boot table is on physical page3F, indexed byID & 0x7FFF. Its real entries occupy IDs0x8018–0x80D2and0x80E4–0x8129; bytes3F:40D5–3F:40E3between those ranges are executable dispatch-stub bytes, not five table entries.D84PBE1.8Xvsupplies the retail page3F;D84PBE2.8Xvsupplies the companion USB boot support page2F. Most entries resolve to3F:addr; USB entries such as_AttemptUSBOSReceive(80E4) and_InitUSB(8108) resolve to2F:addr.tools/ti84re/rom/resolve_bcalls.pyrefuses to emit these targets from a BootFree-substituted page. [confirmed]
Both resolved table formats are 3-byte entries: target address (little endian) plus page byte masked with & 0x3F.
RST shortcuts (fast inlined bcalls) [confirmed]
Five of the RST vectors are 1-byte fast paths for the hottest routines (each JPs to its page-0 handler, which is also reachable as a bcall — the table maps the same address). rst 28h is the bcall dispatcher itself (not a shortcut, and ram:2A2F is not a bcall target); it is listed here only to complete the vector set:
| Opcode | Vector → target | Routine |
|---|---|---|
rst 08h | 0008→1A2F | _OP1ToOP2 (copy FP reg) |
rst 10h | 0010→0E65 | _FindSym (VAT lookup) |
rst 18h | 0018→155C | _PushRealO1 (push OP1 to FPS) |
rst 20h | 0020→1B01 | _Mov9ToOP1 (copy 9 bytes → OP1) |
rst 28h | 0028→2A2F | bcall dispatcher |
rst 30h | 0030→229E | _FPAdd (float add) |
All six match the documented TI-83+/84+ RST assignments — strong cross-confirmation of the table resolution.
bjump — the sibling mechanism (OS-internal cross-page calls)
Besides bcalls, the OS calls its own cross-page routines via bjump. Its
encoding is:
CALL cross_page_jump ; = CALL ram:2B09
.dw addr
.db page
cross_page_jump reads the stacked return address (its caller’s, via an
SP-relative load — it does not POP it), fetches the 2-byte target + 1-byte
page from the inline descriptor there, rewrites the return frame past those 3
bytes, banks the page (& 0x3F), and returns into the target. The target’s
RET then returns to the bjump’s caller, so it behaves like a call that
consumes the 3 inline bytes.
There is a trampoline table in the page-0 address range ram:3B01–ram:3D0A: 87 packed 6-byte entries, each a bjump to a hot OS routine on another page (ram:3D0B already begins a separate CALL ram:2B49 table). The static Ghidra database models it in the page-0/ROM address space; whether the table is copied to RAM at runtime remains a hypothesis. Code invokes a routine by CALL ram:3Bxx into the table. tools/symbols/bjumps.txt lists every entry’s (offset → page:addr); tools/ghidra/RamRoutines.java marks the inline .dw/.db as data and comments each target.
Example: _PutMap’s glyph blitter is reached via the trampoline at ram:3B3D → 07:4588.
Inline bjumps. Besides this trampoline table, the three-line bjump encoding
above appears in packed dispatch tables and inside OS routines. The target returns
to the bjump caller, so cross_page_jump consumes the three inline descriptor
bytes as a non-returning tail-jump. tools/ghidra/FixInlineBjumps.java runs before and
after the scripts that seed parser handlers and reviewed function entries. Each
pass marks every disassembled inline site, including the 87 trampoline-table
entries. Raw byte matches outside disassembled code are not counted. [confirmed]
Limitations
- Keep the BootFree guard in place when regenerating from emulator-derived ROM images.
- Some bcalls are thunks: e.g.
_FindSym’s page-0 entry usescross_page_jumpto reach the real body on page 0x07.
Interrupts (IM1)
TI-84 Plus OS 2.55MP — Interrupt masks, status, acknowledgement, dispatch, and low-power wake.
The TI-84 Plus OS runs the Z80 in interrupt mode 1 (IM1) and polls the ASIC’s interrupt status. This page separates the USB event gate from the legacy controller at ports 0x03 and 0x04, then follows acknowledgement, priority, and HALT wake behavior.
Evidence layers
| Evidence | Scope | Confidence |
|---|---|---|
Page-0 bytes from im1_vector at ram:0038 through ram:0244 | IM1 entry, USB and legacy gates, source-test order, handlers, acknowledgement, and exit | [confirmed] |
Power-cycle trace from tools/macros/power-cycle.macro | OS mask writes, low-power HALT, ON wake, status read, debounce, and restoration | [confirmed] |
WikiTI ports 0x03 and 0x04 | Bit-level enable, status, clear-on-zero, timer-rate, mapping, battery-selector, and low-power contract | [standard] |
TilEm commit f56ad63 and Wabbitemu commit 48c2dc0 | Two executable interpretations of the registers and their fidelity gaps | [standard] |
MAME 0.287 ti84pv3 driver and Lua I/O trace | Third implementation, headless ON-wake execution, and explicit MACHINE_NOT_WORKING gaps | [standard] |
| Guarded TilEm direct-core interrupt probe | Stored-mask readback, internal policy, acknowledgement, ON/link edges, timer callbacks, and reset ordering | [standard] |
| Guarded TilEm direct-core link probe | Raw link-activity and assist idle, receive, and error interrupt transitions | [standard] |
| Guarded Wabbitemu interrupt edge probe | Initialized-core mask, timer, acknowledgement, completion, and low-power transitions | [standard] |
| Guarded MAME legacy-interrupt probe | CPU-I/O-space status, mask, ON-edge, fixed-timer, and soft-reset observations | [standard] |
The ROM proves how OS 2.55MP uses the registers. Public notes and emulators describe behavior inside the ASIC that the ROM cannot prove by itself. Emulator agreement is supporting evidence, not physical confirmation.
IM1 entry and context
IM1 accepts a maskable interrupt at im1_vector, the fixed address ram:0038.
The vector jumps to int_entry_save_alt_regs at ram:006D, which swaps AF,
BC, DE, and HL with the alternate register set. The normal exit swaps
them back, executes EI, and returns with RETI. [confirmed]
ram:0038 jr ram:006D
ram:006D ex af,af'
ram:006E exx
ram:006F in a,(0x55)
ram:0071 xor 0xFF
ram:0073 and 0x1F
ram:0075 jr z,ram:003A
The handler uses the alternate general registers as its working context. It assumes IY = flags at 0x89F0 and uses the interrupted stack. It does not push IY, IX, or a complete register frame at entry. [confirmed]
The normal exit restores the standard OS mask after source-specific work: [confirmed]
ram:00E4 ld a,0x0B
ram:00E6 bit 0,(iy+0x16)
ram:00EA jr z,ram:00EE
ram:00EC add a,0x04 ; select 0x0F when timer 2 is wanted
ram:00EE out (0x03),a
ram:00F0 ex af,af'
ram:00F1 exx
ram:00F2 ei
ram:00F3 reti
USB gate and legacy controller
Port 0x55 is the active-low USB interrupt summary. The three instructions at
int_entry_save_alt_regs + 0x02 invert and mask its low five bits. A result of
zero jumps directly to interrupt_legacy_status at ram:003A. A nonzero
result enters the USB activity-hook and port-0x56 event paths before the
handler considers the legacy controller. [confirmed]
This ordering does not make port 0x55 a summary of ON, standard-timer, or legacy link requests. Those sources appear at port 0x04. Port 0x56 is a USB line-event bitmap, not the mask for port 0x04. [confirmed] for the separate ROM paths; [standard] for the register roles.
The disconnected TilEm x4 model returns 0x1F from port 0x55 and zero from
port 0x56. Its ordinary trace therefore takes
int_entry_save_alt_regs + 0x02 → interrupt_legacy_status without USB event
work. [standard]
See USB ASIC and link assist for the port-0x56 event-bit branches and page-35 handlers.
Port 0x03: mask, acknowledgement, and power mode
Port 0x03 controls the four legacy interrupt sources and the ASIC’s behavior when the Z80 executes HALT. Public notes document readback for enable bits 0, 1, 2, and 4. TilEm and Wabbitemu return the complete stored byte, but physical readback of bit 3 is not documented. [standard] for the public fields and emulator behavior; [hypothesis] for physical bit-3 readback.
| Bit | Meaning | Effect of writing zero | Evidence |
|---|---|---|---|
| 0 | ON interrupt enabled | disable and acknowledge the ON request | Public register contract; OS writes and both emulators [standard] |
| 1 | standard timer 1 enabled | disable and acknowledge timer 1 | Public register contract; OS writes and TilEm [standard] |
| 2 | standard timer 2 enabled | disable and acknowledge timer 2 | Public register contract; OS writes and TilEm [standard] |
| 3 | write control: one keeps hardware powered during HALT; zero selects low power on HALT | select low power for the next HALT | Public register contract; OS shutdown sequence and both emulators [standard] |
| 4 | legacy link-activity interrupt enabled | disable and acknowledge link activity | Public register contract; OS shutdown mask and TilEm [standard] |
| 5–7 | no documented function | — | Public register contract [standard] |
Bit 3 changes what HALT does. A write with bit 3 clear does not enter low power by itself. The CPU must execute HALT, and an enabled wake source must later request an interrupt. [standard]
The OS uses these values: [confirmed] for each ROM write and branch; [standard] for the hardware effect.
| Value | Enabled legacy sources | HALT behavior | OS use |
|---|---|---|---|
0x08 | none | powered | common clear-on-zero acknowledgement and shutdown cleanup |
0x09 | ON | powered | transient standard-timer-1 acknowledgement path |
0x0A | standard timer 1 | powered | transient ON acknowledgement path |
0x0B | ON and standard timer 1 | powered | normal mask |
0x0F | ON and both standard timers | powered | normal exit when (IY+0x16) bit 0 requests timer 2 |
0x11 | ON and link activity | low power | shutdown and wake loop |
Port 0x04 read: source and ON status
Reading port 0x04 returns status. Bit 3 is the live active-low ON level; it is not an interrupt request. Bits 5–7 report programmable-timer completion even when the corresponding timer mode did not request a maskable interrupt. [standard]
| Bit | Read meaning | OS use | Evidence |
|---|---|---|---|
| 0 | ON request pending | branch to on_irq at ram:015B | ROM test at ram:00D2–ram:00D5 [confirmed]; latch role [standard] |
| 1 | standard timer 1 pending | branch to standard_timer1_irq at ram:0167 | ROM test at ram:00D6–ram:00D9 [confirmed]; pending role [standard] |
| 2 | standard timer 2 pending | branch to ram:01F1 | ROM test at ram:00C8–ram:00CB [confirmed]; pending role [standard] |
| 3 | one when ON is released, zero while pressed | debounce reads at ram:0975 | ROM interpretation [confirmed]; electrical level [standard] |
| 4 | legacy link activity pending | branch to legacy_link_irq at ram:01E0 | ROM test at ram:00CD–ram:00D0 [confirmed]; pending role [standard] |
| 5 | programmable timer 1 finished | test timer-1 mode at port 0x31 | ROM tests at ram:0041 and ram:013A [confirmed]; completion role [standard] |
| 6 | programmable timer 2 finished | page-35 handler with A = 0x0B | ROM tests at ram:0046 and ram:0154 [confirmed]; completion role [standard] |
| 7 | programmable timer 3 finished | test timer-3 mode at port 0x37 | ROM tests at ram:003C and ram:012C [confirmed]; completion role [standard] |
The OS reads one status byte and retains it in A while testing the source bits. Programmable timers 1 and 3 receive an extra check of bit 1 in their own mode/status ports before the OS calls their banked handlers. A finished bit can therefore be visible without being eligible for interrupt service. [confirmed]
Port 0x04 write: three unrelated controls
Writing port 0x04 does not acknowledge the status returned by a read. A write selects the memory-map mode, standard-timer rate, and battery-comparator input. [standard]
| Bits | Write meaning | Evidence |
|---|---|---|
| 0 | zero selects independent mapping; one selects paired mapping | OS writes and mapper behavior [confirmed] for use; public contract and emulators [standard] for hardware |
| 2–1 | standard-timer rate index 0–3, fastest to slowest | OS writes 0x06; public formula and emulators [standard] |
| 5–3 | unused in the public contract | [standard] |
| 7–6 | raw battery-comparator selector | OS battery-test writes; public contract [standard] |
For TI-84 Plus standard timer 1, the published quartz-domain period is
$$ T_1 = \frac{64 + 80i}{32768}\text{ seconds}, $$
where $i$ is bits 2–1. Timer 2 runs at twice that frequency. OS value 0x06 selects independent mapping, rate index 3, and battery selector zero. [confirmed] for the OS value; [standard] for the field meanings and formula.
The battery-selector bits identify comparator configurations. The ROM’s write order does not prove that the raw two-bit number is a monotonic voltage level. Physical threshold and bit-order measurements remain open. [hypothesis]
See Paging for paired mapping and Clock, timers, and power for exact timer rates.
Dispatch order and simultaneous sources
The legacy-status path tests port-0x04 bits in this order: [confirmed]
| Priority | Status bit | Candidate | Additional gate |
|---|---|---|---|
| 1 | 7 | programmable timer 3 | port 0x37 bit 1 |
| 2 | 5 | programmable timer 1 | port 0x31 bit 1 |
| 3 | 6 | programmable timer 2 | handler selected through ram:0154 |
| 4 | 2 | standard timer 2 | none in the dispatcher |
| 5 | 4 | legacy link activity | none in the dispatcher |
| 6 | 0 | ON request | none in the dispatcher |
| 7 | 1 | standard timer 1 | none in the dispatcher |
ram:003A in a,(0x04)
ram:003C bit 7,a
ram:0041 bit 5,a
ram:0046 bit 6,a
ram:00C8 bit 2,a
ram:00CD bit 4,a
ram:00D2 rra ; original bit 0 enters carry
ram:00D6 rra ; original bit 1 enters carry
An eligible handler exits through acknowledgement instead of resuming at the next lower-priority bit. Simultaneous eligible sources are therefore not all dispatched from one port-0x04 read. A timer-1 or timer-3 completion bit whose mode gate is clear is skipped, allowing the next candidate to be tested. [confirmed]
The common port-0x03 write of 0x08 clears all four legacy source bits at once under the public clear-on-zero contract and TilEm’s model. Simultaneous lower-priority legacy requests can therefore be coalesced by the higher-priority service. A programmable-timer completion is acknowledged separately through its mode/status port. [standard] for latch behavior; [confirmed] for the OS write sequence.
Clear-on-zero acknowledgement
The common acknowledgement helper preserves the handler-supplied byte in A, writes 0x08, then writes the saved byte: [confirmed]
ram:00DC push af
ram:00DD ld a,0x08
ram:00DF out (0x03),a
ram:00E1 pop af
ram:00E2 out (0x03),a
The first write clears the ON, standard-timer, and link source latches because their enable bits are all zero. Bit 3 remains one, so this acknowledgement does not request low power. The second write exposes the handler-supplied value only until the normal exit writes 0x0B or 0x0F. [confirmed] for values and ordering; [standard] for clear-on-zero semantics.
Leaving a legacy pending bit uncleared causes another maskable request after EI. Rewriting the same all-enabled mask is not an acknowledgement because each set bit leaves its source enabled and pending. [standard]
Programmable timers use a separate contract. Writing their mode/status ports 0x31, 0x34, or 0x37 clears finished/overflow state and removes that timer’s request in TilEm and the public hardware description. Port 0x03 does not clear bits 5–7. [standard]
Standard and programmable timer distinction
The standard timers belong to the legacy mask block. Port-0x03 bits 1 and 2 enable their interrupt requests, and clearing those bits acknowledges them. Their rate comes from port-0x04 bits 2–1. [standard]
The three programmable timers have source, mode/status, and counter triplets at ports 0x30–0x38. Their port-0x04 bits are completion observations, not enable bits. Timer mode bit 1 selects whether completion requests a maskable interrupt. [standard]
OS 2.55MP uses programmable timer 1 for its timer bcall state machine and
programmable timer 3 for a USB timeout path. standard_timer1_irq drives keypad
scanning, cursor blink, the run indicator, and Auto Power Down (APD). [confirmed]
See Clock, timers, and power for programmable-timer modes, the bcall ABI, and kernel-tick consumers.
ON request versus ON level
Port-0x04 bit 0 is the ON interrupt latch. Bit 3 is the button’s current active-low level. The dispatcher selects the ON handler from bit 0, while on_key_debounce_power at ram:0964 repeatedly samples bit 3 to classify a stable press or release. [confirmed]
The power-cycle trace reads 0x01 after the injected ON press. Bit 0 reports the request and bit 3 clear reports the held key. The two meanings coincide in this event but remain independent fields. [confirmed]
TilEm requests an ON interrupt on both press and release transitions when enabled. Wabbitemu latches only a transition into its pressed state. The ROM handles either stable level, but it cannot determine the physical ASIC’s edge policy. [standard] for emulator behavior; [hypothesis] for the unmeasured physical policy.
See Keypad and ON-key hardware for debounce timing and the OS ON state machine.
Link interrupt versus periodic link polling
Port-0x03 bit 4 controls the legacy link-activity interrupt. The shutdown
mask 0x11 uses it as a wake source. The port-0x04 bit-4 dispatcher branch
enters legacy_link_irq, the power-restoration path. [confirmed] for OS use;
[standard] for the interrupt source.
Normal operation uses mask 0x0B, so legacy link interrupts are disabled. Standard timer 1 still performs a periodic silent-link check at ram:01B1: the raw path reads port 0x00, and the assist path reads port 0x09. This polling is separate from a direct port-0x04 bit-4 request. [confirmed]
See Two-wire link port hardware for both detection paths.
Low-power entry and wake
poweroff_shared_tail at ram:0A24 first acknowledges legacy sources with
0x08. It then writes 0x06 to port 0x04, writes 0x11 to port 0x03,
enables interrupts, and reaches poweroff_halt_loop at ram:0A5C. [confirmed]
ram:0A4B out (0x04),a ; A = 0x06
ram:0A4F out (0x03),a ; A = 0x11
ram:0A5B ei
ram:0A5C halt
ram:0A5D jr ram:0A5C
Mask 0x11 disables both standard timers, enables ON and link activity, and clears the powered-HALT bit. The next HALT enters the ASIC low-power mode under the public contract. [confirmed] for the sequence; [standard] for the physical effect.
A resolved TilEm trace records the transition and ON wake: [confirmed]
clk=98010423 ram:0A29 OUT (0x03) <- 0x08
clk=99871166 ram:0A4B OUT (0x04) <- 0x06
clk=99871186 ram:0A4F OUT (0x03) <- 0x11
clk=99871258 ram:0A5C HALT
clk=99915117 ON pressed
clk=99915172 ram:006F IN (0x55) -> 0x1F
clk=99915213 ram:003A IN (0x04) -> 0x01
clk=99915377 ram:0964 ON wake/debounce path
clk=100195536 ram:09B5 power-on restoration
TilEm prevents programmable timers from waking HALT when both standard-timer bits in port 0x03 are clear. A programmable timer can still interrupt a running CPU in that state. This is an emulator policy approximating public reports that programmable timers do not reliably wake a halted CPU; it does not identify the physical ASIC mechanism. [standard]
MAME ON-wake trace
MAME 0.287 includes the ti84pv3 machine and accepts a 1 MiB OS 2.55MP image. The repository ROM has SHA-1 ffddb460d7d4e79cc8fbd288d6895fd113d7f3bf, while MAME’s reference image has SHA-1 d500540feca974f6e8fa269981cfb25dc951c338. MAME warns about this difference because the repository image contains locally assembled boot pages. [confirmed]
The Lua tap records the program counter after each I/O instruction. MAME reaches the shutdown mask, accepts an injected ON press, and enters the ROM debounce path: [confirmed]
MAME_IO frame=20 pc_after=0DF3 OUT (0x03) <- 0x08
MAME_IO frame=20 pc_after=0DF6 OUT (0x03) <- 0x00
MAME_IO frame=20 pc_after=0C97 OUT (0x03) <- 0x11
MAME_KEY frame=30 ON press
MAME_IO frame=31 pc_after=0071 IN (0x55) -> 0x1F
MAME_IO frame=31 pc_after=003C IN (0x04) -> 0x01
MAME_IO frame=31 pc_after=0977 IN (0x04) -> 0x01
The trace confirms ROM control flow under MAME. It does not confirm MAME’s register semantics. The driver itself marks every monochrome TI-84 Plus configuration MACHINE_NOT_WORKING. [standard]
Custom handler rules
A custom IM2 handler, or code that replaces OS interrupt service, must account for each controller independently: [standard]
- Preserve every register and mapping state that interrupted code can observe. The OS shadow-register convention is safe only while the interrupted program leaves those alternate registers to the OS.
- Read port
0x55as an active-low USB gate and port0x04as legacy/completion status. Do not interpret port-0x04bit 3 as a pending source. - Acknowledge legacy sources by clearing their port-
0x03bits, then restore the intended mask. Acknowledge programmable timers through their own mode/status ports. - Keep handler code and any data it requires in mapped memory. Banked calls must preserve the interrupted mapping or restore it before returning.
- Service or deliberately disable USB events. A port-
0x03acknowledgement does not clear port-0x55/0x56state. - Enable a source capable of waking the chosen power mode before executing
HALT.
Chaining to the OS handler also inherits its assumptions: IY points to flags, normal RAM and page mappings are active, and the alternate general registers are available. [confirmed]
Emulator comparison
| Behavior | TilEm x4 | Wabbitemu 83+SE/84+ | jsTIfied 20170706a | Consequence |
|---|---|---|---|---|
Port 0x03 read | returns stored mask | returns stored mask | stores the interrupt mask | mask reads agree [standard] |
| Legacy clear-on-zero | clears ON, timer 1, timer 2, and link pending state on port-0x03 writes | clears ON directly; disabling an overdue standard timer catches its phase up in the same port-0x03 handler; port 0x02 can also catch it up | tracks standard-timer and ON latches in emulator state | OS-style acknowledgement is modeled with different internal policies [standard] |
| Link status | implements port-0x04 bit 4 | omits bit 4 from port-0x04 reads | link state participates in the interrupt model | software agreement does not establish electrical wake behavior [standard] |
| Standard timers | explicit pending interrupt bits when enabled | derives status from elapsed phase while enabled | schedules timer state in emulator cycle counters | simultaneous-source and latch tests can differ [standard] |
| Programmable completion | exposes finished bits 5–7 independently of interrupt mode | exposes timer-underflow bits 5–7 | retains per-timer completion and loop state | all separate completion from mode, with different timer cores [standard] |
HALT behavior | port-0x03 bit 3 selects powered/low-power behavior; standard-timer mask controls programmable wake suppression | approximates low power by changing LCD activity and suppresses programmable-timer requests while halted | halted CPU state is part of the browser scheduler | no model proves physical ASIC power domains [standard] |
| USB gate | disconnected fixed values 0x55 = 0x1F, 0x56 = 0 | partial Fake USB event model | fixed disconnected values | connected USB interrupt service needs another test target [standard] |
Wabbitemu’s source comments state uncertainty about its standard-interrupt write behavior. Its model is useful as an independent implementation comparison, but disagreement must remain explicit. [standard]
Native TilEm interrupt edges
A guarded direct-core run compiles the complete TilEm tree at commit
f56ad637d0524ee841dd381be6ecbaf5b8975600. It calls the registered x4 port
handlers and timer callbacks, plus the public keypad, link, timer, and reset
functions. It does not execute the ROM. [standard]
Port 0x03 stores all eight written bits. Writes 00, 01, 02, 04, 08,
10, and FF produce identical readback. The same writes select internal ON,
power-on-HALT, and link enables from bits 0, 3, and 4. Either standard-timer bit
clears TilEm’s NO_HALT_INT flag on all three programmable timers; both bits
clear set that flag. All three timer flags agree in every case. [standard]
The probe seeds all four legacy latches, all three programmable completion
flags, and all three programmable CPU requests. Applying the seven values
above through port 0x03 produces status E8 E9 EA EC E8 F8 FF. Applying
them through port 0x02 produces the same sequence. Both paths leave the
three programmable requests at internal mask 0x38, and port 0x03 leaves
completion bits 5–7 visible. This directly checks TilEm’s two clear-on-zero
implementations. [standard]
The ON sequence produces 00 00 09 08 01 00 09 08 00: masked press, enable
while held, release, acknowledge released, press, acknowledge held, release,
disable, and press while disabled. TilEm therefore latches both enabled
transitions. Timer callbacks with both masks clear remain at 08. With both
enabled, timer 1, either timer-2 callback, and both sources produce 0A,
0C, 0C, and 0E. [standard]
Reset schedules timer 1, timer 2A, and timer 2B at initial delays 1,600,
1,300, and 1,000 µs with 9,277 µs periods. Port-0x04 writes select periods
1953, 4395, 6836, and 9277 µs for all three without changing the
current interval. An enabled external link-line transition sets status 18;
either acknowledgement path clears it to 08, and a disabled transition does
not latch. [standard]
With the CPU halted and both standard timers disabled, programmable-timer-1
expiry leaves completion status 0x28 but no CPU request. Enabling either
standard timer clears the gate and produces internal request 0x08; a running
CPU also receives the request when both standard timers are disabled. This is
TilEm’s policy, not a physical wake measurement. [standard]
TilEm reset writes stored port 0x03 = 0x0B directly after the generic keypad
reset clears the internal ON enable. A fresh core therefore reads 0x0B while
the internal ON enable is zero. Reset also retains poweronhalt; after the
probe first clears it, reset again reads 0x0B while the retained power field
is zero. Writing 0x0B through the port handler synchronizes both fields to
one. This inconsistency affects experiments that begin at initialized-core
reset without executing the ROM’s first mask write. [standard]
A separate guarded link matrix checks the interrupt-facing parts of the same
implementation while exercising complete raw and assist transactions. An
enabled external peer-line transition asserts the raw link-activity request.
Assist idle-ready reports 0x22, receive-ready reports 0x31, and both
assert the CPU interrupt. Reading receive data changes status to 0x20.
Illegal both-low input reports 0x64; its first status read clears the CPU
request while retaining error status 0x60. These are direct TilEm handler
results, not physical interrupt-edge or acknowledgement measurements.
[standard]
Two isolated executions produce identical canonical native JSON with SHA-256
1c1209e9c3f625b07c42288c21e9a5dbadddb38f12aee995c1fbc8daf1f8e8ad.
The binary SHA-256 is
23037df0fee48b3ec15656aae80b6181d97211e8eec325c2be81eef02b1ff840.
[standard]
Native Wabbitemu interrupt edges
A guarded initialized-core run checks the source model through the registered
port handlers. Port 0x03 resets to 0x00 and stores all eight bits of
0xFF. Writing 0xFE clears a seeded ON latch while preserving 0xFE as the
mask readback. [standard]
Wabbitemu selects standard-timer-1 rates of 512, 227, 158, and 108 Hz. The native periods round to 1,953,125, 4,405,286, 6,329,114, and 9,259,259 ns. Timer 2 at index 3 has a 4,629,630 ns period and starts 2,314,815 ns after timer 1. These are emulator intervals, not host-clock measurements or the documented physical formula. [standard]
The expiry comparison is strict. At exactly one 108 Hz timer-1 period, port
0x04 reads released-ON status 0x08 and the CPU interrupt line remains
clear. At the next representable emulator time, status is 0x0A and the line
asserts. The sequence 0x0A → 0x08 → 0x0A then reads 0x08: disabling
the overdue timer runs the handler’s catch-up loop before re-enabling it. A
port-0x02 zero write produces the same idle result through Wabbitemu’s second
acknowledgement path. [standard]
With all three programmable underflow flags seeded, port 0x04 reads 0xE8.
With the CPU halted, port-0x03 bit 3 clear changes Wabbitemu’s LCD activity
field from on to off; setting bit 3 restores it. This is its visual low-power
approximation, not evidence that a physical ASIC powers down the controller.
[standard]
Native Wabbitemu USB interrupt edges
A separate initialized-core run checks Wabbitemu’s USB gate. With both USB
request fields clear, port 0x55 reads 0x1F. Directly selecting line,
protocol, or both requests produces 0x1B, 0x0F, and 0x0B. These values
confirm active-low bits 2 and 4 in this emulator. [standard]
Writing zero to port 0x57 does not mask a line event. A subsequent write of
0x08 to port 0x4A raises the CPU interrupt, changes port 0x56 from
0x50 to 0x58, and changes port 0x55 from 0x1F to 0x1B. Repeating the
same write after clearing only the CPU interrupt raises it again. The run
invokes Wabbitemu’s handlers directly; it does not execute the ROM dispatcher
or establish physical interrupt acknowledgement behavior. [standard]
MAME 0.287 provides a third comparison with larger known gaps: [standard]
| Area | MAME ti84pv3 behavior | Difference from the public contract |
|---|---|---|
Port 0x03 read | calls the same status reader used by port 0x04 | returns status and ON level instead of the stored mask |
Port 0x03 write | masks ON and standard-timer pending fields with the written enable bits | models clear-on-zero for bits 0–2, but omits link bit 4 and low-power bit 3 |
Port 0x02 write | writes ON and standard-timer status through a handler whose comment says it is being ignored | does not match the documented port-0x03 acknowledgement ownership |
| Standard timers | allocates fixed 256 Hz and 512 Hz callbacks | port-0x04 rate writes do not select the published 107.79–512 Hz range |
| Programmable timers | requests an interrupt when mode bit 1 is clear and sets the port-0x04 completion field on that same branch | reverses the documented interrupt-enable polarity and loses independent completion visibility |
| Link and low power | no legacy link-pending field or ASIC power-domain transition | can execute the ROM wake path but cannot test physical link wake or low-power behavior |
| USB | returns fixed 0x1F and zero from ports 0x55 and 0x56 | disconnected path only |
Native MAME interrupt edges
The guarded MAME run parks the Z80 in a DI loop on page-0 RAM and disables
the programmable timers. At reset, ports 0x03 and 0x04 both read released
ON status 0x08. Writes 00, 01, 02, 04, 08, 10, and FF to port
0x03 leave both reads at 0x08; the driver does not return the written mask.
[standard]
Writing 0x07 to port 0x02 directly creates status 0x0F. Subsequent
port-0x03 writes 0x01, 0x06, 0xFF, and 0x00 retain ON only, retain
both standard timers, retain all three fields, and clear all three fields.
Port 0x04 consequently reads 09, 0E, 0F, and 08. Port 0x02 still
reads ASIC status 0xC3; its write and read handlers have unrelated meanings.
[standard]
The live-input sequence begins with ON masked. A press, enabling ON while the
button remains held, release, enabled press, enabled release, and bit-0-clear
acknowledgement produce 00, 00, 08, 01, 09, and 08. The adapter
waits through both the video input update and a 256 Hz timer-1 sample after
each forced transition. This confirms MAME’s press-only latch and release
rearming without executing the ROM handler. [standard]
One 20 ms frame with only timer 1, only timer 2, or both enabled produces
status 0x0A, 0x0C, or 0x0E. Timer-1 status reaches 0x0A after both
port-0x04 configuration writes 0x00 and 0x06. These frame-level results
show that both callbacks run and that neither configuration suppresses timer
1; the pinned source, rather than this coarse interval, establishes the exact
fixed 256 Hz and 512 Hz rates. [standard]
A scheduled soft reset retains seeded status 0x0F through the reset callback.
After a port-0x02 zero write clears pending state, the retained standard-timer
masks regenerate 0x0E; a new ON press changes the status to 0x07. MAME’s
reset hook restores the mapper but does not restore these interrupt fields.
This is emulator reset behavior, not physical warm-reset evidence. Two isolated
runs produce identical canonical parsed native JSON with SHA-256
bb4b38d444692b5136d96264fa3acf9fe95ef2f6a1879ab72e9a2ad8077c1def.
[standard]
Reusable debugging tools
tools/ti84re/hardware/interrupt_controller.py provides typed decoders, exact timer periods,
clear-on-zero acknowledgement, ROM status-test order, USB active-low decoding,
and immutable TilEm and MAME legacy-interrupt state models.
tools/ti84re/hardware/describe_interrupt_controller.py exposes focused CLI commands:
[confirmed] for the ROM/public decoders; [standard] for the emulator model.
nix develop -c python3 -m ti84re.hardware.describe_interrupt_controller mask 0x0B
nix develop -c python3 -m ti84re.hardware.describe_interrupt_controller status 0x8B
nix develop -c python3 -m ti84re.hardware.describe_interrupt_controller config 0x06
nix develop -c python3 -m ti84re.hardware.describe_interrupt_controller ack 0xF7 0x08
The trace command resolves mapper state, restricts output to interrupt ports, annotates each value, and collapses consecutive identical polls: [confirmed]
nix develop -c python3 -m ti84re.hardware.describe_interrupt_controller trace \
/tmp/tilem-power-cycle.trace --clock 97000000-101000000
Use --all to retain repeated ON-level polling and --json for machine-readable output.
tools/ti84re/emulators/wabbitemu/interrupt_probe.py adds the pinned Wabbitemu timer-rate and
edge oracle. Run its exact-ROM, hash-recording CLI with:
wabbit_interrupt_parent=$(mktemp -d /tmp/ti84-wabbit-interrupt.XXXXXX)
python3 -m ti84re.emulators.wabbitemu.run_interrupt_edge_probe \
--rom tools/rom.bin \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$wabbit_interrupt_parent/run" --json
The ROM is only a core-initialization fixture in this mode. No TI-OS instruction executes.
tools/ti84re/emulators/tilem/interrupt.py supplies the typed direct-core report and checks it
against the reusable TilEm state model. The guarded builder requires the exact
clean source commit and tree. The runner requires the binary SHA-256 and
refuses to reuse an output directory. The “Legacy interrupt matrix” section in
tools/notes/emulator-probes.md contains the reproduction commands.
tools/ti84re/emulators/tilem/link.py checks the raw-activity and assist-interrupt transitions
against the shared link model in tools/ti84re/link/port.py. Its guarded builder and
runner use the same clean-source and binary-hash requirements. The “Raw link
and assist matrix” section in tools/notes/emulator-probes.md contains the command.
tools/ti84re/emulators/mame/interrupt.py parses the complete MAME report and checks it against
the reusable state model. Its guarded CLI records the exact MAME, ROM, Lua,
logs, and evidence scope:
mame_interrupt_parent=$(mktemp -d /tmp/ti84-mame-interrupt.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_interrupt_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_interrupt_parent/run" --json
tools/ti84re/emulators/mame/trace.py, tools/ti84re/emulators/mame/run_io_trace.py, and tools/probes/mame/mame_io_trace.lua provide the equivalent headless MAME path. The Lua tap accepts comma-separated ports and ranges, collapses identical polls, records post-I/O PCs, and can inject ON at selected video frames: [confirmed]
nix shell nixpkgs#mame -c python3 -m ti84re.emulators.mame.run_io_trace \
--seconds 2 --ports 03-04,55-56 \
--on-press-frame 30 --on-release-frame 34
MAME prints a checksum warning for the locally assembled ROM and identifies the expected and actual hashes. Keep that warning with captured evidence.
Resolved findings and open hardware tests
- [confirmed] OS 2.55MP tests USB activity before reading legacy status, but ports
0x55/0x56remain separate from ports0x03/0x04. - [confirmed] The port-
0x04test order is programmable timer 3, timer 1, timer 2, standard timer 2, link, ON, then standard timer 1. - [confirmed] The OS acknowledgement sequence is
0x08→ handler-supplied byte →0x0Bor0x0F. - [confirmed] Shutdown writes
0x11and executesHALT; the trace wakes through port-0x04value0x01after an ON press. - [standard] Programmable completion bits remain observable independently of their interrupt-mode bits.
- [standard] The guarded TilEm run verifies stored mask readback, both clear-on-zero paths, ON press and release latches, both timer-2 callbacks, link transitions, programmable-timer HALT gating, and the reset readback/internal-policy mismatch.
- [standard] The guarded TilEm link run independently verifies raw activity, assist idle and receive requests, data acknowledgement, and interrupt-only acknowledgement of a retained error flag.
- [standard] The guarded Wabbitemu run verifies complete mask readback, ON clearing, strict standard-timer expiry, both standard-timer acknowledgement paths, programmable completion readback, and its LCD-based low-power approximation.
- [standard] The guarded Wabbitemu USB run verifies its active-low line/protocol summary, mask-independent line event, and repeat-event path. It does not verify the ROM dispatcher or physical USB interrupt behavior.
- [standard] The guarded MAME run verifies shared status reads, three-bit mask behavior, direct port-
0x02status injection, press-only ON sampling, both standard-timer pending bits within scheduled frames, and interrupt-field retention across soft reset. - [hypothesis] Physical TA2 and TA3 tests should measure ON request edges, link wake transitions, simultaneous legacy-source coalescing, programmable-timer wake behavior, and battery-selector thresholds.
Sources
| Source | Used for |
|---|---|
WikiTI port 0x03 | enable mask, clear-on-zero acknowledgement, normal value, and low-power-on-HALT contract |
WikiTI port 0x04 | read-status fields, write controls, timer formula, and programmable completion distinction |
TilEm x4_io.c, x4_init.c, keypad.c, link.c, and timers.c | legacy latches, reset ordering, ON/link edges, timer completion, HALT policy, and disconnected USB values |
Wabbitemu 83psehw.c | independent standard-interrupt, mapping, ON, timer, and low-power implementation |
MAME 0.287 ti85.cpp and ti85_m.cpp | TI-84 Plus machine status, I/O map, interrupt masks, standard timers, programmable timers, and fixed USB reads |
jsTIfied deployed 20170706a artifact and readable mirror | fourth interrupt-mask, timer, ON, link, halted-state, and fixed-USB implementation |
| Local OS 2.55MP page-0 bytes | entry, gates, test order, handlers, acknowledgement, and exit |
/tmp/tilem-power-cycle.trace | shutdown and ON-wake execution sequence |
Clock, timers, and power
TI-84 Plus OS 2.55MP — Clock domains, timer APIs, APD, RTC, and low-power control.
The TI-84 Plus derives OS timekeeping from a 32.768 kHz crystal while the Z80 runs at a separately selectable CPU rate. This page separates the standard interrupt timers, three programmable timers, and real-time clock; reconstructs the undocumented timer bcall state machine; and follows both explicit and automatic shutdown into the ASIC’s low-power state.
Evidence layers
The subsystem crosses ROM code, public hardware observations, and emulator policy. A claim marked [confirmed] comes from the local OS 2.55MP image or a complete instruction trace. A claim marked [standard] comes from the named hardware or emulator source and agrees with the ROM where their scopes overlap. Emulator behavior is identified by implementation and revision rather than treated as physical-ASIC proof.
| Layer | Main evidence | What it establishes |
|---|---|---|
| TI-OS kernel | ram:0038–ram:04B2 and ram:09B5–ram:0A5F | interrupt routing, standard-timer consumers, APD counters, and shutdown [confirmed] |
| TI-OS banked code | 33:5E1E–33:5F69 and 37:5359–37:5950 | programmable-timer API and RTC conversion/access [confirmed] |
| TI-OS dynamic execution | tools/macros/power-cycle.macro and resolved TilEm traces | standard-timer cadence and the explicit shutdown/HALT path [confirmed] |
| Public hardware notes | WikiTI ports 0x03, 0x04, 0x20, 0x2D, 0x2F, 0x30–0x38, and 0x40–0x48 | register semantics and oscillator-derived rates [standard] |
| Emulator models | TilEm commit f56ad63, Wabbitemu commit 48c2dc0, MAME 0.287, and jsTIfied 20170706a | independent timer decode, scheduling, status, interrupt, and RTC policies [standard] |
| Native emulator execution | guarded TilEm, Wabbitemu, and MAME timer/interrupt runs | source decode, scheduling, counter, acknowledgement, callback, HALT-line, reset, and RTC transitions [standard] |
Hardware blocks and clock domains
The TA2/TA3 ASIC integrates the Z80-compatible core, RAM interface, USB, and supporting logic. WikiTI’s hardware history places the variable CPU clock, 32.768 kHz quartz oscillator, programmable timers, and MD5 assist in the advanced gate array introduced with the TI-83 Plus Silver Edition. The TI-84 Plus adds a real-time clock driven from that low-frequency domain. Datamath identifies the local calculator family as using TI REF 83PLUSB/TA2 or 84PLUSB/TA3 ASIC revisions. [standard]
The MD5 accelerator and boot API page checks the MD5 port block and boot routines independently.
flowchart LR
CPU["CPU clock<br/>port 20 · 6 or 15 MHz"] --> Z80["Z80 core"]
CPU --> PTCPU["programmable timers<br/>CPU-clock modes"]
XTAL["32.768 kHz quartz"] --> ST["standard timers 1 and 2"]
XTAL --> PTX["programmable timers<br/>crystal modes"]
XTAL --> RTC["32-bit RTC seconds counter"]
ST --> ISR["im1_vector · ram:0038"]
PTCPU --> ISR
PTX --> ISR
ISR --> APD["keypad · cursor · APD"]
The three timing blocks have different contracts: [standard]
| Block | Registers | Resolution or source | OS use |
|---|---|---|---|
| Standard hardware timers | port 0x04 rate; port 0x03 mask/ack | four crystal-derived rates | kernel tick, keypad scan, cursor, APD |
| Programmable timers 1–3 | triplets 0x30–0x38 | crystal or divided CPU clock | timer bcall API and USB timeouts |
| Real-time clock | 0x40–0x48 | one-second, 32-bit counter | date/time bcalls and TI-BASIC clock commands |
CPU speed
Port 0x20 selects CPU speed. Value 0 selects the nominal 6 MHz mode; values 1–3 select the nominal 15 MHz mode on the TI-84 Plus. TilEm models these as exactly 6 MHz and 15 MHz. Physical measurements published by WikiTI vary by ASIC revision and unit, so cycle-count conversion should name whether it uses nominal or measured frequency. [standard]
Pinned Wabbitemu starts at exactly 6 MHz. Its default TI-84 Plus context maps
port-0x20 values 0–3 to 6, 15, 15, and 15 MHz. An internal
timer_version = 1 setting maps them to 6, 15, 20, and 25 MHz. A guarded
initialized-core run verifies both matrices. The internal setting is
front-end configuration, not a calculator port or evidence of additional
physical TI-84 Plus clock modes. [standard]
The low two speed bits also select one of ports 0x29–0x2C for LCD and
memory wait states, plus a field in port 0x2F. See
Bus timing and wait states. [standard]
The standard timers and RTC remain tied to the quartz domain when the CPU speed changes. Programmable timers can instead select the CPU clock, so their wall time then changes with port 0x20. [standard]
Interrupt-source routing
TI-OS uses IM1. im1_vector at ram:0038 jumps to
int_entry_save_alt_regs at ram:006D. That entry swaps in the alternate
general registers, polls the active-low USB summary at port 0x55, and falls
through to the separate legacy status port 0x04 when the USB block reports no
source. [confirmed]
Reading port 0x04 reports legacy pending state, live ON level, and programmable-timer completion: [standard]
| Bit | Source | OS branch from the dispatcher |
|---|---|---|
| 0 | ON key | on_irq at ram:015B |
| 1 | standard hardware timer 1 | standard_timer1_irq at ram:0167 |
| 2 | standard hardware timer 2 | ram:01F1 |
| 3 | ON key level, active low | tested as state rather than a source |
| 4 | link activity | legacy_link_irq at ram:01E0 |
| 5 | programmable timer 1 complete | status check at ram:013A; handler 33:5EB4 |
| 6 | programmable timer 2 complete | ram:0154 path |
| 7 | programmable timer 3 complete | status check at ram:012C; handler 35:4792 |
The two status handlers visible in this dispatch are unrelated to the kernel’s APD tick: [confirmed]
33:5EB4continues the OS timer API’s programmable-timer-1 countdown.35:4792stops programmable timer 3 and services a USB timeout/event structure through ports0x8E,0x91, and0x92.standard_timer1_irqhandles the tick that reaches keypad scanning, cursor blink, the run indicator, and APD.
The status-test order is programmable timer 3, timer 1, timer 2, standard timer 2, link, ON, then standard timer 1. Programmable completion bits remain visible when their timer mode does not request an interrupt, so timers 1 and 3 receive an additional mode-bit check before their handlers run. [confirmed] for the test order and mode checks; [standard] for completion visibility.
The kernel normally writes 0x0B to port 0x03: ON and standard timer 1 can interrupt, timer 2 and link cannot, and bit 3 keeps the ASIC powered during HALT. The acknowledge sequence at ram:00DC writes 0x08, which clears all legacy source bits under the clear-on-zero contract, and then writes a handler-supplied byte. See Interrupts (IM1) for the complete register tables and simultaneous-source behavior. [confirmed] for the writes; [standard] for latch semantics.
Standard hardware timers
Writing port 0x04 selects both memory-map mode and the standard-timer rate. Bits 1–2 form an index $i$ from 0 through 3. On the TI-84 Plus, standard timer 1 has period [standard]
$$ T_1 = \frac{64 + 80i}{32768}\text{ seconds} $$
and timer 2 runs at twice its frequency. [standard]
Port-0x04 bits 2–1 | $i$ | Timer-1 period | Timer-1 frequency | Timer-2 frequency |
|---|---|---|---|---|
00 | 0 | 1.953125 ms | 512 Hz | 1,024 Hz |
01 | 1 | 4.39453125 ms | 227.555556 Hz | 455.111111 Hz |
10 | 2 | 6.8359375 ms | 146.285714 Hz | 292.571429 Hz |
11 | 3 | 9.27734375 ms | 107.789474 Hz | 215.578947 Hz |
TI-OS writes 0x06 to port 0x04 at several setup sites, including ram:09B7. Bit 0 is clear, selecting memory-map mode 0, and bits 1–2 select the slowest standard-timer rate. The kernel tick period is therefore exactly $304/32768$ seconds under the documented quartz model. [confirmed] for the write; [standard] for the physical rate.
Wabbitemu instead stores a rounded rate table of 512, 227, 158, and 108 Hz. Its index-2 value differs from the documented 146.285714 Hz, while the other three approximate their corresponding public rates. A guarded initialized-core run records the resulting internal periods and checks the expiry boundary through the registered port handler. These values describe Wabbitemu only. [standard]
Dynamic cadence
A resolved TilEm trace enters standard_timer1_irq at steady intervals of
139,153–139,157 emulated CPU cycles after the OS reaches its 15 MHz state.
TilEm schedules this timer at 9,277 µs, so its nominal interval is 139,155
cycles. Instruction-boundary acceptance accounts for the small spread.
[confirmed]
The hardware formula gives 9,277.34375 µs, or 139,160.15625 nominal 15 MHz cycles. TilEm rounds each rate to whole microseconds with the table {1953, 4395, 6836, 9277}. The five-cycle difference at the slow setting is emulator quantization, not evidence that the quartz formula differs. [standard]
A guarded direct-core probe confirms the table through port-0x04 writes.
Each selection applies the same period to timer 1 and both timer-2 callbacks.
The initial intervals remain 1,600, 1,300, and 1,000 µs across those writes,
matching TilEm’s tilem_z80_set_timer_period contract. Direct callbacks with
port-0x03 mask 0x06 produce status 0x0A for timer 1 and 0x0C for
either timer-2 callback. These are scheduler observations from pinned TilEm,
not physical phase or frequency measurements. [standard]
Kernel-tick consumers
standard_timer1_irq at ram:0167 performs the periodic kernel work below before returning through the common interrupt acknowledge path. [confirmed]
| Consumer | Gate or counter | Code |
|---|---|---|
| Run indicator | indicCounter at 0x8476 | run_indicator_tick at ram:027B |
| Keypad scan and repeat | state at 0x8440–0x8443 | kbd_tick_debounce_repeat at ram:03B4 → kbd_scan_matrix at ram:0406 |
| Cursor blink | curTime at 0x844A | cursor_blink_tick at 06:7C45 through the ram:3FCF bjump |
| General countdown | word at 0x9C24 | apd_timer_tick at ram:0355 |
| APD | apdSubTimer/apdTimer at 0x8448/0x8449 | ram:036C–ram:0382 |
The keypad mechanism is covered in Keypad and ON-key hardware. These consumers advance from standard timer 1, not from a programmable timer. [confirmed]
Auto Power Down timing
_ApdSetup = 4C93 has body ram:03AE. It reloads only the high byte: [confirmed]
ram:03AE ld hl,0x8449 ; apdTimer
ram:03B1 ld (hl),0x74
ram:03B3 ret
When apdAble and apdRunning are set, ram:036C decrements the low byte first and the high byte only when the low byte reaches zero: [confirmed]
ram:036C ld hl,0x8448 ; apdSubTimer
ram:036F dec (hl)
ram:0370 ret nz
ram:0371 inc hl ; apdTimer
ram:0372 dec (hl)
ram:0373 ret nz
Because _ApdSetup leaves apdSubTimer unchanged, the timeout depends on its phase. If $d$ is the number of ticks until the low byte next reaches zero, with $1 \le d \le 256$, expiry takes [confirmed]
$$ N = d + 115 \times 256 $$
standard timer-1 ticks. The exact range is: [confirmed] for the counter arithmetic; [standard] for conversion through the documented timer rate.
| Quantity | Minimum | Maximum |
|---|---|---|
| Timer ticks | 29,441 | 29,696 |
| Seconds | 273.134277 | 275.500000 |
| Minutes | 4.552238 | 4.591667 |
The low byte’s free-running phase explains the roughly 2.37-second spread after a reload. The high-byte constant alone therefore does not encode one exact number of minutes. [confirmed]
On expiry, ram:0374 performs display/context cleanup, clears apdRunning,
sets apdWarmStart, and jumps to poweroff_shared_tail at ram:0A24.
[confirmed]
Cursor blink cadence
_CursorOn and _CursorOff reload curTime with 0x32 (50).
cursor_blink_tick decrements it, toggles curOn on expiry, and reloads the
same value. [confirmed]
At the OS standard-timer setting, one visible-state interval is [confirmed] for the tick count; [standard] for wall time.
$$ 50 \times \frac{304}{32768} = 0.4638671875\text{ seconds} $$
A complete on/off cycle is 0.927734375 seconds. The run indicator has a separate counter at 0x8476; it does not share the APD word. [confirmed]
Programmable timers
The ASIC provides three independent eight-bit countdown timers. Each uses a source/frequency register, a mode/status register, and a counter register. [standard]
| Timer | Source/frequency | Mode/status | Counter | Port-0x04 completion bit |
|---|---|---|---|---|
| 1 | 0x30 | 0x31 | 0x32 | 5 |
| 2 | 0x33 | 0x34 | 0x35 | 6 |
| 3 | 0x36 | 0x37 | 0x38 | 7 |
Source and divisor
The high two frequency-register bits choose the clock family. The low bits encode a family-specific divisor. [standard]
| Value or family | Result |
|---|---|
0x00 | timer off |
0x40 | 32.768 kHz divided by 3 |
0x41 | 32.768 kHz divided by 33 |
0x42 | 32.768 kHz divided by 328 |
0x43 | 32.768 kHz divided by 3,277 |
0x44, 0x45, 0x46, 0x47 | 32.768 kHz divided by 1, 16, 256, or 4,096 |
0x80, 0x81, 0x82, 0x84, 0x88, 0x90, 0xA0 | CPU clock divided by 1, 2, 4, 8, 16, 32, or 64 |
0xC0 family | CPU clock plus the speed-dependent port-0x2F prescaler |
Writing a nonzero counter starts it when a valid source is selected. Counter value zero represents 256 ticks, loops continuously, and does not assert the port-0x04 completion bit. [standard]
TilEm stops a timer on every source-register write and retains the current counter as the next loop value. This is emulator behavior in tilem_user_timer_set_frequency, not evidence that every physical ASIC revision retains the counter the same way. [standard]
TilEm rounds crystal-family durations to whole microseconds before scheduling.
For a freshly loaded counter value of one, sources 0x40–0x47 schedule at
92, 1007, 10010, 100006, 31, 488, 7813, and 125000 µs. The
counter read rescales that rounded remainder against a separately rounded
256-count duration. It consequently reads 1 0 1 0 1 0 1 1 immediately
after those eight starts. This readback pattern is a TilEm quantization effect,
not a physical counter claim. [standard]
Mode, completion, and acknowledgement
| Mode/status bit | Meaning |
|---|---|
| 0 | loop after expiry |
| 1 | request a maskable interrupt on expiry |
| 2 | overflow: another expiry occurred before acknowledgement |
Writing the mode/status port acknowledges completion, clears overflow, and removes the timer’s interrupt request. The corresponding port-0x04 bit records completion even when mode bit 1 did not request an interrupt. If looping remains active without a new mode write before the next expiry, the counter continues through a 256-count overflow cycle and sets status bit 2. [standard]
TilEm also assigns a recurring 256-tick period when software restarts an already completed non-looping timer without first writing its mode port. The completion bit remains set, the low mode read remains zero, and the next callback sets overflow bit 2. OS 2.55MP acknowledges before programming its next chunk, so the timer bcall path does not use this emulator edge. [standard]
Bad Apple audio timer case
The third-party Bad Apple application sets CPU-speed port 0x20 to 1, then
writes source 0x82, mode 0x03, and counter 120 to timer 1. Its interrupt
routine acknowledges by rewriting 0x03 to port 0x31 and emits one link-port
sample through port 0x00. [confirmed] for the application source.
Source 0x82 is the CPU-clock family divided by 4. At the nominal 15 MHz
TI-84 Plus speed, the programmed cadence is therefore
$$ \frac{15{,}000{,}000}{4 \times 120} = 31{,}250\ \mathrm{Hz}. $$
The program’s companion encoder instead uses 33,333.3 Hz when converting notes to oscillator counts. That value is an encoder tuning assumption rather than a decode of the active timer registers. The program advances its tracker after $24 \times 75 = 1{,}800$ interrupts, so both note pitch and tracker tempo depend on the actual timer cadence. Published CPU-frequency variation and unresolved physical timer edges prevent the nominal calculation from serving as a physical measurement. [confirmed] for the constants and control flow; [standard] for the timer decode; [hypothesis] for physical cadence.
Cadence evidence. The top lane combines [confirmed] application bytes with the [standard] timer decode. The encoder and trace lanes preserve their source contexts; neither measures physical calculator cadence.
Port 0x2D controls low-power behavior. Bit 0 keeps the quartz oscillator active on the TI-83 Plus Silver Edition; the TI-84 Plus RTC already requires it. Bit 1 allows the programmable timers to continue counting in low power. TI writes 0x03. Public hardware tests report that these timers still do not reliably interrupt a halted CPU, so software should keep a standard timer enabled when it must escape HALT. [standard]
Prepared physical discriminator
The guarded HWTMR probe
tests the four source-model disagreements without using HALT. It compares
source 0x41 with the common source-0x45 reference, measures source 0xE0
across CPU-speed requests 0–3, starts a source-0x45 timer with counter zero,
and captures status after two unacknowledged expiries. It snapshots ports
0x02, 0x03, 0x04, 0x15, 0x20, 0x2D, 0x2F, and 0x30–0x35.
It runs only when timers 1 and 2 are idle and their completion bits are clear.
Every polling loop is bounded. [confirmed] for the assembled source and host
decoder.
The exact image completes through its cleanup boundary in pinned Wabbitemu and
selects that implementation’s divisor-32, omitted-port-0x2F, counter-zero
completion, and first-expiry-bit-2 behaviors. This validates the program and
decoder against a known model. No exported HWTMR001 result from a calculator
has been recorded, so the physical divisor, prescaler, zero-counter, and
expiry-status edges remain [hypothesis].
Undocumented timer bcall API
OS 2.55MP exposes one software timer backed by programmable timer 1. ti83plus.inc supplies official equate names, but the WikiTI pages for IDs 526C–5281 are absent. The ABI below is reconstructed from 33:5E1E–33:5F69. [confirmed]
Entry points
| Bcall | ID | Body | Inputs | Success result |
|---|---|---|---|---|
_InitTimer | 526C | 33:5E38 | none | B=0x70, A=0, carry clear |
_KillTimer | 526F | 33:5E4E | A=0x70 | stops hardware and clears all state |
_StartTimer | 5272 | 33:5E58 | A=0x70, DE duration, C!=0 for auto-restart | starts or completes immediately |
_RestartTimer | 5275 | 33:5E9D | same duration/restart inputs | replaces the current run |
_StopTimer | 5278 | 33:5F42 | A=0x70 | stops hardware and clears running |
_WaitTimer | 527B | 33:5EA4 | A=0x70, DE duration | starts once and busy-waits for finished |
_CheckTimer | 527E | 33:5F16 | A=0x70 | HL expiry count; Z if unfinished, NZ if finished |
_CheckTimerRestart | 5281 | 33:5F27 | A=0x70 | returns old HL, then clears finished/count |
All operations except _InitTimer validate A=0x70. An invalid or uninitialized ID returns carry set and A=2. _InitTimer returns carry set and A=1 when already initialized. _StartTimer returns carry set and A=3 when already running. [confirmed]
State block
| Address | Size | Meaning |
|---|---|---|
0x9C0C | 1 | bit 0 initialized; bit 1 running; bit 2 finished; bit 3 auto-restart |
0x9C0D | 2 | original DE duration for auto-restart |
0x9C0F | 2 | remaining chunk word |
0x9C11 | 2 | saturating completed-expiry count |
_InitTimer sets only initialized. _KillTimer writes zero to ports 0x30 and 0x31, then clears all seven bytes. _StopTimer stops those ports and clears running, but preserves finished, auto-restart, the saved duration, and the expiry count. [confirmed]
Duration encoding and hardware programming
_StartTimer selects source 0x41, whose tick period is $33/32768$ seconds, and mode 0x02, which requests an interrupt without hardware looping. timer_program_next_chunk at 33:5EF3 programs counter 0x32 in chunks. [confirmed]
For input DE, the high byte D counts full chunks of 255 and the low byte E supplies the final chunk. The total hardware count is therefore [confirmed]
$$ N = 255D + E $$
rather than the ordinary 16-bit value $256D+E$. Each tick is about 1.007080078125 ms under the crystal specification. For example, DE=0x0100 programs 255 ticks, and DE=0x0101 programs 256. This radix-255 chunking is an ABI quirk, not a generic property of the hardware counter. [confirmed]
After each hardware expiry, timer_irq acknowledges mode port 0x31, programs the next chunk, and returns while chunks remain. At the logical expiry it increments the word at 0x9C11, saturating at 0xFFFF, clears running, and sets finished. With auto-restart selected, it restores the original duration, sets running again, and programs the first new chunk. [confirmed]
A zero duration has no hardware chunk. _StartTimer marks the timer finished immediately and increments the expiry count once. [confirmed]
Check and wait quirks
_CheckTimer preserves the BIT 2 result while loading HL: Z means unfinished and NZ means finished. It always returns A=0 and carry clear on a valid timer. The count can exceed one when auto-restart runs faster than the caller checks it. [confirmed]
_CheckTimerRestart disables interrupts, captures the old count, clears finished and the count, then executes EI unconditionally. It does not preserve a caller’s disabled-interrupt state. Its final success path also makes Z set, so use the returned count rather than _CheckTimer’s finished-flag convention. [confirmed]
_WaitTimer sets C=0, calls _StartTimer, and spins on state bit 2. It does not execute HALT. Because timer_irq advances multi-chunk and completed timers, ordinary waits require interrupts to remain enabled. [confirmed]
Real-time clock
The RTC is a 32-bit count of seconds since midnight on 1 January 1997. The set and current registers are little-endian by port number. [standard]
| Ports | Access | Meaning |
|---|---|---|
0x40 | read/write | bit 0 enable; rising edge on bit 1 commits a new count |
0x41–0x44 | read/write | staged set value, least-significant byte first |
0x45–0x48 | read | current seconds, least-significant byte first |
To set the clock, software writes all four staged bytes, writes 0x01 to port 0x40 so command bit 1 is low, then writes 0x03 to create its rising edge while leaving the clock enabled. [standard]
Raw OS access
rtc_read_seconds at 37:58A1 reads current ports in the order 0x48, 0x47, 0x46, 0x45 into 0x8499–0x849C. The following conversion loop turns that 32-bit integer into the OS floating-point/date representation. [confirmed]
rtc_write_seconds at 37:593F writes the four converted bytes in the reverse port order 0x44, 0x43, 0x42, 0x41, then emits the 0x01 → 0x03 control sequence. [confirmed]
The exact-ROM disassembly shows both block-I/O loops:
; 37:58A1 — bytes 21 99 84 06 04 0E 49 0D ED A2 20 FB
ld hl,0x8499
ld b,4
ld c,0x49
.read_byte:
dec c
ini
jr nz,.read_byte
; 37:593C — bytes 21 99 84 06 04 0E 45 0D ED A3 20 FB
ld hl,0x8499
ld b,4
ld c,0x45
.write_byte:
dec c
outi
jr nz,.write_byte
INI increments HL and decrements B, so the first loop pairs ascending RAM
addresses with descending current-time ports. OUTI applies the same register
updates to the staged set ports. [confirmed]
tools/ti84re/rom/describe_io_coverage.py reproduces this result from the pinned ROM.
Its raw scan covers all 37 possible register and block-I/O opcode pairs in the
image, including pairs inside operands and data. It also verifies that no
16 KiB page ends with an ED prefix. Only 37:58A9 and 37:5944 survive as
aligned instructions with a statically resolved port. Regression tests pin
their ports to 0x48 and 0x44. This is ROM evidence; the separate TilEm RTC
probes below test emulator behavior. [confirmed]
The hardware documentation does not describe a snapshot/latch operation for current-time reads. The OS reads high byte first, which reduces but does not eliminate the possibility of a rollover between the four port reads. No retry or two-pass coherence check appears at 37:58A1. [confirmed] for the OS sequence; [hypothesis] for physical rollover behavior.
TilEm reads host time_t separately on every current-register access. A
probe-controlled rollover from 0x00FFFFFF to 0x01000000 between the
port-0x48 read and the remaining bytes assembles 0x00000000. This proves
that pinned TilEm has no multi-byte RTC latch. It does not resolve whether the
physical ASIC snapshots the current count. [standard]
Date and time bcalls
| Bcall | ID | Body | Role |
|---|---|---|---|
_chkTmr | 5143 | 37:54C1 | clock-value conversion/check entry |
_getDate | 514F | 37:550B | date into the floating-point stack |
_GetDateString | 5152 | 37:55E8 | format the current date into DE buffer |
_getDtFmt | 5155 | 37:5581 | return date-order setting 1, 2, or 3 |
_getDtStr | 5158 | 37:55A9 | date-string wrapper using current format |
_getTime | 515B | 37:5551 | seconds, minutes, and 24-hour hour values |
_GetTimeString | 515E | 37:567E | format current time into DE buffer |
_getTmFmt | 5161 | 37:5593 | return 12- or 24-hour setting |
_getTmStr | 5164 | 37:55CF | time-string wrapper using current format |
_SetZeroOne | 5167 | 37:5359 | helper for clock-setting parser state |
_setDate | 516A | 37:536E | validate and set a date |
_IsOneTwoThree | 516D | 37:5438 | validate the three date formats |
_setTime | 5170 | 37:540D | validate and set a time |
_IsOP112or24 | 5173 | 37:5413 | validate 12/24-hour selection |
_chkTimer0 | 5176 | 37:557E | jump directly to rtc_read_seconds |
_timeCnv | 5179 | 37:56C4 | clock/date conversion entry |
WikiTI documents _getDate as returning day, month, and year through OP1 and floating-point stack slots. _getTime returns seconds, minutes, and hours, with hours always in 24-hour form. _GetTimeString applies the user’s 12/24-hour setting and writes a null-terminated string such as 1:41AM or 15:56. [standard]
Power-off and wake flow
_PowerOff = 5008 resolves to ram:09E6. It performs context and display
cleanup before joining poweroff_shared_tail at ram:0A24. APD performs its
own cleanup at ram:0374 and joins the same tail. [confirmed]
flowchart TD
EX["_PowerOff · ram:09E6"] --> CLEAN["put-away and display cleanup"]
APD["APD expiry · ram:0374"] --> APDF["clear apdRunning<br/>set apdWarmStart"]
CLEAN --> JOIN["poweroff_shared_tail · ram:0A24"]
APDF --> JOIN
JOIN --> P4["port 04 = 06"]
P4 --> P3["port 03 = 11"]
P3 --> H["poweroff_halt_loop · ram:0A5C"]
H --> WAKE["ON/link interrupt and wake path"]
The final writes are: [confirmed]
| Address | Operation | Effect |
|---|---|---|
ram:0A4B | OUT (0x04),0x06 | map mode 0 and slow standard-timer rate |
ram:0A4F | OUT (0x03),0x11 | ON and link interrupts enabled; both standard timers disabled; low-power-on-HALT selected |
ram:0A51 | clear shift2nd | remove the [2nd] modifier |
ram:0A55 | clear onRunning | mark the OS as powered down |
ram:0A5B | EI | allow the selected wake interrupt |
poweroff_halt_loop | HALTJR ram:0A5C | remain in the ASIC low-power loop |
The low-power request is port-0x03 bit 3 clear combined with Z80 HALT;
writing 0x11 by itself does not finish the transition. ON and link activity
remain enabled as wake sources. The wake interrupt follows im1_vector →
int_entry_save_alt_regs → on_irq → on_key_debounce_power. After
debouncing the active-low ON level, the power-on branch at ram:09AC restores
the CPU-speed setting and writes 0x06 to port 0x04 at ram:09B7. It does
not return to the suspended _PowerOff caller. [confirmed]
Dynamic power-cycle trace
tools/macros/power-cycle.macro cold-boots the OS, presses [2nd]+ON,
waits in low power, then presses ON. The resolved trace enters _PowerOff
once, reaches poweroff_shared_tail, executes both port writes, and repeats
HALT in poweroff_halt_loop until the wake event. It then records the wake
route through on_key_debounce_power, ram:09AC, and ram:09B5. [confirmed]
TILEM=~/Git/tilem-headless/result/bin/tilem2
$TILEM --headless --rom tools/rom.bin --model ti84p --normal-speed --reset \
--macro tools/macros/power-cycle.macro \
--trace /tmp/tilem-power-cycle.trace --trace-range all \
--trace-limit 500000000
nix develop -c python3 -m ti84re.trace.resolve \
/tmp/tilem-power-cycle.trace --initial-mapping ti84p-reset \
--names tools/symbols/names.txt --only-space ram \
--only-addr 09e6-0a5d --print 180
Emulator comparison
The comparison below reproduces pinned source behavior. Agreement between implementations is useful corroboration of a software contract, but it is not a substitute for a physical TA2 or TA3 measurement. [standard]
| Area | Documented contract | TilEm f56ad63 | Wabbitemu 48c2dc0 | MAME 0.287 | jsTIfied 20170706a |
|---|---|---|---|---|---|
Crystal divisors for 0x40–0x43 | 3, 33, 328, 3277 | 3, 33, 328, 3277 | 3, 32, 327, 3276 | 3, 32, 327, 3276 | 3, 33, 328, 3277 |
| CPU families | CPU clock divided by 1–64 | implemented | implemented | all nonzero values instead use 32.768 kHz and the low-three-bit crystal table | implemented with divisors 1–64 |
| Mode-3 source | additional port-0x2F divisor | ordinary CPU-family decode | ordinary CPU-family decode | same fixed-crystal decode; port 0x2F is unmapped | ordinary CPU-family decode |
Counter 0 | recurring 256-count timer without completion | implemented | reaches ordinary underflow after 256 decrements | never decremented by the callback | scheduled by the same countdown path as other reload values |
| Mode bit 1 | set requests interrupt | set requests interrupt | set requests interrupt | clear requests interrupt | set requests interrupt |
| Mode/status bit 2 | missed acknowledgement/overflow | set on a second unacknowledged expiry | set on the first underflow | never exposed; mode writes retain only bits 0–1 | completion/loop state is held in emulator timer fields |
| RTC | ports 0x40–0x48 | host wall time plus offset | emulated elapsed time plus base | unmapped | implemented |
TilEm timer and RTC policy
TilEm reproduces the paths used by this OS, but several model choices matter for timing experiments. [standard]
Port-0x03 bits 1 and 2 jointly control TilEm’s programmable-timer
NO_HALT_INT flag. With both bits clear, a halted CPU receives no programmable
request even though port-0x04 exposes completion. Either bit set removes the
gate for all three timers. A running CPU receives the request in either state.
The guarded interrupt probe exercises all three cases through the direct timer
callback. [standard]
- Standard-timer periods are rounded to whole microseconds:
{1953, 4395, 6836, 9277}. - Crystal-family programmable timers use the documented divisor table. CPU-family duration is measured in Z80 clocks and follows the speed selected at port
0x20. - The
0xC0family uses the ordinary CPU-family decode, so port0x2Fdoes not prescale it. - Port
0x2Dstores its low two bits but does not pause the oscillator or programmable timers in low power. - An internal
NO_HALT_INTflag suppresses programmable-timer interrupts duringHALTwhen neither standard timer is enabled at port0x03. - The RTC uses host
time_tplus an offset. Disabling it freezes the stored count rather than making current ports read zero.
A full TilEm reset disables all three programmable timers and clears their
frequency, reload, and status fields. It reschedules the standard timers but
retains the global Z80 clock and dynamically allocated scheduler timers. The
TI-84 Plus callback also leaves CLOCK_MODE, CLOCK_INPUT, and CLOCK_DIFF
unchanged. A guarded direct-core run verifies each boundary. These are emulator
lifecycle rules, not physical RTC or reset behavior. [standard]
TilEm tracks completion internally for port 0x04 while exposing loop, interrupt enable, and overflow through the low three mode/status bits. The first nonzero-counter expiry sets completion; a second expiry without a mode write sets visible overflow bit 2. [standard]
Native TilEm confirmation. The guarded direct-core matrix verifies all
eight rounded crystal durations and all seven CPU divisors. Sources 0x00,
0x01, and 0x3F leave the scheduler stopped while preserving a written
counter. Source 0xC0 schedules one CPU clock with port 0x2F set to 0x00,
0x4A, or 0xFF. [standard]
Counter zero schedules a recurring 256-tick callback without completion.
One non-looping expiry produces internal status 0x100, port-0x04 = 0x28,
and no request. A second unacknowledged expiry changes the visible mode/status
read to 0x04. Interrupt mode produces internal request 0x08; completing
timers 1–3 cumulatively produces port-0x04 values 0x28, 0x68, and
0xE8, with internal request masks 0x08, 0x18, and 0x38. A mode write
clears completion and the matching request. A source write after four of ten
CPU ticks — advanced directly in the probe’s scheduler clock — retains counter
six and stops the timer. [standard]
The RTC case substitutes a deterministic time_t source inside the probe
process. Committing 0x12345678, advancing ten seconds, disabling for ninety,
and re-enabling for five produces 0x12345678, 0x12345682, 0x12345682,
and 0x12345687. A disabled commit of 0xDEADBEEF survives full TilEm reset
with control mode 0x02. Two isolated executions produce identical canonical
native JSON with SHA-256
0da06edc402dfb14945d28577f212face4c04c22b3b6ffc3e283a70e0ecb4aa5.
The binary SHA-256 is
fa665079fac1ace807930be8a3836385f6821ee9994c6454039b8ca85bb75d77.
[standard]
Wabbitemu timer and RTC policy
Wabbitemu stops a programmable timer and clears its pending interrupt generation on a source write. It decodes the crystal-family divisors as 3, 32, 327, 3276, 1, 16, 256, and 4096; the three near-decimal divisors therefore differ from both the published table and TilEm. Its 0x80 and 0xC0 families both use the divided-CPU decode and ignore port 0x2F. [standard]
Wabbitemu’s low-level CPU_reset and frontend calc_reset do not reset the
timer context, delay registers, standard interrupt controller, programmable
timers, or RTC. A guarded initialized-core run retains seeded T-states
123456, frequency 25 MHz, timer version 1, and byte-complete state for those
peripherals. Direct seeding verifies emulator field retention only. It does not
establish warm-reset, cold-reset, or power-loss behavior on an ASIC. [standard]
for source; [confirmed] for the pinned run.
Wabbitemu registers ports 0x29–0x2F through one delay-latch handler. Port
0x2D consequently stores all eight bits and only recomputes the memory-wait
booleans from the active speed register and port 0x2E. A native write to
0x2D leaves the programmable-timer state, clock frequency, LCD-active state,
HALT, interrupt line, and T-state count unchanged. This differs from the
public low-power contract and cannot establish physical port-0x2D behavior.
[standard]
The crystal handler computes elapsed 32.768 kHz ticks but uses a single if, so one invocation decrements each crystal timer at most once even if multiple source periods elapsed. The CPU path uses while and catches up all elapsed divisors. On the first expiry, Wabbitemu reloads the original counter, stops if loop bit 0 is clear, sets the underflow flag exposed as mode/status bit 2 and port-0x04 completion, and retains interrupt generation when mode bit 1 is set. It does not assert that interrupt while the emulated CPU is in HALT. [standard]
Wabbitemu implements ports 0x40–0x48 from emulated elapsed seconds rather than host wall time. A bit-1 rising edge copies the staged value into the base. Bit-0 transitions start or stop elapsed-time accumulation, and disabled reads return the frozen base. Each staged-byte write also resets the stored elapsed-time reference; the OS set sequence commits immediately afterward, so this does not disturb the traced ROM path. [standard]
Native Wabbitemu confirmation. The guarded initialized-core probe loads counter 3 with crystal source 0x41, advances the emulated crystal by 320 ticks, and reads the counter three times without advancing time again. The reads are 0x02, 0x01, and 0x03: each device evaluation consumes one pending divisor, and the third reloads the original count. Mode/status reads 0x04, and port 0x04 reads 0x28. [standard]
The corresponding CPU-source case loads counter 3 with source 0x80 and advances four T-states. One counter read catches up three divisors, reloads 0x03, and produces the same 0x04 mode/status and 0x28 port status. Loading counter zero and advancing 257 T-states reaches underflow after 256 decrements. It reads back zero with status 0x04 and port 0x04 = 0x28. A mode write acknowledges that state, returning mode/status to 0x00 and port 0x04 to 0x08. [standard]
With mode 0x02, source 0x80, and counter 1, expiry during HALT leaves the CPU interrupt line clear while mode/status reads 0x06. Evaluating the timer after leaving HALT asserts the retained interrupt request. The RTC case commits 0x12345678, advances emulated time by 10.75 seconds, and reads 0x12345682. Disabling the RTC freezes that value through an advance to 100 seconds. These tests inject emulator clock values directly; they do not measure wall-clock accuracy, callback cadence under CPU execution, or physical low-power behavior. [standard]
Assembled-probe confirmation. The exact 835-byte HWTMR image also runs
after a retail OS 2.55MP boot. The guarded runner stops at 01:9EE4 before
_CreateAppVar, after 1,645,212 probe instructions and 12,937,610 modeled
T-states, with no execution-violation reset. Four samples infer source-0x41
divisor 3568/111, about 32.144. Speed requests 0–3 read back as 0, 1, 1,
and 1, and the nonzero cases infer prescalers near one. Counter zero produces
mode/status 0x04 and port 0x04 = 0x68; both expiry samples expose bit 2.
All saved timer, speed, port-0x2F, power-control, and interrupt-mask fields
compare equal after cleanup. [confirmed] for the pinned Wabbitemu run.
The shared injected-program adapter has SHA-256
3acb6a18280f9c42d6fe324188eab73f87280ee70b973e1251fcfa50f54fb14e.
The machine-code SHA-256 is
6767caf1d714bc15e642de2f791151a060015fa0d9faebe1ebddd92d184df68a.
This execution does not create the result AppVar or measure physical timing.
MAME timer and RTC policy
MAME maps only timer ports 0x30–0x38 from this block. Ports 0x2D–0x2F and RTC ports 0x40–0x48 are unmapped. For every nonzero source value, a counter write selects one of the eight Wabbitemu-style crystal divisors from the low three bits and schedules at 32768/divisor Hz. CPU-source family bits do not select the CPU clock. [standard]
The initial callback is scheduled at zero delay, so a nonzero counter value $N$ reaches its first modeled expiry after $N-1$ periodic intervals; a value of one can expire immediately. Counter zero remains zero because the callback decrements only a nonzero count. At expiry, loop bit 0 reloads the counter once, but the callback then applies loop &= 2 and discards that bit. Mode bit 1 has inverted polarity: an interrupt and port-0x04 completion are produced only when it is clear. A mode write also clears all three programmable completion bits globally rather than only the selected timer. [standard]
The TI-84 Plus driver is marked MACHINE_NOT_WORKING. Its standard timers remain fixed at 256 Hz and 512 Hz, and port-0x04 writes do not retime them. It can still run the repository ROM’s page-0 ON-wake path, as shown in Interrupts (IM1), but that execution does not validate the timer model. [standard]
Native MAME confirmation. The guarded CPU-I/O-space probe parks the Z80 in a DI loop on isolated RAM. Source bytes 0x01, 0x41, and 0x81 each reduce counter 0xFF to 0xEA over 20 ms of emulated time. The 21 decrements comprise one zero-delay callback and 20 periods at 1,024 Hz. This confirms that the documented off, crystal, and CPU families all use low-three-bit divisor 32. [standard]
Counter zero remains zero with source 0x07 after 15 frames. The source readback remains 0x07, and port 0x04 remains 0x08. Source zero disables a running timer while preserving count 0x05. A mode write retains only bits 0–1. Mode bit 1 set produces no completion; the same count with bit 1 clear sets timer-3 completion and changes port 0x04 from 0x08 to 0x88. [standard]
Loop mode reloads count one, clears bit 0, and schedules another zero-delay callback. The second callback stops the timer, leaving count, source, and mode at zero with completion set. Simultaneous timer-1 and timer-2 completion produces port 0x04 = 0x68; writing timer 1’s mode clears both bits and returns 0x08. Ports 0x2D–0x2F and 0x40–0x48 return zero before and after patterned writes. Two isolated runs produce byte-identical reports with SHA-256 5aab56b737495fef9c953522e1a3eee47d3e96637bc8266ce6258ff10d3e2c26. [standard]
A separate guarded legacy-interrupt run enables standard timer 1, standard
timer 2, and both for one 20 ms frame. Port 0x04 reads 0x0A, 0x0C, and
0x0E. Timer 1 produces 0x0A after both configuration writes 0x00 and
0x06, consistent with the source’s fixed callbacks. A soft reset retains
both masks: clearing their pending fields after reset allows both to regenerate
status 0x0E during the next frame. These are MAME scheduler and reset results,
not physical timer periods or retention. [standard]
Reusable timer tools
tools/ti84re/hardware/timer.py exposes exact rational source rates, first-expiry timing, callback outcomes, the ROM’s radix-255 chunks, RTC implementation profiles, and the physical-probe discriminator. tools/ti84re/hardware/describe_timer.py is a JSON-capable front end. The TilEm, Wabbitemu, and MAME report oracles validate native observations against reusable source models; tools/ti84re/emulators/jstified.py supplies a separately hash-guarded source profile without claiming a native run. tools/ti84re/emulators/tilem/timer.py adds the complete direct-core programmable-timer and deterministic RTC matrix. tools/ti84re/emulators/tilem/interrupt.py adds direct standard-timer scheduling and programmable-timer HALT-gate observations. tools/ti84re/emulators/mame/interrupt.py adds fixed standard-timer and reset-retention observations through the immutable MAME state in tools/ti84re/hardware/interrupt_controller.py. Their guarded CLIs retain exact binary, ROM, adapter, output, and evidence-scope identities. CPU-speed and port-0x2D implementation edges use tools/ti84re/emulators/wabbitemu/speed_probe.py and its guarded CLI. tools/ti84re/emulators/wabbitemu/run_timer_physical_probe.py executes the assembled physical discriminator through the shared injected-program runner. These are emulator-comparison tools, not physical-hardware simulators.
nix develop -c python3 -m ti84re.hardware.describe_timer \
source 0x41 0x80 0xC0 --mode3-prescaler 4
nix develop -c python3 -m ti84re.hardware.describe_timer \
duration --source 0x41 --counter 0xFF
nix develop -c python3 -m ti84re.hardware.describe_timer \
expiry --mode 0x02 --halted --no-standard-timer
nix develop -c python3 -m ti84re.hardware.describe_timer chunks 0x0100 0x0101
nix develop -c python3 -m ti84re.hardware.describe_timer --json rtc
timer_probe_parent=$(mktemp -d /tmp/ti84-timer-probe.XXXXXX)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_timer_edge_probe \
--rom tools/rom.bin \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$timer_probe_parent/run" --json
physical_timer_parent=$(mktemp -d /tmp/ti84-physical-timer.XXXXXX)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_timer_physical_probe \
--rom tools/rom.bin \
--binary "$wabbit_tmp/wabbitemu-headless" \
--expected-binary-sha256 \
3acb6a18280f9c42d6fe324188eab73f87280ee70b973e1251fcfa50f54fb14e \
--output-dir "$physical_timer_parent/run" --json
mame_timer_parent=$(mktemp -d /tmp/ti84-mame-timer.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_timer_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_timer_parent/run" --json
Resolved findings and open hardware questions
- [confirmed]
standard_timer1_irqdrives APD, keypad scanning, cursor blink, and the run indicator. - [confirmed]
33:5EB4is the programmable-timer API interrupt handler;35:4792is a USB timer-3 handler. - [confirmed] APD expires 29,441–29,696 kernel ticks after
_ApdSetup, depending on the untouched low-byte phase. - [confirmed] The cursor toggles every 50 kernel ticks.
- [confirmed] The timer bcall API exposes only ID
0x70, uses radix-255 duration chunking, and keeps a saturating expiry count. - [confirmed] The Bad Apple application writes timer-1 tuple
0x82/0x03/120; the documented CPU-clock decode gives 31.25 kHz at nominal 15 MHz, while its companion encoder assumes 33,333.3 Hz. - [confirmed] Explicit power-off and APD share
poweroff_shared_tail. - [standard] TilEm matches the published
33/328/3277crystal divisors; pinned Wabbitemu and MAME sources use32/327/3276. - [standard] TilEm, Wabbitemu, MAME, and jsTIfied all omit the published port-
0x2Fprescaler from their0xC0-family timer models. - [standard] A guarded TilEm run verifies its four whole-microsecond standard-timer periods, unchanged current intervals on rate writes, two timer-2 callbacks sharing one pending bit, and the three programmable-timer HALT-gate cases.
- [standard] A guarded TilEm timer/RTC run verifies every source divisor, rounded count readback, off sources, ignored port-
0x2F, counter-zero behavior, overflow and acknowledgement, the unacknowledged non-loop restart, per-timer status mapping, source-write retention, RTC freeze/re-enable/reset, and an exact torn read. - [standard] A guarded initialized-core Wabbitemu run verifies single-step crystal catch-up, full CPU catch-up, first-underflow status bit 2, counter-zero completion, acknowledgement, HALT-line suppression with retained generation, and frozen disabled RTC reads.
- [confirmed] The exact assembled
HWTMRimage reproduces Wabbitemu’s divisor-32, omitted-prescaler, counter-zero-completion, and first-expiry-bit-2 model through its cleanup boundary. No physical result has been recorded. - [standard] A guarded MAME run verifies fixed-crystal source-family collapse, its zero-delay first callback, idle counter zero, inverted mode-bit polarity, one-reload loop behavior, global completion clearing, source-off preservation, and unmapped auxiliary and RTC blocks.
- [standard] A separate guarded MAME run verifies both standard-timer pending bits, timer-1 status within one frame after port-
0x04writes0x00and0x06, and retained legacy masks across soft reset. - [standard] Wabbitemu’s low-level and frontend reset paths retain the timer context, delay registers, interrupt controller, programmable timers, and RTC. A guarded run confirms the directly seeded state. Physical reset retention remains open.
- [standard] TilEm’s full reset clears programmable timers and reschedules standard timers while retaining the global clock, RTC fields, and dynamic scheduler timers. A guarded direct-core run confirms the seeded boundaries. Physical reset retention remains open.
- [confirmed] The prepared memory-bus timing probe uses timer 2 only when its source and mode are zero, records completion state for every sample, and restores the idle counter byte. No physical result has been recorded.
- [hypothesis] Physical RTC reads can tear across a one-second rollover because no latch or OS retry is documented.
- [hypothesis] The physical crystal divisors, port-
0x2Fprescaler, first-versus-second-expiry meaning of mode/status bit 2, counter-zero edge, and precise reason programmable timers fail to wakeHALTneed direct TA2/TA3 measurements. - [hypothesis] Low-power behavior of port
0x2D, disabled RTC reads, control-edge behavior, and rollover coherence should be checked on TA2 and TA3 hardware rather than inferred from emulators.
Sources
| Source | Used for |
|---|---|
| WikiTI interrupt overview | source bits, masks, acknowledgement, and HALT notes |
WikiTI port 0x04 | standard-timer rates and programmable completion bits |
WikiTI port 0x20 | CPU-speed settings and physical measurements |
WikiTI ports 0x2D and 0x2F | low-power crystal control and mode-3 prescaler |
| WikiTI programmable timers | timer triplets, divisors, modes, overflow, and HALT quirk |
Bad Apple application source at 111dcf1 and companion encoder | third-party timer setup, ISR output, tracker cadence, and note-counter constant |
| WikiTI RTC control, set registers, and current registers | RTC protocol and 1997 epoch |
| WikiTI hardware history | ASIC integration, quartz oscillator, and TI-84 Plus RTC |
| Datamath TI-84 Plus hardware | TA2/TA3 identification, ASIC/PCB photographs, and 15 MHz specification |
TilEm x4_io.c, x4_init.c, and timers.c | emulator timer, RTC, interrupt, and power policy |
TilEm calcs.c and z80.c | reset sequencing and scheduler-state retention |
Wabbitemu 83psehw.c, 83psehw.h, core.c, and calc.c | independent source decode, catch-up, underflow, HALT, RTC, and reset-retention policies |
MAME 0.287 ti85.cpp and ti85_m.cpp | mapped ports, scheduling, callback polarity, standard timers, and driver status |
jsTIfied deployed 20170706a artifact and readable mirror | fourth timer-source decoder, cycle scheduling, interrupt, low-power, and RTC policy |
Boot, contexts, and errors
This page connects three cross-cutting mechanisms: OS startup, context switching, and error unwinding.
Boot [confirmed]
3F:4000: LD A,0x07
OUT (0x04),A ; paired mapping
LD A,0x7F
OUT (0x06),A ; pages 3E/3F in 4000/8000
LD A,0x03
OUT (0x0E),A ; extended Flash bits
JP 0x812C ; page 3F in the 8000 window
The emulator reset trace begins at logical 0x8000, where paired mode exposes
retail_boot_reset_stub at 3F:4000. The stub keeps page 3F in the B window
and jumps to logical 0x812C, corresponding to boot_os_entry at 3F:412C.
Page 0 also has a restart vector at
00:0000 → 00:028C that establishes the same paired mapping and reaches the
same continuation. At boot_os_entry + 0x20 (3F:414C), the continuation
changes to independent mode while page 3F remains visible for the next
instruction. See
Paging for the complete window-by-window
transition. [confirmed]
Retail boot hardware initialization follows the continuation through its reset delay, RAM-window safety checks, ordered ASIC programming, first keypad scan, OS-validity decision, and destructive RAM diagnostic. It also reconciles standard Z80 timing with the pinned TilEm trace.
The assembled rom.bin validates and installs the retail D84PBE1.8Xv
payload at page 3F; the pinned base already contains the same 16 KiB page, so
the installation changes no bytes. The continuation begins:
IM 1
LD B,0
LD SP,0xFDFA
The boot page eventually initializes RAM, the VAT, system flags, the LCD, and enters the main context (the homescreen). [confirmed]
The boot page (3F) and its version queries are exposed to the OS through ti83plus.inc bcalls: _getBootVer (bcall 0x80B7 → 3F:477C) and _getHardwareVersion (bcall 0x80BA → 3F:4781). The USB boot support entry points route through the same table but land on page 2F, for example _AttemptUSBOSReceive (0x80E4 → 2F:4145) and _InitUSB (0x8108 → 2F:52A4).
RAM clearing and reinitialization (ram_reset_wipe at ram:0BD9) [confirmed]
The RAM-init proper is ram_reset_wipe (35:719F, reached on a full reset; the same routine backs the [2nd]+[+] · 7 · 1 · 2 RAM-reset and the post-boot RAM clear). It zero-fills RAM in two blocks, preserving a handful of flag bits and 0x9B73 across the wipe:
ram_reset_wipe (35:719f):
; save flags to preserve: (9B73), (IY+34).6, (IY+35).0, (IY+35).1, (IY+3F)&0x7F
DI
LD HL,0x8000
LD DE,0x8001
LD BC,0x1BC3
LD (HL),0
LDIR ; clear 8000..9BC3
... restore the saved flag bits ...
LD HL,0x9BD0
LD DE,0x9BD1
LD BC,0x642F
LD (HL),0
LDIR ; clear 9BD0..FFFF
JP 0x0BD9
ram_init_after_reset (ram:0BD9):
LD A,0xC0
OUT (0),A ; port 0 = memory-map control
LD SP,0xFFF7 ; reset stack to top of RAM
CALL 0x3EC1 ; continue init (page-0 kernel): VAT, sysflags, LCD …
So RAM is wiped in two LDIR runs (0x8000–0x9BC3, then 0x9BD0–0xFFFF, leaving the 0x9BC4–0x9BCF window and the explicitly-saved flag bytes intact), then ram:0BD9 resets the memory map (port 0) and the stack and hands off through ram:3EC1. This ram:0BD9 entry is the same RAM re-init point cross-referenced from Memory management. The ram:3EC1 continuation (VAT/sysflag/LCD bring-up) is page-0 kernel code and begins with CALL 0x2B09. The reset jump to boot_os_entry is also present in the assembled database, so the page-0 and retail boot portions can be followed in one project.
The main event loop [confirmed]
main_event_loop @ ram:05e6 (page 0) is the OS root dispatcher. Structure:
05e6: LD B,8
LD HL,0x84BE ; iterate an 8-entry event/context stack
05eb: INC HL ; first slot is 0x84BF
05ec: LD A,(HL)
OR A
JR Z,... ; skip empty slots
05f5: CALL 0x3f3f ; per-entry dispatch (event/key router)
0601: CP 0x7F / 0xFE / 0xFC / 0xFB ; branch on the handler's return code
...
0690: LD A,0x7F
CALL call_context_main ; run the active context's handler
0699: POP AF
JP Z,0x05e6 ; loop
So the loop pumps an event/context stack (8 slots from 0x84BF, after the INC HL), routes each via the dispatcher at ram:3F3F, and ultimately runs the active context’s cxMain handler through call_context_main, looping forever.
The ram:3F3F router is a bjump trampoline → event_key_router (07:4539): given a key code, it scans key→context dispatch tables (07:4099, ~105 entries, for 1-byte keys; 07:422C/4426 for extended 2-byte keys, using _LdHLind/_CpHLDE) and returns a routing code:
0xFE— normal: hand the key to the active context’s handler.0xFB/0xFC— context switch / app launch (the key maps to a different context — recallcxCurAppis a key code, so e.g.[GRAPH]→ the graph context).0xFF/0x7F— quit / no-op.
So the router classifies a mode key before the active context sees it and returns a context-switch code (0xFB/0xFC); the caller then swaps the cx* vectors. The router itself only writes keyExtend (0x8446, the extended-key state) — its body holds no store to the cx* block. [confirmed]
Contexts and OS modes [confirmed]
The OS is single-tasking but multi-context. A context is the set of handler routines for whatever is currently in front of the user (homescreen, an editor, the graph screen, a Flash App). The active context’s vectors live in RAM at cxMain (and friends), with cxPage holding which flash page their code is on.
_AppInit(ram:0936) installs a context: copies 12 bytes of handler vectors →cxMain, setsflags.appFlags, and savescxPage = port_mapBankA(the page the app runs from). [confirmed]- The dispatched handlers include things like a key handler, (re)display/paint handler, and a PutAway (suspend) handler — the OS calls them through the
cx*vectors, paging incxPagefirst. _PutAway(ram:08AF) calls the current context’s PutAway handler (cxPPutAway) to suspend/clean up — used on APD, when switching apps, or on2nd+QUIT. [confirmed]_PowerOff(5008, bodyram:09E6) performs context/display cleanup and joinspoweroff_shared_tailatram:0A24. The shared tail disables the standard timers, enables ON/link wake, and enterspoweroff_halt_loopatram:0A5C. See Clock, timers, and power. [confirmed]
The UI runs on this mechanism: the main event loop reads a key (_GetKey), then calls the active context’s key handler; switching screens swaps the cx* vectors.
Context block layout [confirmed]
The active context lives at a fixed RAM block (Context struct, base cxMain=0x858D):
| Off | Addr | Field | Meaning |
|---|---|---|---|
| +0 | 858D | cxMain | main/event handler ptr |
| +2 | 858F | cxPPutAway | putaway handler ptr |
| +4 | 8591 | cxPutAway | putaway |
| +6 | 8593 | cxRedisp | redisplay/repaint handler ptr (the inc’s cxRedisp bcall, id 0x4C6C, body ram:08D0, reads this slot via LD HL,(8593) and dispatches it) |
| +8 | 8595 | cxErrorEP | error entry point ptr |
| +10 | 8597 | cxSizeWind | window-size handler ptr |
| +12 | 8599 | cxPage | flash page the handlers live on |
| +13 | 859A | cxCurApp | current context id — equals a key code (cxGraph=kGraph, cxCmd=kQuit, cxPrgmEdit=kPrgmEd …) |
| +14 | 859B | cxPrev | base of the 14-byte shadow of cxMain…cxCurApp (plus a separately-saved appFlags byte) — the suspended previous context |
_AppInit copies the 6 vectors (12 bytes, +0..+11) from an app’s header into this block, then sets cxPage. Because cxCurApp is a key code, a mode-switch key naturally selects the context to load.
The full _AppInit body confirms the offsets directly — HL points at the app’s 12-byte vector header, LDIR lands them at cxMain=0x858D, and the byte that follows the 12 vectors becomes a flags byte; cxPage is then loaded from the live bank-A page-select (port 6), not copied from the header:
_AppInit (ram:0936):
; HL = source (12-byte vector header) on entry
LD DE,0x858D ; -> cxMain
LD BC,0x000C ; 12 bytes = the 6 handler vectors
LDIR ; cxMain..cxSizeWind+1 (+0..+11)
LD A,(HL) ; the 13th header byte (appFlags)
LD (0x89FD),A ; -> appFlagsAddr (system flag byte)
IN A,(0x6) ; current bank-A flash page
LD (0x8599),A ; -> cxPage (+12, the page the handlers run from)
RET
The destination 0x858D and length 0x000C pin the six 2-byte handler slots cxMain(+0) cxPPutAway(+2) cxPutAway(+4) cxRedisp(+6) cxErrorEP(+8) cxSizeWind(+10), and the explicit LD (0x8599),A writes cxPage at +12 from port 6. _AppInit installs a context, but it is not the only writer: _POPCX (bcall 0x49E1, body 07:6D1C) restores a suspended context by LDIRing 14 bytes cxPrev→cxMain (0x859B→0x858D) and copying a 15th byte into the app-flags, and a matching save path (the LDIR at 07:5A8C) copies cxMain→cxPrev. cxCurApp(+13, 0x859A) is the current context id (a key code); the shadow at cxPrev(0x859B) holds the suspended context.
How a context handler is invoked [confirmed]
call_context_main (ram:08fa):
set_bankA_page(cxPage)
call (cxMain) via jp_hl
ret # control returns here after the paged handler
call_context_savepage (ram:08e9):
save port6
set_bankA_page(cxPage)
jp_hl
restore port6
Primitives: set_bankA_page (ram:078c, port6 = page) and jp_hl (ram:090b, jp (hl) dynamic dispatch). The OS pages the handler in, runs it, and (for the savepage variant) restores the caller’s page.
_newContext and string-input entry [confirmed]
_newContext = 0x4030, body ram:077E, takes the requested context selector
in A. It copies that byte to C, clears kbdKey at ram:8444, clears B,
saves port 0x06, calls the shared context/key installer at ram:0791, and
restores port 0x06. The routine therefore preserves the caller’s bank-A
mapping; the supplied selector can still change context and key state.
A controlled TilEm call with A = 0x40 clears kbdKey, returns, and preserves
port 0x06 at 0x07. This establishes the callable contract for that input,
not the broader community description that every call “restores the home
context.” [confirmed] under TilEm.
_GetStringInput2 = 0x4E61, body 37:5194, is a higher-level input-context
entry. It sets bit 1 of IY + 0x09, saves the byte at ram:9653, sets bit 0 of
IY + 0x29, calls _newContext with A = 0x50, and enters the shared context
dispatcher at ram:04F9. A valid caller must prepare ioPrompt at ram:865F
and protect the temporary-allocation state; the archived Elite caller saves
cleanTmp, replaces it with pTempCnt, invokes the bcall, then restores it.
The reconstructed caller follows that sequence with prompt A=?, submits
1 then ENTER, and regains control with OP1 equal to the 11-byte real
encoding of 1 (00 80 10 00 00 00 00 00 00 00 00). The trace reaches
37:5194 once, ram:077E twice, and ram:04F9 twice before the caller copies
OP1. The additional context visits belong to the interactive editor path.
Cancel and invalid-expression exits remain untraced. The reduced result is in
tools/data/community-string-input.csv. [confirmed] under TilEm.
Error handling [confirmed]
Errors use a non-local exit, not return codes:
- A routine detects a fault and calls
_JError(ram:2793) with an error code inA(theTIErrorenum:E_Domain,E_DivBy0,E_Memory, … each ORed withE_EDIT=0x80 if re-editable)._JErrorstores the code toerrNo(0x86DD); the sibling entry_JErrorNo(ram:2799) raises the already-storederrNowithout taking a new code. - The handler restores the stack from
errSP(0x86DE,LD SP,(errSP)atram:27BB), restores a sane state, and displays the error screen (ERR:+ message, with1:Quit 2:Goto).errSPis the current error frame;_resetStacksseeds it fromonSP(0x85BC, the context-level saved SP) at context/parse start. - The
E_EDITbit (0x80) tells the handler the error is editable (offer “2:Goto” to jump to the offending token).
So errSP + _JError together implement try/catch: a context seeds errSP (from onSP) at entry, and any depth of nested calls can abort straight back to it.
Custom-error wrapper [confirmed]
_ErrCustom1 (bcall 0x4D41 → ram:2771) loads A = 0xAB and branches to
_JError at ram:2793. After the error handler masks off E_EDIT, code
0x2B selects the pointer at 07:6B20. That table entry is 0x984D, the
appErr1 custom-message buffer. The display path treats the buffer as a
null-terminated string. [confirmed]
The pinned include places appErr1 at 0x984D and appErr2 at 0x985A, so
the first buffer occupies 13 bytes. A bounded message can contain at most 12
bytes plus its null terminator. _ErrCustom1 takes the same non-local path as
_JError, so instructions after the bcall do not perform caller cleanup.
[confirmed]
The community example
programs/generateerror.zip
(SHA-256
1731b2b2cf7580855f7007478d55299e12b1ec4b7430d371feedea764c9139cf)
copies the Ans string payload into appErr1 and invokes bcall 0x4D41. Its
generr.z80 member (SHA-256
5f28fca2dec72dc4d495aff18a8d6b076f02d34b8cafcdbd78c11c05f364386d)
does not bound the copy to 13 bytes or write an explicit null terminator.
This static source is evidence of community usage, not evidence that arbitrary
Ans strings are safe. [confirmed]
The bounded ERRPROBE fixture copies COMMTRACE\0 into appErr1 and invokes
the same 4D41h bcall under TilEm x4. Its 3,634,222-instruction trace reaches
ram:2771, _JError at ram:2793, and the display path at 07:6A72 once
each. The rendered error is ERR:COMMTRACE; control does not reach the
fixture’s post-bcall halt. The trace SHA-256 is
1c9dbb368b74258ab3b82dc46f9fbb23710b23e343455415dddcef4d37a959eb.
The patched TilEm executable SHA-256 is
b8ee505483c79732a4ca21efb8b904de0792477795f6fc717874dcd5addaed09.
tools/data/community-custom-error.csv records both executable identities,
the fixture, macro, recording, and hit counts. It also records zero visits to
the fixture’s unexpected-return path. This confirms the nonlocal path in the
identified emulator scenario;
it is not a physical-hardware result. [confirmed]
Error-message table [confirmed]
The error screen shows ERR:<MESSAGE>; the ERR: prefix is at 01:4008. The handler at 07:6A72 masks the code with 0x7F, then indexes a little-endian pointer table at 07:6ACC by (code) − 1 for codes below 0x3A. It fetches the pointer through _LdHLind and copies the selected null-terminated string. Codes 0x36, 0x37, 0x39, and values at least 0x3A bypass the table and select ? at 07:6C5A. [confirmed]
| Code | TIError | Message @ page_07 |
|---|---|---|
| 1 | E_Overflow | OVERFLOW (6B3C) |
| 2 | E_DivBy0 | DIVIDE BY 0 (6B45) |
| 3 | E_SingularMat | SINGULAR MAT (6B51) |
| 4 | E_Domain | DOMAIN (6B5E) |
| 5 | E_Increment | INCREMENT (6B65) |
| 6 | E_Break | BREAK (6B6F) |
| 7 | E_NonReal | NONREAL ANS (6B75) |
| 8 | E_Syntax | SYNTAX (6B81) |
| 9 | E_DataType | DATA TYPE (6B88) |
| 10 | E_Argument | ARGUMENT (6B92) |
| 11 | E_DimMismatch | DIM MISMATCH (6B9B) |
| 12 | E_Dimension | INVALID DIM (6BA8) |
| … | … | UNDEFINED, MEMORY, INVALID, ILLEGAL NEST, BOUND, WINDOW RANGE, ZOOM, LABEL, STAT, SOLVER, … |
| 31–35 | link-error aliases | LINK (6C55) |
The Code column is each error’s low 7 bits. Re-editable errors set the E_EDIT (0x80) bit on top — E_Overflow equ 1+E_EDIT, E_DivBy0 equ 2+E_EDIT, … — while non-editable ones (E_Label equ 20, E_Stat equ 21, …) carry no such bit. The handler masks the code (AND 0x7F) before indexing. Thus _JError(0x22) and _JError(0x9F) both select LINK at 07:6C55, through pointer entries 07:6B0E and 07:6B08. tools/ti84re/rom/describe_error.py reproduces the table lookup from the ROM. [confirmed]
Confirmed details
cx*vector layout — confirmed. The six 2-byte handler slots andcxPageoffsets are pinned by tracing_AppInit(ram:0936):LD DE,0x858D / LD BC,0x000C / LDIRthenIN A,(6) / LD (0x8599),A. See Context block layout above for the full offset table and_AppInitbody._AppInitinstalls the block; it is not the sole writer —_POPCX(bcall0x49E1→07:6D1C) restores a saved context intocxMain, and a save path at07:5A8CcopiescxMaininto thecxPrevshadow.- Boot RAM-init trace — raw-disassembly trace. Emulator reset starts at logical
0x8000on page3Fand reachesboot_os_entry; the page-0 restart vector atram:0000→ram:028Creaches the same continuation. The RAM clear/re-init isram_reset_wipe(35:719F): twoLDIRzero-fills (0x8000–0x9BC3,0x9BD0–0xFFFF) preserving a few flag bytes, thenJP 0x0BD9(ram_init_after_reset: port 0 =0xC0, stack reset in the raw trace,CALL 0x3EC1). Theram:0BD9entry matches the re-init point cross-referenced in Memory management. See RAM clearing and reinitialization. - Flash write and erase. The retail boot table maps
_WriteFlash(80C9) to3F:4C8F,_WriteFlashUnsafe(8087) to3F:4CA6,_WriteAByte(8021) to3F:4C9F, and_EraseFlash(8024) to3F:4C2A. Their program and erase loops are copied toramCodeat0x8100. A successful archive trace executesarchive_write_recordat3D:64AA, three_WriteABytecalls, and six entries through_WriteFlashUnsafe. See Flash memory. [confirmed]
The JP 0x812C target and the ram:3EC1 init continuation are both present in
the assembled database. The retail hardware work before the first keypad scan,
the MODE RAM diagnostic, and boot_lcd_keypad_diagnostic at 3F:4658
are decoded in
Retail boot hardware initialization.
The sole branch to boot_lcd_keypad_diagnostic is constant-false; later recovery/UI paths remain
open. [confirmed]
Retail boot page
TI-84 Plus OS 2.55MP — page layout, startup dispatch, recovery, and validation.
Flash page 3F is the calculator’s retail boot block. It owns the reset stub,
the 0x8xxx bcall table, OS-validation and certificate services, the serial
recovery receiver, and the hardware setup that hands control to either the
installed OS or an installer. [confirmed]
This page maps that control plane. See Retail boot hardware initialization for the register-level reset sequence and the destructive hardware diagnostics.
Evidence boundaries
| Evidence | What it establishes | Confidence |
|---|---|---|
OS 2.55MP page 3F bytes | page layout, instructions, direct branch targets, table entries, and validation checks | [confirmed] |
| Rebuilt Ghidra database | function boundaries and cross-references within page 3F and the USB payload on page 2F | [confirmed] |
| Four reset-origin TilEm traces | ordinary, DEL-held, STAT-held, and MODE-held startup behavior in the pinned emulator | [confirmed] for those runs |
Full 2007 ti83plus.inc | official names for 83 of the 87 callable table entries | [confirmed] |
| Physical calculator with a sending peer | electrical link/USB behavior and a complete installer transaction | [hypothesis] until measured |
The checked trace reduction is tools/oracles/boot/retail-boot-traces.json. Its ROM hash,
emulator source commit, emulator-binary hash, trace hashes, visit counts, and
first-visit clocks keep each dynamic claim tied to a specific run. The raw
TLMT traces are too large for the repository and remain external.
Physical layout
Boot version 1.03 partitions page 3F as follows: [confirmed]
Page-3F range | Contents |
|---|---|
3F:4000–3F:400E | reset stub |
3F:400F–3F:4017 | NUL-terminated version string and header data |
3F:4018–3F:40D4 | 63 bcall entries, IDs 0x8018–0x80D2 |
3F:40D5–3F:40E3 | bank/return dispatch stub, not bcall entries |
3F:40E4–3F:412B | 24 bcall entries, IDs 0x80E4–0x8129 |
3F:412C–3F:7E4D | executable code and data |
3F:7E4E–3F:7FFF | 434 erased bytes (0xFF) |
The table therefore has 87 populated three-byte entries in two ranges, not
one continuous range. Treating 3F:40D5–3F:40E3 as five more entries
decodes executable stub bytes as bogus targets. tools/ti84re/rom/bcall_tables.py rejects
those reserved IDs. [confirmed]
Each real entry stores a little-endian target address followed by a page byte.
Eighty-one targets stay on page 3F; six enter the companion USB boot payload
on page 2F. The public include file names 83 entries. These four populated
slots lack public equates: [confirmed] for addresses and bytes; routine names
and summaries are inferred.
| ID | Body | Inferred role |
|---|---|---|
0x804E | certificate_reconcile_id_fields at 3F:4924 | reconcile calculator-ID certificate fields and rewrite the certificate/validation data |
0x8066 | certificate_find_matching_field_data at 3F:4F91 | find matching data under certificate field 0x0310 and subfield 0x0610 |
0x8069 | certificate_count_matching_fields at 3F:4EFF | count or match certificate fields beginning with field 0x0300 |
0x810B | usb_set_port81_bit0_delay at 2F:62C5 | set bit 0 of USB port 0x81, then delay |
The three certificate summaries agree with the comments beside their unnamed
slots in ti83plus.inc. The 0x804E body also calls certificate erase/write,
RSA-validation, and _WriteValidationNumber services. These semantic names
remain [hypothesis] pending complete caller and data-format reconstruction.
Reset dispatch
After the delay, memory-map transition, and ASIC initialization, the reset path
calls the raw keypad scanner and reaches boot_startup_dispatch at 3F:422D.
Only two scan codes have first-scan boot meanings: [confirmed]
3F:422D call raw_key_scan
3F:4230 cp 0x38 ; DEL
3F:4232 jp z,3F:4279
3F:4235 cp 0x20 ; STAT
3F:4237 jr z,3F:4270
All other keys, including MODE (0x37), take the fast installed-OS check:
[confirmed]
3F:4238 ld a,(0x0038)
3F:423B cp 0xFF
3F:423F ld hl,(0x0056)
3F:4242 ld bc,0xA55A
3F:4246 sbc hl,bc
3F:4248 jp z,0x0053
The jump requires byte 0x0038 != 0xFF and word 0x0056 = 0xA55A.
ram:0053 jumps to ram:0C4F, the installed-OS handoff body. A failed check
enters recovery initialization at 3F:42B3. [confirmed]
flowchart TD
reset[Reset at 3F:4000] --> init[Delay, mapping, and ASIC setup]
init --> scan[First raw key scan]
scan -->|DEL 0x38| del[Initialize recovery]
scan -->|STAT 0x20| stat[Set USB-first flag and initialize recovery]
scan -->|Any other code| marker{RAM sentinels valid?}
marker -->|Yes| os[ram:0053 to installed OS]
marker -->|No| del
del --> receive[boot_receive_dispatch]
stat --> receive
receive -->|USB-first flag clear| serial[Serial link receive wait]
receive -->|USB-first flag set| usb[Attempt USB OS receive]
The following trace outcomes distinguish those branches: [confirmed] for the pinned emulator runs.
| Held key | First scan | Observed endpoint within the trace |
|---|---|---|
| none | 0x00 | ram:0053, then ram:0C4F |
| DEL | 0x38 | boot_link_receive_wait at 3F:63B2 |
| STAT | 0x20 | _AttemptUSBOSReceive at 2F:4145 |
| MODE | 0x37 | ram:0053, then ram:0C4F |
The DEL and STAT runs both display Waiting..., Please install, operating,
and system now. DEL enters the page-3F serial-link wait without visiting
the page-2F USB attempt. STAT sets bit 5 at IY + 0x1B, reaches the USB
attempt, and does not return to the serial wait during the three-second run.
[confirmed]
The orphan MODE dispatcher
boot_mode_diagnostic_dispatch at 3F:427E contains another raw key scan,
compares its result with MODE (0x37), and can jump to
boot_flash_ram_diagnostic at 3F:4504. It is not part of the reset dispatch
described above. [confirmed]
No direct page-3F branch or call targets 3F:427E, and none of the four
reset-origin traces visits it. In particular, holding MODE at reset produces
scan code 0x37 at 3F:4230 and hands off to the installed OS; it visits
neither 3F:427E nor 3F:4504. [confirmed]
An undiscovered computed entry remains possible, so the stronger claim that
the block is unreachable under every boot-page state is [hypothesis]. Directly
entering 3F:427E with a MODE result does select the destructive Flash/RAM
diagnostic documented on the hardware page. [confirmed]
Recovery initialization and transport
boot_recovery_init at 3F:42B3 selects CPU speed 1, restores the runtime
mapping with RAM page 0x81 in window B, initializes the link port and LCD,
and clears RAM from 0x8000 through 0xFE70. It then displays the installer
prompt and enters boot_receive_dispatch at 3F:5C7E. [confirmed]
The first-scan key determines the initial transport:
- DEL enters the ordinary recovery initializer and reaches
boot_link_receive_waitat3F:63B2. [confirmed] - STAT first sets bit 5 of the boot flag byte at
IY + 0x1B. The receive dispatcher observes that flag and calls_AttemptUSBOSReceiveat2F:4145. [confirmed]
The traces stop while each path waits for a peer. They prove transport selection, not receipt, signature validation, Flash programming, fallback after a USB error, or a successful reboot. Those end-to-end behaviors require a controlled sending peer. [hypothesis]
OS validation and invalid-image handling
The fast reset path deliberately uses only the RAM sentinel at 0x0038 and
handoff marker 0xA55A at 0x0056. It does not call _CheckOSValidated.
[confirmed]
Recovery and diagnostic paths can instead call boot_check_os_validated at
3F:43A9. That predicate rejects ram:0026 = 0xFF, opens the protected Flash
gate, invokes _CheckOSValidated (0x809C, body 3F:52C6), closes the gate,
and, on its zero-result path, tail-checks the same 0xA55A marker in
boot_check_os_handoff_marker at 3F:4425. [confirmed]
Error handling at 3F:57E2 can transfer to boot_erase_invalid_os at
3F:4308 after that full validation fails. The routine reinitializes recovery
state, validates again, erases Flash page 0 if the image remains invalid,
closes the protected gate, and enters a power/HALT loop. [confirmed]
This split matters when interpreting a normal trace: reaching the installed OS proves that the two RAM markers passed on that boot, not that the trace executed a fresh certificate or cryptographic validation. [confirmed]
Reproduction
Capture each macro from reset with the pinned TilEm build, --normal-speed,
and --trace-range all, as described in tools/notes/dynamic-tracing.md. Reduce the
four TLMT files with: [confirmed]
PYTHONPATH=tools python3 -m ti84re.boot.analyze_retail_boot \
--rom tools/rom.bin \
--trace normal=/tmp/retail-boot-normal.trace \
--trace del=/tmp/retail-boot-del.trace \
--trace stat=/tmp/retail-boot-stat.trace \
--trace mode_ignored=/tmp/retail-boot-mode-ignored.trace \
--output tools/oracles/boot/retail-boot-traces.json
The corresponding macros are tools/macros/boot-idle.macro,
tools/macros/boot-del-recovery.macro,
tools/macros/boot-stat-recovery.macro, and
tools/macros/boot-mode-ignored.macro. The reducer validates the ROM and
emulator hashes before replacing the checked report.
Retail boot hardware initialization
TI-84 Plus OS 2.55MP — reset delay, hardware writes, safety checks, and boot diagnostics.
The retail boot page changes the reset memory map into the OS runtime map,
checks that both RAM windows are writable, programs the ASIC registers, and
selects an OS or recovery path. Retail boot page maps the
page-level control flow, bcall table, recovery transports, and validation
logic. This page follows retail_boot_reset_stub at
3F:4000 through the first keypad scan at 3F:422D and decodes
boot_ram_test at 3F:461A. It also documents
boot_lcd_keypad_diagnostic at 3F:4658.
Evidence boundaries
| Evidence | What it establishes | Confidence |
|---|---|---|
OS 2.55MP page 3F bytes | instructions, branch targets, port values, safety checks, OS-validity tests, and RAM-test pattern | [confirmed] |
| Rebuilt Ghidra database | function boundaries and cross-references for the keypad, display, OS-validation, and recovery helpers | [confirmed] |
| Full-reset TilEm instruction trace | one executed no-key startup path, ordered I/O values, mapper state, register values, and emulator clocks | [confirmed] for that pinned emulator run |
| Direct-entry Wabbitemu probe | actual page-3F LCD helpers executing after a retail boot baseline, plus controller-RAM and contrast effects | [confirmed] for that pinned emulator run; not evidence that retail control flow reaches 3F:4658 |
| Standard Z80 timing tables | instruction timing used for the reset-delay calculation | [standard] |
| Public port descriptions and emulator implementations | proposed electrical roles for the bytes written to link-assist, GPIO, USB, and wait-state ports | [standard] or [hypothesis] as marked on the subsystem pages |
| Physical reset measurements | oscillator startup, electrical register effects, and RAM power-on state | [hypothesis] until measured |
The ROM confirms what software executes. The trace confirms that TilEm follows one path through those bytes. Neither source measures a physical ASIC reset.
Reset entry and delay
retail_boot_reset_stub at 3F:4000 establishes a paired mapping and jumps to
boot_os_entry at 3F:412C: [confirmed]
3F:4000 ld a,0x07
3F:4002 out (0x04),a
3F:4004 ld a,0x7F
3F:4006 out (0x06),a
3F:4008 ld a,0x03
3F:400A out (0x0E),a
3F:400C jp 0x812C
boot_os_entry spends 518 outer iterations in a nested delay: [confirmed]
3F:412C im 1
3F:412E ld b,0
3F:4130 ld sp,0xFDFA
3F:4133 djnz 3F:4133
3F:4135 ld ix,1
3F:4139 add ix,sp
3F:413B ld sp,ix
3F:413D jr nc,3F:4133
3F:413F ld sp,0xFFC5
DJNZ runs 256 times per outer pass because B starts at zero. The stack
pointer advances from 0xFDFA until the 518th addition wraps to zero and sets
carry. The loop executes 132,608 DJNZ instructions and 2,072 outer-control
instructions. Including IM 1, LD B,0, and LD SP,0xFDFA, the region
contains 134,683 executed instructions. [confirmed]
Standard Z80 timing gives 1,747,727 T-states for the loop and 1,747,752 including the three setup instructions. At a nominal 6 MHz that is 0.291292 seconds. This conversion assumes that the CPU already runs at 6 MHz; the ROM bytes do not establish the physical reset oscillator frequency. [standard]
The pinned TilEm trace reports 1,746,716 T-states over the same region. TilEm
charges 13 T-states for ADD IX,SP, while the standard Z80 timing is 15.
The two-T-state difference occurs 518 times, accounting for all 1,036 missing
T-states. Emulator clock output should therefore not replace the standard
instruction timing for this delay. [confirmed] for the trace and source model;
[standard] for Z80 timing.
Mapping transition
After the delay, the boot page moves from paired to independent mapping without paging out the next instruction: [confirmed]
3F:4142 ld a,0x03
3F:4144 out (0x0F),a
3F:4146 ld a,0x7F
3F:4148 out (0x07),a
3F:414A ld a,0x06
3F:414C out (0x04),a
3F:414E jp 0x4151
3F:4151 nop ; six NOPs through 3F:4156
3F:4157 ld a,0x81
3F:4159 out (0x07),a
The write to port 0x07 first places page 3F in window C under paired mode.
Port 0x04 = 0x06 then selects independent mode, where port 0x06 = 0x7F
keeps page 3F in window A. Port 0x07 = 0x81 finally maps RAM page 0x81
into window B. [confirmed] See Paging for
the complete window table and emulator mapper comparisons.
Flash-gate safety wrapper
Ten page-3F sites enable the protected Flash gate through the same 91-byte
wrapper. Sixteen other sites disable it through the same 15-byte checked
wrapper. Exact-byte scanning accounts for all 26 immediate writes to port
0x14 on the page. [confirmed]
The first enable begins at 3F:415B: [confirmed]
3F:415B push af
3F:415C ld a,1
3F:415E nop
3F:415F nop
3F:4160 im 1
3F:4162 di
3F:4163 out (0x14),a
3F:4165 di
The remaining 80 bytes enforce these invariants before restoring AF at
3F:41B5: [confirmed]
- the saved stack pointer has a high byte in
0xC0–0xFF; - adding eight to the saved stack pointer does not carry;
- port
0x06 & 0x3Fis page0x3For one of pages0x2C–0x2F; - port
0x07equals0x81; - complementing one byte at
0xC000, reading it back, and complementing it again reproduces the original byte; - the same complement-write-read-restore test succeeds at
0x8000.
Every failed test reaches JP 0x0000. The two byte probes are destructive for
the interval between the first and second writes, then restore the original
value before continuing. An interrupt or reset between those writes could
leave one byte complemented; the routine executes with interrupts disabled.
[confirmed]
The disable wrapper saves AF, clears A, emits the protected instruction
sequence, and writes port 0x14. It then executes:
OR A
JP NZ,0x0000
The wrapper restores AF afterward. The test is necessarily zero when normal
sequential execution reaches it. It detects a control-flow or
instruction-corruption error rather than reading the write back. [confirmed]
Ordered hardware programming
The first guarded enable is followed by the complete initialization sequence
below. The order includes the call from 3F:41BA into
boot_link_assist_init at 3F:6278. [confirmed]
| Stage | Writes in execution order | ROM evidence |
|---|---|---|
| Link and low-power setup | 0x2D = 0x02; 0x00 = 0x00; 0x09 = 0x97; 0x0A = 0xB4; 0x0B = 0xB4; 0x0C = 0xB4; 0x08 = 0x80; 0x08 = 0x00 | 3F:41B6–41BA; boot_link_assist_init |
| Bus timing | 0x29 = 0x17; 0x2A = 0x27; 0x2B = 0x2F; 0x2C = 0x3B; 0x2E = 0x45; 0x2F = 0x4B | boot_bus_timing_init at 3F:41BD–41D3 |
| Execution controls | 0x21 = 0x00; 0x22 = 0x08; 0x23 = 0x29; 0x25 = 0x10; 0x26 = 0x20 | boot_execution_protection_init at 3F:41D5–4206 |
| Runtime mapping | 0x0E = 0; 0x0F = 0; 0x05 = 0; 0x06 = 0x3F | 3F:4207–4210 |
| GPIO and USB control | 0x39 = 0xF0; 0x4A = 0x20 | 3F:4212–4218 |
| Gate and final RAM window | protected 0x14 = 0; 0x07 = 0x80 | 3F:421A–422B |
The bytes and their execution order are [confirmed]. Their subsystem meanings have separate evidence limits:
- Bus timing and wait states decodes the wait-state values and distinguishes public timing from emulator models.
- Execution protection gives the modeled no-execute ranges and unresolved physical boundaries.
- ASIC status, identity, protection, and GPIO separates
the ROM’s
0x21and0x39use from conflicting GPIO implementations. - USB ASIC and link assist
distinguishes ROM use of ports
0x08–0x0Cand0x4Afrom public and emulator interpretations.
First boot decision
The raw keypad scanner at 3F:6503 returns a scan code to 3F:422D.
The reset dispatcher assigns boot actions only to DEL (0x38) and
STAT (0x20). Every other result, including MODE (0x37), takes the
installed-OS check below. A reset-origin MODE trace confirms that behavior.
[confirmed]
Without those keys, the normal path checks two page-0 values: [confirmed]
3F:4238 ld a,(0x0038)
3F:423B cp 0xFF
3F:423F ld hl,(0x0056)
3F:4242 ld bc,0xA55A
3F:4246 sbc hl,bc
3F:4248 jp z,0x0053
The jump requires byte 0x0038 != 0xFF and word 0x0056 = 0xA55A.
Page-0 entry 00:0053 is JP 0x0C4F, which begins the OS handoff. A failed
check instead initializes display and RAM state through 3F:42B3, selects CPU
speed zero, polls the ON-key state, restores speed one, and enters the receive
flow. [confirmed] The electrical ON-key polarity and oscillator frequencies
remain subject to the evidence limits on
Keypad and ON-key hardware and
Clock, timers, and power. The DEL/STAT transport split
and trace results are detailed on Retail boot page.
Destructive RAM diagnostic
The unreferenced dispatcher at 3F:427E contains a second raw key scan. If
entered directly with MODE (0x37), it jumps to
boot_flash_ram_diagnostic at 3F:4504. No direct page-3F caller or
reset-origin trace reaches 3F:427E; MODE at the first reset scan instead
takes the installed-OS check. [confirmed] An undiscovered computed entry is
[hypothesis].
The diagnostic’s later RAM-test path calls boot_ram_test at 3F:461A to
test main RAM and banked RAM. [confirmed]
boot_ram_test takes the start address in DE, computes length
0x10000 - DE, writes a repeating byte pattern, rewinds, and verifies the same
pattern. The pattern is 0x00, 0x01, …, 0xFA, then repeats at 0x00.
[confirmed]
The first call uses DE = 0x8A52, covering 0x8A52–0xFFFF. The caller then
clears port 0x27 and tests RAM pages selected by port 0x05 = 2 through 7,
each from 0xC000–0xFFFF. A mismatch clears port 0x05, selects the error
text at 3F:4800, and jumps to the diagnostic display path. A successful pass
calls the raw key reader at 3F:6569 before returning. A zero scan continues
the RAM-page loop. A nonzero scan aborts it through 3F:460F. [confirmed]
This test overwrites every byte in each range and leaves the test pattern in place. It is suitable only for the boot diagnostic path, not for checking RAM that contains live state. [confirmed]
Dormant LCD and keypad diagnostic
The retail ROM contains boot_lcd_keypad_diagnostic at 3F:4658, a complete
LCD pattern, contrast, and keypad test. Its only incoming branch,
boot_diagnostic_gate at 3F:4615, is constant-false. The complete predecessor
is: [confirmed]
3F:4610 xor a
3F:4611 out (0x05),a
3F:4613 cp 0x09
3F:4615 jr z,3F:4658
3F:4617 jp 3F:4510
OUT preserves A. The XOR A therefore makes CP 0x09 nonzero, and the
conditional branch cannot be taken under Z80 semantics. A key read performed
by the RAM worker can reach 3F:4610, but XOR A discards that scan code
before the compare. MODE can reach the RAM diagnostic; it does not make
the LCD/keypad diagnostic reachable. [confirmed]
The remaining subsections describe the dormant bytes. Dynamic results use an explicit RAM-harness entry and do not represent ordinary boot behavior.
LCD pattern helpers
boot_lcd_fill_pattern at 3F:46EF takes alternating row bytes in D and E.
It selects row command 0x80 and each visible byte-column command from 0x20
through 0x2B, then writes 64 data bytes per column. One call emits 24 command
writes and 768 data writes. [confirmed]
boot_lcd_write_row at 3F:472E takes a row command in B and a data byte in
D. It writes the value once in each of the 12 visible byte columns. One call
emits 24 command writes and 12 data writes. [confirmed]
The diagnostic presents these six screens: [confirmed]
| Order | Visible screen | Construction |
|---|---|---|
| 1 | 0x81 in every visible byte, with rows 0 and 63 set to 0xFF | one boot_lcd_fill_pattern call plus two boot_lcd_write_row calls |
| 2 | all 0xFF | equal-byte fill |
| 3 | all 0x00 | equal-byte fill |
| 4 | alternating 0x55 and 0xAA rows | alternating-byte fill |
| 5 | alternating 0x00 and 0xFF rows | alternating-byte fill |
| 6 | all 0xAA | equal-byte fill |
The six stages emit 192 command writes and 4,632 data writes. After each stage,
3F:471A selects CPU-speed value 0, polls 3F:6569 until a key appears,
restores CPU-speed value 1, and returns Z for scan code 0x09 (ENTER).
ENTER skips the remaining screen or contrast stages and advances to the
keypad test. Other nonzero scan codes advance one stage. [confirmed]
Contrast sweep
The contrast loop passes values 0x27 down through 0x01 to
boot_lcd_write_contrast at 3F:74F8. The helper adds 0x18, forces bits 7–6,
waits for the LCD, and writes the result to port 0x10. The emitted commands
descend from 0xFF through
0xD9, covering controller contrast arguments 63 through 25 in 39
key-advanced steps. [confirmed]
Toshiba defines the larger T6K04 argument as darker. This direction comes from
the controller data sheet, not from the ROM arithmetic. [standard] The routine
clears the LCD after the sweep and calls boot_lcd_restore_contrast at
3F:74F5 to restore the contrast stored at 0x8447. [confirmed]
Keypad sequence
The table at 3F:478D contains 49 two-byte entries. Each pair holds an expected
scan code and a decimal position label. 3F:73A2 converts the label for
display. The labels cover 11–15, 21–26, 31–34, the five-entry rows
41–45 through 91–95, and 102–105. The first entry expects Y=
(0x35) at position 11; the final entry expects ENTER (0x09) at
position 105. All 49 scan codes are unique. [confirmed]
For each entry, the loop ignores wrong keys and continues polling. ENTER
aborts unless it is the final expected key. Successful completion clears the
LCD, displays OK from 3F:4807, waits for a non-MODE key, then jumps to
3F:424B. [confirmed]
Direct-entry emulator validation
The guarded Wabbitemu probe boots until the retail protection bounds are
established, maps page 3F, and injects a 30-byte RAM harness. The harness
calls boot_lcd_initialize (3F:74C6), boot_lcd_fill_pattern (3F:46EF),
boot_lcd_write_row (3F:472E), and boot_lcd_write_contrast (3F:74F8).
It does not patch the constant-false branch. [confirmed] for the harness and
pinned emulator run.
The native run executes 8,040 probe instructions and retains only helper-visit
counters, transfer counts, two visible-screen hashes, boundary cells, and the
final contrast field. It observes: [confirmed] for pinned Wabbitemu commit
48c2dc0.
boot_lcd_initializeemits seven commands and falls throughboot_lcd_restore_contrastintoboot_lcd_write_contrast;boot_lcd_fill_patternemits 24 command and 768 data writes for alternating0x55/0xAArows;boot_lcd_write_rowemits 24 command and 12 data writes, changing all 12 bytes of row 63 to0xFF;- an explicit
A = 0x27call toboot_lcd_write_contrastemits0xFF, which Wabbitemu stores as its adjusted contrast level 39.
These observations validate retail-ROM execution against Wabbitemu’s controller model. They do not establish normal boot execution, analog contrast, controller timing, or panel appearance. Physical behavior remains unmeasured. [hypothesis]
Reproducing the checks
tools/ti84re/boot/hardware.py contains the timing arithmetic, ordered write
manifest, exact Flash-gate wrapper classifier, and RAM-test pattern model.
tools/ti84re/boot/describe_hardware.py exposes guarded text and JSON reports:
python3 -m ti84re.boot.describe_hardware delay
python3 -m ti84re.boot.describe_hardware --json manifest
python3 -m ti84re.boot.describe_hardware protected-writes
python3 -m ti84re.boot.describe_hardware trace /path/to/full-reset.trace
python3 -m ti84re.boot.describe_hardware ram-pattern 0x200
python3 -m ti84re.boot.describe_hardware --json lcd-diagnostic
The trace analyzer reads binary TLMT records in one streaming pass, retains
only counters and the 35 selected output events, and stops after the final
boot write at 3F:422B. It does not construct a text line or retain a register
snapshot for every executed instruction. [confirmed]
The saved full-reset TilEm trace matches all 35 ordered output events from
retail_boot_reset_stub + 0x02 through 3F:422B, including the call to
boot_link_assist_init. The trace starts
at logical 0x8000 under the TI-84 Plus reset mapping and resolves every
banked instruction in this interval. [confirmed] for the pinned emulator run.
tools/ti84re/emulators/wabbitemu/run_lcd_diagnostic_probe.py builds a hash-bearing evidence
manifest for the explicit helper calls. It requires the pinned native runner
and exact OS 2.55MP ROM:
python3 -m ti84re.emulators.wabbitemu.run_lcd_diagnostic_probe \
--binary /path/to/wabbitemu-headless \
--output-dir /tmp/ti84-lcd-diagnostic --json
Remaining physical tests
- Measure the time from reset assertion to
3F:413Fon calculators with known ASIC and oscillator revisions. - Observe whether the protected writes produce any externally measurable transient and whether an interrupted complement test can leave RAM changed.
- Measure the effects and reset readback of each initialized register instead of inferring electrical behavior from public names or emulator fields.
- Record cold-start RAM contents before the boot wrapper probes
0x8000and0xC000. - Enter the dormant LCD helper sequence on identified physical controller revisions and capture port timing, contrast voltage, and panel output.
These measurements remain [hypothesis].
Memory management (RAM heap and Flash archive)
The memory manager divides about 24 KiB of user RAM among variables, temporaries, the floating-point stack, and the active program. The archive path moves variables between that RAM heap and Flash.
The RAM heap [standard]
The dynamic region runs from userMem (0x9D95) up to symTable (0xFE66). Two structures grow toward each other with free RAM in the middle:
flowchart TB
A["0xFE66 · symTable — top of user RAM"]
B["VAT — variable names + metadata<br/>type, data ptr/page, name · grows DOWNWARD ↓"]
C["( free RAM )"]
D["user data — variable contents<br/>grows UPWARD ↑"]
E["0x9D95 · userMem — bottom of user RAM"]
A --- B --- C --- D --- E
style C fill:#1b1b1b,stroke-dasharray:5 5
VAT entry layout: type, data ptr/page, name — see variables-vat.md.
Boundary/work pointers (clustered at 0x9820-0x983A) [confirmed]:
| Ptr | Addr | Role |
|---|---|---|
tempMem | 0x9820 | base of the temporary area |
fpBase | 0x9822 | floating-point stack base |
FPS | 0x9824 | FP stack pointer (grows; _PushReal/_PopReal) |
OPBase | 0x9826 | base of OP/symbol scratch |
OPS | 0x9828 | OP/symbol scratch stack pointer (top) |
pTemp | 0x982E | temp-variable pointer |
progPtr | 0x9830 | currently-executing program pointer |
pagedBuf | 0x983A | paged scratch buffer |
_MemChk reports free RAM as OPS - FPS + 1: the inclusive span between the
floating-point stack and the operand/symbol stack in the middle of the region.
User data grows upward, the VAT grows downward, and a variable resize shifts
everything above the resized object. [confirmed]
Core allocation primitives [confirmed]
_InsertMem(ram:0F81) — open a gap ofHLbytes at addressDEby shifting all memory above it up. It callsinsertmem_setup(ram:0F8B), which does theLDDRblock move (atram:0FA1), thendelmem_fixup_tail(ram:1398) to fix up pointers._InsertMemdoes not check free space itself — callers must ensure room first via_EnoughMem(the wrapper_ErrNotEnoughMematram:1735calls_EnoughMemthen jumps to_ErrMemoryatram:2721on shortfall)._DelMem(ram:1368) — the inverse: close a gap, shifting memory down._EnoughMem(ram:0FA6) — ensure N free bytes; if short, it walks the temp/scratch entries (9-byte stride frompTempdown toOPBase) and_DelVars reclaimable temporaries to make room. [confirmed]_MemChk(ram:0E20) — compute current free RAM.
Variable-creation bcalls — _CreateReal, _CreateStrng, _CreateAppVar,
_CreateRList, and others — share var_create_core at ram:1011.
_CreateReal at ram:10B8 jumps into that core. The core calls
var_create_gap at ram:0F0C, which moves the block and updates the temporary
and FP-stack pointers before registering the variable in the VAT. This path is
distinct from the public _InsertMem. See Variables & the VAT.
[confirmed]
Resident assembly programs need an additional rule: OS pointer repair updates
OS-owned pointer slots, not program counters or runtime-owned pointers. The
compiled Asm( path also caps its internal program-data size at 0x2000, below
the full ram:9D95–ram:BFFF span. See
Resident assembly programs for the launch copy,
cleanup, AppVar handle protocol, and archived streaming contract. [confirmed]
The familiar OP, display, graph, statistics, and context buffers are not one pool of universally safe scratch space. See Resident scratch RAM for direct clobber measurements, conditional ownership, and the bank-A page-mapping protocol. [confirmed]
Flash archive [confirmed]
To save scarce RAM, variables can be archived to Flash. The archive entry point is on flash page 0x07, while the low-level flash read/write/erase workers are on page 0x3D:
_Arc_Unarc(07:6248) — move OP1’s variable between RAM and the Flash archive (toggles the archive bit, then relocates the data and rewrites the VAT entry’s page to the Flash page)._FlashToRam(id5017→ body3D:6745) — copy archived data back into RAM. Archived vars are appended to Flash, which cannot be overwritten in place, so deleting one only marks it dead.archive_gc_collectat3C:7733rewrites live records in 64 KiB sector units and erases the old sectors.gc_show_screenat3C:7E0Ddisplays"Garbage"and"Collecting..."from page01. The collector also journals its phase in the inactive 8 KiB half of page3E. [confirmed]
_CleanAll is RAM cleanup (not Flash GC) [confirmed]: _CleanAll (07:52CF) compacts the floating-point stack down to tempMem (fpBase/FPS) and the OP/scratch stack down to pTemp (it sets OPBase = pTemp, LDDRs the live span down, and sets OPS to its new top), reclaiming temporary RAM after a command/expression finishes. It does not touch Flash.
Flash is erased a physical sector at a time but programmed byte by byte. archive_write_record at 3D:64AA calls _WriteAByte (8021) and _WriteFlashUnsafe (8087) through the Flash-control port 0x14. See Flash memory for the hardware and boot-bcall path, and Variables, archive & unarchive for record format and allocation. [confirmed]
_FlashToRam(3D:6745) copies archived bytes through a worker at0x8100.ram_worker_launcherat3D:678Cinstalls that worker. The same launcher also runs the internal certificate-page program worker. [confirmed]archive_find_free_span(3D:62C2) scans upward from page08to the dynamic App boundary fromarchive_app_boundary(3D:6413). The OS-only trace returns boundary0x29and selects08:4000. [confirmed]archive_write_record(3D:64AA) writes record states0xFEthen0xFC; the helpers at3D:7C8F,3D:7C93, and3D:7C97implement additional monotonic bit-clears. [confirmed]- Archive workers:
_Arc_Unarc(07:6248) →arc_ram_to_flash(07:6107, RAM→Flash) /arc_flash_to_ram(07:61F4, Flash→RAM). (_Arc_Unarcdispatches on the FindSym page byteB:B==0/in-RAM →6107archive,B≠0/in-Flash →61F4unarchive.)
The _FindSym VAT walk, public Flash workers, and normal garbage-collection path are byte-verified in Variables, archive & unarchive and Flash memory. TilEm and Wabbitemu restart successfully from each persistent phase marker. Physical power loss at those markers and cuts during busy commands remain untested. [confirmed] for the emulator command-boundary runs; [hypothesis] for physical interruption behavior.
Variables, archive, and unarchive
The variable-management paths scan the VAT, store and recall values, move objects between RAM and the Flash archive, and collect unused Flash records. Variables and the VAT defines the object formats; Memory management provides the heap and archive overview.
Raw disassembly supplies the indexed-bit operations and cross-page trampolines that the decompiler can mis-render.
arcInfo workspace and RAM pointers [confirmed]
The archive engine keeps a confirmed 15-byte workspace prefix at arcInfo
(0x83EE). The prefix contains seven named fields followed by two bytes whose
meaning remains open:
#pragma pack(push, 1)
typedef struct {
uint8_t page; /* +0x00, 0x83EE */
uint16_t data_ptr; /* +0x01, 0x83EF */
uint16_t vat_ptr; /* +0x03, 0x83F1 */
uint16_t dest_ptr; /* +0x05, 0x83F3 */
uint16_t data_size; /* +0x07, 0x83F5 */
uint16_t size; /* +0x09, 0x83F7 */
uint16_t size_full; /* +0x0B, 0x83F9 */
uint8_t unknown_tail[2]; /* +0x0D, 0x83FB */
} ArchiveWorkspacePrefix; /* 15 bytes */
#pragma pack(pop)
savedArcInfo at 0x8406 is not a copy of that whole prefix. _Arc_Unarc’s
reentrant mover at 07:61DC copies the distinct 12-byte tail beginning at
arcInfo.vat_ptr: LD HL,83F1 / LD DE,8406 / LD BC,0C / LDIR. The slice runs
through arcInfo.unknown_tail[1]. [confirmed]
The matching 07:61E8 restore candidate is an inferred label, not
byte-confirmed in the disassembly. [hypothesis]
| Addr | Field | Meaning |
|---|---|---|
0x83EE | arcInfo.page | page byte of the data (Flash page if archived; RAM marker otherwise) |
0x83EF | arcInfo.data_ptr | 2-byte data address (in Flash window 0x4000–0x7FFF, or RAM) |
0x83F1 | arcInfo.vat_ptr | pointer to the VAT entry’s type byte (the symbol record) |
0x83F3 | arcInfo.dest_ptr | destination data pointer (RAM target on unarchive) |
0x83F5 | arcInfo.data_size | a header/record-size component (loaded from BC after CALL ram:0FDE) |
0x83F7 | arcInfo.size | the variable’s data byte count (from _DataSize; 07:614B does CALL ram:1485 → LD (83F7),DE) |
0x83F9 | arcInfo.size_full | size + header overhead |
0x83FB | arcInfo.unknown_tail | two bytes included in the saved tail; semantics unresolved |
0x8406 | savedArcInfo | 12-byte save slot for arcInfo.vat_ptr through unknown_tail[1] |
RAM-heap pointers used by the mem checks (cluster at 0x9820–0x983A, confirmed in .inc):
FPS=9824, OPBase=9826, OPS=9828 (top of the upward data heap), pTemp=982E,
progPtr=9830. The VAT grows down from symTable=0xFE66. chkDelPtr3=981C holds the result
pointer from the last lookup (_Arc_Unarc does LD (981C),HL) — note 981C is chkDelPtr3 in
ti83plus.inc, not tSymPtr1 (which is 9818h). ramCode=8100h is where Flash
read/write routines are copied to run (you cannot execute from a Flash page while erasing it).
_FindSym and VAT traversal [confirmed]
_FindSym (00:0E65, also reached through RST 10h) cross-page-jumps to
findsym_scan at 07:565F. That body calls the name classifier at ram:20D6.
Length-prefixed names jump to 07:55D1; fixed-token names continue at
07:5665. _FindSym therefore handles both encodings when OP1 is formed
correctly. [confirmed]
_ChkFindSym (00:0E60) calls the helper at ram:2042. The helper recognizes
AppVarObj, GroupObj, ProgObj, TempProgObj, and ProtProgObj, then routes
those classes directly to 07:55D1. Other classes fall through the _FindSym
entry. TI’s SDK recommends _ChkFindSym for Programs and AppVars; that public
contract does not imply that this ROM’s _FindSym body lacks the named-object
branch. [confirmed] for the ROM; [standard] for the SDK contract.
The scanner keys off OP1 at 8478: OP1.value.type/varType and the name token at 8479 (=OP1+1),
with the 2 name bytes at 847A/847B:
findsym_scan (07:565F):
CALL FUN_ram_20d6 ; classify OP1 name
if name-token (8479) == 0x24 (list-name token):
scan the temp/list region: HL from progPtr(9830) down toward OPBase(9826), pTemp(982E)
else: HL = symTable (0xFE66), scan downward to progPtr
loop:
A = (HL); A &= 0x1F ; *** mask off archive flag bits in high nibble ***
SBC HL,DE
RET C (ran past end → not found)
CP (HL) against token (8479); on match check name bytes (847A/847B) at HL-1/HL-2
else step HL -= 3 from the name pointer (9 bytes type-to-type for fixed entries)
/ -= (6+nameLen) for named entries, and continue
on match: B=(entry).pageByte, DE=dataPtr, A=(entry+6)=type; store type→8478
So each VAT entry is read high-address-first; the type byte’s low 5 bits are the TIVarType; the
high bits flag the archive state. _FindSym returns: type in A and 8478, data pointer in DE,
and the page byte in B — B is the discriminator: zero for an in-RAM var, nonzero for a var
whose data lives on a Flash page.
VAT entry shapes (consistent with _CreateR* header writes — see variables-vat.md):
- fixed-token entries (real/cplx/
Ln/[A]/sysvars) occupy nine bytes. Relative to matched name tokenN,findsym_scanreads page atN+1, the high/low data-address bytes atN+2/N+3, skips version and T2 atN+4/N+5, and reads type atN+6. - named entries (prog/appvar/group/str/equ) use a high-address-first variable-length name plus the same six metadata bytes. The exact byte order is easiest to reason about relative to the matched name token rather than as a forward C struct.
For an archived entry the data address (addrLSB/MSB) points into the Flash window and the
page byte selects the Flash page; the VAT record itself always stays in RAM.
Ordinary and protected programs after lookup
ProgObj (0x05) and ProtProgObj (0x06) use the same data representation.
The helper at ram:2042–ram:2058, called by _ChkFindSym, admits both types
as named program objects. findsym_scan returns the matched VAT type in A and
OP1, so the caller can still distinguish 0x05 from 0x06. [confirmed]
The shared data and archive paths do not branch on that distinction:
_DataSizeatram:1485–ram:14BBsends both types through the same leading size-word case and includes the same two-byte header in its result._Arc_Unarcat07:6248branches on the returned page byte inB. Its only post-lookup object-type rejection isGroupObj(0x17) at07:6263.07:614B–07:6158obtains either program type’s size through_DataSize.- The archive writer masks the VAT type with
0x1Fat07:616F–07:6178and preserves that low-five-bit value in the record. Copy length and page crossing do not depend on whether the preserved value is0x05or0x06. _FlashToRamat3D:6745receives a page, source pointer, destination pointer, and byte count. It does not receive or read a VAT type.
Protected-program behavior remains a type-routing policy outside those data
paths. The alphabetical VAT selector maps 0x06 to the ordinary program class
at 07:524D–07:5251. The parser accepts either value and joins the same
program path at 38:6012–38:601C. _ExecuteNewPrgm instead requires
ProtProgObj at ram:2670–ram:2677; this internal helper is not a general
TI-BASIC execution API. These sites accept, normalize, or reject a type before
using the common size-word payload. [confirmed]
An archived-source reader therefore uses the same mapping, _FlashToRam, and
cross-page rules for both program types after lookup. _ChkFindSym accepts
either program class as a named-object query, and findsym_scan matches the
name before replacing the OP1 type with the stored VAT type. A caller therefore
does not need to retain the stored type merely to find the program again, but
it must inspect the type returned by lookup before applying type-specific
policy. This conclusion covers TI-84 Plus OS 2.55MP; shell-specific loading
and writeback policies remain separate. [confirmed]
Community VAT mutation utilities
Two identified community utilities illustrate why a shared payload format is not, by itself, a safe type-conversion contract.
PRGMHIDE toggles bit 6 of the first stored name byte. Its source obtains the
selected VAT type pointer, subtracts seven to the first name byte, and applies
XOR 0x40; for example, stored A (0x41) becomes 0x01. Its own display
path restores bit 6 only while drawing the name. The 471-byte source rebuild is
byte-identical to the packaged PRGMHIDE.8xp body. [confirmed] for the
identified community source and binary.
A source-matched TilEm trace reaches the utility’s write at ram:9F2E and
changes ZTARGET’s first stored byte from 0x5A to 0x1A. The same run then
archives the program through _Arc_Unarc; its VAT entry becomes type 0x05,
page 0x08, address 0x4001, with the changed name intact. A cold reset from
that output Flash image rebuilds the same VAT entry, and the ordinary program
menu contains no entry. The hidden-name persistence and menu filtering are
therefore confirmed under TilEm on OS 2.55MP, but not on physical hardware.
[confirmed]
Creating a visible name that differs only by this stored bit may still produce confusing lookup or display results. [hypothesis]
The older HIDE utility directly replaces the complete VAT type byte with
ProgObj, ProtProgObj, or AppVarObj. It does not test the returned Flash
page or preserve archive-state bits. A source-matched trace reaches its write
at ram:9E29 and changes a RAM ProgObj into an AppVarObj without moving its
data. [confirmed] Applying it to an archived object can clear the archived flag
while leaving the page and address fields unchanged. That unsafe case was not
executed because it deliberately creates an inconsistent VAT; the resulting OS
behavior remains a hypothesis. [hypothesis]
PRGMAPPV provides the safer counterexample. It refuses archived inputs, creates
a destination through _CreateAppVar or _CreateProtProg, copies the data,
then deletes the original with _DelVarNoArc. Its ordinary/protected lock
toggle changes only 0x05 and 0x06, and also refuses archived inputs. The
packaged 550-byte body matches the included source. [confirmed] for the
identified community source and binary. A source-matched trace of the
program-to-AppVar path reaches _CreateAppVar at ram:9F31 and _DelVarNoArc
at ram:9F5E; the resulting RAM entry is type 0x15. A second trace selects
an archived program, reaches the refusal at ram:9F6A, and reaches neither
create nor delete. [confirmed] The lock-toggle refusal uses the same source
gate, but it was not keyed separately. [confirmed] for source; [hypothesis]
for that untraced interaction.
The exact artifacts are:
| Utility | Archive SHA-256 | Source member SHA-256 | Packaged member SHA-256 |
|---|---|---|---|
programs/prgmhide.zip | 6342d57b18a1277aa3ce13ba514d986fc3c485dfc4b747a157972fad61756103 | source/PRGMHIDE.z80: 10b7b0bfd902ebdef63f70120ef08d0fcb0fa3144885e8daf53e4880572648a1 | PRGMHIDE.8xp: 16e0ad05b138ccd15cde0648312b5032bd1f450faa544cd3b6ab1b882d4f0a43 |
programs/programtoappvar.zip | 4e27be8774fca769f26f1ce9984026f250fc8c4e9222277d3458a67e7fb25dc9 | Hide.asm: 65d290071a4ca2837f2e7ad08b3614939ec024a946057c75452f7b174b081a57 | HIDE.8XP: 0b03d6d7c97322140eb051844458adba1acf95009bf28d827c510e38f545e756 |
programs/prgmappv.zip | 33ba32795488b17e316f2efe556f5b323b6ca5b9fa9a1bf08db8dc59bac44ddf | PRGMAPPV.z80: 5d75239635d4791b08519e366276aebef3de51d1fe81f68c8cd5ee59f65f97bb | PRGMAPPV.8XP: a035c67a56e31dd8504d9d2f293d955a0ce56638a4506b0fcdd8ed9b27d54eb0 |
The reproducible macros and analyzer are in tools/probes/community/vat/.
tools/data/community-vat-dynamic-observations.csv records the complete trace,
snapshot, input-ROM, output-ROM, and emulator hashes. The ROM hash is
dbb47afae091ab36f9abe74e32083013fbeff3d7e0516bbf5d1abf4ee57adc09;
the patched TilEm executable hash is
b8ee505483c79732a4ca21efb8b904de0792477795f6fc717874dcd5addaed09.
Base-page table, archive accounting, and group extraction
_FillBasePageTable = 0x5011, body ram:2692, saves port 0x06 and selects
a model-specific Flash source and RAM destination. The older path begins with
page 0x15 and destination ram:8015; the TI-84 Plus path selects page
0x29 and ram:8029, with a second hardware probe able to select page
0x69 and ram:8069. It maps the source into bank A, walks its descriptor
data through the inline helpers at ram:3DC5 and ram:3DCB, then restores
port 0x06. [confirmed]
The archived Cherries comment says this “restores first 1087 bytes in RAM.”
The ROM body is not a fixed 1,087-byte LDIR: its copy count is selected from
the mapped descriptor and its outer loop continues until selector 0x07.
The fixed-size community wording is therefore not established. A controlled
fixture reaches ram:2692 and returns, but does not corrupt a live base-page
table to prove every rewritten byte. [confirmed] for the ROM control flow and
return; [hypothesis] for a universal 1,087-byte extent.
_ArcChk = 0x5014, body 3D:61AF, prepares the archive accounting words at
ram:839F–ram:83A2. Bit 6 of IY + 0x24 lazily guards the first Flash scan,
whose four-byte intermediate result is saved at ram:9C96; bit 7 guards the
second scan and the intermediate result at ram:9C9A. The tail adds four bytes
from the temporary result into ram:839F–ram:83A2, restores the caller’s
HL and AF, and returns. The first scan temporarily enables Flash writing
through port 0x14, runs its archive check, then disables writes and restores
the prior interrupt-enable state. [confirmed]
The numeric-bcall fixture reaches _FillBasePageTable and _ArcChk, regains
control, and snapshots the four accounting bytes. Its accepted trace records
the fixture-local call sites separately from OS loader calls that also reach
_ArcChk. Results and artifact hashes are in
tools/data/community-vat-dynamic-observations.csv. [confirmed] under TilEm.
_UngroupVar = 0x50C8, body 39:764A, is not a generic blob unpacker. It
copies eight bytes from OP1 to ram:85E7, calls the group-state initializer at
39:765D, sets bit 6 of IY + 0x26, and jumps into the extraction state
machine at 39:6E11. That initializer clears link/group scratch at
ram:8670, ram:97A5, ram:85D9, and ram:8672 before calling the page-7
group helper. The Celtic III caller first requires OP1.type = GroupObj
(0x17). [confirmed] for ROM and caller control flow.
No dynamic call is accepted yet: the fixture set does not contain an authentic on-calculator Group object plus the surrounding group state, and calling the routine with a fabricated OP1 can create or overwrite variables. The ABI and side effects above are static ROM evidence; successful extraction, collisions, and error unwinding remain [hypothesis].
Store and recall [confirmed]
Store. _StoOther (38:62A9) and siblings (_StoAns, _StoX, _StoY, … 38:6251-62A3):
- Set OP1 type = 0xFF placeholder (
62A9: LD A,FF / LD (8478),A), parse the destination name. 5F45resolves/creates the target symbol; then it copies the value. It dispatches on the destination name token (849B): list-element store (0x2A→ bounds-checks via_ErrDimension), matrix element, etc. Ultimately a_Create*routine carves RAM with_InsertMemand the data is copied.- A store into an archived var is not done in place; the OS unarchives first (you cannot rewrite
Flash in place); see the
_Arc_Unarcdirection logic. [hypothesis]
Recall. _RclVarSym (38:67B1) and rcl_var_push (3A:5D07):
_RclVarSymcalls the wrapper at00:17A6. It runs_FindSym, raisesERR:UNDEFINEDon a miss, then tests the returned page byteB; a nonzero page jumps through00:2779toERR:ARCHIVED. It then checks the name token atram:8479. For a list recall (63/2A) it sizes the data with_DataSize(00:1485) and copies it into a work buffer atram:91E0, using_LdHLindand cross-page helpers; it ends withJP _OP4ToOP1._DataSize(00:1485): returns the variable’s data byte-count in DE from the type byte — real=9, list/cplx-list read theword countheader, matrix uses cols×rows, and named types (0x15AppVar,0x16,0x17Group) read the leadingword size.- Flash is memory-mapped read-only in the
0x4000window, but this recall wrapper rejects a Flash-backed symbol before reading its data. The same page check is visible when invoking an archived TI-BASIC program, as described in archived-target receipt. Explicit copy primitives such as_FlashToRamremain available to callers that intentionally handle Flash-backed data.
Archive and unarchive [confirmed]
bcall(_Arc_Unarc), OP1 = the variable name. It toggles the var between RAM and
the Flash archive (the same entry point does both directions, deciding from the current state).
_Arc_Unarc (07:6248):
SET 0,(IY+0x24) ; flag: an archive operation is in progress
CALL 628B ; validate OP1 name is an archivable class
; Z ⇒ JP 26E0, whose shim loads E_Variable (0xB2)
CALL _OP1ToOP3 (1A0F)
CALL _ChkFindSym (0E60) ; locate the VAT entry; C ⇒ JP 271D (undefined)
DI
LD (981C),HL ; chkDelPtr3 = entry ptr
LD A,B
OR A
JR Z,6272 ; B = 0 means RAM; nonzero means Flash
LD A,(HL) ; Flash-backed object type
CP 0x17
JP Z,26E0 ; reject GroupObj
CALL 61F4 ; Flash → RAM: unarchive
6272:
CALL 6107 ; RAM → Flash: archive
... name-token-0x5D (list name, `tVarLst`) special-case via 32A9 / cross_page 05:4A6E
LD A,(83EE)
OR A
EI
RET
628B is the archivable-name guard: after _CkOP1Real it returns Z for the non-archivable
single-letter real/sysvar name tokens 0x58 0x59 0x54 0x5B 0x52 0x72 0xFC (CP nRET Z chain), so
_Arc_Unarc’s JP Z,26E0 rejects them via the 26E0 shim (LD A,0xB2 = E_Variable, ERR:VARIABLE →
_JError); archivable classes (lists, matrices, programs, appvars, …) return NZ and continue. (arc_59f1 @07:59F1 and arc_5936 @07:5936 are companion name/range
validators for the catalog archive command.)
Direction note: the B-page test sends an in-RAM var (B==0) to 6107 (archive) and an
in-Flash var (B≠0) to 61F4 (unarchive). 6107 is the one that programs Flash and frees the
RAM copy; 61F4 is the one that carves RAM and copies the data back out of Flash.
RAM-to-Flash archive path [confirmed]
6107: CALL 7866
DI
CALL 614B ; arcInfo.vat_ptr and arcInfo.size
; 616C reserves the archive-Flash slot
CALL 2FF1 (cross_page 3D:64AA) ; program the data into archive Flash
LD HL,(83F3)
LD DE,(83F7)
CALL _DelMem (1368) ; release the old RAM copy
RET
616C: reads vatPtr type, AND 0x1F (clean type for the record header),
LD HL,(83F7)+(83F5)
ADC
JP C,2729 (E_Invalid, 0x8F) ; size overflow?
reserves a Flash slot via archive_prepare_scan / archive_find_free_span
The data is appended to the archive Flash (Flash cannot be overwritten in place). The VAT entry’s
type byte gets its archive flag set and its data ptr/page rewritten to point into Flash; the old RAM
copy is then released (the upward data heap shrinks). archive_write_record at 3D:64AA lays down a fresh archived record plus a copy of the symbol header, name, and data. The status markers are 0xFE for in progress, 0xFC for valid, 0xF0 for deleted, and 0xFF for erased space. The successful archive trace executes the complete body and its six boot-page writes. [confirmed] _Chk_Batt_Low (00:0D07) gates the Flash write — archiving aborts on low battery (07:61C5: CALL _Chk_Batt_Low).
Flash-to-RAM unarchive path [confirmed]
61F4: LD (83EF),DE
LD (83EE),A ; arcInfo.data_ptr/page = source
CALL 6335 ; set arcInfo.vat_ptr and arcInfo.data_size
CALL 32D3 ; size accounting
LD A,(HL)
CALL 146C ; add header overhead → arcInfo.size_full
EX DE,HL
CALL _EnoughMem(0FA6) ; ensure there is RAM room
JP C,_ErrMemory(2721)
OR 1
CALL 0F0C ; carve the RAM gap (internal create-gap routine)
LD (83F3),DE ; arcInfo.dest_ptr = new RAM address
CALL 3003 (unarchive_record_to_ram) ; copy Flash→RAM, retire old record
RET
The data is copied from Flash into the freshly-carved RAM gap. The VAT entry’s archive flag is
cleared and its data ptr/page rewritten back to the new RAM address; the old Flash record is left
marked dead (0xF0, reclaimed at the next GC). unarchive_record_to_ram
at 3D:6440 shares the page-3D flash-control prologue
(OUT (0x14)) and is an inferred label, not byte-confirmed in the disassembly.
Errors [confirmed]
2785: LD A,0x31→_JError=E_ArchFull(0x31) “ERR:ARCHIVE FULL” (no room even after GC).2729/272D/2731:LD A,0x8F/0x90/0x91→ E_Invalid / E_IllegalNest / E_Bound. The archive size check (616C) takes the2729(E_Invalid,0x8F) entry on overflow.26E0+ is a cluster of local error shims: each loads its code (0xB2=E_Variable,0xB3=E_Duplicate,0x81=E_Overflow,0x82=E_DivBy0) intoAand enters_JError— not_ErrDataType.- Error-name strings live at
07:6CA9:ARCHIVED, VERSION, ARCHIVE FULL, VARIABLE, DUPLICATE.
Reading archived data with _FlashToRam [confirmed]
bcall(_FlashToRam) (ID 5017h, body 3D:6745) copies BC bytes from a Flash page:addr to
a RAM destination, transparently advancing the Flash page when the read crosses the 0x8000
window boundary:
3D:6745: mask page (AND 1F / AND 3F per port-2 model check FUN 1837/182F)
PUSH IX
LD IX,6761
CALL 678C
POP IX
RET
3D:678C: copies the small arg-block to ramCode, sets DE=0x8100, JP 8100 ; runs the copier from RAM
the copier (6761..678A):
IN A,(6) saved
OUT (6),A ; map the source Flash page into bank A
loop:
LDI
BIT 7,H
; crossing 0x8000 advances the mapped page
IN A,(6) ; advance after the 0x8000 boundary
INC A
OUT (6),A
LD HL,0x4000
Port 6 is the bank-A page-select; the read code itself runs from ramCode
at ram:8100. This is an explicit Flash-to-RAM byte-copy primitive, not proof
that ordinary TI-BASIC recall or program execution invokes it automatically.
ti83plus.inc also names a sibling _FlashToRam2 (id 8054h); the retail boot
table maps it to 3F:4888.
Archive record allocation and programming [confirmed]
The archive manager chooses a free record and then calls the boot-page Flash API. Flash memory reconstructs port 0x14, _WriteFlash, _WriteFlashUnsafe, _WriteAByte, erase sectors, DQ polling, and the RAM workers. This section covers the archive-specific layer above that API.
| Trampoline | Target | Role |
|---|---|---|
ram:2FDF | 3D:61AF archive_prepare_scan | prepare archive accounting and scan state |
ram:2FF7 | 3D:62C2 archive_find_free_span | scan records for a span large enough for the new object |
ram:2FF1 | 3D:64AA archive_write_record | write the record marker, header, name, data, and final status |
ram:3003 | 3D:6440 unarchive_record_to_ram | copy an archived record to RAM and retire its Flash record |
archive_write_record unlocks Flash with the protected port-0x14 sequence. It writes an initial 0xF0 marker when the selected position requires one, starts the record with 0xFE, writes the size and variable metadata, copies the data, and finalizes the status as 0xFC. It uses _WriteAByte (8021, body 3F:4C9F) for marker bytes and _WriteFlashUnsafe (8087, body 3F:4CA6) for blocks. [confirmed]
The bounds checks at 3D:6B6D and 3D:6B9B reject pages below 08 and pages at or above the dynamic App boundary from 3D:6413. Both require the Flash destination to be at least 0x4000; the block form at 3D:6B6D also requires HL >= 0x4000. Carry reports rejection to the caller, which raises E_ArchFull. [confirmed]
A generated 17,000-byte program makes the record data span pages. The traced record writer passes its 17,002-byte [size][body] field to one _WriteFlashUnsafe invocation, which programs physical 0x20013–0x2427C continuously, crossing from 08:7FFF to 09:4000. The copied worker increments port 0x06 from 0x08 to 0x09, resets DE to 0x4000, and finishes with its 0xF0 reset at the final target. This is direct TilEm evidence for the ordinary archive page-crossing path, not a physical-calculator measurement. [confirmed]
Record-status byte [confirmed]
The status byte is a classic AMD/Am29F monotonic bit-clear marker: erased Flash is all-ones
(0xFF), and the OS advances a record’s state by clearing bits (program can only flip 1→0; only
a sector erase restores 1s). The writers are three tiny routines on page 0x3D that load an AND-mask
into C and then read-modify-write the status byte (3D:7C9A: CALL flash_read_byteAND C…):
| Routine | Mask in C | Bit cleared | State after |
|---|---|---|---|
flash_op_fe (3D:7C97) | 0xFE | bit 0 | record in-progress (newly begun) |
flash_op_fd (3D:7C8F) | 0xFD | bit 1 | (intermediate / “swap” marker) |
flash_op_fb (3D:7C93) | 0xFB | bit 2 | (intermediate) |
Successive clears compose: the three helpers take a record 0xFF (erased) → 0xFE (started) →
0xFC (valid/complete, bits 0+1 clear). Deletion marks the record 0xF0 (deleted/dead, bits
0–3 clear) with a direct write in the delete and garbage-collection path, not via those three in-progress/valid
helpers. Because only bits go 1→0, a deleted record
can never be re-validated in place — it is reclaimed only by GC erasing the whole sector.
flash_find_nonff (3D:7DEA) confirms 0xFF = empty: it reads the 13-byte record header and CP 0xFF
on each, treating an all-0xFF run as a free slot. (3D:7C99 additionally folds in AND 0xE7 and
conditional OR 0x10/OR 0x08 for the swap/relocate state bits driven by (IY+0x1A).0 and (IY+0).2.)
Deleted-record recovery before collection
Marking a record 0xF0 does not erase its remaining bytes. Archive Utility 1.0
uses that interval before garbage collection: it scans after each sector header,
accepts live 0xFC and deleted 0xF0 records of program, protected-program,
and AppVar type, creates a RAM program, copies the record’s original type into
the new VAT entry, and calls _FlashToRam for the saved data. It does not
rewrite or revalidate the old Flash record. Its 1,547-byte source rebuild is
byte-identical to the packaged body. [confirmed] for the identified community
source and binary.
A controlled page-0x08 image places a live ProgObj record at 08:4001
and a deleted ProtProgObj record at 08:4013. The source-matched utility’s
scan reaches ram:9E68 twice and its deleted-record branch at ram:9EBF once.
Separate retrieval traces reach _CreateProg at ram:9FD0 and _FlashToRam
at ram:9FE7. They create RCVLIVE as type 0x05 and RCVDEAD as type
0x06, with the exact two-byte-size and two-byte data fields from their
records. Both output Flash images are byte-identical to the controlled input,
SHA-256
b532eb990567ea3d48b73e4f69e5b9e864d7455872c3d231f9bf9853e413a59e.
[confirmed] under TilEm.
Recovery is opportunistic. The next garbage collection can erase or repurpose the containing sector, and a stale record must still have intact metadata and data. The utility permits a record to cross a 16 KiB page boundary but rejects one that extends beyond its four-page sector. That last rule is community reader policy; ROM-level proof that the allocator never crosses a 64 KiB erase sector remains open. [confirmed] for the utility; [hypothesis] for the general allocator invariant.
A second controlled image places a record at 08:7FE0; its size-and-data field
begins at 08:7FEF. Retrieval creates a RAM ProgObj whose size word and bytes
0x00–0x1F match across the page-0x09 boundary. The output image remains
byte-identical to its input,
SHA-256
7fef8578f31c2becf15b1c1d940a5231594df970a035dcbb64cb71d511d5cb90.
This confirms the utility’s page-crossing read under TilEm. It does not test a
record crossing a 64 KiB sector boundary. [confirmed]
An earlier Archive Recover release describes recovery of “programs,” but its
source accepts deleted records only when their type is ProtProgObj (0x06)
and recreates them with _CreateProtProg. Ordinary ProgObj records are not
accepted. [confirmed] for the identified source; the release claim is broader
than its implementation.
| Utility | Archive SHA-256 | Source member SHA-256 | Packaged member SHA-256 |
|---|---|---|---|
programs/archive_utility.zip | 04ca940aacb229a65378450c0c673644bea6fce723215396d195552487e2a7e8 | archutil.z80: 152f96d1d1b3eee178cd728527c10bacc5cdaa3498941ef750e4df79e2d2b2b2 | ARCHUTIL.8XP: 0b74c5a8eb6a2daa4e6b598d307cd6cca24948d7a7aa707dcc446bcabbf87569 |
programs/mirageos/arcrecov.zip | 991c0324521ac9099276a3de1bc795e87b272cd4c56eba276268b6f100c7d19c | arcrecov.asm: 1b30935bad150965b42fc75cf786570469d3df63a7ddd33816d664c46acb0a68 | ARCRECOV.8XP: d1e5c3faf49eb65b9fd3fcc1cbb6cbf69883087f5996ad3a3d481562b2f7d71a |
Dynamic archive and application boundary [confirmed]
The archive begins at page 08. archive_app_boundary (3D:6413) computes its exclusive upper bound by starting at the model-specific top App page from 3D:726E, validating each installed App header, obtaining its span from _FindAppNumPages (3D:4AA3), and subtracting that span until it reaches the first page below the installed App run. [confirmed]
| Model test | Top App page from 3D:726E | Certificate page from 3D:738B |
|---|---|---|
port 0x02 bit 7 clear | 0x15 | 0x1E |
port 0x21 & 3 equals zero | 0x29 | 0x3E |
| remaining branch | 0x69 | 0x7E |
The second column is the App scan start, not an archive base. The third column selects the certificate page, not an archive endpoint. archive_find_free_span stores the computed boundary, starts at page 08, and scans upward. On the OS-only TI-84 Plus image, the boundary is 0x29; the successful Archive prgmA trace selects 08:4000. Installed Apps consume pages downward from the upper end and reduce the archive interval. [confirmed]
The ASIC pages Flash in 16 KiB units, but the chip erases ordinary sectors in 64 KiB units. Page 3E contains two 8 KiB certificate sectors, and page 3F is a 16 KiB boot sector. See Sector geometry. [standard]
Flash garbage collector [confirmed]
The archive garbage collector compacts records in 64 KiB sector units. It also journals its phase
in the inactive half of page 3E, so startup code can distinguish an interrupted collection from a
normal archive layout. This mechanism is separate from _CleanAll, which only compacts RAM.
Collector entries
gc_command at 3C:71F8 displays the two-line banner, runs a recovery preflight, and calls the
normal collector: [confirmed]
3C:71F8 di
3C:71F9 call 7E0Dh ; gc_show_screen
3C:71FC call 7219h ; gc_recovery_preflight
3C:71FF call 7733h ; archive_gc_collect
3C:7202 ei
3C:7203 ret
gc_show_screen at 3C:7E0D is byte-confirmed. It loads the strings at 01:4126 ("Garbage")
and 01:412E ("Collecting..."). The related path at 3C:7E23 loads 01:4076
("Defragmenting..."). [confirmed]
The deterministic GCFLASH fixture archives A and B, unarchives A, accepts the
GarbageCollect prompt, and reaches 3C:71F8, 3C:7219, 3C:7733, and 3C:7CFB once each. The
preflight branch at 3C:7232 sees carry set and returns through 3C:7246; it does not enter the
recovery dispatcher during this normal run. [confirmed]
Four-page archive sectors
3C:749C groups the current archive page into one physical 64 KiB sector: [confirmed]
ld a,(8435h)
or 03h
ld c,a ; last 16 KiB page
and 0FCh
ld b,a ; first 16 KiB page
ret
gc_check_archive_sectors at 3C:7768 applies that grouping while scanning downward from the
dynamic App boundary. It examines the byte at 4000 on the first page of each group, then checks
record status bytes within a selected sector. In the fixture it tests nine group starts and finds
the source sector at page 08. [confirmed]
Sector-header bytes use the same monotonic bit-clearing property as record statuses, but they are
a separate structure. In the observed collection, 0xFE identifies the erased scratch sector,
0xFC and 0xF8 are copy-progress states, and 0xF0 identifies the committed sector containing
the compacted records. Record bytes one or more bytes after the sector header independently use
0xFE, 0xFC, 0xF8, and 0xF0. [confirmed]
Observed sector-copy sequence
archive_gc_collect at 3C:7733 executes the protected port-0x14 unlock sequence. It checks the
archive sectors, adjusts the Flash execution bound, prepares a destination sector, initializes the
certificate journal, runs gc_run_phase_machine at 3C:7CFB, restores the Flash bound with
_SetFlashLowerBound (80CF), and returns. [confirmed]
The trace decodes to 1,133 byte-program commands and seven physical sector erases. The ordinary archive-sector operations occur in this order: [confirmed]
| Clock | Operation | Meaning |
|---|---|---|
325020849 | erase sector containing 0C:4000 | create the 64 KiB destination sector |
328027494 | program 0C:4000 = 0xFE | mark page 0C’s sector as the scratch destination |
334678845 | program 0C:4000 = 0xFC | advance the destination-sector phase before record copy |
334829015–334924553 | program 0C:4001–0C:4015 | copy and finalize the surviving B record |
334939256 | program 08:4016 = 0xF8 | mark the old B record as moved |
335005172 | program 0C:4000 = 0xF8 | advance the destination-sector phase |
335063060 | program 08:4016 = 0xF0 | retire the old B record |
335227372 | erase sector containing 08:4000 | reclaim the original 64 KiB sector |
338253448 | program 08:4000 = 0xFE | make page 08 the next empty scratch sector |
338293984 | program 0C:4000 = 0xF0 | commit page 0C’s sector with the compacted record |
The copied record begins at 0C:4001, immediately after the sector header. Its bytes are
FE 12 00 00 00 00 01 40 0C 42 00 00 00 80 20 00 00 00 00 00 00; the first byte then changes
to 0xFC. The record matches the surviving archived real variable B. 3C:79A6 also updates its
VAT location while moving the record. [confirmed]
The final layout therefore contains an empty 0xFE scratch sector at page 08 and a committed
0xF0 sector at page 0C. The live B record remains 0xFC at 0C:4001. The 0xF0 byte at
0C:4000 is a sector header, not a deletion marker for the record that follows it. [confirmed]
Certificate-sector journal
The collector uses the two 8 KiB halves of page 3E transactionally. _GetCertificateStart
(8057) selects the active half. 3D:48E3 toggles H with 0x20, and
_EraseCertificateSector (8060) erases the inactive half before the page-3D certificate rewrite
helper runs. [confirmed]
The fixture first erases 3E:6000–3E:7FFF. It copies the used tail at 3E:7DD2–3E:7FFF into
that half, programs its base byte through 0x8F to 0x00, and erases the old half at
3E:4000–3E:5FFF. After archive relocation it reverses the operation: it copies
3E:5DD2–3E:5FFF, programs 3E:4000 through 0x8F to 0x00, and erases the temporary
3E:6000 half. Most copied bytes are 0xFF; the boot worker still issues a program command for
each one. [confirmed]
The certificate rebuild dispatcher confirms the journal’s half-relative span.
Mode 3 replaces 0x1DEA–0x1E4F. Mode 4 replaces that block and the
validity tail at 0x1FE0–0x1FFF. [confirmed]
The GC block has model-dependent RAM mirrors beginning at 0x837B or
0x82A5. The helper addresses pin its first fields: [confirmed]
| Block offset | Certificate offset | Helper | RAM mirrors | ROM use |
|---|---|---|---|---|
+0x00 | 0x1DEA | 3C:7E78 | 0x837B, 0x82A5 | Control flags tested during preparation and recovery. |
+0x01 | 0x1DEB | 3C:7E83 | 0x837C, 0x82A6 | The archive App boundary from 3D:6413, incremented once. |
+0x02 | 0x1DEC | 3C:7E8E | 0x837D, 0x82A7 | Selected 64 KiB archive-sector page. |
+0x03 | 0x1DED | 3C:7E99 | 0x837E, 0x82A8 | Master recovery phase. |
+0x04 | 0x1DEE | 3C:7EA4 | 0x837F, 0x82A9 | Page erased by the phase-0xF8 recovery branch. |
+0x05 | 0x1DEF | 3C:7EBA | 0x8380, 0x82AA | Optional second page erased by the phase-0xFC branch. |
+0x06 | 0x1DF0 | 3C:7EAF | 0x8381, 0x82AB | Start of the archive-sector state array. |
The initialization bounds are narrower than the certificate rebuild span.
3C:7E6B first loads the current certificate data into RAM. The initializer at
3C:7317 then writes 0xFF to 100 bytes beginning at 0x82A5 when port-0x02
bit 7 is set; its bit-clear branch writes 18 bytes beginning at 0x837B.
Because the first six bytes are the fields above, those lengths leave capacity
for 94 and 12 sector-state bytes respectively. [confirmed]
The mode-4 certificate rebuild path at 3D:4274 copies 0x66 bytes from
0x82A5, two more than the TI-84 Plus initializer erases. Offsets +0x64 and
+0x65 are therefore retained from the previously loaded certificate block;
they are not initialized sector states. The load, initialization, and rebuild
bounds are confirmed. [confirmed] No direct semantic accessor for the trailing
bytes has been found. [hypothesis]
3C:7DA9 indexes the sector-state array as
(archive_page >> 2) - 2. Page 08 maps to slot 0, page 0C maps to slot
1, and each later 64 KiB sector advances one slot. The normal path writes
0xFE through 3C:7848, then 0xFC through 3C:7853. The recovery path can
write 0xFC through 3C:7C54. [confirmed]
Capacity is not the live range. The no-App TI-84 Plus archive limit is 0x2A,
so the only possible sector-start pages below it are 08, 0C, 10, 14,
18, 1C, 20, 24, and 28: slots 0–8. Installed Apps can lower the
limit further. The ROM’s larger advanced-family branch can raise the exclusive
limit to 0x6A, which still uses only 25 slots. The remaining initialized bytes
are spare capacity in this ROM’s reachable archive geometry. [confirmed]
gc_recover_by_phase at 3C:7C1F dispatches the master byte. The branch
targets and their joins show how each interrupted phase resumes: [confirmed]
| Phase | Branch | Recovery action visible in the ROM | Join |
|---|---|---|---|
0xFF | 3C:7C43 | Run the phase-0xFE initializer. | gc_run_phase_machine at 3C:7CFB |
0xFE | 3C:7C48 | Inspect pending sector slots, repair scratch-sector setup, and resume phase processing. | 3C:7CFB after internal repair branches |
0xFC | 3C:7CC6 | Erase the selected recovery page and an optional second page through _EraseFlashPage = 8084h. | 3C:7D0A, after the phase-0xFC write point |
0xF8 | 3C:7CDA | Erase the page stored at block offset +0x04. | 3C:7D1B, after the phase-0xF8 write point |
0xF0 | 3C:7CE3 | Search 0xFC and 0xF8 archive-sector headers, then repair or erase the remaining sector. | finalization at 3C:7D25 or 3C:7D2B |
0xE0 | 3C:7D30 | Run final journal cleanup through 3C:7B90 and 3C:7B2A. | return |
The shared writer at 3C:7AA6 updates Flash and the model-dependent RAM
mirror. The normal phase machine emits the following values: [confirmed]
| Value | Load and call | Condition |
|---|---|---|
0xFE | 3C:7ACF → 3C:7AD1 | Always after optional scratch-sector header programming. |
0xFC | 3C:7D05 → 3C:7D07 | Journal flags bit 3 is clear. |
0xF8 | 3C:7D10 → 3C:7D12 | Journal flags bit 3 is clear. |
0xF0 | 3C:7D20 → 3C:7D22 | The archive-sector consistency check returns carry. |
0xE0 | 3C:7D2B → 3C:7D2D | Always before final cleanup. |
Every transition only clears bits. The complete ROM-reachable progression is
FF → FE → FC → F8 → F0 → E0, with conditional edges that skip FC/F8 or
F0. The direct skip edges are FE → F0, FE → E0, and F8 → E0.
[confirmed]
The GCFLASH trace takes a short path. The master byte at 3E:7DED receives
0xFE at clock 334587331 and 0xE0 at clock 338262732. Slot 0 at
3E:7DF0, which maps page 08, receives 0xFE at clock 335222873 and
0xFC at clock 338237430 in the decoded TilEm trace. [confirmed]
The rebuild worker also issues program commands with data 0xFF while copying
the block. Those commands cannot clear NOR bits and are not phase transitions.
tools/ti84re/flash/gc_journal.py separates them from state-changing commands. Its CLI can
report the static structure alone or correlate a trace: [confirmed]
python3 -m ti84re.flash.analyze_gc_journal --json
python3 -m ti84re.flash.analyze_gc_journal \
--trace /tmp/tibasic-smoke/gcflash.trace --json
gc_check_interrupted begins at 3C:7BC7. The fixture’s startup check reads
an erased status and skips the branch to 3C:7BDD and 3C:7C1F.
[confirmed]
TilEm restart at six journal boundaries
The GCFLASH command trace can produce interrupted Flash images without
guessing archive contents. tools/ti84re/flash/replay.py applies decoded byte-program
commands as old & requested and applies sector erases with the top-boot
geometry. tools/ti84re/flash/replay_trace.py stops when an initialized journal phase
belongs to the sole certificate half whose base marker is 0x00. [confirmed]
This replay treats the command-shaped CPU writes as accepted device commands.
The fixture supports that assumption in three independent ways: all 62 ordinary
program invocations and all six certificate invocations end at OS success
resets, the decoder finds no unmatched writes, and later trace reads use the
programmed archive state. This supports the assumption for this fixture, and
the CLI requires --accept-command-shapes. [confirmed] TLMT does not directly
record ASIC or Flash-device acceptance, so applying the assumption to an
arbitrary trace remains unverified. [hypothesis]
The active journal has flags 0xFB, archive limit 0x2A, selected sector page
0x08, and active half base 0xFA000. The 0xFF snapshot begins only when
3E:6000 reaches its final 0x00 marker at clock 334577678. The earlier
0xFF program at 3E:7DED occurs while that half is still inactive. The same
trace then supplies active 0xFE and 0xE0 snapshots: [confirmed]
| Input phase | Original-trace trigger | Input image SHA-256 | Cold-restart path | Recovery command shapes |
|---|---|---|---|---|
0xFF | 334577678 | 4e484ad4b99f07a333ae3845ee795b36cb6181e9a829261b2d52ff7931ac8f05 | 3C:7BC7 → 3C:7C1F → 3C:7C43 → 3C:7CFB → 3C:7D30 | 582 programs, three erases, 36 resets |
0xFE | 334587331 | b59cb47398bd186e2eaf7791ad42729e6f29f670da6b1854497eb7fbdbc362a8 | 3C:7BC7 → 3C:7C1F → 3C:7C48 → 3C:7CFB → 3C:7D30 | 581 programs, three erases, 35 resets |
0xE0 | 338262732 | 9c85a13be6d123443457eb772a16664a4a49f06a3d1dc0340b8b8d96a9b12b6b | 3C:7BC7 → 3C:7C1F → 3C:7D30 | 551 programs, two erases, 20 resets |
Each input image boots with a fresh RAM reset under the pinned TilEm build. The
0xFF and 0xFE paths erase the page-08 archive sector and both certificate
halves while completing the sector move. The 0xE0 path programs the page-0C
sector header to 0xF0 and performs certificate cleanup without erasing page
08 under TilEm. [confirmed]
Replaying each recovery trace over its input image produces SHA-256
8c857701d7da118d5c5f4c240ee21af91a10b95539059e74fb5e423368a683f9.
Replaying the uninterrupted GCFLASH trace from the pinned ROM produces the
same 1 MiB image. cmp reports exact equality for all four images. This proves
TilEm convergence after successful command boundaries at 0xFF, 0xFE, and
0xE0; it does not model a cut during a pending program or erase. [confirmed]
for TilEm.
Two controlled archive topologies reach the other dispatcher states. The first
starts with only two synthetic bytes: page 08’s erased header becomes 0xFE,
and page 28’s erased header becomes 0xF0. tools/ti84re/flash/gc_layout.py builds the
copy without modifying its source; tools/ti84re/flash/build_gc_layout.py requires the
source hash, refuses existing output by default, and reports every mutation.
The pinned-ROM input and controlled output hashes are: [confirmed]
source: 7d9a7d96d89fc552ebee6afdbdd011fdc6047be9c16d308245dff07eb1f7bd6d
controlled: 788b3c088e2954be5e53689afa7ac07d80159086a45d213a53f88952a65dd2e1
The starting topology is synthetic, but the unmodified ROM writes the journal
and all later archive state. Its GCFLASH trace reaches 3C:7801 and writes
active 0xFC and 0xF8 phases. Fresh-RAM cold boots under TilEm visit the
statically decoded recovery branches: [confirmed]
| Input phase | Original-trace trigger | Input image SHA-256 | Recovery branch | Recovery command shapes |
|---|---|---|---|---|
0xFC | 340858598 | f88f242026c8ae633764573f6dce0e2ef322668dbd149c36a8fb0732987da491 | 3C:7BC7 → 3C:7C1F → 3C:7CC6 → 3C:7D30 | 554 programs, four erases, 23 resets |
0xF8 | 340966279 | 77b7671e1bdd287022e1863de50a324b9818487be4f05403016e7f4e57b3f782 | 3C:7BC7 → 3C:7C1F → 3C:7CDA → 3C:7D30 | 553 programs, three erases, 22 resets |
Both recovery replays and uninterrupted execution produce the same complete
image, SHA-256
0dcf62f7445f5bc44b93effb7fd4cdf90d1cf813ad5ea55dd1f7445e0c14003f.
This is byte-for-byte convergence from ROM-written phases; it does not make the
two input header bytes calculator-authentic. [confirmed]
The 0xF0 reference input contains eight ordinary program records in the
page-08 and page-0C sectors. Three 17,000-byte records and one 14,454-byte
record fill each sector, leaving one erased trailing byte. Normal archive-UI
runs and successful OS Flash-worker traces produce SHA-256
389ed80fe8635740f855c7b8ffec6312a5182027dd0605e8a6e2b094c8481452.
tools/ti84re/flash/archive_fixture.py independently serializes the observed record header
and first-fit placement into erased 64 KiB sectors. Its guarded CLI reproduces
that complete image byte for byte from tools/rom.bin and the eight ordered
name/size pairs. [confirmed]
Running GCFLASH from the reconstructed input puts its dead record in page
10; page 08 and page 0C remain occupied when
gc_check_archive_consistency runs. An unmodified-ROM recapture takes the
direct 0xFE → 0xF0 transition and reproduces the phase image hash below under
TilEm. [confirmed]
| Phase | Reference trigger clock | Input image SHA-256 | Recovery branch |
|---|---|---|---|
0xF0 | 333006337 | df49d6ec77483e33944fdbcee969084fc065b01a4e44327f83246a9de363fcb2 | 3C:7BC7 → 3C:7C1F → 3C:7CE3 → 3C:7D30 |
The reconstructed run reaches 0xF0 at clock 339126369. Its trace SHA-256
is ffd6b2fb7a18713a2814666516f25f76bc9999314dfab83f3361c35e7bdd42ac.
Clock and whole-trace differences therefore leave the materialized phase image
unchanged. [confirmed]
The first uninterrupted and recovered outputs are not byte-identical. Their
archive regions match, but 11 certificate bytes differ: uninterrupted execution
ends with an active 0xE0 cleanup journal, while the 0xF0 restart completes
that cleanup during its boot. Cold-booting the uninterrupted output once erases
both certificate halves and produces the recovered SHA-256
39113ee67921340b8817e35576a8f8fda467122af7713b099f399512d65d9bc3.
Cold-booting the recovered output produces no Flash commands. Thus the 0xF0
case converges to the same stable Flash image after deferred 0xE0 cleanup,
not at the first trace endpoint under TilEm. [confirmed]
TilEm and Wabbitemu exercise all six phase boundaries after successful command boundaries. [confirmed] Cuts during busy commands and physical power loss remain untested. [hypothesis]
Wabbitemu restart at six journal boundaries
A Linux headless adapter now runs the pinned Wabbitemu commit 48c2dc0 without
its Windows interface. The acquisition procedure verifies the codeload archive
hash; the builder then verifies a path-and-content hash over all 334 extracted
source files and the individual translation units. Its only compatibility
changes remove the MSVC-only __pragma tokens and provide inert callbacks for
debugger registration and disabled audio. The CPU, memory, Flash, device,
keypad, interrupt, and LCD implementations are unmodified. [confirmed]
Each run begins with fresh RAM, presses ON at 24,000,000 t-states, releases it
at 24,900,000 t-states, executes at least 20,000,000 instructions, and requires
ten identical Flash samples one million instructions apart. An unmodified-ROM
baseline reaches the OS after the same wake transition without changing any of
the 1 MiB Flash image. The interrupted runs execute these page-0x3C points:
[confirmed]
| Input phase | Wabbitemu dispatcher visits | Changed input bytes | Output SHA-256 |
|---|---|---|---|
0xFF | 7BC7 → 7C1F → 7C43 → 7CFB → 7D30 | 74 | 8c857701d7da118d5c5f4c240ee21af91a10b95539059e74fb5e423368a683f9 |
0xFE | 7BC7 → 7C1F → 7C48 → 7CFB → 7D30 | 75 | 8c857701d7da118d5c5f4c240ee21af91a10b95539059e74fb5e423368a683f9 |
0xFC | 7BC7 → 7C1F → 7CC6 → 7D30 | 14 | 0dcf62f7445f5bc44b93effb7fd4cdf90d1cf813ad5ea55dd1f7445e0c14003f |
0xF8 | 7BC7 → 7C1F → 7CDA → 7D30 | 14 | 0dcf62f7445f5bc44b93effb7fd4cdf90d1cf813ad5ea55dd1f7445e0c14003f |
0xF0 | 7BC7 → 7C1F → 7CE3 → 7D30 | 131,082 | 39113ee67921340b8817e35576a8f8fda467122af7713b099f399512d65d9bc3 |
0xE0 | 7BC7 → 7C1F → 7D30 | 12 | 8c857701d7da118d5c5f4c240ee21af91a10b95539059e74fb5e423368a683f9 |
The outputs equal the corresponding uninterrupted TilEm replays byte for byte;
matching only the journal byte or archive range was not used as the criterion.
The 0xF0 run starts from the deterministically reconstructed df49d6… phase
image. It executes 20,000,000 instructions and 231,942,592 t-states before
reaching ten unchanged Flash samples. Its complete 1 MiB output equals both the
TilEm recovery and the normalized uninterrupted result.
tools/ti84re/flash/compare_images.py enforces the input hashes and complete-image
equality for all six Wabbitemu command-boundary runs. [confirmed] Cuts during
busy commands and physical power loss remain untested. [hypothesis]
The cold-start caller at 00:0D73 reaches the wrapper at 3D:6098 through
the bjump stub at 00:3EEB. Wabbitemu accepts the protected OUT (0x14),A
at 3D:60A6, changing its gate from locked to unlocked. The wrapper enters
gc_check_interrupted at 3C:7BC7 through 00:2BAD, then relocks at
3D:5CEF after recovery returns. Every phase run records the same unlock and
relock transitions. Between them, each run reaches _WriteFlashUnsafe, the
byte-identical 124-byte worker copied from 3F:4CCA to 0x8100, and its
success tail. No phase reaches that worker’s failure tail. This is a genuine
retail startup and recovery path under Wabbitemu, with no injected CPU state or
direct assignment to flash_locked. [confirmed]
The native adapter, importable orchestration library, and guarded build/run
CLIs are documented in tools/notes/emulator-probes.md. Their JSON reports include
typed gate writes and transitions, retail-bcall and copied-worker coverage,
input and output hashes, exact dispatcher visits, instruction and t-state
counts, changed-byte counts, wake completion, and Flash-settling status.
Reproducing the command timeline
tools/ti84re/flash/trace.py is the importable AMD-command decoder. The CLI resolves mapping changes,
decodes command sequences, and compacts adjacent program operations: [confirmed]
python3 -m ti84re.flash.analyze_trace \
/tmp/tibasic-smoke/gcflash.trace \
--clock 321347460-344829074 \
--timeline
python3 -m ti84re.trace.analyze_points \
/tmp/tibasic-smoke/gcflash.trace \
--point page_3C:71f8 \
--point page_3C:7733 \
--point page_3C:7cfb
The user command is also reachable through the MEM prompt whose "Garbage Collect?" string is at
01:76C9. Automatic collection on archive exhaustion calls the same collector before retrying the
archive operation at 3C:7F1C. [confirmed]
Memory checks [confirmed]
_MemChk(00:0E20) — free RAM =OPS(0x9828) − FPS(0x9824); returns 0 if the heap top has met the FP stack, elsecount(INC HL⇒ off-by-one inclusive).OPSis the top of the upward data heap; the gap to the downward VAT is the real free RAM (see_InsertMemcollision check). The decompiler’s trivial 2-line view is wrong — the real routine subtracts the two pointers._EnoughMem(00:0FA6) — ensure N free bytes; if short it walks the temp/scratch entries frompTemp(982E)down towardOPBase(9826)at a 9-byte stride, and_DelVars any entry whose flag byte has bit 7 (& 0x80) set (a reclaimable temporary), looping until enough or exhausted. Used by the_Create*routines and by the unarchive RAM-fit check (61F4calls it before allocating)._InsertMem(00:0F81) /_DelMem(00:1368) — open / close a gap at HL by block-moving everything above;_InsertMemfailsE_Memoryif it would collide with the VAT.- Free archive is computed inside the page-3D archive layer.
archive_prepare_scanat3D:61AFprepares its accounting state,archive_find_free_spanat3D:62C2searches for placement, andarchive_app_boundaryat3D:6413supplies the dynamic exclusive upper page. The catalog MEM path runs through3C:7121. [confirmed]
Routine index
| space:addr | name | what |
|---|---|---|
07:6248 | _Arc_Unarc | archive/unarchive entry; toggles arc flag, dispatches RAM↔Flash |
07:628B | arc_chk_name | archivable-name validator |
07:6107 | arc_ram_to_flash | RAM→Flash archive worker (programs Flash, frees old RAM) |
07:61F4 | arc_flash_to_ram | Flash→RAM unarchive worker (carves RAM, copies from Flash) |
07:6331 | arc_size_setup | stash vatPtr, compute dataSize into arcInfo |
07:61DC | arc_save_info | save the 12-byte tail from arcInfo.vat_ptr into savedArcInfo; 07:61E8 is an inferred restore candidate |
07:565F | findsym_scan | the real _FindSym VAT scanner |
00:0E65 | _FindSym | RST10 trampoline → findsym_scan |
00:0E60 | _ChkFindSym | type-check OP1 then FindSym |
00:1485 | _DataSize | variable data byte-size by type |
38:62A9 | _StoOther | store value into named var |
38:67B1 | _RclVarSym | recall var by symbol |
3A:5D07 | rcl_var_push | recall var, push to FPS |
3D:6745 | _FlashToRam | copy archived data Flash→RAM (page-aware); ti83plus.inc sibling _FlashToRam2 (ID 8054h) maps to 3F:4888 |
3D:678C | ram_worker_launcher | copy a length-prefixed worker to 0x8100 and execute it; used by _FlashToRam and certificate-page programming |
3D:61AF | archive_prepare_scan | prepare archive accounting and scan state |
3D:64AA | archive_write_record | program a complete archive record; executed in the archive trace |
3D:6440 | unarchive_record_to_ram | copy an archived record to RAM and retire its Flash record |
3D:62C2 | archive_find_free_span | scan from page 08 to the dynamic App boundary for space |
3D:6413 | archive_app_boundary | return the first page below the installed App run in B |
3D:726E | model_app_top_page | model-specific App scan start (0x15/0x29/0x69) |
3D:738B | model_certificate_page | model-specific certificate page (0x1E/0x3E/0x7E) |
3D:727D | init_flash_page_counter | set appSearchPage (0x82A3) to top App page + 1 |
3D:7C97 / 3D:7C8F / 3D:7C93 | flash_op_fe/fd/fb | clear status bit (0xFE/0xFD/0xFB AND-mask) |
3D:7DEA | flash_find_nonff | scan 13-byte header for all-0xFF (free slot) |
00:1837 / 00:182F | probe_hw_model_keep_a / probe_port21_keep_a | model bits: port 2 bit7 / port 0x21 low |
3D:6B6D / 3D:6B9B | flash_write_bounds_check / flash_write_byte_bounds_check | enforce page 08 and dynamic App-boundary limits before block or byte writes |
3C:71F8 | gc_command | display the Garbage Collecting screen, run recovery preflight, and call the collector |
3C:7219 | gc_recovery_preflight | inspect persistent GC state and enter recovery only when needed |
3C:7733 | archive_gc_collect | normal collector entry and Flash-unlock wrapper |
3C:7768 | gc_check_archive_sectors | scan four-page archive sectors for a valid starting state |
3C:77B5 | gc_prepare_journal | initialize the RAM phase table and inactive certificate half |
3C:781A | gc_process_sector_states | dispatch ordinary-sector copy, erase, and finalization work |
3C:7BC7 | gc_check_interrupted | test persistent journal bits at startup |
3C:7C1F | gc_recover_by_phase | dispatch interrupted states FF/FE/FC/F8/F0/E0 |
3C:7CFB | gc_run_phase_machine | run the normal sector pass and advance persistent phases |
3C:7E0D | gc_show_screen | display "Garbage" and "Collecting..." from page 01 |
00:0E20 | _MemChk | free RAM = OPS − FPS |
00:0FA6 | _EnoughMem | ensure N bytes; reclaim temps |
00:0F81 | _InsertMem | open a RAM gap |
00:1368 | _DelMem | close a RAM gap |
00:12D9 | _DelVarArc | delete var incl. archived copy |
00:1308 | _DelVar | delete var + VAT entry |
Strings: 01:4126 “Garbage Collecting…”, 01:4076 “Defragmenting…”, 07:6CA9
“ARCHIVED/VERSION/ARCHIVE FULL/VARIABLE/DUPLICATE”, 01:76C9 “Garbage Collect?”.
Ports: 0x06 = bank-A page select (Flash window), 0x14 = Flash write/erase control,
0x02 bit7 = Flash-size/model. RAM run-from-RAM stub: ramCode = 0x8100.
Resolved behavior and open items
-
Archive allocation. [confirmed] The allocator scans upward from page
08to the exclusive App boundary from3D:6413. On the traced OS-only TI-84 Plus, that interval is pages08–28; the sector header is at08:4000, and the first record begins at08:4001. -
Hardware Flash path. [confirmed]
archive_write_recordat3D:64AAinvokes_WriteAByteand_WriteFlashUnsafe; the boot worker runs at0x8100, issues AMD byte-program commands, polls DQ7/DQ5, and returns success. See Flash memory. -
Erase granularity. [standard] Ordinary sectors are 64 KiB, not one 16 KiB paging unit. The top-boot geometry also has 32, 8, 8, and 16 KiB sectors at physical
0xF0000–0xFFFFF. -
Record-status bytes. [confirmed] The record-status byte uses monotonic bit clearing:
0xFFerased →0xFEin-progress →0xFCvalid viaflash_op_fe/fd/fb(3D:7C97/3D:7C8F/3D:7C93) AND-masking. The delete and GC paths write0xF0directly to the status byte;flash_find_nonff(3D:7DEA) treats an all-0xFFheader as free. -
Garbage collection. [confirmed]
archive_gc_collectat3C:7733moves live records in 64 KiB sector units and uses the inactive 8 KiB certificate half as a persistent journal. The ordinaryGCFLASHtrace copies the survivingBrecord from the page-08sector to page0C, erases the old sector, and rotates the empty scratch sector back to page08. TilEm and pinned Wabbitemu cold restarts exercise all six ROM-written journal phases. Five converge byte-for-byte with uninterrupted execution;0xF0converges after the uninterrupted result performs deferred0xE0cleanup on its next boot. [hypothesis] Physical power loss and cuts inside busy commands remain untested. -
Group receive path. [confirmed] The standard link variable-receive loop is the member walk.
_DataSize(00:1485) confirms that a Group (type0x17) carries a leading word-size header._Arc_Unarc’sCP 0x17→26E0reject is on the Flash-backed branch before worker07:61F4. Its only bcall site at38:56E2is a wrapper gated by theArchive/UnArchivestatement handler at38:56E8. That handler checkstStore(5Fh) and rejects16h. Page07also contains the group guard cluster: the reject at07:6266, type-class checker at07:62C8, insertion guards at07:7338and07:739B, and group-aware VAT walker at07:73B7.A two-program
.8xgfixture containsHELLOandFACTOR. It concatenates standard variable records — a000Dhheader-length word, 13-byte header, echoed size, and payload — followed by a 16-bit checksum and no end marker. Libtifiles rejects a trailing80hbyte. A headless TilEm trace compares this fixture with a single-variableHELLObaseline:- The members land as individual variables. The RAM dump shows
FACTORin a VAT record with type byte05h, not a0x17hGroup object. No group blob remains in the final RAM image. - The coverage difference contains no new page-
07addresses. Neither trace executes the guard cluster at07:6266,07:62C8,07:7338,07:739B, and07:73B7. Those guards belong to theArchive/UnArchivestatement path, not to receipt. - The trace contains no dedicated receiver-side member walker. The standard
variable-receive loop runs once per entry. Its per-member anchors execute
once in the single-variable baseline and twice in the two-member trace.
These anchors include the post-transfer finalization block at
38:7858–38:790F, the checksum verifier at3C:6356, and the receive-and-store sequence at3C:6994. The finalization block resets flags, performs_ChkFindSym-adjacent stores, and reloads VAT pointers fromram:96EE–ram:96F0intoram:8588–ram:858B. The receive-and-store sequence is documented in Link transfer. The.8xgframing exists only in the file and sender. On the wire, each member is an independently framed variable transfer. The receiver’s existing loop until end of transfer is the member walk.
A second fixture contains archive-flagged
HELLOfollowed by ordinaryFACTOR; the complete link transfer succeeds. In the final VAT,HELLOhas page08, data address0x4001, and type05h, whileFACTORhas page00and type05h. The PRGM menu marks onlyHELLOas archived. This confirms that the receiver honors each member’s80hattribute independently and still creates no type-17hobject. [confirmed]Invoking that archived
HELLOreachesERR:ARCHIVED. A bounded dynamic trace records00:179D–00:17A1withB=08, followed byJP NZ,00:2779. The target loads error code0xAF(E_Archived) and enters_JErrorat00:2793. The observed path does not call_FlashToRamautomatically. [confirmed] - The members land as individual variables. The RAM dump shows
Resident assembly programs
TI-OS copies a compiled assembly program to userMem (0x9D95), executes it,
and removes that copy when it returns. This page documents the OS 2.55MP launch
path, the memory limit enforced by the ROM, pointer stability during variable
allocation, and archived-data access for long-running runtimes. It also
compares third-party launchers’ movement, archive-writeback, and cleanup
policies.
Compiled Asm( launch
The compiled-program path starts at _ExecutePrgm (07:5758). [confirmed]
The routine finds the program, reads its two-byte internal data size, and
requires the BB 6D marker at the start of that data. The following steps are
visible at 07:5762–07:57D1:
07:5766loads the internal data size intoBC.07:576Aand07:5771check theBB 6Dmarker.07:577Bsubtracts the internal size from0x2000and raises an error on borrow._ErrNotEnoughMematram:1735checks that the complete internal size fits in free RAM._InsertMematram:0F81opens that many bytes atram:9D95.07:579Cadds the allocation size to the saved source pointer because the insertion moved the source variable upward.- The
LDIRat07:579Dcopies toram:9D95. 07:57FDjumps toram:9D95through the error-context wrapper atram:27DA.
The internal size excludes the variable’s two-byte size field. It includes the
two-byte BB 6D marker. The largest marker-plus-payload value accepted by this
path is therefore 0x2000, leaving at most 0x1FFE bytes after the marker.
[confirmed]
| Quantity | Maximum |
|---|---|
| Internal program-data size | 0x2000 = 8,192 bytes |
BB 6D marker inside that size | 2 bytes |
| Bytes after the marker inside the variable | 0x1FFE = 8,190 bytes |
| Execution allocation | 0x2000 = 8,192 bytes |
| Last allocated byte | ram:BD94 |
Full ram:9D95–ram:BFFF span | 0x226B = 8,811 bytes |
The often-quoted 8,811-byte span describes the address range through
ram:BFFF; it is not this launcher’s accepted-size limit. The launcher leaves
the final 619 bytes of that span outside its maximum allocation. [confirmed]
The copy begins after the BB 6D marker but uses the complete internal size as
its length. It consequently reads two bytes beyond the size-described program
data. Those bytes occupy the final two bytes of the execution allocation.
[confirmed] A boundary fixture should place guard bytes after the variable to
record their exact values at ram:BD93 and ram:BD94.
The builders and TilEm runner in tools/probes/launch-fixtures/ exercise the three
adjacent boundary sizes. Each accepted trace reaches _ExecutePrgm, the limit
test, the _InsertMem call site, the payload handoff, and ram:9D95. The
rejected trace reaches the E_Invalid shim at ram:2729 before insertion.
[confirmed]
tools/data/launch-boundary-results.csv records the ROM, fixture, and trace
hashes together with instruction counts and reached checkpoints. [confirmed]
| Internal size | Bytes after marker | TilEm result |
|---|---|---|
0x1FFF | 8,189 | Accepted |
0x2000 | 8,190 | Accepted |
0x2001 | 8,191 | Rejected with ERR:INVALID |
Internal sizes of 8,808–8,814 bytes are all above this ROM limit, so they cannot distinguish the boundary. The fixture uses adjacent sizes instead.
Text AsmPrgm launch
The BB 6C text path begins at 07:57D4. 07:5717–07:5731 counts decoded
hex-byte pairs while ignoring ? separators, then applies the same 0x2000
limit to the decoded length. _InsertMem allocates that decoded length and
07:5734–07:5755 writes the decoded bytes at ram:9D95. [confirmed]
The source variable remains in RAM while the execution gap exists. Its hex
text and the decoded copy both consume free memory, so _EnoughMem may impose
a lower practical limit than the ROM’s 8,192-byte decoded cap. [confirmed]
Return and error cleanup
asm_prgm_size at 0x89EC records the execution allocation length. Normal
return clears it at 07:57C4–07:57CB, then calls _DelMem with
HL=ram:9D95 at 07:57CE–07:57D1. The error handler at
07:5800–07:581A performs the same clear and deletion before entering the OS
error path. [confirmed]
An assembly program must not move its execution copy. _InsertMem and
_DelMem repair OS pointer slots and VAT data pointers, but they cannot repair
the program counter, return addresses, or arbitrary runtime pointers. Cleanup
also assumes that the copy still begins at ram:9D95. [confirmed]
Observed handoff state
A headless TilEm trace of Asm(prgmASMRET) on a TI-84 Plus with OS 2.55MP
reaches asm_payload_handoff at 07:57B4, then executes RET at logical
0x9D95. TLMT instruction records contain the register state after the named
instruction. The CALL 07:57FD record at 07:57B4 and the JP ram:9D95
record at 07:57FD both report payload-entry SP=0xFFC9. The RET record at
ram:9D95 reports SP=0xFFCB after popping the launcher return. [confirmed]
| Register | Post-instruction RET record at ram:9D95 |
|---|---|
AF | 0x01BB |
BC | 0xFCCD |
DE | 0xFFEC |
HL | 0x57B4 |
SP | 0xFFCB |
These registers are observations, not an ABI. The payload should initialize every register it needs and return with the hardware stack balanced.
The trace resolver may label the first instruction page_??:5D95 when no
port-7 write has occurred since capture began. The logical PC is 0x9D95, and
the launcher’s JP 0x9D95 establishes that the instruction is in RAM. This is
a trace-reconstruction limitation. [confirmed]
Timed heap and stack snapshots
The RTSNAP fixture records the heap fields from inside the payload, while a
TLMT v2 replay applies every logical-memory write and samples the same fields
at OS-side checkpoints. The trace used TI-84 Plus OS 2.55MP, an unarchived
compiled program launched by Asm(prgmRTSNAP), and the ROM with SHA-256
dbb47afae091ab36f9abe74e32083013fbeff3d7e0516bbf5d1abf4ee57adc09.
[confirmed]
That complete-image identity is the BootFree 11.259 variant, not the canonical
retail-boot image. The launch implementation on Flash page 0x07 is
byte-identical in both images; page 0x07 has SHA-256
6335c5f15cb5d534423b8d018dd412d21905e5ca448cc3f84d6c53d15b3aa60e.
The trace therefore supports the page-0x07 launch result, but no retail-boot
claim. [confirmed]
| Checkpoint | FPS | OPS | pTemp | progPtr | SP | _MemChk |
|---|---|---|---|---|---|---|
_ExecutePrgm entry | 0x9FFA | 0xFCBA | 0xFCCE | 0xFD34 | 0xFFD7 | 0x5CC1 |
| First payload instruction | 0xA171 | 0xFCBA | 0xFCCE | 0xFD34 | 0xFFC9 | 0x5B4A |
Nested _MemChk entry | 0xA171 | 0xFCBA | 0xFCCE | 0xFD34 | 0xFFC3 | 0x5B4A |
Final payload RET (post-instruction) | 0xA171 | 0xFCBA | 0xFCCE | 0xFD34 | 0xFFCB | 0x5B4A |
| Cleanup entry | 0xA171 | 0xFCBA | 0xFCCE | 0xFD34 | 0xFFD7 | 0x5B4A |
| Cleanup return | 0x9FFA | 0xFCBA | 0xFCCE | 0xFD34 | 0xFFD9 | 0x5CC1 |
fpBase moves from 0x9FE8 to 0xA15F, and FPS moves from 0x9FFA
to 0xA171. Both shifts are the fixture’s 0x0177-byte internal program
size. OPBase, OPS, pTemp, progPtr, and symTable remain unchanged at
these checkpoints; cleanup restores the two shifted fields. The nested bcall
uses six more stack bytes than payload entry, but does not change the measured
heap pointers or _MemChk. [confirmed]
The fixture, decoder, analyzer, capture recipe, and compact provenance rows are
under tools/probes/launch/ and tools/data/resident-launch-snapshot.csv.
These observations do not establish an entry ABI or cover _ExecAsm, an
archived launcher path, shell launchers, other OS releases, or hardware.
[confirmed]
Free RAM and stack headroom
_MemChk at ram:0E20 returns (OPS - FPS) + 1 when OPS >= FPS; otherwise
it returns zero. _EnoughMem at ram:0FA6 compares a request with that result
and may delete reclaimable temporary variables before retrying. [confirmed]
Neither routine reads the hardware SP. The launch capacity check therefore
does not reserve hardware-stack headroom or detect a collision between the Z80
stack and OS data. [confirmed] A resident runtime must impose its own stack
limit or guard region. The available margin depends on current VAT and
temporary state, so a single OS-wide stack-headroom number is not supported.
Variable relocation while resident
_InsertMem and _DelMem move the OS data region and update a fixed set of
OS-owned pointers. The repair list includes iMathPtr1–iMathPtr5,
asm_data_ptr1, asm_data_ptr2, newDataPtr, and other delete, edit, and
floating-output pointers. The VAT scan adjusts a data pointer only for a RAM
entry whose data lies on the moved side of the gap. Archived entries are not
RAM-relocated. [confirmed]
The repair code does not scan arbitrary memory for pointers. A runtime’s absolute pointer into a program, AppVar, or workspace can become stale after a create, delete, resize, archive, unarchive, temporary cleanup, variable receive, or archive garbage collection. [confirmed]
Explicit source writeback
Changing bytes in the direct Asm( execution copy does not change the named
program variable. RUNCOUNT 16, whose release describes itself as
self-modifying, handles that distinction explicitly: it relies on OP1 still
naming the running program, calls _ChkFindSym, and increments two BCD counter
bytes through the returned source pointer. It performs a second lookup before
converting the counter to Ans. [confirmed] for the identified community
source.
Viewed from the data-size pointer returned in DE, RUNCOUNT’s fixed prefix is:
#pragma pack(push, 1)
typedef struct {
uint16_t data_size; /* +0x00 */
uint8_t asm_marker[2]; /* +0x02, BB 6D */
uint8_t entry_jump[3]; /* +0x04, JP start */
uint8_t count_msb_bcd; /* +0x07 */
uint8_t count_lsb_bcd; /* +0x08 */
uint8_t code[]; /* +0x09 */
} RunCountVariablePrefix; /* 9-byte fixed prefix */
#pragma pack(pop)
The first lookup increments count_lsb_bcd and carries into
count_msb_bcd. The second lookup passes the address of count_msb_bcd to
the BCD-to-Ans conversion. [confirmed]
The raw 136-byte source build is SHA-256
3e506c4330cd5499a031ae56c73d0487f811278b5fc3d52949bc0f56a69b2f05.
The packaged program body is exactly BB 6D followed by that build, so this
writeback design is also confirmed for the identified release binary. The
archive programs/runcounter16.zip has SHA-256
736212242e2e9a97e90908ce42fa051b27dff52a84ef1141546d58cd4e5eaf08;
member Source/RunCounter16.z80 has SHA-256
2ea3efc9d4764813f6f57fa19ae4a3564ef2f3855f4daf997619ff29f54316d5,
and RUNCOUNT.8xp has SHA-256
41615816759a6cb2df1aee41f906956a6823aa09c19e4ec8e79712868c1d889a.
[confirmed]
An instrumented TI-84 Plus OS 2.55MP direct Asm( run executes this packaged
program twice. Both passes reach the store at ram:9DAC; _ChkFindSym returns
the named variable’s data-size pointer at ram:9EBD, and the program advances
to count_lsb_bcd at ram:9EC5. The attributed writes store 1, then 2, at
that source field. The result dynamically confirms explicit source writeback
for this unarchived route rather than mutation of the separate execution copy.
The ROM, program, wrapper, macro, emulator, and trace hashes are pinned in
tools/data/community-loader-traces.csv. [confirmed]
The source does not test the returned page byte in B. Its direct store is
therefore supported only when lookup returns a RAM source. An archived source
or a shell that moves the named body can produce a Flash pointer or a
shell-defined in-flight representation instead. Persistent self-modification
must follow the launcher’s lookup and writeback contract, not merely reuse this
fixed-offset pattern. [confirmed] for the missing guard; [hypothesis] for
untraced launcher outcomes.
Repeating the same link-only fixture with RUNCOUNT’s archive flag set does not
reach ram:9D95 under this OS and headless launch macro. It therefore confirms
non-execution for that exact emulator scenario, not the result of an archived
source store. Shell-mediated archived launches remain open. [confirmed] for
the observed boundary.
Resident allocation trace
The fixture under tools/probes/allocation/ calls _EnoughMem,
_CreateAppVar, _DelVar, _CreateProg, _InsertMem, and _DelMem from a
compiled program at ram:9D95. Its trace replays RAM writes at ten timed
checkpoints. [confirmed]
A 32-byte AppVar or program moves FPS from 0xA076 to 0xA098 and OPS,
OPBase, and pTemp downward by 12 bytes. _MemChk falls from 0x5C44 to
0x5C16. Deleting either object restores all recorded heap pointers.
[confirmed]
The direct _InsertMem probe opens 16 bytes at the source variable’s data
pointer. _ChkFindSym then returns 0x9F3B instead of 0x9F2B. _DelMem
restores 0x9F2B. An eight-byte guard in the execution copy remains unchanged,
and every payload checkpoint remains at its assembled ram:9D95-relative
address. TI-OS repairs the source VAT pointer; it does not move the executing
copy. [confirmed]
The ROM repair pass at ram:11E8–ram:128A conditionally adjusts 24 OS pointer
slots. Named slots include iMathPtr1–iMathPtr5, asm_data_ptr1,
asm_data_ptr2, fmtMatMem, newDataPtr, EQS, insDelPtr, editDat,
chkDelPtr1, chkDelPtr2, XOutDat, YOutDat, fOutDat, and inputDat.
The adjacent pass at ram:11C7 repairs basic_start, nextParseByte, and
basic_end. The VAT scan at ram:139D repairs each affected RAM entry’s data
pointer. [confirmed]
The reset-state run, with only the required wrapper and probe variables,
measures _MemChk=0x5C44 before its maximum AppVar.
For the five-character name ALMAX, _CreateAppVar consumes a 14-byte
overhead: two data-size bytes plus a 12-byte VAT entry. A payload request of
0x5C36 succeeds, producing FPS=0xFCAE, OPS=0xFCAD, and _MemChk=0.
The hardware stack remains at SP=0xFFC9; the creator does not include that
remaining stack-to-VAT distance in its capacity decision. [confirmed]
tools/data/resident-allocation.csv records the checkpoints, ROM and trace
hashes, model, and OS version. This run covers the direct-Asm( reset state.
It does not measure representative user-variable populations, shell
move-loaders, Flash Apps, or physical hardware. [confirmed]
Stable RAM AppVar protocol
A long-running runtime can use a RAM AppVar as a movable workspace if it treats the VAT lookup as a handle operation:
- Keep the AppVar name and expected type in program-owned storage.
- Rebuild OP1 and call
_ChkFindSymimmediately before access. - Require carry clear and
B=0.DEpoints to the two-byte data-size field; the payload begins atDE+2. - Do not retain
DE, the payload base, or an interior pointer across a call that can move variables or reclaim temporaries. - Reacquire the base afterward. Store internal references as offsets from the payload base.
- If a stored image contains absolute pointers, relocate every pointer after reacquisition and before use.
This protocol keeps no data pointer across a moving operation; it does not make an update transactional. A reset-tolerant format can add a version, length, checksum, and commit marker. Two records or copy-on-write storage can preserve the last committed generation during an interrupted update.
Archived lookup contract
The VAT scanner reached through _ChkFindSym returns the archive page in B
and the data pointer in DE. [confirmed]
| Result | Meaning |
|---|---|
| Carry set | No matching VAT entry |
Carry clear, B=0 | DE is a RAM pointer to the two-byte data-size field |
Carry clear, B!=0 | B:DE identifies Flash data through bank A |
An archived DE value is not a flat RAM pointer. Direct access must preserve
port 0x06, map page B, handle the 0x8000 → next-page crossing, and restore
the caller’s mapping. Any operation that can run archive garbage collection
invalidates a saved B:DE; call _ChkFindSym again afterward. [confirmed]
Streaming archived data
The three useful access strategies have different memory and paging costs:
| Strategy | Full object must fit free RAM | Page crossing | Bank-A restoration |
|---|---|---|---|
| Direct page mapping | No | Caller handles it | Caller handles it |
Chunked _FlashToRam | No | OS handles it | OS restores port 0x06 |
_Arc_Unarc, then RAM access | Yes | OS handles it | Not exposed |
_FlashToRam has bcall ID 5017h and body 3D:6745. Its copier saves port
0x06, maps the source page, advances from 0x8000 to 0x4000 on the next
Flash page, and restores the saved mapping. A streaming interpreter can copy
repeated bounded chunks into a RAM buffer and process a source larger than free
RAM. [confirmed]
_Arc_Unarc at 07:6248 enters the unarchive path at 07:61F4 when B is
nonzero. Unarchiving requires enough RAM for the complete variable and cannot
stream an object larger than free RAM. [confirmed]
Direct mapping and _FlashToRam reads do not themselves make a saved archive
location stable. Reacquire it after an operation that may write or collect the
archive.
Shell loaders and writeback
Ion, Plasma, TSE, MirageOS, Doors CS, and zStart all place assembly code at
userMem (0x9D95), but they do not preserve the source variable in the same
way. This section compares their RAM cost, writeback policy, lookup behavior, and
cleanup contract using identified original releases.
Comparison
| Launcher | RAM-resident input | Archived input | Archive writeback | Error cleanup | Evidence |
|---|---|---|---|---|---|
| Ion 1.6 | Moves the original body | Unarchives the original, then moves its body | Always rearchives | No Ion-owned error handler | Identified original source; runtime untraced |
| Plasma 1.4 | Copies the body into the current userMem execution allocation | Copies the body from Flash with _FlashToRam | None; clients can save to an AppVar | No Plasma-owned error handler | Byte-matched release source; release entry traced, client paths untraced |
| TSE 1.5/1.6 | Moves the active body and task state between the variable and userMem | Unarchives the original, then uses the RAM task path | Leaves the program in RAM | Cooperative switch and exit paths only | Byte-matched release source; infrastructure loader traced, task switching untraced |
| MirageOS 1.2 | Uses a symmetric move loader | Creates a named TempProgObj RAM copy, then moves its body | Rewrites only if changed | Installs an OS error handler | Identified release-binary disassembly; runtime untraced |
| Doors CS 7.4 | Moves the original body | Creates a complete RAM variable under a derived temporary name | Compares the temporary variable with the archive; replaces the archive only if changed | Routes OS errors through reverse-swap cleanup | Identified source commit; runtime untraced |
| zStart 1.3.013 | Moves the original body | Copies the body into a raw userMem allocation | Uses a 16-bit checksum; replaces the archive only if changed | Routes OS errors through local cleanup | Identified source commit; runtime untraced |
Unless a trace is stated explicitly below, [confirmed] in these launcher
subsections refers to the identified source or release-binary control flow,
not a replicated runtime outcome.
The three source-available move loaders show that “run at userMem” does not
imply “make a second complete copy.” Ion, Doors CS, and zStart move a
RAM-resident body in chunks through a 768-byte shuttle. Plasma copies the body
and relocates its shell tail around it. TSE moves the body together with an
appended task-state area. Archived inputs also differ: Ion and TSE first
unarchive the original, MirageOS and Doors CS build named temporary variables,
Plasma copies from Flash into its current execution allocation, and zStart
builds an unnamed execution allocation. [confirmed] for the identified source
and release-binary control flow.
The source identity, execution strategy, and open evidence boundary for the
original four comparison rows are also recorded in
tools/data/shell-loader-observations.csv. Plasma and TSE are pinned below by
release-archive and member hashes.
The move-loader pattern
Ion, Doors CS, and zStart use the same broad transformation for a RAM-resident program:
- Copy at most 768 bytes of the variable body to a screen buffer.
- Call
_DelMemto remove that source chunk. - Call
_InsertMemto open space at the destination. - Copy the buffered chunk into the new space.
- Repeat until the body is at
0x9D95. - Reverse the operation after the program returns.
Ion and Doors CS use plotSScreen (0x9340) as the shuttle. zStart uses
saveSScreen (0x86EC). The original-source implementations confirm this
structure. [confirmed]
The operation preserves only one complete body for a RAM-resident input. The
VAT entry and name can remain findable while the bytes described by that entry
are being moved. A successful _ChkFindSym therefore does not prove that its
returned data pointer identifies a valid, contiguous copy of the running
program. [confirmed]
Ion 1.6
Ion’s ionm.z80 first calls _EnoughRam and _Arc_Unarc for an archived
program. The complete original variable must therefore fit in RAM before the
move loader starts. The loader then moves the body to 0x9D95 through
plotSScreen, using _DelMem and _InsertMem rather than allocating a second
complete execution copy. [confirmed]
On return, Ion reverses the move. If the input was originally archived, it
calls _Arc_Unarc again, so modified bytes persist but an archive write occurs
even when the program did not change. [confirmed]
Ion calls the client without installing an Ion-owned OS error handler. The source therefore provides no cleanup path for an OS error or nonlocal exit that bypasses the normal return. [confirmed] The resulting calculator state still needs a dynamic trace. [hypothesis]
The loader stages its preloader in appBackUpScreen (0x9872) and its move
routine in cmdShadow (0x966E). It uses plotSScreen during the forward and
reverse moves. The client may reuse plotSScreen while it runs; the reverse
move overwrites that buffer before Ion redraws its interface. [confirmed]
Plasma 1.4
Plasma’s documentation says that programs “run right from flash” and that the
shell does not perform program writeback. The first phrase describes the
storage of the source variable, not the address executed by the CPU. The
included plasma.asm copies the client into a RAM image at userMem and jumps
there. [confirmed]
For an archived input, exec_prog_real obtains the page and data pointer from
_ChkFindSym. It installs Prog_Loader in saferam3, prepares
DE=userMem-2, and passes the source page, pointer, and size to the loader.
Prog_Loader calls _FlashToRam, copies the Ion library block after the
client, and jumps to userMem+1. The named source remains archived. For a RAM
input, the same path calls _FlashToRam with A=0; the fixed-RAM source
address is copied without moving the original variable. [confirmed]
Plasma relocates the shell code and Ion-library jump table around the client
inside the current execution allocation. It calls _InsertMem only when the
client and library tail need more space than the shell image already occupies.
This is a copy loader, but its peak allocation is not the sum of two fixed,
complete program images. The allocation structure is [confirmed]. The exact
peak still needs a dynamic trace. [hypothesis]
Normal return does not compare or rewrite the source program. Plasma instead
exports savedata and fetchdata: the first replaces a named AppVar with
_CreateAppVar, and the second reads a RAM or archived AppVar through
_FlashToRam. A client that needs persistent data must use an explicit storage
path such as this one. [confirmed]
No Plasma-owned OS error handler appears in the release source. [confirmed] The state after an OS error or nonlocal exit remains [hypothesis].
The release archive has SHA-256
62965a41fe071902043ebcbbd1254f710d29729bf86a78f20b6f14d6974f5d5a.
Within it, Plasma/plasma.asm has SHA-256
b424980285adf3f16225239c3ba3f133a42efb38d0666d968eee4b1fe24b810f,
Plasma/plasma.txt has SHA-256
970ec3908da27bdbabc630c38c9546eade19059a9902f111bd116e3d3c77a750,
and Plasma/PLASMA.8XP has SHA-256
a55816b3ea9462c4e7ef16750d3ad6f8955b0a51de6a096c8b7e59f1242f0df1.
SPASM-ng with TI83P defined produces a 2,447-byte body that matches the
packaged program byte for byte. [confirmed]
An instrumented TI-84 Plus OS 2.55MP run reaches the packaged Plasma entry at
ram:9D95. The deterministic keyboard macro does not reach the protected Ion
client, so the RAM and archived client-copy paths above remain static results.
It also does not reach the copied raw-key hook’s RST 28h at ram:9881 or
_newContext at ram:077E. The 4030h nonlocal context transition is
therefore interaction-blocked rather than dynamically confirmed. The trace,
ROM, link-file, macro, and emulator hashes are pinned in
tools/data/community-loader-traces.csv. [confirmed] for the observed entry
and boundary.
TSE 1.5/1.6
TSE is a cooperative task-switching runtime rather than a normal-return shell loader. Its release README identifies version 1.5, while the byte-matched kernel source returns version 1.6. The combined label records that release inconsistency. [confirmed]
starttask appends the program’s requested external-data area followed by a
62-byte state tail. The tail contains a saved SP word and 60 bytes copied
from flags at 0x89F0. Its packed layout is:
#pragma pack(push, 1)
typedef struct {
uint16_t saved_sp; /* +0x00 */
uint8_t flags[60]; /* +0x02, copy of 0x89F0-0x8A2B */
} TSETaskStateTail; /* 62 bytes */
#pragma pack(pop)
The tail begins immediately after the requested external-data area. TSE writes
the initial code address into the last two bytes of that area as the first
stack return address, while saved_sp records the resulting stack pointer.
[confirmed] for the byte-matched source layout; live save and restore remain
untraced.
cpy_prgm_in reduces the stored variable to its three-byte BB 6D C9 header,
then moves the remaining body and task state to userMem in chunks no larger
than 0x100. Each chunk opens the destination with _InsertMem, copies the
bytes, and closes the source with _DelMem. cpy_prgm_out reverses the move.
Only one active body is retained, and the named VAT entry describes the
three-byte dormant header while its task is active. [confirmed]
Before a task switch, TSE stores the live SP and flag bytes in the active
task block. The reverse move writes the complete active image back to its RAM
variable, including client modifications. TSE then moves the selected task to
userMem, restores its flags and SP, and resumes it with RET. Ending a task
removes the external-data area and 62-byte state tail. [confirmed] for the
byte-matched source control flow; a live task switch and writeback remain
untraced.
Utopia handles an archived TSE program by calling _Arc_Unarc before
_tseStartTask. It does not rearchive that program when the task ends. The
separate LOADTSE program can stream the archived TSEKRNL and TSELIBS
variables into saferam1 and saferam2 with _FlashToRam; that infrastructure
path does not make an archived client execute in place. [confirmed]
The mover requires a 0x100-byte free-RAM buffer. The release documentation
warns that a task block can be created when too little memory remains for a
later switch. The buffer requirement is [confirmed]. The exact peak RAM cost,
machine state after an error, and every low-memory failure path still need
dynamic traces. [hypothesis]
The matched release archive shells/old/tsekrnl.zip has SHA-256
d640729fcb4ebf2a166fe37f3ae59741a50a571578e5091863295bb08dba6a3b;
its Tse.8xg member has SHA-256
4cde52eb0ec37c16a5ac17f2f6eb94c7e3f4ebc4020b577b70d32ac34b29f3cf.
Its tse.txt and tsedev.txt members have SHA-256 values
d0b8e033eef29e370ea8bac145019c5f369a789daf03b753c69b1e83d52f60ae
and b8b566b5dac0f7084a7eb0a6cae259b70151bb3457dfe7849f5e708141d75c8e.
The source archive source/tsesrc.zip has SHA-256
d16407c2125133b24a86ad8e88819b3ae0fcc826a55ded5cb4155c19e6239592.
Its tsekrnl.asm, loadtse.asm, and tselibs.asm members have SHA-256 values
02eb4c0723ac5d3a74bcbdd3283c44fb7a87a5e509083aa6e68080398c6a4cc0,
34578f59f6324a4e82764ab38d5406eed6591019abf0a8e3bc78130cbe9b9f0e,
and ca55ca2ae6bf64dc5f944e79ad9fbc4f23c349c2bb15d88836830d5e3d9d62e2.
SPASM-ng rejects the unused kX-1 equate in the release tse.inc. Renaming
that unused equate for parser compatibility makes those sources reproduce the
packaged TSEKRNL, LOADTSE, and TSELIBS bodies byte for byte. [confirmed]
The packaged TSELIBS entry contains a two-byte internal size and a 391-byte
program body. LOADTSE skips the size, BB 6D, and leading C9, leaving 388
library-code bytes, but loads a fixed 531 bytes into saferam2 (ram:8A3A). It
therefore reads and writes 143 bytes beyond the packaged library code. This is
not merely dead source: two instrumented RAM-path runs enter loadRAM four
times, execute 2,598 LDIR iterations in total, and record 1,062 writes to the
531-byte saferam2 range, including 286 writes beyond the 388-byte code extent.
[confirmed]
The archived fixture changes only the archive flags of the byte-identical
TSEKRNL and TSELIBS entries. Two runs enter streamFlash four times, reach
its _FlashToRam call site at ram:9DDD four times, make the same 1,062 range
writes including 286 beyond-code writes, and enter the packaged kernel at
ram:9872 twice. The traces confirm both infrastructure loader paths and the
143-byte over-read on each run. They do not start a cooperative client task,
so cpy_prgm_in, cpy_prgm_out, task writeback, and low-memory switching
remain static results. Hashes and counts are in
tools/data/community-loader-traces.csv. [confirmed]
MirageOS 1.2
MirageOS 1.2 is a one-page, binary-only Flash App. Static disassembly of the
original App shows a symmetric loader at mapped bank-A logical addresses
0x75CF–0x76C0. It uses saveSScreen as a 768-byte shuttle, calls _DelMem
and _InsertMem, and
moves the client to 0x9D95. [confirmed]
For archived input, mapped bank-A logical addresses 0x7899–0x78FD create a
program named Z,1., change its VAT type to TempProgObj (0x16), and copy
the archived object into it
with _FlashToRam. The original name continues to identify the archived
object while the temporary object’s body is moved to 0x9D95. Before creating
the temporary, the loader deletes any existing Z,1. program. [confirmed]
The writeback path at mapped bank-A logical addresses 0x77D5–0x7870
compares RAM bytes with archive bytes through a temporary page-read thunk. It
replaces and rearchives
the program only when the comparison differs. The release changelog describes
the same smart-writeback policy. [confirmed]
The launch wrapper installs an OS error handler and conditionally prepares the
MirageOS tasker before calling the client. With tasker flag bit 6 at 0x9689
set, mapped bank-A logical addresses 0x7176–0x71E9 install IM2. The path
writes tasker state beginning at 0x8A3A, code at 0x8A4F–0x8A88 and
0x8A8A–0x8AFE, and an IM2 handler
at 0x8C01–0x8C1B. It also builds the 257-byte IM2 vector table at
0x8B00–0x8C00. It uses the word at 0x966F as an optional custom interrupt
target. These statVars ranges are not client scratch while the tasker or
custom interrupt is active. [confirmed]
The changelog states that ON+^ quits immediately without writeback. [confirmed] for the identified changelog text. Whether that path restores every intermediate body and mapping remains [hypothesis].
Doors CS 7.4
Doors CS classifies TI-OS assembly, Ion, MirageOS, Doors CS assembly, BASIC,
and associated program types in runprog.asm. For a RAM-resident assembly
program, hook1 and the swap1–swap4 routines move its body between
the variable and 0x9D95 through plotSScreen. [confirmed]
For an archived assembly program, initTmpASM checks available memory,
creates a complete RAM temporary variable under a derived name, and calls
_FlashToRam before invoking the same move loader. The name is made by adding
the program-chain size to the original name’s first byte. initTmpASM deletes
an existing variable on collision, so the name is not globally unique. This
path needs space for one complete RAM variable plus loader state; it does not
retain another complete execution copy after the move. [confirmed]
The asmcheckwriteback path behaves differently according to the original
storage class:
- For a RAM-resident input, the reverse move has already put modified bytes back into the original variable.
- For an archived input, it compares the complete RAM temporary variable with the archived original page by page. An unchanged temporary variable is deleted. A changed one replaces the archive under the original name.
Both behaviors are confirmed by writeback.asm. Doors CS also installs
AppOnErr(hook1reterror), which routes an ordinary OS error through the
reverse move. [confirmed]
The shell’s mos_quittoshell path manually rewrites SP and returns through
a thunk in cmdShadow. Whether every forced shell-to-shell exit reaches the
archive comparison still needs a dynamic trace. [hypothesis]
zStart 1.3.013
zStart’s runPrograms.z80 handles archived input without creating a named RAM
temporary variable. It checks space, opens a raw allocation at 0x9D95, copies
the archived body there, and records a 16-bit additive checksum. The archived
original remains present while this execution image runs. [confirmed]
For RAM-resident input, zStart temporarily reduces the stored body to its
two-byte header and uses moveMemory to transfer the rest to 0x9D95 through
saveSScreen in chunks no larger than 0x300. Normal return reverses the move
and restores the stored size. [confirmed]
The client runs inside errHandOn(programRet), so ordinary TI-OS errors reach
the same cleanup path. For an archived input, zStart deletes the raw allocation
when its checksum is unchanged. When it differs, zStart deletes the archived
original, creates a new program containing the BB 6D marker and changed
body, then archives the replacement. [confirmed]
zStart also places a shell-call thunk at 0x8000, installs Ion-compatible
vectors in cmdShadow, and selects IM1 for the client. A nonlocal jump into
another shell can bypass the local error frame; that path remains untraced.
[hypothesis]
Lookup and persistence consequences
Self-lookup has several distinct outcomes across these loaders:
- A RAM-resident input can remain named in the VAT while its body is moved and is not a valid contiguous self-image.
- A MirageOS archived input keeps the archived original name and uses the
TempProgObjnamedZ,1.for the RAM body that is moved during execution. - A Doors CS archived input has an immutable archived original plus a derived-name temporary variable whose body is moved during execution. A collision with that derived name is deleted during setup.
- A zStart archived input has an immutable archived original plus an unnamed execution allocation.
- A Plasma input keeps the complete named source and runs a copied image. The
source may be in RAM or Flash, but
_ChkFindSymdoes not return the execution image. - An active TSE task keeps a named three-byte header in its variable while the
remaining body and appended task state occupy
userMem.
Code that needs its running image should use a shell-defined pointer or a
position-independent scheme rather than assume that _ChkFindSym returns it.
Writeback also changes the persistence guarantee. Ion writes every originally archived input back to Flash; MirageOS, Doors CS, and zStart avoid a Flash rewrite on their confirmed unchanged paths. Plasma never writes the source program back. TSE writes an active image back to its RAM variable on a cooperative switch and leaves an automatically unarchived program in RAM. Forced shell exits can bypass writeback under at least the documented MirageOS ON+^ path, and nonlocal exit behavior remains launcher-specific.
Evidence limits
The Ion, Plasma, TSE, Doors CS, and zStart results above come from source included in an identified release, byte-matched release source, or an identified source commit. MirageOS results come from its identified original binary and release changelog. TSE’s RAM and archived infrastructure copies and Plasma’s release entry also have the dynamic boundaries described above. These artifacts do not confirm peak free-RAM measurements or the machine state after every abnormal exit.
The following questions still require a common instrumented payload under all six launcher and runtime designs:
- the exact peak RAM cost for RAM-resident and archived inputs;
_ChkFindSymresults and pointed-to bytes while each client is running;- self-modification persistence on normal return, OS error, ON+^, and shell-to-shell exit;
- restoration of the stack, interrupt mode, page mapping, and scratch RAM on every abnormal path.
The loader-source numeric bcall scan finds 4F66h for _SetGetKeyHook and
4030h for _newContext in Plasma. Both IDs already have those names in the
current bcall map. No absent or misnamed numeric bcall was found in Plasma,
TSE, or RUNCOUNT. [confirmed]
Source provenance
| Artifact | Exact identity | Source |
|---|---|---|
| Ion 1.6 release archive | SHA-256 b5a5ba97f325f8779aa35cda23e38152087930298ff8b7b8573905710230e6e6 | ion.zip |
| Plasma 1.4 release archive | SHA-256 62965a41fe071902043ebcbbd1254f710d29729bf86a78f20b6f14d6974f5d5a | plasma141.zip |
| TSE 1.5/1.6 matched release archive | SHA-256 d640729fcb4ebf2a166fe37f3ae59741a50a571578e5091863295bb08dba6a3b | tsekrnl.zip |
| TSE matched source archive | SHA-256 d16407c2125133b24a86ad8e88819b3ae0fcc826a55ded5cb4155c19e6239592 | tsesrc.zip |
| MirageOS 1.2 release archive | SHA-256 38dc70173818972de8c5eb78099e8870c7acb9ad4c62d290f6c6f5840c71d43b | mirageos.zip |
| Doors CS 7.4 release archive | SHA-256 3a16161ce1d091438b0ea9f5e72774f8e8b4fdfba9ab1024bad0b55569555230 | dcs7.zip |
| Doors CS source repository | Commit 33af4f5ede199eee77cf2f89b5463a0a6ec9a1af | Doors CS 7 commit |
| zStart 1.3.013 release archive | SHA-256 7a1b7c69c85030b412bb6ea11ae71ac608b9882a9de3ab7dbef1faf69519c5e9 | zstart.zip |
Evidence limits
The ROM and traces on this page cover the compiled and text Asm( paths,
timed unarchived compiled-launch heap snapshots, RUNCOUNT’s unarchived source
writeback, its archived non-execution boundary, normal cleanup bytes,
pointer-repair code, VAT results, and Flash-page copying on TI-84 Plus OS
2.55MP. They do not establish behavior for _ExecAsm, an archived shell
launch, another OS version, a 48 KiB ASIC, or a physical calculator.
Useful next fixtures should guard the two-byte over-read, repeat the timed
snapshots for archived, _ExecAsm, and shell routes, and force archive garbage
collection followed by a fresh lookup.
Resident scratch RAM
Long-running assembly programs cannot treat the usual TI-OS work buffers as
anonymous memory while continuing to call arbitrary OS routines. Two OS
2.55MP traces directly overwrite OP1–OP6, iMathPtr1–iMathPtr5,
textShadow, and every byte of saveSScreen. The remaining advertised ranges
have subsystem owners that make them conditional, even where these traces did
not touch them. [confirmed]
The measurements are in tools/data/scratch-ram-observations.csv and
tools/data/scratch-guard-results.csv.
Regenerate a row set from a full-range TilEm trace with:
nix develop --command python3 -m ti84re.trace.analyze_scratch TRACE \
--initial-port-5 0 --initial-port-7 0x81 \
--scenario NAME --model ti84p --os-version 2.55MP --format csv
The initial selectors matter when a trace starts after TI-OS established its normal mapping. A zero result means only that the scenario did not write the range; it is not evidence that the range is safe.
The launch and interactive trace SHA-256 values are
e61293d420f92b37dfa0d118f14896287735989c9292210933f1abca4ef6b0fa
and 23338cdef33bc3f47988a3bf48089f25405205109b83421c3c1a9219f2e90505.
The recorded rows identify the emulator only as emulator-unspecified; the
trace contents are pinned, but the emulator binary and source revision are not.
Each TLMT initial snapshot has fixed Flash page 0x00 SHA-256
bfc698e445d98d6d0905589ec34a88c9372a90cb0ed2d1fe9aa9b6fca0962fc1.
That hash matches page 0x00 in both known OS 2.55MP images. [confirmed]
Neither trace has a complete-ROM sidecar, so the hash does not identify the
boot pages or the complete image.
Observed clobbers [confirmed]
Both scenarios are direct compiled Asm( launches of the local ti84-forth
runtime. The second continues through cooked-input activity. Counts are memory
writes, followed by the number of distinct bytes touched.
| Buffer | Address range | Launch | Interactive input | Classification |
|---|---|---|---|---|
OP1–OP6 | ram:8478–ram:84B9 | 218 / 44 | 824 / 44 | Unsafe across ordinary parser, VAT, and floating-point calls |
iMathPtr1–iMathPtr5 | ram:84D3–ram:84DC | 80 / 8 | 88 / 10 | Unsafe across VAT, graph, table, and link activity |
textShadow | ram:8508–ram:8587 | 442 / 128 | 1,097 / 21 | Unsafe with ordinary text display |
saveSScreen | ram:86EC–ram:89EB | 2,304 / 768 | 3,072 / 768 | Unsafe in the normal launch state |
statVars | ram:8A3A–ram:8C4C | 0 / 0 | 0 / 0 | Candidate only after _DelRes, with statistics and shell interrupts excluded |
| Table/solver workspace | ram:91DC–ram:9301 | 0 / 0 | 0 / 0 | Candidate only while table, solver, finance, and graph-table contexts are excluded |
plotSScreen | ram:9340–ram:963F | 0 / 0 | 0 / 0 | Unsafe when graph or buffered-display routines remain available |
appBackUpScreen | ram:9872–ram:9B71 | 0 / 0 | 0 / 0 | Candidate only without app/menu transitions or installed hooks that own the buffer |
The traces include writers at ram:1F37 and 07:51FA for operand storage,
07:4F70–07:4F82 for iMath pointers, and 01:617A and 01:61C2 for text
state. The page-0x3B display-save loop writes every byte of saveSScreen at
its 3B:69C2 store. [confirmed]
Static ROM ownership disqualifies the zero-write rows as unconditional storage:
_GrBufClr clears plotSScreen; graph primitives consume it; table, solver,
finance, and graph-table code own the ram:91DC workspace; statistics routines
deposit named results in statVars; and app/menu state paths use
appBackUpScreen. [confirmed]
There is consequently no buffer in this table that remains stable while every display, keyboard, VAT, archive, graph, table, statistics, app, error, APD, link, and USB path is allowed. A runtime may borrow a conditional range only by defining the excluded calls and contexts as part of its ABI.
Third-party hook ownership of appBackUpScreen
Archived community source and complete TilEm traces identify persistent
raw-key, parser, and IM2 residents as additional owners of
appBackUpScreen. The runs use OS 2.55MP image SHA-256
dbb47afae091ab36f9abe74e32083013fbeff3d7e0516bbf5d1abf4ee57adc09
and patched TilEm commit d1bdc58dd321ae462a701e556fcb62bb925a78b1.
They establish emulator execution, not physical-hardware behavior. [confirmed]
- NoExec copies a raw-key hook to
appBackUpScreenand a parser hook toappBackUpScreen + 500. It calls_EnRawKeyHookand_EnParserHook, then returns while the hooks remain installed according to its control flow. - Plasma 1.4.1 copies its raw-key hook to
0x9872, installs it with bcall4F66h, and launches its loader while that hook can receive key and ON events. - Remote Control copies a key hook to its
KEYLOCequate atappBackUpScreen, installs it with bcall ID4F66h, and returns. The hook sends bytes through_SendABytewhen TI-OS invokes it. - ONBLOCK fills
0x9900–0x99FFwith an IM2 vector, copies its handler to0x9A9A, selects IM2, and returns while both ranges remain live insideappBackUpScreen. Its handler clears port-0x03bit 0 before calling the TI-OS IM1 entry atram:003A.
Dynamic confirmation. NoExec writes 0x83 at both ram:9872 and
ram:9A66. _SetGetKeyHook at 3B:7D00 then stores pointer
0x9872, page 0x07, and sets bit 5 of IY + 0x34.
_SetParserHook at 3B:7D6E stores pointer 0x9A66, page 0x07, and sets
bit 1 of IY + 0x36. Remote Control reaches the same raw-key installer with
pointer 0x9872, while Plasma stores pointer 0x9872 with its supplied page
byte 0x41. These runs stop before invoking Remote Control’s link sender.
[confirmed]
The ROM installers store each callback as a packed three-byte target:
#pragma pack(push, 1)
typedef struct {
uint16_t callback_addr; /* +0x00, little-endian */
uint8_t page; /* +0x02 */
} OSHookTarget; /* 3 bytes */
#pragma pack(pop)
The same packed record is used by these audited setters: [confirmed]
| Hook | Target record | Active flag | Setter | Clearer |
|---|---|---|---|---|
| raw key | ram:9B84 | bit 5 of IY + 0x34 | _SetGetKeyHook = 0x4F66, body 3B:7D00 | _ClrRawKeyHook = 0x4F6F, body 3B:7B88 |
| token | ram:9BC8 | bit 0 of IY + 0x35 | _SetTokenHook = 0x4F99, body 3B:7D0B | _ClearTokenHook |
| parser | ram:9BAC | bit 1 of IY + 0x36 | _SetParserHook = 0x5026, body 3B:7D6E | _ClearParserHook = 0x5029, body 3B:7C3B |
| silent link | ram:9BD0 | bit 7 of IY + 0x36 | _SetSilentLinkHook = 0x50CE, body 3B:7DBB | _DisableSilentLinkHook |
Each setter stores HL as the callback address, stores A as the page byte,
sets only its active bit, and returns. The clear bodies at 3B:7B88 and
3B:7C3B only reset their respective active bits; they do not wipe the target
records. A controlled trace additionally installs token target
{ ram:9872, page 0 } and silent-link target { ram:9875, page 0 }, observes
their active bits, and restores both records and all affected flag bytes before
halting. The result is in tools/data/community-bcall-semantics.csv.
[confirmed] under TilEm.
_ClrCursorHook = 0x4F69, body 3B:7AEA, is not a silent-link clearer. It
only resets bit 7 of IY + 0x34. The DisLink source first clears the real
silent-link bit, bit 7 of IY + 0x36, itself and then calls 0x4F69 under the
comment “actually uninstall it.” The first instruction disables its silent
hook; the bcall separately disables any cursor hook. A controlled trace seeds
bit 7 of IY + 0x34, calls the bcall, observes that bit clear, and restores the
original flag byte. [confirmed]
After the selected NoExec removal sequence, the resident reaches
_ClrRawKeyHook at 3B:7B88 and _ClearParserHook at 3B:7C3B; the two
active bytes at IY + 0x34 and IY + 0x36 become zero. Remote Control’s
resident removal path was not exercised because its ordinary hook events send
link bytes. [confirmed] for NoExec; [hypothesis] for Remote Control removal.
The ONBLOCK trace writes 0x9A at ram:9900 and ram:9901, copies byte
0x08 to its handler entry at ram:9A9A, and subsequently executes that
entry 1,303 times. The source handler tail calls the TI-OS IM1 entry at
ram:003A. [confirmed]
An assembly runtime must therefore exclude or explicitly remove installed
raw-key, parser, and shell hooks and persistent IM2 residents before borrowing
this buffer. The documented _DisableApd and _DelRes conditions for other
buffers do not establish that appBackUpScreen is unowned.
The pinned include names 0x4F66 _SetGetKeyHook; the Plasma and NoExec
sources instead describe raw-key-hook enablement. The ROM target stores the
supplied pointer and page and sets the active flag, so this page records both
names as aliases without deriving the callback ABI from either name.
Conditional saveSScreen and statVars claims
Public TI-83 Plus documentation permits saveSScreen after _DisableApd, and
permits statVars after _DelRes when statistics code is excluded. _DelRes
invalidates existing statistics results; it does not reserve the block against
later statistics commands or third-party interrupt handlers. [standard]
Direct TI-OS guard
The guarded direct-launch fixture calls _DisableApd and _DelRes, fills all
768 bytes of saveSScreen with 0xA5 and all 531 bytes of statVars with
0x5A, polls _GetCSC, blocks in _GetKey, and receives one injected ON
event. Both complete checks pass, and the fixture displays SAVE STAT 1 1.
This result is limited to TI-OS 2.55MP in one TilEm x4 run. Its trace SHA-256
is 716fe78c274536d2d486d53c2d1b89606c0aafee101c5562202d658250b52508.
Its TLMT Flash page 0x00 hash matches the launch traces, but it also lacks a
complete-ROM sidecar. The recorded capture context and trace content establish
this emulator result; the trace does not independently identify every ROM page.
[confirmed]
Build the TI-BASIC Asm(prgmSCRPROBE) wrapper with
tools/ti84re/tibasic/build_scratch_probe_wrapper.py; then assemble
tools/probes/scratch-guard/scratch_guard_probe.asm and run
tools/macros/scratch-guard-probe.macro. The full trace executes 14,736
instructions in the payload range, including the 767- and 530-byte fill
LDIRs and the complete 768- and 531-byte comparison loops. The fixture halts
after rendering the result so a held ON key cannot enter another _GetKey.
This confirms the documented saveSScreen condition for that direct emulator
scenario. It does not cover an APD timeout, error unwinding, physical hardware,
or statistics code after _DelRes.
Additional third-party saveSScreen owners
Three older community sources deliberately execute from or overlap
saveSScreen. Weird and LCD2 have complete OS 2.55MP emulator traces; GPP
remains a static source finding. [confirmed]
- GPP 1.1 builds IM2 tables at
ram:8600andram:8700, places interrupt code atram:8686andram:8787, and alternates display buffers from the handler. Both code ranges overlapsaveSScreen. - Weird places a signature at
ram:86EC, an ISR atram:8888, and an IM2 table atram:8700. Its handler also readsapdTimerand writes the LCD through ports0x10and0x11. - LCD2 emits a timing routine into
saveSScreen, calls the buffer as code, and uses direct LCD reads to adjust its delay.
The source-built Weird fixture writes its 0xFF signature at ram:86EC,
copies an ISR beginning with 0xF3 to ram:8888, fills its table at
ram:8700 with 0x88, and executes the ISR entry seven times. LCD2 rewrites
the timing entry at ram:86EC during calibration and executes that address
141 times before reaching its result screen. [confirmed]
GPP’s distributed examples require the legacy Ion client environment. No
byte-matched direct-client run is recorded, so execution at ram:8686 and
ram:8787 remains unmeasured. [hypothesis]
These examples are evidence of additional third-party ownership, not evidence that the range is safe on current OS or shell combinations.
Third-party-owned statVars ranges
Common shells and interrupt installers do not share one statVars contract.
The table separates static third-party ownership from the direct dynamic guard.
Identified owned ranges come from the release source or binary. [confirmed]
Untraced runtime cells remain open.
| Context | Result | Evidence and boundary |
|---|---|---|
| Direct TI-OS 2.55MP | All 531 bytes pass | One TilEm x4 guard with _DelRes, statistics excluded, and IM1; no physical run |
| MirageOS 1.2 with tasker disabled | Candidate only | The setup routine returns while tasker flag bit 6 at 0x9689 is clear; no client guard run |
| MirageOS 1.2 with tasker or custom interrupt active | Unsafe | The original binary installs timers, handler code, and an IM2 vector table inside statVars before client execution |
| Doors CS 7.4 | Unsafe as general client storage | Source reserves the block for shell state; its Mirage-compatible interrupt also installs code and vectors there |
| ViewRegs interrupt installed | Unsafe | The release binary dynamically copies code to ram:8790, statVars, and ram:8C01, fills ram:8B00 with 0x8A, and executes ram:8790 656 times in TilEm; no physical run |
| Ion 1.6 | Unresolved | Source review pins no Ion-owned interrupt in this block; no client guard run |
| zStart 1.3.013 | Unresolved | The launcher selects IM1 for the client and pins no explicit shell owner in this block; no client guard run |
MirageOS’s tasker setup at mapped 0x7176–0x71E9 writes these ranges:
| Range | MirageOS owner |
|---|---|
0x8A3A–0x8A3E | three timer counters and two reload values |
0x8A4F–0x8A88 | relocated interrupt code |
0x8A8A–0x8AFE | relocated interrupt dispatcher |
0x8B00–0x8C00 | 257-byte IM2 vector table built by _MemSet = 4C33h |
0x8C01–0x8C1B | relocated timer worker |
The timer worker at mapped 0x7140–0x715A updates the first five bytes. The
launch paths call the setup routine before calling the client at 0x9680 or
0x9D96; they select IM1 only after that call returns at mapped 0x7584 or
0x75A3. A client can therefore run while the MirageOS IM2 owner is active.
[confirmed]
Doors CS source defines pendfile = 0x8A3A and places up to 48 bytes of ALE
vectors after its ten-byte record, occupying 0x8A3A–0x8A73. Its source
also declares the complete 531-byte statVars/anovaf_vars span as internal
storage. The Mirage-compatible mos_setupint routine installs its handler at
0x8A8A, its vector table at 0x8B00–0x8C00, and optional timers at
0x8A3A–0x8A3E. _DelRes does not release these shell-owned objects.
[confirmed]
ViewRegs calls _DelRes, then copies a 603-byte interrupt block to
IntAddress = saveSScreen + 767 - 603. It copies another block to the start of
statVars, builds an IM2 vector table at 0x8B00–0x8C00, and places another
handler block at 0x8C01. Its readme warns that statVars must not be accessed
and statistics must not run while the interrupt is active. The trace observes
first writes at ram:8790, ram:8A3A, ram:8B00, and ram:8C01, followed
by 656 executions of the resident entry. _DelRes therefore does not reserve
either buffer against a subsequently installed third-party interrupt.
[confirmed]
tools/data/scratch-guard-results.csv records the direct trace, the exact
MirageOS and Doors CS owned ranges, and explicit not-run rows for Ion and
zStart. Dynamic guards under all four shells and physical-calculator runs
remain required before the checklist’s common-shell requirement is complete.
The community-program trace identities and exact write observations are in
tools/data/community-runtime-observations.csv.
Page 0x83 during resident execution
Page 0x83 is OS state rather than a spare 16 KiB page. The two resident traces
add the following observations to the boot and expression traces documented in
RAM pages:
| Scenario | Writes | Touched range |
|---|---|---|
| Direct resident launch | 2,304 | 83:5A7E–83:5D7D |
| Interactive resident input | 3,072 | 83:5A7E–83:5D7D |
Guarded _GetKey wait interrupted by ON | 3,893 | 83:4373–83:4390, 83:577E–83:5794, and 83:5A7E–83:5D7D |
The range is the LCD/home-display capture area. Combining these runs with ROM, boot, and expression evidence gives these known owners:
Page-0x83 range | Owner |
|---|---|
83:4000–83:4080 | App base-page staging [standard] |
83:4100–83:433A | USB communication buffers [standard] |
83:4373–83:4390 | Expression-path block copy [confirmed] |
83:43D9–83:44BD | Boot/home block copy [confirmed] |
83:577E–83:5A7D | MathPrint previous-entry history [confirmed] |
83:5A7E–83:5D7D | LCD/home-display capture [confirmed] |
83:5D7E–83:5DF2 | Additional boot/home writes in the measured scenario [confirmed] |
All holes are candidates, not safe ranges. A separate direct-TI-OS trace covers
one division-by-zero dialog without adding a range beyond the boot baseline.
Current coverage still omits USB receive, archive garbage collection,
statistics, the program editor, app transitions, APD timeout, and third-party
interrupts. The _GetKey guard called _DisableApd, so its ON event does not
cover APD. [confirmed]
Selectors 0x82–0x87 alias one physical RAM page on 48 KiB ASICs. Pages
0x84–0x87 still need forced read/write/hash probes on 128 KiB calculators;
an emulator’s unused page is not hardware confirmation. [standard]
Mapping a RAM page through bank A
The page-zero bcall dispatcher at ram:2A2F restores the caller’s port-0x06
selector on ordinary return. During the bcall, however, the dispatcher maps its
own target into 0x4000–0x7FFF, so a pointer into a borrowed bank-A page is
invalid. An OS error can also bypass caller-owned cleanup. [confirmed]
A bounded copy operation should save the selector and interrupt state, map the page, copy, and restore before making another bcall:
LD A,I
PUSH AF ; P/V records IFF2
DI
IN A,(0x0E)
PUSH AF
IN A,(0x06)
PUSH AF
XOR A
OUT (0x0E),A
LD A,0x83
OUT (0x06),A
; Copy only. Do not call a bcall with a pointer into bank A.
POP AF
OUT (0x06),A
POP AF
OUT (0x0E),A
POP AF
JP PO,interrupts_were_disabled
EI
interrupts_were_disabled:
Code running in bank A cannot use this sequence to map out its own instruction
stream. Keep interrupts disabled for the entire nonstandard mapping unless the
interrupt handler is proven independent of normal bank-A ROM. Restore both
ports even though port 0x0E is ignored by TI-84 Plus and TI-84 Plus SE
hardware; doing so keeps the helper transparent and portable to related models.
Sources
| Source | Use here |
|---|---|
OS 2.55MP ROM and tools/ti84re/trace/analyze_scratch.py | ROM ownership and trace write attribution |
tools/data/scratch-ram-observations.csv | launch scenarios, selector assumptions, and write counts |
tools/data/scratch-guard-results.csv | guard trace identity, shell-owned ranges, and evidence limits |
| TI-83 Plus Developer Guide | documented saveSScreen, statVars, _DisableApd, and _DelRes conditions |
| WikiTI RAM pages, revision 11670 | public page-0x83 owners and 0x82–0x87 alias behavior |
MirageOS 1.2 release archive, SHA-256 38dc70173818972de8c5eb78099e8870c7acb9ad4c62d290f6c6f5840c71d43b | tasker setup and client-launch control flow |
Doors CS source at 33af4f5 | shell state, ALE vectors, and Mirage-compatible interrupt ownership |
Ion 1.6 release archive, SHA-256 b5a5ba97f325f8779aa35cda23e38152087930298ff8b7b8573905710230e6e6 | source review for the unresolved Ion row |
zStart 1.3.013 release archive, SHA-256 7a1b7c69c85030b412bb6ea11ae71ac608b9882a9de3ab7dbef1faf69519c5e9 | source review for the unresolved zStart row |
NoExec release archive, SHA-256 dc3ddf2dd4de8a802a2862d6aaf671a4ff5e618eb98377844eb711b90a443a84; member noexec.z80, SHA-256 de323ead58eea7b9590865da2694905b775b8f900c798fa438b4aa9b035d58b5 | static raw-key and parser hook placement in appBackUpScreen |
Plasma 1.4.1 release archive, SHA-256 62965a41fe071902043ebcbbd1254f710d29729bf86a78f20b6f14d6974f5d5a; member Plasma/plasma.asm, SHA-256 b424980285adf3f16225239c3ba3f133a42efb38d0666d968eee4b1fe24b810f | static raw-key hook placement in appBackUpScreen |
Remote Control release archive, SHA-256 9eb1d4bb9beabe0ae31e49756c2a23938c6301a27f3d553a5d3381651262e591; member RemoteC.z80, SHA-256 19eb8c5b8b20a1f9139ac89c8603727f76977ddb9548c8ff318ef5eec07285c4 | static key-hook placement and link-send behavior |
ONBLOCK release archive, SHA-256 40a5139d378608a303691fb34f3edf79ae4968bf39801b75bc311371b66f69d2; member ONBLOCK.asm, SHA-256 3023dc7654db87f8f2ea60f54a4b61beba1ca1252cc3fff975409631384ed750 | static persistent IM2 vector and handler placement in appBackUpScreen |
ViewRegs release archive, SHA-256 84837e779315f799b53f8115e8c4e9563babc5add0541f52d72117d93a68e2b2; member ViewRegs/ViewRegs.z80, SHA-256 120d8a7845a0f0f7a4f3c32f4f53b1e1f1d5efd210af542e64e1408546bba13b; member ViewRegs/readme.txt, SHA-256 a4f66bc84f2f7d17e2dbfa5603fb3b65ed57f311e20dbb7143ad95bde20d2cf7 | static IM2 ownership of saveSScreen and statVars, plus the statistics warning |
GPP 1.1 release archive, SHA-256 08167b71e72cca031782d5154048fdb4b3f2b8a6a088b476936b5abd1f53ed10; member graydev/template/graylib.inc, SHA-256 2891c83665a136c02fd43df5a5db05ef9be229a5f98263539c4596458f211fa8 | static IM2 table and interrupt-code placement overlapping saveSScreen |
Weird source, SHA-256 881cb1b39c41da3e2629e8cc39765f4cc8e337e6e5a2b65d0539df2cb9fd8ca4 | static persistent IM2 ownership inside saveSScreen |
LCD2 release archive, SHA-256 46532d795aadfff782a83ca52001da87ad73cef9e2013c7800291f4b26af94ab; member lcd2.asm, SHA-256 01033202eb0439a7a6dcdb1b28abf62f2c52aeecc630c4688c8603de75b97780 | static timing-code execution from saveSScreen |
Apps, memory reset, and settings
This page traces Flash App launch and resident runtime design, the MEM → Reset paths, and the format and graph flags controlled by the MODE screen. Addresses and confidence flags follow Conventions and methodology.
Cross-references: Boot, contexts, and errors
(contexts, _AppInit, event router), Memory management
(RAM heap, _CleanAll), and Flash page map (Flash page
map). Flag bits use the ti83plus.inc equates; the SystemFlags base is
IY = flags = 0x89F0, so, for example, (IY+0x0A) = flags + fmtFlags.
Flash Apps — find and launch
This ROM ships with zero bundled apps in the local ROM-byte scan (zero 80 0F headers found at page starts) [hypothesis],
but the entire find/launch machinery is present on page 0x3D (_FindApp*) and
page 0x3B (_AppInit glue / app-quit). Apps are TI Flash Applications: a contiguous
run of 16 KiB flash pages whose first page begins with a TLV app header.
App header format (TLV) [confirmed]
An app header is a sequence of type-length-value fields starting at offset 0 of the
app’s first page. Each field begins with two bytes in WikiTI’s TT TS notation: the high
12 bits are the field number. A low size nibble from 0 through C is the
payload length; D, E, and F instead select one, two, or four following
length bytes. The decoder bytes are at init_flash_page_counter+0x08
(3D:7285), but the disassembly does not expose a separate function there:
| size nibble | following length bytes |
|---|---|
0xD | 1 byte |
0xE | 2 bytes |
0xF | 4 bytes |
3D:7285 AND 0x0F
CP 0x0F ; B=4
CP 0x0E ; B=2
CP 0x0D ; B=1
The master field at offset 0 is usually 80 0F … (field 800, size nibble F, followed
by a 4-byte size) — this is what the page-scan keys on to recognise an app. Fields carry
the app name, the page count, flags, the date stamp, and signature-related data.
The public header descriptions match the ROM parser and the local app corpus. Useful
references are WikiTI’s
application-header
and certificate/header format
pages, TI’s
AppHeader guide, and
Tari’s Cemetech disassembly note,
which describes .8xk data as Intel HEX pages based at 0x4000 and app code as starting
after field 807.
Common app-header fields in the sample corpus:
| field | meaning | observed payload |
|---|---|---|
800 | master Flash-variable field | 800F with a four-byte app length at the start of every sampled app |
801 | developer/signing key | 0104, the TI-83+/84+ freeware/shareware app key |
802 | program revision | one-byte revision, usually 1 |
803 | build number | one-byte build number, usually 1; MirageOS uses 2 |
804 | app name | up to 8 bytes; examples include Axe, MirageOS, USBDRV8X, and zStart |
808 | page count | one byte; matches the decoded page count for Axe and CtlgHelp’s two-page apps |
809 | disable TI splash screen | usually zero-length when present; zStart uses a 15-byte app-owned payload |
80C | lowest basecode | usb8x uses 02 1E, decoded as basecode 2.30 |
032 | date stamp | six-byte payload: nested 09 04, then a four-byte count of seconds since 1997-01-01 |
020 | date-stamp signature / unchecked payload | usually 64 bytes; Axe stores executable helper bytes here |
807 | final field | terminates the parsed header; the 807F length bytes are ignored |
The app header is not a fixed 128-byte struct. The 807 final field terminates it. The
common 80 7F 00 00 00 00 form uses size nibble F with a four-byte zero, but WikiTI
documents that length as ignored; the shorter 80 70 form is valid. The app body begins
after the final field and any app-controlled padding. Bytes before the conventional
4080 entry point are not loader magic; they are field payload or padding, and an app can
choose payload bytes that also decode as Z80. [standard]
External sample check (not ROM evidence): the local Axe Parser Axe.8xk sample decodes to
a base page whose 020D date-stamp-signature field starts at 4027 and has a 64-byte
payload. Part of that payload is a Z80 helper at 4037:
ti-kid identified this Axe header case and published an annotated decode in
Hatchet-Compiler;
the local decode below uses that lead and verifies it against the extracted Axe.8xk bytes.
4037 POP AF
4038 POP BC
4039 POP DE
403A POP HL
403B PUSH HL
403C PUSH DE
403D PUSH BC
403E PUSH AF
; ...
4056 LD A,0C9h
4058 CPIR
405A PUSH HL
405B IN A,(6)
405D DEC A
405E LD HL,4065h
4061 RST 20h
4062 JP 8478h
4065 OUT (6),A
4067 RET
RST 20h is _Mov9ToOP1, so the helper copies the thunk at 4065 into OP1
(0x8478) and jumps to OP1. That makes the following thunk run from RAM after
A has been set to the current bank-A page minus one:
OUT (6),A
RET
The preceding CPIR searches
from HL for a RET byte (0xC9) and pushes the byte after it as the return
address. The first half preserves the popped registers while it probes caller-owned
bytes and can return early; the later page switch and RAM-thunk behavior are directly
decoded from the sample bytes.
The same sample’s conventional entry area at 4080 starts:
NOP
JR 408C
JP 4097
JP 4548
tools/ti84re/community/app_headers.py reproduces this pass: --fetch-known downloads a
local corpus from ticalc.org into ignored tools/app-samples/, and --markdown prints
the decoded header table. The corpus keeps the same parser boundary rule:
| app sample | pages field / decoded pages | final field end | entry bytes at 4080 | header-area note |
|---|---|---|---|---|
| Axe | 2 / 2 | 4070 | 00 18 09 C3 97 40 C3 48 | 020 payload contains the 4037 helper; then padding |
| MirageOS | 1 / 1 | 4070 | C3 D3 65 C3 D9 47 C3 D6 | padding to 4080 |
| Omnicalc | 1 / 1 | 4070 | C3 8C 40 C3 E5 79 C3 70 | padding to 4080 |
| CalcSys | 1 / 1 | 4070 | C3 89 40 21 AA 98 CB DE | padding to 4080 |
| Symbolic | 1 / 1 | 4070 | 18 2E 3A 4A 42 4A 4D 4A | padding to 4080 |
| BatLib | 1 / 1 | 4070 | C3 25 61 C3 6E 43 C3 DE | padding to 4080 |
| BatLib-modified Celtic 3 / Grammer / Omnicalc | 1 / 1 | 4070 | app-specific jump/vector bytes | same boundary; nonzero 807F size bytes are ignored |
| zStart 1.3.013 / zStart83 | 1 / 1 | 4080 | 18 11 83 C3 ... | 809D0F carries a 15-byte Z80 helper at 406B |
| CtlgHelp / zChem from zStart | 2 / 2 or 1 / 1 | 4070 | app-specific bytes | padding to 4080 |
| usb8x | 1 / 1 | 4029 | 00 00 00 00 00 00 00 96 | mostly zero padding, plus JP 4180hJP 42EAh at 4049 |
So 4080 is a common app-entry convention, not the OS’s header parser boundary. Some
apps end the parsed header at 4029, 4070, or exactly 4080, and all remain valid
because the 807 final field terminates the header.
The public entry points for walking these fields are bcalls in ti83plus.inc:
_FindAppHeaderSubField (ID 80ABh, body 3F:500A) locates a field in an App
header, and _FindOSHeaderSubField (ID 8075h, body 3F:5018) does the same
for the OS header. Both build on _FindSubField (ID 805Dh, body 3F:4DFB),
_FindGroupedField (ID 8030h, body 3F:4E8C), and _GetFieldSize (ID
805Ah, body 3F:4DB8), which decode the TLV length nibble shown above. These
retail targets are recorded in tools/data/boot-page-comparison.csv.
[confirmed]
_FindApp, _FindAppUp, and _FindAppDn [confirmed]
_FindApp(3D:5EE3) — locate an app by name (OP1). Inits the search page, then loopsapp_find_next_page (5FB1)+ a header-match step until done, returning the app’s start page and a found/not-found flag viaRST 28(bcall) into RAM flash helpers.5EE3 CALL 727D ; init_flash_page_counter -> appSearchPage (0x82A3) 5EE6 CALL 5FB1 ; step to next candidate page (DEC appSearchPage) 5EE9 RET C ; ran off the end -> not found 5EEA CALL 5EB2 ; read/compare this page's header 5EED BIT 3,C JR Z,5EE6 ; not a match -> keep scanningapp_find_next_page(3D:5FB1) —appSearchPage (0x82A3) -= 1; stops at page 7 (low boundary of the app region); bjumpsappSearchPage:0x4000to inspect the header.init_flash_page_counter(3D:727D→model_app_top_pageat3D:726E) — initializesappSearchPageat0x82A3to the model-selected top App page plus one._FindAppUp(3D:5DDA) /_FindAppDn(3D:5DE6) — enumerate the previous / next app in flash (for the APPS-menu list), both wrapping the common walkerapp_5de7(3D:5DE7).app_5de7keeps two counts in BC (apps before/after) and tracks the current name in OP3._FindAppNumPages(ID509Bh) maps to3D:4AA3; the current Ghidra database has no function record at that body address.
State variables: appSearchPage = 0x82A3, 0x8497/0x8481/0x9C87 are search-mode
scratch (0x9C87=‘i’ selects the in-RAM “temp app” search variant).
Launching an app as a context [confirmed]
_AppInit (ram:0936, bcall 0x404B) installs a context from an app header:
_AppInit(byte *hdr): # HL -> 13-byte vector block in the header
copy 12 bytes hdr[0..11] -> cxMain (0x858D) # the 6 context vectors
flags.appFlags (IY+0x0D) = hdr[12] # appFlags byte
cxPage (0x8599) = port_mapBankA # the flash page the handlers run from
The 12 bytes are the 6 little-endian handler pointers (cxMain, cxPPutAway, cxPutAway,
cxRedisp, cxErrorEP, cxSizeWind — see Boot contexts & errors §Context block). Example: the OS’s own
default app vectors live at 3B:7571:
3E 75 | 4B 75 | 9F 74 | 4B 75 | 4B 75 | 4B 75 | 0A
cxMain=753E cxPPutAway=754B cxPutAway=749F cxRedisp=754B cxErrorEP=754B cxSizeWind=754B appFlags=0A
_ReloadAppEntryVecs (3B:73E4, ID 4C36h) calls _AppInit on that block, then
overrides cxErrorEP (0x8595)=0x27D9. After _AppInit, the main event loop runs the app
through call_context_main (pages in cxPage, jumps (cxMain) — Boot contexts & errors).
Because cxCurApp (0x859A) is a key code, pressing a mode key selects the context to
load (Boot contexts & errors). The App quit restore-path candidate at
3B:7412 is not a defined function in the disassembly; the saved-context restore behavior
is a byte-trace note (the label is project-local, not a WikiTI or ti83plus.inc equate).
Flash Apps as resident runtimes
A Flash App can keep its runtime in Flash instead of copying it to userMem.
Some Apps instead use Flash as a launcher for generated RAM code. Neither
design turns ram:9D95–ram:BFFF into private storage; the OS still owns that
range as part of the user heap.
Code in Flash, state in RAM
The current App page executes in bank A at 0x4000–0x7FFF. An App
therefore avoids the temporary copy that the compiled Asm( launcher creates
at ram:9D95. The conventional ram:9D95–ram:BFFF interval contains
8,811 bytes. [confirmed]
That interval remains part of the movable user-data region. Variables, temporary objects, the FPS/OPS gap, and the VAT determine how much space is available. An App must reserve mutable storage through the OS or start from a controlled memory image. It cannot claim 8,811 bytes merely because its code executes from Flash. [standard]
The practical maximum contiguous allocation depends on the calculator model,
OS version, VAT contents, temporary objects, and current heap pointers. Measure
it with _MemChk and a real allocation in each fixture state. Do not use the
address-map span as the capacity result. [confirmed]
Generated native code must also remain in executable physical RAM. Staying
below ram:C000 matches the retail executable window, but the exact physical
RAM-page rules differ among emulators and remain partly unverified on hardware.
See RAM execution protection. A threaded bytecode or
data image does not require executable RAM, although its interpreter still
does. The map is confirmed. [confirmed] The physical hardware boundary remains
untested. [hypothesis]
Community RAM-core launcher
The TruVid App build uses Flash as a wrapper around a RAM playback core. Its
source calls _InsertMem for programEnd - program bytes at ram:9D95, copies
program into that allocation, and calls ram:9D95. After the RAM core
returns, the Flash wrapper exits through _JForceCmdNoChar. App packaging
therefore does not establish that all executable code stays in Flash.
[confirmed] for the identified community source.
The RAM core finds its two-byte settings AppVar through _ChkFindSym. It reads
a RAM payload directly. For an archived payload, it normalizes a wrapped
bank-A pointer and page before calling _LoadCIndPaged and
_LoadDEIndPaged. This is a concrete page-aware archived-data reader, not a
general guarantee that an archived pointer remains valid across memory-moving
calls. [confirmed] for the identified community source.
The AppVar payload has this packed layout:
#pragma pack(push, 1)
typedef struct {
uint8_t contrast; /* +0x00 */
uint8_t delay; /* +0x01 */
} TruVidSettings; /* 2 bytes */
#pragma pack(pop)
The missing-settings path seeds contrast from the OS contrast byte and sets
delay to 178. The save path copies these two bytes from curContrast and
delayValue into the replacement AppVar. [confirmed] for the identified
community source.
The normal CLEAR quit path restores IM 1, the saved stack pointer, mapped page, display state, several hardware ports, and the OS base-page table. When the settings are dirty, it deletes any old AppVar, creates a two-byte replacement, and archives it before returning to the Flash wrapper. No error frame appears in this source, so it does not establish cleanup after a reset or an unhandled OS error. [confirmed] for the static source path; reset and error cleanup remain [hypothesis].
A source-derived App fixture appends one silent same-page frame to the pinned
release source. Its complete TilEm trace reaches _InsertMem at ram:0F81,
the copied core at ram:9D95, the missing-settings branch at ram:9DCF, and
the normal quit entry at ram:9F5F, each once. The trace SHA-256 is
c2481399d804a3b5232e9dde99c80fb77a28afc54eaf57ad0f84ed5e89d43b56;
the generated App SHA-256 is
11250fda7ce6892c79bcbe44958bd16f77f1faead58ebf99eaf6fa04712f6ab1.
The run uses OS image SHA-256
dbb47afae091ab36f9abe74e32083013fbeff3d7e0516bbf5d1abf4ee57adc09
and patched TilEm commit d1bdc58dd321ae462a701e556fcb62bb925a78b1.
[confirmed] for this emulator fixture.
Build the App with tools/probes/community/truvid/truvid_probe.asm and run
tools/macros/community-truvid-cleanup.macro. The wrapper resumes once at
page_29:4095 after the RAM core returns. The trace does not exercise an
existing archived settings AppVar, the media page-wrap branch, an error, a
reset, or physical hardware. Those paths remain [hypothesis].
Cross-page code and data
A multi-page App cannot treat bank-A addresses as flat pointers. Mapping
another App or OS page changes the bytes visible at the same logical address.
Any pointer into 0x4000–0x7FFF is valid only while its page remains
mapped. [confirmed]
RPN83P provides a concrete multi-page design. Its page-0 branch table stores entries in this form:
.dw target_routine
.db relative_app_page
Other App pages call those entries through an App-aware bcall() macro. App
page 0 contains the branch table and event handlers, while larger modules live
on later App pages. The identified RPN83P source at commit
e2ad0bff98c94a13f34ae461b13f79384a75c17f confirms this layout. [confirmed]
An OS bcall or App-page call may remap bank A. This affects Flash data as well
as code. RPN83P does not pass a Flash string to _PutS. Its putS helper
reads each byte on the current App page and passes the byte to _PutC; its
source comments identify _PutS and _VPutS as RAM-string routines. [confirmed]
Reacquire or remap the App page before an untested API dereferences a Flash
pointer after a page-changing call. That broader API rule remains a
[hypothesis].
Context cleanup
A resident App should make cleanup an explicit part of this context:
- Save each OS setting that the App changes.
- Open or validate persistent AppVars.
- Call
_AppInitwith acxPutAwayhandler that reaches the normal cleanup routine. - Install an error frame around each command dispatched by the App.
- On explicit quit,
PutAway, or a handled error, close mutable variables, persist state, restore settings, and restore the default context. - Call
_ReloadAppEntryVecs, then return through_JForceCmdNoCharor_PutAwayas appropriate.
RPN83P routes both explicit exit and its cxPutAway handler through
mainExit, which closes its AppVars, saves state, and restores OS settings.
Its identified source does not install AppOnErr, so this example does not
cover a TI-OS error that unwinds past the App. A reset or nonlocal jump can also
bypass the App’s cleanup. [confirmed]
Keep persistent state relocatable
RPN83P stores mutable state in four RAM AppVars and uses 1,044–2,545 bytes, depending on its register count. Its structured variables carry a size, CRC16, App ID, variable type, and schema version. Startup rejects stale, truncated, or corrupt structures and initializes the affected state again. These sizes and checks come from the identified RPN83P source and README. [confirmed]
Its RPN83SAV update is not atomic: StoreAppState deletes the old variable
before _CreateAppVar creates the replacement. Validation detects corruption
on the next launch, but an interruption can lose the last valid state.
[confirmed]
A runtime that needs the last committed dictionary should use two named slots. This design remains a fixture target: [hypothesis]
- Store a magic value, format version, generation, payload length, CRC, payload, and final commit marker in each slot.
- Write and validate the inactive slot before archiving it.
- Reacquire the archived slot with
_ChkFindSym, read it with page-aware access, then validate the copied record again. - Delete the old slot only after the new archived slot is proven valid.
- At startup, validate both RAM and archived candidates and select the highest valid generation.
- Test resets at every write, archive, garbage-collection, and delete boundary.
AppVar data can move during allocation, deletion, archive, unarchive, or garbage collection. Retain names and offsets, not long-lived absolute payload pointers. Reacquire a RAM payload after memory-moving calls and an archived payload after any operation that can collect the archive. See Resident assembly programs for the detailed handle protocol. [confirmed]
Minimal developer-key fixture
tools/probes/flash-apps/minimal_flash_app.asm builds a one-page App named REPROBE.
It immediately exits through
_JForceCmdNoChar; it is a packaging and launch fixture, not yet a RAM-budget
probe.
The source uses SPASM-ng’s app.inc and ti83plus.inc:
spasm -N -I path/to/spasm-ng/inc \
tools/probes/flash-apps/minimal_flash_app.asm /tmp/minimal-flash-app.8xk
Use a SPASM-ng build with GMP and OpenSSL App signing enabled. A build compiled
with NO_APPSIGN=1 can emit raw App bytes, but its .8xk wrapper is not a
usable signed developer-key transfer file.
The reference build used SPASM-ng commit
5f0786d38f064835be674d4b7df42969967bb73c. It produces 668 bytes with SHA-256
4dcbd992e71734b2255db34321d6980f5a908f8c0641ad4b79408e68e8334981.
The repository header decoder reports:
name REPROBE; pages 1/1; final field ends at 4070
entry bytes at 4080: CD 50 00 27 40 02 2D 40
The header field 801 contains key ID 0104, the TI-83+/84+ freeware and
shareware developer key. This fixture has been assembled and decoded. It has
not been transferred to physical hardware. The build and decoded header are
confirmed. [confirmed] Physical launch remains untested. [hypothesis]
Source provenance
| Artifact | Exact identity | Source |
|---|---|---|
| RPN83P source | Commit e2ad0bff98c94a13f34ae461b13f79384a75c17f | RPN83P commit |
| TruVid release archive | Archive SHA-256 ea61474625bc56ef1397fd67f978e29e8bd026ffd4ffc9c2f17c3bdc17f25ca9; member TruVid/source/truVid.z80, SHA-256 2a9a042177197583dae5af51367cfe906e2d7e84f0d15d1e5859a5dd20ee7953 | truvid.zip |
| SPASM-ng used for the reference build | Commit 5f0786d38f064835be674d4b7df42969967bb73c | SPASM-ng commit |
Remaining measurements
The fixture still needs these extensions:
- reserve generated-code RAM through a named AppVar;
- record
_MemChk, heap pointers, VAT endpoints, andSPbefore and after the allocation; - compare one-page and multi-page App builds on clean and representative VAT states;
- force an error,
PutAway, reset, and archive garbage collection during persistence updates; - repeat the executable-RAM boundary on identified 48 KiB and 128 KiB physical calculators.
Until those runs exist, report Flash residency as removal of the assembly copy,
not as proof that the complete ram:9D95–ram:BFFF range is available.
RAM clearing and memory reset
The MEM menu ([2nd][+], “MEMORY MANAGEMENT/DELETE” + “RESET”) and its messages are on
page 0x01 (text/homescreen page). The reset engine is on page 0x35; the user-RAM
re-init lands in page-0 boot code.
User-facing strings on page 01 [confirmed]
| Addr | String |
|---|---|
01:4076 | Defragmenting... |
01:4098 | Arc Vars Cleared |
01:40A9 | Apps Cleared |
01:40B8 | Arc Vars & Apps Cleared |
01:4109 | Resetting All... |
01:4126+412E | Garbage + Collecting... |
01:4234 | Resetting... |
01:7425–01:746E | menu titles: RESET MEMORY, RESET DEFAULTS, RESET ARC VARS, RESET ARC APPS, RESET ARC BOTH, RESET RAM |
01:747E | the long “Resetting ALL / RAM / Vars / Apps / Both …” warning help text |
Reset dispatcher (mem_reset_dispatch at 35:7180) [confirmed]
Dispatch is on the selected reset item held in keyExtend (0x8446):
keyExtend | action | message shown |
|---|---|---|
| 1 | reset archived vars | Arc Vars Cleared (path 720B) |
| 2 | reset archived apps | Apps Cleared (path 7267) |
| 3 | reset both arc vars+apps | Arc Vars & Apps Cleared (path 7275) |
| 4 | reset all (RAM+archive) | Resetting All... (path 71F0) |
| else (0) | RAM reset (“RAM Cleared”) | wipe + re-init (path 719F) |
What RAM reset clears [confirmed]
The RAM-reset path (35:719F):
719F BIT 1,(IY+0x35)
JP Z,0x0B2F ; first-stage vs full path select
71A6 LD HL,(0x9B73) ; preserve a saved word
71B4 LD A,(IY+0x3F)
AND 0x7F ; keep low 7 bits (clear bit 7) of flag byte 0x3F
71B9 DI
71BA LD HL,0x8000
LD DE,0x8001
LD BC,0x1BC3
LD (HL),0
LDIR ; *** zero system RAM 0x8000-0x9BC3 ***
71C7 LD (IY+0x3F),A ; restore the saved low 7 bits
... (restore IY+0x34 bit6, IY+0x35 bit0 from the preserved state)
71E0 LD HL,0x9BD0
LD DE,0x9BD1
LD BC,0x642F
LD (HL),0
LDIR ; *** zero user RAM 0x9BD0-0xFFFF ***
71ED JP 0x0BD9 ; re-init RAM (page-0 boot init)
So a RAM reset clears two blocks to 0:
- System RAM: the half-open interval
[appData, 0x9BC4), corresponding to0x8000–0x9BC3. - User RAM:
[restartClr, 0x10000), corresponding to0x9BD0–0xFFFF(0x6430bytes).
The first interval contains OS scratch, the context block, and system buffers. The second contains the VAT and user variables and programs. [confirmed]
A small amount of state survives the wipe. The path restores bits 0–6 of
IY+0x3F and clears bit 7. It conditionally restores IY+0x34 bit 6 and
IY+0x35 bit 0, sets IY+0x35 bit 1, and restores localLanguage at
0x9B73. It then JP 0x0BD9, the RAM-init entry (OUT (0) page select, LD SP,0xFFF7,
then CALL 0x3EC1 — the cross-page trampoline that rebuilds the VAT, system vars, and LCD; see Boot contexts & errors), which rebuilds a
clean default VAT and system state and re-enters the homescreen. The Flash archive is not
touched by a plain RAM reset.
Full reset (ram:0B27) [confirmed]
The harder reset (RESET ALL / power-on cold start) is at ram:0B27:
0B27 LD SP,0
...
0B37 DI
OUT (0),0xC0
0B41 LD HL,0x8000
LD DE,0x8001
LD BC,0x7FFF
LD (HL),0
LDIR ; zero ALL of 0x8000-0xFFFF (32 KiB)
0B4E ... preserve/inspect IY+0x3F
... select sub-path
JP 0x3EA9/0x3EAF
This zeroes the entire 32 KiB RAM and does the deepest re-init.
_CleanAll and cleanup_temp_ram (07:52CF) [confirmed]
Distinct from the MEM reset. _CleanAll (bcall 0x4A50) only compacts temporary RAM
after a command finishes: it shifts the FP stack (fpBase/FPS) down to tempMem, resets
the OPBase/OPS/pTemp scratch pointers, and clears pTempCnt/cleanTmp. It does not
clear the VAT, user vars, or Flash (see Memory management). _FixTempCnt (07:4FEC) marks temps
≥ a count reclaimable then tail-calls the same compaction.
Flash archive garbage collection [confirmed]
Separate from RAM reset: gc_show_screen at 3C:7E0D displays Garbage Collecting..., while the
related entry at 3C:7E23 displays Defragmenting.... archive_gc_collect at 3C:7733 rewrites
live archive records in 64 KiB sector units and journals its phase in the inactive 8 KiB half of
page 3E. It clears 0x844B (curRow) before drawing the banner and runs with interrupts disabled.
The erase and program workers execute from RAM through Flash-control port 0x14; see
Variables, archive and unarchive. [confirmed]
MODE settings flags
The flag bytes live in the SystemFlags area at IY = 0x89F0. The MODE screen (cxMode = kMode = 0x45) is a menu context that flips these bits; the canonical setters below show
exactly which bits.
Angle mode in trigFlags (IY+0) [confirmed]
trigDeg = bit 2 of trigFlags (0x89F0): 1 = Degrees, 0 = Radians. (Confirmed against WikiTI Flags:00 and the ROM — _Sin (02:7342) tests BIT 2,(IY+0) to pick the degree path.)
SET 2,(IY+0) ; FD CB 00 D6 -> Degree
RES 2,(IY+0) ; FD CB 00 96 -> Radian
BIT 2,(IY+0) ; FD CB 00 56 -> tested by _Sin/_Cos/_Tan to select degree vs radian
Math routines branch on this bit to choose degree/radian variants (_SinCosRad etc. force
radians; the degree paths convert first).
Graph type in grfModeFlags (IY+0x02) [confirmed]
The four graph-mode setters on page 0x36 are mutually exclusive: each first clears
all four bits via clr_grfmode (36:7D00), then ORs in its own bit, then calls
_SetTblGraphDraw. param_1 is IY, so *(param_1+2) = grfModeFlags.
clr_grfmode (36:7D00): grfModeFlags &= 0xEF & 0xDF & 0xBF & 0x7F # clear bits 4,5,6,7
| bcall | addr | bit set | flag (inc) |
|---|---|---|---|
_SetFuncM | 36:7D11 | bit 4 (|0x10) | grfFuncM (Function) |
_SetPolM | 36:7D2C | bit 5 (|0x20) | grfPolarM (Polar) |
_SetParM | 36:7D39 | bit 6 (|0x40) | grfParamM (Parametric) |
_SetSeqM | 36:7D1F | bit 7 (|0x80) | grfRecurM (Sequence/Recursion) |
Each setter first calls a small predicate (36:0013/0254/0259/025E) and only re-sets
the mode if the parity/condition flag (F bit6) requires it, avoiding needless redraws.
Other grfModeFlags bits (from inc, not in the setters above): bit3 grfPolar
(rect↔polar coordinate readout). Related graph bytes: grfDBFlags (IY+0x04) bit0
grfDot (line/dot), bit1 grfSimul (sequential/simultaneous), bit4 grfNoCoord,
bit5 grfNoAxis; seqFlags (IY+0x0F).
Numeric format in fmtFlags (IY+0x0A) [confirmed]
fmtFlags byte at 0x89FA:
| bit | name | meaning |
|---|---|---|
| 0 | fmtExponent | 1 = show exponent (Sci/Eng), 0 = Normal |
| 1 | fmtEng | 1 = Engineering, 0 = Scientific (when exponent on) |
| 2-4 | fmtBaseMask (fmtHex/fmtOct/fmtBin) | integer base (Dec/Hex/Oct/Bin) |
| 5 | fmtReal | real display mode |
| 6 | fmtRect | rectangular complex display (a+bi) |
| 7 | fmtPolar | polar complex display (re^θi) |
So Normal/Sci/Eng = (bit0, bit1): Normal = 00, Sci = 01, Eng = 11.
fmtOverride (IY+0x0B, 0x89FB) is a working copy used during conversions.
Float vs Fix N is not in fmtFlags — it is the separate byte fmtDigits =
0x97B0: value 0x00-0x09 = Fix-N decimal places, 0xFF = Float.
MODE screen plumbing
The MODE screen is a menu context (cxMode/kMode=0x45) reached via the event/key router
(Boot contexts & errors). Its row strings live as token names on page 0x01 (RadianN/DegreeO/NormalP/
Float at 01:49E4–01:4A06; trailing letters are token-id bytes) and full-caps menu
labels on page 0x37 (DEGREE 4A85, RADIAN 4A8C). The setters and inc
equates confirm the target bits and bytes. [confirmed] The per-row path through
the menu dispatcher to the corresponding SET/RES or fmtDigits store has
not been traced line by line. [hypothesis]
Routine index
3D:5EE3 _FindApp
3D:5DDA _FindAppUp
3D:5DE6 _FindAppDn
3D:5DE7 app_5de7
3D:5FB1 app_find_next_page
3D:727D init_flash_page_counter
3D:7285 init_flash_page_counter+0x08 ; TLV-length decode block, not a function
3D:4AA3 _FindAppNumPages bcall target; no live function in current DB
ram:0936 _AppInit
ram:08AF _PutAway
3B:73E4 _ReloadAppEntryVecs
3B:7571 default app vectors data block (12 bytes + appFlags), not a function
3B:7412 app-quit restore candidate (inferred label); no defined function in live DB
35:7180 mem_reset_dispatch
35:719F ram_reset_wipe (zeroes [appData,9BC4) and [restartClr,10000))
ram:0BD9 ram_init_after_reset
ram:0B27 full_reset_wipe (zeroes all 0x8000-0xFFFF)
3C:71F8 gc_command
3C:7733 archive_gc_collect
3C:7E0D gc_show_screen
07:52CF _CleanAll (cleanup_temp_ram)
07:4FEC _FixTempCnt
36:7D11 _SetFuncM (grfModeFlags bit4)
36:7D1F _SetSeqM (grfModeFlags bit7)
36:7D2C _SetPolM (grfModeFlags bit5)
36:7D39 _SetParM (grfModeFlags bit6)
36:7D00 clr_grfmode (clears grfModeFlags bits 4-7)
Key SystemFlags and RAM addresses
0x89F0 flags (IY base)
+0x00 trigFlags (bit2 trigDeg: 1=Degree,0=Radian)
+0x02 grfModeFlags(bit4 Func,bit5 Polar,bit6 Param,bit7 Seq; bit3 grfPolar)
+0x04 grfDBFlags (bit0 Dot, bit1 Simul, bit4 NoCoord, bit5 NoAxis)
+0x0A fmtFlags (bit0 Exponent, bit1 Eng, bit2-4 base, bit5 Real, bit6 Rect, bit7 Polar)
+0x0B fmtOverride
+0x0D appFlags
0x97B0 fmtDigits (0-9 = Fix N, 0xFF = Float)
0x82A3 appSearchPage
0x8446 keyExtend (reset-submenu selector 1..4; extended-key state)
0x858D cxMain ... 0x8599 cxPage 0x859A cxCurApp (Context block, see Boot contexts & errors)
Flash memory
TI-84 Plus OS 2.55MP — Flash hardware, boot bcalls, and archive writes.
The TI-84 Plus programs Flash through three distinct layers: ASIC access control, an AMD-compatible command state machine in the Flash chip, and boot-page bcalls that execute their write loops from RAM. This page separates those layers, gives calling conventions and examples for the Flash bcalls, reconstructs their workers byte for byte, and follows a normal Archive prgmA operation into the hardware path.
Evidence layers
The mechanisms below use several evidence sources. A claim marked [confirmed] comes from the local OS 2.55MP image or a complete TilEm execution trace. A claim marked [standard] comes from the named hardware source and agrees with the ROM. Emulator behavior is identified explicitly. It establishes what that emulator implements, not what the physical ASIC or Flash chip does.
| Layer | Main evidence | What it establishes |
|---|---|---|
| TI-OS and boot code | tools/rom.bin, especially 3D:61AF–3D:6BC4 and 3F:4784–3F:4E56 | bcall ABI, guards, RAM workers, archive allocation, and status handling [confirmed] |
| Dynamic execution | archive and GCFLASH TilEm traces plus guarded TilEm, Wabbitemu, and MAME runs | ROM worker paths, GC sector ordering, execution limits, and native command-state behavior [confirmed] for the pinned emulator runs |
| ASIC model | TilEm x4_memory.c, x4_io.c, and x4_init.c | protected-byte recognizer, port gates, execution limits, and modeled sector protection [standard] |
| Flash device | Datamath’s March 2004 board photograph and Fujitsu MBM29LV800TA data sheet | observed package marking, sector geometry, command cycles, DQ status semantics, and rated limits [standard] |
| Emulator comparison | pinned TilEm, Wabbitemu, MAME, and jsTIfied source | modeled command decode, mutation rules, status reads, timing, and missing ASIC gates [standard] |
Write-layer schematic. Bcall entry guards, remaining caller obligations, and RAM-worker execution are [confirmed]. ASIC gate details and the AMD-compatible command state machine are [standard].
Physical organization
Identified board part and compatible family
Datamath’s photographed March 2004 TI-84 Plus board carries a Fujitsu package
marked 29LV800TA-70PFTN. Fujitsu’s orderable part number adds its MBM
prefix: MBM29LV800TA-70PFTN. This identifies the device on that photographed
board. It does not establish one vendor for every TI-84 Plus revision.
[standard]
Datamath’s NOR component index also lists AMIC A29L800A, Fujitsu 29LV800,
Spansion S29AL008D, and Macronix MX29LV800 as compatible 1 MiB families.
Those entries establish a reported compatible family, not which part a
particular calculator contains. [standard]
The Fujitsu suffixes and rated limits decode as follows. These are data-sheet limits rather than measurements of a calculator. [standard]
| Marking or field | Meaning |
|---|---|
8M (1M × 8/512K × 16) | 8 Mbit array, used here as one MiB of byte-addressable NOR Flash |
TA | top-boot sector geometry |
-70 | 70 ns maximum read access |
PFTN | 48-pin TSOP(I), normal-bend package |
| supply | 3.0 V-only read, program, and erase |
| program/erase endurance | minimum 100,000 cycles |
| byte program | 8 µs typical, 300 µs maximum |
| sector erase | 1 s typical, 10 s maximum |
The local ROM image and TilEm’s TI-84 Plus model use 64 logical pages of
16 KiB. A logical Flash page is an ASIC paging unit, not an erase unit. Port
0x06 maps one page into the Z80’s 0x4000–0x7FFF bank-A window. The Flash
device erases the larger physical sector containing the command address.
[confirmed] for the ROM page count; [standard] for the device organization.
Data-sheet command and status interface
In byte mode, the Fujitsu device decodes unlock addresses 0xAAA and 0x555.
The command table defines the following operations. Address and data cycles
after a command prefix are shown separately. [standard]
| Operation | Byte-mode command cycles |
|---|---|
| Read/reset | F0, or AA 55 F0 |
| Autoselect | AA 55 90 |
| Byte program | AA 55 A0, then destination and data |
| Chip erase | AA 55 80 AA 55 10 |
| Sector erase | AA 55 80 AA 55 30 |
| Erase suspend | B0 at any address during sector erase or its timeout window |
| Erase resume | 30 at any address while erase is suspended |
| Enter fast mode | AA 55 20 |
| Fast program | A0, then destination and data; repeat in fast mode |
| Exit fast mode | 90, then F0 or 00 |
The data sheet defines no CFI query command for this part. A reset returns the device to array-read mode, including after DQ5 reports an exceeded timing limit. [standard]
The status outputs distinguish more states than the boot workers consume: [standard]
| Bit | Fujitsu data-sheet behavior |
|---|---|
| DQ7 | complements programmed data bit 7 while program is active; reads 0 during erase and the array value after completion |
| DQ6 | toggles during program, erase, and the sector-erase timeout window |
| DQ5 | indicates exceeded program/erase timing; it can also follow an attempt to program a nonblank location without erasing |
| DQ3 | distinguishes the open sector-erase command window from the active erase algorithm |
| DQ2 | toggles for an erasing or erase-suspended sector and helps distinguish erase states from program states |
Erase suspend applies only to sector erase, including its 50 µs timeout window. The device ignores it during chip erase and byte program. The data sheet bounds suspend latency at 20 µs. DQ7 becomes one and DQ6 stops toggling; DQ2 continues toggling when the suspended sector is read. Reads and programs remain available in sectors that are not being erased. [standard]
Fujitsu autoselect returns manufacturer 0x04 and top-boot byte-mode device
0xDA at byte-mode offsets XX00 and XX02. Offset XX04 reports the
selected sector’s protection state in DQ0. Wabbitemu and MAME instead return
manufacturer 0x01 with the same device code. Their values identify an
AMD-compatible emulator model, not the Fujitsu package in the Datamath
photograph. [standard]
Retail ROM command coverage
The retail ROM’s instruction-aligned direct stores to logical unlock addresses
0x6AAA and 0x5555 occur at 11 locations. They belong to three
length-prefixed command bodies. [confirmed]
| Command body | Direct unlock-address stores | Command use |
|---|---|---|
Page-3D program worker at 3D:730A | 3D:7342, 3D:734B, 3D:7354 | AA 55 A0, then program data through LDI |
| Boot erase worker | 3F:4C48, 3F:4C51, 3F:4C5A, 3F:4C63, 3F:4C6C | AA 55 80 AA 55 30 |
| Boot program worker | 3F:4CFB, 3F:4D04, 3F:4D0D | AA 55 A0, then program data through LDI |
No direct unlock-address candidate has a nearby command-valued LD A,n for
chip erase (0x10), fast-mode entry (0x20), autoselect (0x90), erase
suspend (0xB0), or CFI query (0x98). The worker bodies use byte program,
sector erase, and array reset. [confirmed]
tools/ti84re/flash/rom_commands.py performs the structural match, and
tools/ti84re/flash/analyze_rom_commands.py emits text or JSON. The scan deliberately
does not treat raw literals as commands. Linear disassembly can decode data as
instructions, indirect stores can hide a destination, and a standalone command
can target an address other than the two unlock addresses. The result therefore
establishes coverage of exact LD (nn),A candidates, not universal absence of
every dynamically constructed command. [confirmed]
Sector geometry
The Fujitsu MBM29LV800TA data sheet defines the top-boot geometry below.
TilEm, Wabbitemu, MAME, and jsTIfied use the same boundaries. [standard]
| Physical range | Size | Logical pages or page portion |
|---|---|---|
0x000000–0x0EFFFF | 15 × 64 KiB | pages 00–3B, four pages per sector |
0x0F0000–0x0F7FFF | 32 KiB | pages 3C–3D |
0x0F8000–0x0F9FFF | 8 KiB | 3E:4000–3E:5FFF |
0x0FA000–0x0FBFFF | 8 KiB | 3E:6000–3E:7FFF |
0x0FC000–0x0FFFFF | 16 KiB | page 3F |
The two halves of logical page 3E are separate 8 KiB sectors. This is why _EraseCertificateSector accepts logical address 0x4000 or 0x6000. Page 3F is one 16 KiB boot sector. A sector erase directed anywhere in an ordinary archive page erases all four 16 KiB pages in its 64 KiB sector. [confirmed] for the certificate API; [standard] for chip geometry.
Three independent protection mechanisms
“Flash protection” can refer to three different controls. Treating them as one switch obscures several ROM checks.
Flash command lock — port 0x14
Port 0x14 controls whether writes reach the Flash command state machine. Writing 1 unlocks Flash command writes; writing 0 locks them. The write is accepted only after the ASIC observes this byte sequence fetched from a privileged Flash region: [standard]
00 00 ED 56 F3 D3
The usual instruction spelling is:
nop
nop
im 1
di
out (0x14),a
The ASIC recognizes fetched bytes rather than the semantic instruction stream. WikiTI documents alternate instruction sequences that produce the same bytes. TilEm’s TI-84 Plus model advances its recognizer only when the bytes come from physical 0xB0000–0xBFFFF or 0xF0000–0xFFFFF; other Flash or RAM reads reset the recognizer. It accepts the following port-0x14 output only in recognizer state 7. [standard]
Unlocking port 0x14 does not program a byte. It allows subsequent memory writes to reach the Flash chip, where they must still form a valid AMD command sequence. [standard]
The public write and erase bcalls expect Flash to be unlocked by their caller. The archive record writer at 3D:64AA performs the protected port-0x14 sequence itself before calling those APIs. [confirmed]
Physical sector protection
TilEm assigns protection group 1 to physical 0xB0000–0xBFFFF and 0xFC000–0xFFFFF. Port 0x21 bits 0–1 select the modeled override group while Flash is unlocked. A command can therefore pass the port-0x14 lock and still be rejected for a protected physical sector. [standard]
The retail boot programs port 0x21 = 0 at 3F:41DC. Its low field also
selects model-specific Flash page bounds, while bits 4–5 configure the RAM
execution mask. See ASIC status, identity, protection, and GPIO
for the ROM uses, emulator equations, and public size tables. [confirmed] for
the boot write; [standard] for the modeled protection behavior.
This protection is separate from the safe bcall checks. For example, _WriteAByte permits starting page 3E at the software layer, while the hardware still controls whether the affected sector is writable. [confirmed] for the bcall; [standard] for the ASIC model.
Read and execution protection
The certificate page is read-censored while Flash is locked. WikiTI documents the model-selected page as 1E, 3E, or 7E; TilEm returns 0xFF for locked reads of page 3E on its TI-84 Plus model. [standard]
Ports 0x22 and 0x23 define a forbidden Flash-execution interval. TilEm
includes both endpoints, while Wabbitemu allows the lower page. The retail boot
writes 0x08 and 0x29. Ports 0x25 and 0x26 bound executable RAM in 1 KiB
units. Both emulators accept writes to these protected ports only while Flash
is unlocked. See Execution protection for the ROM
sequence, exact equations, guarded Flash and RAM execution runs, and unresolved
physical boundaries. [confirmed] for the boot values and pinned emulator runs;
[standard] for the source models.
These execution limits explain why the byte-poke loops run at ramCode (0x8100). They are distinct from the Flash chip’s inability to provide ordinary array data while a program or erase operation is active. [confirmed] for the RAM workers; [standard] for the execution controls.
Boot-page Flash API
The retail boot bcall table maps the Flash APIs below. The bcall ID is the word after rst 28h; the body address is where the resolved code executes. [confirmed]
| Bcall | ID | Body | Inputs | Intended distinction |
|---|---|---|---|---|
_WriteAByte | 8021 | 3F:4C9F | A page, DE destination, B byte | one byte; permits page 3E, rejects page 3F |
_EraseFlash | 8024 | 3F:4C2A | A page, HL address in the sector | raw sector selector; no page guard |
_EraseCertificateSector | 8060 | 3F:4E3F | H=0x40 or H=0x60; L unchecked | select one 8 KiB certificate sector; hides erase result |
_EraseFlashPage | 8084 | 3F:4C1E | A page | use 0x4000 in that page; rejects page 3E |
_WriteFlashUnsafe | 8087 | 3F:4CA6 | A page, DE destination, BC length, HL RAM source | block write; permits page 3E, rejects page 3F |
_WriteAByteSafe | 80C6 | 3F:4C9A | A page, DE destination, B byte | one byte; rejects pages 3E and 3F |
_WriteFlash | 80C9 | 3F:4C8F | A page, DE destination, BC length, HL RAM source | block write; rejects pages 3E and 3F |
_SetFlashLowerBound | 80CF | 3F:4784 | A value for port 0x23 | change an execution-protection bound; leaves interrupts disabled |
WikiTI’s ABI agrees with these register uses and says the block-write source must be RAM. The ROM adds exact page guards, call-site checks, return values, and boundary behavior described below. [standard] for the published ABI; [confirmed] for the additions.
Programmer-facing bcall guide
The bcall-level guide — entry contracts, register conventions, worker
selection, and executable examples for _WriteFlash, _WriteAByte,
_EraseFlash, and their neighbors — is on Flash bcall programming
guide.
_WriteFlash entry paths
The four write entry points converge on the core at 3F:4CA6. [confirmed]
flowchart TD
WF["_WriteFlash · 3F:4C8F<br/>mask page; reject 3E"] --> U["_WriteFlashUnsafe · 3F:4CA6"]
WS["_WriteAByteSafe · 3F:4C9A<br/>mask page; reject 3E"] --> W1["_WriteAByte · 3F:4C9F<br/>copy B to OP1; BC=1"]
W1 --> U
U --> G["direct-call and page-3F guards"]
G --> R["copy worker to 0x8100 and execute"]
Safe and unsafe page guards
_WriteFlash masks A with 0x3F and returns immediately for page 3E. _WriteAByteSafe does the same before falling into _WriteAByte. The unsafe core masks the page again and returns for page 3F. Safe writes therefore reject both pages 3E and 3F. [confirmed]
_WriteAByte enters the unsafe core without the page-3E test. It stores B in OP1 at 0x8478, replaces HL with that address, and sets BC=1. It permits page 3E but still inherits the page-3F rejection. [confirmed]
The page guards return the result of an equality comparison. Rejected page
3E and page 3F calls therefore return Z, the same condition as a successful
worker. Callers must obey the page contract; Z alone does not prove that a
write occurred. [confirmed]
Direct-call-site check
Both _WriteFlashUnsafe and _EraseFlash inspect the immediate stacked return address:
ex (sp),hl
bit 7,h
ex (sp),hl
ret nz
The routine returns NZ when that address is at least 0x8000. It does so
before masking A. A normal bcall passes because the bcall dispatcher
interposes a low-memory return frame; the archive trace reaches 3F:4CA6 with
the relevant return address at 0x2B41. This is a direct-call-site check. It
does not prevent a RAM program from invoking the public bcall through
rst 28h. [confirmed]
Zero-length write
After the guards, _WriteFlashUnsafe saves AF, tests B|C, and restores
AF when the length is zero. A zero-length call therefore returns the masked
page in A and the flags from the preceding CP 0x3F. An accepted page is not
equal to 0x3F, so this no-op returns NZ. It never copies or executes the RAM
worker. [confirmed]
Early-return trace
The read-only entry-returns fixture runs on the unmodified ROM. It verifies
the first eight bytes at 3F:4CA6, never writes port 0x14, and exercises
four paths that return before worker launch. ti84re.flash.analyze_trace reports
zero CPU write attempts targeting mapped Flash. The captured bcall-visible
values are: [confirmed] for
TilEm execution of the ROM paths.
| Clock | Call and trigger | Return AF | Condition |
|---|---|---|---|
| 186,993,567 | _WriteFlash, input page 0x7E → masked page 3E | 0x3E42 | Z |
| 186,995,033 | _WriteFlashUnsafe, input page 0x7F → masked page 3F | 0x3F42 | Z |
| 186,996,552 | _WriteFlashUnsafe, input page 0x7D, BC=0 | 0x3DBB | NZ |
| 186,996,732 | direct CALL 3F:4CA6 from RAM, input A=0xA5 | 0xA591 | NZ |
The direct call reaches 3F:4CA6 and returns from 3F:4CAA; it does not reach
the page mask at 3F:4CAB. The zero-length call reaches 3F:4CB3, branches to
3F:4CC6, restores the saved comparison result, and returns. [confirmed]
Byte-entry return trace
_WriteAByteSafe checks page 3E before entering _WriteAByte. A page-3E
rejection returns from 3F:4C9E without changing OP1, BC, DE, or HL.
Page 3F passes that first comparison. _WriteAByte then stores B at
OP1 (0x8478), loads HL=0x8478 and BC=1, and reaches the unsafe core.
The page-3F rejection at 3F:4CAF therefore exposes those wrapper side
effects even though no worker runs. [confirmed]
A direct CALL 3F:4C9F from RAM also performs the byte-wrapper setup before
the unsafe core inspects the return address at 3F:4CA6. It returns from
3F:4CAA with OP1, BC, and HL changed. [confirmed]
The read-only byte-entry-returns fixture verifies all 16 bytes from
3F:4C9A through 3F:4CA9 on the unmodified ROM. It restores the original
OP1 byte before returning and never unlocks Flash. Its machine-code SHA-256
is 6851da991e031ea7df1a31ab3bf62816ad992e3d1946566d31b0a02e16dd50e1.
The trace contains zero CPU write attempts targeting mapped Flash. [confirmed]
for the fixture and TilEm execution.
| Clock | Call and trigger | Return AF | BC | DE | HL | OP1 |
|---|---|---|---|---|---|---|
| 187,804,393 | _WriteAByteSafe, page 0x7E → 3E | 0x3E42 | 0x2233 | 0x4455 | 0x6677 | 0x11 unchanged |
| 187,806,001 | _WriteAByteSafe, page 0x7F → 3F | 0x3F42 | 0x0001 | 0x6677 | 0x8478 | 0x44 from B |
| 187,807,587 | _WriteAByte, page 0x7F → 3F | 0x3F42 | 0x0001 | 0x7788 | 0x8478 | 0x55 from B |
| 187,807,892 | direct CALL 3F:4C9F, A=0xA5 | 0xA591 | 0x0001 | 0x8899 | 0x8478 | 0x66 from B |
The two page guards still return Z. That condition describes the final
comparison, not whether _WriteAByte changed its scratch registers or launched
a worker. [confirmed]
RAM-worker launcher
boot_ram_worker_launcher at 3F:48C5 launches length-prefixed boot workers.
IX points at this packed descriptor: [confirmed]
typedef struct {
uint16_t length;
uint8_t code[];
} RamWorkerDescriptor;
The flexible code[] member describes the serialized ROM object. Ghidra applies
the reusable type to the two-byte header only because each payload has a
different length; the payload begins at descriptor + 2. The launcher copies
descriptor->length bytes from there to ramCode at 0x8100. It then restores
the caller’s HL, DE, and BC and calls the copied code. [confirmed]
The interrupt wrapper at 3F:48EE records IFF2 from LD A,I in 0x82A2, disables interrupts, and returns to the launcher. After the worker returns, 3F:48E1 executes EI only if interrupts were enabled before entry. The worker therefore runs atomically while preserving the caller’s prior interrupt-enabled state. [confirmed]
| Worker | Prefix | Source bytes | RAM destination |
|---|---|---|---|
| sector erase | boot_flash_erase_worker_descriptor at 3F:4C3B, 0x0052 | descriptor + 2, at 3F:4C3D–3F:4C8E | ramCode–ramCode + 0x51 |
| block program | flash_program_worker_descriptor at 3F:4CC8, 0x007C | flash_program_worker_code at 3F:4CCA–3F:4D45 | ramCode–ramCode + 0x7B |
Page 3D contains a relocated copy of the launcher at 3D:678C. It runs
flash_to_ram_worker_descriptor at 3D:6761 and
certificate_worker_descriptor at 3D:7308. Its interrupt wrapper at
3D:67B5 has the same IFF2-save, DI, conditional-EI behavior as the boot
launcher. The inferred name ram_worker_launcher therefore describes both
call paths. [confirmed]
Block-program worker
The block worker repeats a four-write AMD byte-program sequence for each source byte. It temporarily maps fixed pages 02 and 01 so the command addresses appear in bank A, then restores the target page for the data write. [confirmed]
| Step | Mapped page | Logical write | Value |
|---|---|---|---|
| 1 | 02 | 0x6AAA | 0xAA |
| 2 | 01 | 0x5555 | 0x55 |
| 3 | 02 | 0x6AAA | 0xA0 |
| 4 | target | DE | byte from (HL) |
The device decodes the physical low 12 address bits. Page 02, logical
0x6AAA is physical address 0xAAAA; page 01, logical 0x5555 is physical
0x5555. Their low 12 bits are the Fujitsu byte-mode unlock addresses 0xAAA
and 0x555. [confirmed] for the ROM addresses; [standard] for device decoding.
Completion polling
After LDI writes a byte and advances HL, DE, and BC, the worker steps back to compare the programmed byte with the target read: [confirmed]
- XOR source and target, then test bit 7. Equal DQ7 means the byte completed.
- If DQ7 differs, restore that same target byte and test its DQ5 bit.
- Clear DQ5 repeats the first target read.
- Set DQ5 causes one final target read and DQ7 comparison.
- A second DQ7 mismatch takes the failure path.
This is the algorithm in Fujitsu figure 22. During programming, DQ7 returns the complement of the requested data bit until completion. DQ5 indicates an exceeded timing limit. The data sheet requires the second DQ7 check because DQ7 and DQ5 may change simultaneously. [standard]
Return state
On success, the worker writes reset command 0xF0 at the last target address,
forces port 0x06 to page 3F, and returns A=0, Z. On failure,
3F:4D3D–3F:4D45 writes 0xF0 at the failing target, loads A=0x3F for
the page-select output, executes OR A, and returns A=0x3F, NZ. [confirmed]
After full success, HL and DE point one byte beyond the completed span, and
BC=0. On failure, the branch occurs before 3F:4D2C and 3F:4D2D restore
the backed-up pointers. HL and DE therefore identify the failing source and
target bytes, while BC retains the decrement performed by LDI.
_WriteAByte destroys all three public ABI registers. [confirmed]
Forcing page 3F is part of the worker ABI. The outer bcall dispatcher restores the page mapping required by its caller after the boot routine returns. A direct caller that passes the low-address check must account for this mapping change itself. [confirmed]
Internal certificate-page programmer
certificate_write_byte at 3D:72E5 launches a second byte-program worker.
It sets BC=1, clears (IY+0x25).1, normalizes the target page for the current
calculator model, and passes certificate_worker_descriptor to
ram_worker_launcher. Its code begins at descriptor + 2 and contains 129
bytes at 3D:730A–3D:738A. flash_program_worker_code contains 124 bytes at
3F:4CCA–3F:4D45. [confirmed]
The only direct call to certificate_write_byte is 3D:4332, inside
certificate_copy_from_flash at 3D:431A. The loop obtains an ordinary Flash
page from 3D:5258, stages one byte in OP1 with _FlashToRam, selects the
model-specific certificate page through model_certificate_page at 3D:738B,
and programs the byte at the current certificate destination. Direct callers
at 3D:426A and 3D:4715 reach this loop. [confirmed]
certificate_copy_to_flash at 3D:434B performs the reverse transfer. Its
prologue at 3D:433F obtains and erases the ordinary Flash destination page.
The loop stages a certificate byte in OP1 through 3D:42AC, obtains the
ordinary destination page through 3D:5258, and calls _WriteFlashUnsafe = 8087h. The direct callers at 3D:4127 and 3D:4707 pass destination address
0x4000. [confirmed]
Both loops belong to certificate_rebuild_dispatch at 3D:40F1. The
dispatcher stores its mode byte at 0x9C20, copies certificate data to an
ordinary Flash work area at 3D:4127, rebuilds mode-dependent certificate
fields, erases a model-selected certificate half through 3D:4252, and can
copy the work area back at 3D:426A. This identifies the data directions and
the rebuild role. Direct calls and page-0 bjump calls identify an owner for
each mode. [confirmed]
The dispatcher operates on the last 0x216 bytes of either 8 KiB certificate
half. Its fixed offsets and lengths divide that tail into four contiguous
blocks: [confirmed]
| Half-relative offset | Length | Range |
|---|---|---|
0x1DEA | 0x66 | 0x1DEA–0x1E4F |
0x1E50 | 0xC8 | 0x1E50–0x1F17 |
0x1F18 | 0xC8 | 0x1F18–0x1FDF |
0x1FE0 | 0x20 | 0x1FE0–0x1FFF |
The adjacent App-restriction bytes make the complete decoded tail easier to
address as a partial structure based at half offset 0x1DD2: [confirmed]
typedef struct {
uint8_t restriction_control; // +0x000, half offset 0x1DD2
uint8_t restriction_record[13]; // +0x001, half offset 0x1DD3
uint8_t unresolved_1de0_1de9[10]; // +0x00E
uint8_t gc_recovery[0x66]; // +0x018, half offset 0x1DEA
uint8_t ti84_app_trials[0xC8]; // +0x07E, half offset 0x1E50
uint8_t alternate_model_span[0xC8]; // +0x146, half offset 0x1F18
uint8_t validity[0x20]; // +0x20E, half offset 0x1FE0
} CertificateMetadataTail;
The unresolved_1de0_1de9 name deliberately records only its bounds. The ROM
evidence does not identify an owner for those ten bytes. The notation below
uses certificate_tail for a CertificateMetadataTail view of the selected
certificate half. BuildTypes.java registers this reusable type but does not
apply it at one fixed address: the ROM selects the certificate half at runtime,
so certificate_tail means a conceptual pointer to selected_half + 0x1DD2,
not a global Ghidra symbol. [confirmed]
Six helpers at 3D:5227–3D:5256 add fixed or model-selected offsets to
_GetCertificateStart’s result. Raw CALL scanning finds the complete direct
caller sets without relying on disassembler labels: [confirmed]
| Entry | Selected offset | Direct callers |
|---|---|---|
3D:5227 | 0x1DD3 | 3D:42D4, 3D:7D7A |
3D:522D | 0x1FE0 | 3D:42B3, 3D:4589, 3D:4654, 3D:47A8, 3D:521D, 3D:5448 |
3D:5233 | 0x1F18 | 3D:414B, 3D:4288, 3D:42EA, 3D:42F2, 3D:42FD, 3D:4306, 3D:47B1, 3D:493D, 3D:4CBD, 3D:4F14, 3D:5080, 3D:5184, 3D:51A8, 3D:538F |
3D:5241 | 0x1DEA | 3D:4274, 3D:4298, 3D:42A3 |
3D:5247 | model-selected | 3D:490F, 3D:5385, 3D:548F, 3D:5C0E |
3D:5252 | 0x1FE0 | 3D:430E |
The model-selected helper calls 00:1837. That probe reads port 0x02, masks
bit 7, and preserves the resulting flags while restoring A and BC.
3D:5247 branches to the fixed 0x1F18 helper when the bit is clear and falls
through to 0x1E50 when it is set. The resolved TI-84 Plus traces read 0xE1,
0xE3, or 0xE7, so every observed TI-84 Plus state selects 0x1E50.
[confirmed]
Wabbitemu independently returns a base value with bit 7 set for models at or
above its TI_84P enum and clear for its TI-83 Plus family. This supports the
family split implemented by the ROM but remains evidence about the emulator,
not a physical measurement. [standard]
Consequently certificate_tail.ti84_app_trials, at
0x1E50–0x1F17, is the active App-trial table on TI-84 Plus.
When port-0x02 bit 7 is clear, the same clear, write, query, and display paths
select certificate_tail.alternate_model_span at
0x1F18–0x1FDF. TI-84 Plus rebuild modes 0
and 2 still stage or replace that alternate-model span together with validity
metadata, but no TI-84 Plus per-entry semantic accessor to that span has been
identified. Giving it another TI-84 Plus field name would exceed the evidence.
[confirmed] for selection and access; [hypothesis] for any further TI-84 Plus
meaning.
The helper calls in each dispatch branch identify which span receives
mode-specific replacement data. Other helpers clone retained spans from the
active half to the opposite half. Mode 4 also exports 0x1E50–0x1F17 to
0x8000 and 0x1DD3–0x1DDF to 0x80F0. [confirmed]
| Mode | Branch | Mode-specific replacement span |
|---|---|---|
0 | 3D:423F | 0x1F18–0x1FFF (0xE8 bytes) |
1 | 3D:41ED | 0x1E50–0x1F17 (0xC8 bytes) |
2 | 3D:41DF | 0x1F18–0x1FFF (0xE8 bytes) |
3 | 3D:41FB | 0x1DEA–0x1E4F (0x66 bytes) |
4 | 3D:4209 | 0x1DEA–0x1E4F and 0x1FE0–0x1FFF |
5 | 3D:421D | 0x1FE0–0x1FFF (0x20 bytes) |
6 | 3D:422B | complete 0x1DEA–0x1FFF tail (0x216 bytes) |
Neither copy loop nor the dispatcher writes port 0x14. Five direct call
sites enter the dispatcher: [confirmed]
| Mode | Direct call | Byte-pinned gate context |
|---|---|---|
0 | 3D:66C7 | The full-reset path at 35:7205 reaches 3D:6673 through the page-0 trampoline at 00:2DC3. 3D:6673 opens the gate at 3D:6680; the tail at 3D:66CA jumps to the shared relock routine. |
1 | 3D:5774 | The App-deletion path at 3D:4018 and invalid-App cleanup at 3D:5F71 call 3D:5759. The first path opens at 3D:400A; the second opens at 3D:5F28. |
2 | 3D:437E | The certificate receive path reaches 3D:4721; Flash App receive preparation reaches 3D:5094. Both inherit gate state. |
5 | 3D:51D7 | The enclosing path opens at 3D:70DA; later exits relock at 3D:7194, 3D:71AA, or 3D:71E4. |
6 | 3D:7D87 | _RemoveAppRestrictions at 3D:7C1B opens at 3D:7C46, calls the rebuild wrapper at 3D:7D82, and relocks at 3D:7C8C. |
Modes 3 and 4 enter through the page-0 bjump stub at 00:2B77. The stub’s
inline descriptor is F1 40 7D, which resolves to 3D:40F1. Both callers
belong to gc_recovery_preflight at 3C:7219, which opens the Flash gate at
3C:7228: [confirmed]
| Mode | Page-3C call | Call chain | Role |
|---|---|---|---|
3 | 3C:7558 | 3C:7219 → 3C:724A → 3C:7544 → 3C:7558 → 00:2B77 → 3D:40F1 | Rewrite the 0x1DEA–0x1E4F recovery metadata after an archive-sector operation in the recovery loop. |
4 | 3C:7313 | 3C:7219 → 3C:72A5 → 3C:7313 → 00:2B77 → 3D:40F1 | Initialize the certificate-backed recovery metadata before the loop. |
Mode 0 initializes the OS/App-validity tail during full reset. 3D:6673
erases ordinary Flash page 8, fills the 0xE8-byte replacement buffer with
0xFF, and stores 0xFE at 0x836D. That RAM byte corresponds to
certificate offset 0x1FE0. On models other than the TI-83 Plus, the routine
also stores 0x7F in the next byte before invoking mode 0. [confirmed]
Mode 1 clears a two-byte per-App trial entry when an App is removed. 3D:5759
stages the model-selected table, converts the App page to a two-byte index,
writes FF FF, and invokes mode 1. On TI-84 Plus that table is
0x1E50–0x1F17. The table’s use as an App trial table is also ROM-confirmed.
The App receive path writes the same
two-byte entry at 3D:5BB7. The App-information path at 36:70B5 calls the
reader at 3D:5466, displays the ROM string "Trials Remaining:" at
01:41AA, and prints values derived from the two bytes. The direct mode-1
callers at 3D:4018 and 3D:5F71 belong to App deletion and invalid-App
cleanup. [confirmed]
One mode-2 owner is the certificate receive path. The link header dispatcher
selects certificate type 0x25 at 3C:565D. After _FindFirstCertField, a
field selector with H=3 and L & 0xF0 = 0x10 reaches the page-0 bjump stub
at 00:2BFB from 3C:5714. That stub targets 3D:4771. Its certificate-half
rotation path calls 3D:46EE, which invokes mode 2 at 3D:4721. This pins
mode 2 to rebuilding 0x1F18–0x1FFF for that certificate-field selector.
The other mode-2 caller belongs to Flash App receive preparation. Header
type 0x24 enters at 3C:550D. The per-page call at 3C:55BD reaches
3D:73BE through the page-0 stub at 00:2D81. 3D:73BE checks the App page,
clears its App-validity bit when necessary, and reaches 3D:5019 through
3D:5356. That path stages 0x1F18–0x1FFF and invokes mode 2 at
3D:5094. [confirmed]
These owners establish where mode 2 is used. They do not establish any
additional TI-84 Plus field meaning for the alternate-model App-trial span at
0x1F18–0x1FDF. [hypothesis] for such an additional meaning.
The mode-4 path fills the model-selected journal buffer at 0x82A5 or
0x8000, initializes its phase bytes at 3C:72D1–3C:730D, and invokes the
dispatcher. The recovery loop reaches mode 3 through 3C:7544. That routine
selects an archive sector, calls _EraseFlashPage = 8084h, updates the RAM
journal fields at 3C:7568 and 3C:7576, then persists the 0x66-byte block.
[confirmed]
The main bcall table pins mode 6 to the App-restriction API: [confirmed]
| Bcall | ID | Page-3D entry |
|---|---|---|
_SetAppRestrictions | 52F6h | 3D:7B9B |
_RemoveAppRestrictions | 52F9h | 3D:7C1B |
_QueryAppRestrictions | 52FCh | 3D:7CBA |
certificate_tail.restriction_control occupies certificate offset
0x1DD2. The 13-byte restriction_record field at 0x1DD3–0x1DDF acts as
a record or as an App-restriction bitmap, depending on the API operation. For an
App on Flash page $p$, the bitmap index is $p - 8$. 3D:7D69 divides that
index by eight, and the mask helper at 3D:785D uses least-significant-bit-first
ordering. A clear bitmap bit means that the App is restricted. [confirmed]
The low control-byte bits have these ROM-confirmed roles:
| Bit | Mask | Clear-bit meaning | Evidence |
|---|---|---|---|
0 | 0x01 | Base restriction control is active. | The type-2 set and query paths at 3D:7C02 and 3D:7CD8; aggregate type 3 is queried by _ExecutePrgm at 07:5758, while equation/token paths query type 2. |
1 | 0x02 | logBASE is disabled. | Type 6 selects mask 0x02 at 3D:7CE3; the UI string at 37:4A42 and query at 37:4E43 name logBASE. |
2 | 0x04 | The summation token is disabled. | Type 7 selects mask 0x04 at 3D:7CDD; the UI string at 37:4A54 and query at 37:4E52 name the summation token. |
The API dispatch gives each restriction type the following behavior:
| Type | Role | Set | Query | Remove |
|---|---|---|---|---|
0 | App named in OP1 | Resolve the App page and clear its bitmap bit. | Test the resolved App’s bitmap bit. | Unsupported. |
1 | 13-byte restriction record | Program 0x847A–0x8486 into 0x1DD3–0x1DDF. | Report whether any record byte differs from 0xFF. | Replace the record with 0xFF. |
2 | Base restriction control | Clear control bit 0. | Return 1 when bit 0 is clear. | Set control bit 0. |
3 | Aggregate restriction profile | Clear bit 0 and program the record. | Derive an active-profile mask from the control and record bytes. | Set bits 0–4 and replace the record with 0xFF. |
4 | Bulk App bitmap | Program the control byte and 13 bitmap bytes from 0x848E–0x849B. | Count installed Apps whose bitmap bits are clear. | Unsupported. |
5 | App page in B | Unsupported. | Test the selected App’s bitmap bit. | Unsupported. |
6 | logBASE restriction | Clear control bit 1. | Return 4 when bit 1 is clear. | Set control bits 1 and 2. |
7 | Summation restriction | Clear control bit 2. | Return 8 when bit 2 is clear. | Unsupported. |
_SetAppRestrictions accepts types 0–4, 6, and 7; it rejects type
5. _RemoveAppRestrictions accepts types 1, 2, 3, and 6. Removal
loads the 14-byte span into 0x8479 at 3D:7DCE. The rebuild wrapper at
3D:7D82 invokes mode 6 and writes the updated span back. Set paths clear
Flash bits with direct programming. Removal restores some cleared bits to one,
so it rebuilds the complete 0x216-byte certificate tail. [confirmed]
Mode 5 sets a per-App validity bit. locate_app_validity_bit at 3D:51F6
starts with _GetCertificateStart + 0x1FE0, divides the calculated App index
by eight at 3D:7D6B, and retains the low three bits as the bit index.
set_app_validity_bit at 3D:51BE advances past 0x1FE0 before reading, so
the bitmap starts at half-relative offset 0x1FE1. The mask loop at 3D:785D
uses least-significant-bit-first ordering. [confirmed]
The receive path from _WriteToFlash at 3D:6DA5 reaches the set routine at
3D:70E1. If the bit is clear, stage_app_validity_byte at 3D:51A6 updates
the 0x836D tail buffer and calls mode 5 at 3D:51D7. Setting a NOR Flash
bit from zero to one requires the erase-and-rebuild path. The inverse routine,
clear_app_validity_bit at 3D:51E4, masks the bit to zero and reaches
_WriteAByte = 8021h through 3D:7CB3; programming one to zero does not
require an erase. [confirmed]
The boot bcall table and page-3F bodies independently confirm the OS-validity
bit in certificate_tail.validity[0], at offset 0x1FE0: [confirmed]
| Bcall | ID | Body | Behavior |
|---|---|---|---|
_MarkOSInvalid | 8093h | 3F:5209 | Stage 0x1F18–0x1FFF, set bit 0 in the staged 0x1FE0 byte at 0x836D, and erase/rebuild the certificate data. |
_MarkOSValid | 8099h | 3F:51F5 | Read 0x1FE0, clear bit 0, and program the byte through _WriteAByte = 8021h. |
_CheckOSValidated | 809Ch | 3F:52C6 | Read 0x1FE0 and test bit 0. |
Bit 0 clear means that the OS is valid; bit 0 set means that it is invalid.
WikiTI gives the same field label, but the conclusion above comes from the boot
ROM paths. WikiTI also labels certificate_tail.gc_recovery at
0x1DEA as garbage-collection information. The
mode-3 and mode-4 call chains independently confirm that the
0x1DEA–0x1E4F block stores garbage-collection recovery metadata. The exact
meaning of every byte is not established: the first six fields and the live
sector-state array are decoded, while the two retained trailing bytes at
0x1E4E–0x1E4F have no direct semantic accessor. [confirmed] for block
ownership and access bounds; [hypothesis] for the trailing bytes’ owner.
On the TI-84 Plus path, 3C:7E6B loads the existing block and 3C:7317 writes
0xFF over only the first 0x64 bytes in RAM at 0x82A5. Mode 4 later
rebuilds the full 0x66 bytes from that buffer, retaining offsets +0x64 and
+0x65. The first six initialized bytes are fixed fields, leaving 94 bytes of
state-array capacity; the TI-84 Plus archive limit 0x2A makes only slots
0–8 live. [confirmed]
tools/ti84re/flash/certificate_rebuild.py exposes the signature-checked reconstruction as
a library. Its thin CLI reports the block partition, all seven branches,
direct and bjump invocations, resolved mode owners, OS/App-validity metadata,
and App-restriction behavior:
python3 -m ti84re.flash.analyze_certificate_rebuild --json
tools/ti84re/flash/gc_journal.py decodes the 0x1DEA recovery block, master phase
dispatch, and archive-sector state indexing. Its CLI can correlate the static
ROM paths with state-changing command writes in a TilEm trace. See
Variables, archive and unarchive
for the field and phase tables. [confirmed]
python3 -m ti84re.flash.analyze_gc_journal --json
python3 -m ti84re.flash.analyze_gc_journal --trace /tmp/tibasic-smoke/gcflash.trace
The reusable call analyzer can resolve a banked target back through its page-0 bjump stub and report candidate callers with linear-disassembly context:
nix develop -c python3 -m ti84re.rom.analyze_calls \
3D:4771 --bjump-call --before 5 --after 5
The complete-ROM raw scan and linear disassembly independently find 90 D3 14
occurrences. All 90 use one of four privilege-sequence spellings: 70 load
A=1, and 20 clear A. Page 3D contains 34 unlock forms and the shared lock
form at 3D:5CE6. Page 3D never writes port 0x21. Its only resolved
port-0x21 access is the read at 3D:7392 that selects a model-specific
certificate page. These paths do not change the modeled physical-sector
override. [confirmed] for the ROM scan; [standard] for the emulator-defined
override role.
tools/ti84re/flash/gate.py exposes the raw scanner as a library. The thin CLI keeps
complete privileged sequences separate from unmatched D3 14 candidates:
python3 -m ti84re.flash.analyze_gate --page 0x3D --json
The two program workers share the command writes, LDI, DQ7/DQ5 polling, and
reset write. A sequence comparison aligns 116 bytes. Five byte spans encode
the differences: [confirmed]
| Behavior | Page-3D certificate worker | Boot block worker |
|---|---|---|
| Prologue | saves target page at 0x9868, then saves the current port-0x06 value | masks the target page to six bits and maps it directly |
| Crossing sentinel | skips a page-select output when the next page is 0x7E | skips it when the next page is 0x3E |
| Success mapping | restores the saved port-0x06 value | forces page 0x3F |
| Failure mapping | restores the saved port-0x06 value | forces page 0x3F |
| Failure return | returns the restored page in A; Z if that page is zero | returns A=0x3F, NZ |
The page-3D caller ignores the worker flags after 3D:4332. A DQ5 failure
therefore does not stop its byte-copy loop. Even a caller that inspects the
flags cannot treat Z as unconditional success: the failure tail writes 0xF0
at the target, pops the saved port-0x06 value into A, restores that page,
and executes OR A. A saved page zero produces Z on the failure path.
[confirmed]
The guarded certificate-program-error fixture copies the unmodified 129-byte
worker to 0x8100, saves page zero, and requests 0x80 over stored 0x00 at
3E:4000. It runs only with the patched unlock wrapper and verifies the worker
head, worker tail, and target byte before programming. Its machine-code SHA-256
is 34fc6b71a0015cbcb13578a30ec195883a187ee43b234d6ab671d00275824429.
[confirmed]
Pinned TilEm returns program-status values 0x00, 0x60, and 0x20, then
executes the failure reset at ram:817B. The trace decoder labels the
invocation certificate-failure. The copied worker returns AF=0x0044, Z,
with BC=0, DE=0x4000, HL=0x9E63, and port 0x06 restored to zero. The
final target remains 0x00. This dynamically confirms the worker tail in
TilEm; physical DQ5 behavior remains unmeasured. [confirmed] for the ROM and
TilEm trace; [hypothesis] for hardware.
tools/ti84re/flash/workers.py extracts two-byte-length descriptors and compares
worker bytes. Its CLI reproduces the lengths, hashes, aligned-byte total, and
five edit spans:
python3 -m ti84re.flash.describe_workers --json
Locked write can satisfy DQ7 under TilEm
The Flash APIs do not unlock the ASIC command gate. A caller can therefore
reach the unmodified worker while port 0x14 still blocks every command write.
The worker checks only DQ7 during the normal completion path. It does not
compare the remaining seven data bits after DQ7 agrees. [confirmed]
The read-only locked-byte-noop fixture verifies the 16-byte _WriteAByte
wrapper signature and the 16-byte protected lock-wrapper signature. It calls
the original lock wrapper at 3C:66D5, then aborts unless port 0x02 bit 2 is
clear. The fixture uses the unmodified ROM and restores the original OP1
byte. Its machine-code SHA-256 is
4a843bc617282b44c5a1dac1c6f08627c65c33175908198c908bddc8ba4b82ee.
[confirmed] for the fixture construction.
The source byte at 3D:7FFF is 0x50. The fixture requests legal NOR
programming to 0x40; both values have DQ7 clear. TilEm reports port 0x02
as 0xE3 before the call, confirming that its Flash-unlocked bit is clear.
The trace records five CPU write attempts targeting mapped Flash: [confirmed]
for TilEm execution.
| Clock | Worker address | CPU write or read |
|---|---|---|
| 186,985,124 | ram:8149 | attempt data 0x40 at 3D:7FFF after AA 55 A0 |
| 186,985,143 | ram:814D | read array byte 0x50; requested and observed DQ7 agree |
| 186,985,240 | ram:816B | attempt array reset 0xF0 at 3D:7FFF |
TLMT records CPU writes to the mapped Flash window, not whether the ASIC or
device accepted them. The command decoder consequently recognizes one
command-shaped byte-program sequence and one reset. The final array read is
the acceptance check: 3D:7FFF remains 0x50. Port 0x02 also remains
0xE3. [confirmed]
The bcall returns AF=0x0044, Z, with BC=0, DE=0x8000, HL=0x8479, and
OP1=0x40. The return state is indistinguishable from a completed one-byte
worker call unless the caller verifies array data. This confirms the caller’s
unlock obligation in pinned TilEm and shows another path where Z does not prove
mutation. It does not establish how a physical ASIC handles the same attempt.
[confirmed] for the ROM and emulator trace; [hypothesis] for physical behavior.
Cross-page destination behavior
The intended path uses a RAM source, so source H has bit 7 set. On that path the worker detects DE > 0x7FFF, increments the current target page, and resets DE=0x4000 before the next byte. [confirmed]
The ordinary Archive prgmA trace groups its 17 byte-program commands into six worker invocations. The garbage-collection window groups 1,133 commands into 56 invocations, with a maximum length of 232 bytes. Every one is page-local, physically contiguous, and followed by a reset at its final target. These ordinary paths therefore exercise the worker but do not by themselves test its page-crossing branch. [confirmed]
A deliberate TilEm trace archives a generated 17,000-byte prgmZBIGDATA through MEM > Mem Mgmt/Del > Prgm. One _WriteFlashUnsafe invocation programs all 17,002 bytes of the variable data, from physical 0x20013 (08:4013) through 0x2427C (09:427C). The decoder observes exactly one 08:7FFF to 09:4000 crossing, no discontinuity, and the terminal 0xF0 reset at 09:427C. At the crossing, ram:811B reads port 0x06 with A=0x08 at clock 230,976,551; ram:8122 outputs A=0x09 at clock 230,976,580; and ram:8124 has reset DE from 0x8000 to 0x4000 at clock 230,976,590. This confirms the ordinary 08 to 09 software path in emulation; it is not a physical-calculator Flash test. [confirmed]
The boundary code contains a page-3E quirk:
in a,(0x06)
inc a
cp 0x3e
jr z,skip_out
out (0x06),a
skip_out:
ld de,0x4000
A write that crosses from page 3D computes page 3E but skips the page-select output. It resets DE to 0x4000 and continues on the old mapping. This is not a clean stop at the certificate boundary. Starting _WriteFlashUnsafe on page 3E can increment toward page 3F; the hardware protection layer remains separate. [confirmed]
An emulator-only TilEm fixture exercises the page-3D boundary with A=0x3D,
DE=0x7FFF, BC=2, and RAM source bytes 0x40,0xE0. It patches only the tail
of a protected page-3C unlock wrapper in a copy of the exact OS image. The
copied flash_program_worker_code remains unchanged. The generated
assembly program checks all eight patched bytes and exits on an unmodified ROM
before it can unlock Flash. [confirmed] for the fixture construction.
The trace decodes two byte-program commands followed by one array reset: [confirmed] for TilEm behavior.
| Clock | Command | Physical target | Value |
|---|---|---|---|
| 186,446,349 | byte program | 3D:7FFF (0xF7FFF) | 0x40 |
| 186,446,829 | byte program | 3D:4000 (0xF4000) | 0xE0 |
| 186,447,016 | array reset | 3D:4000 (0xF4000) | 0xF0 |
At clock 186,446,607, ram:811B reads port 0x06 while the mapping is page
3D. ram:811D increments the value to 0x3E; ram:811E compares it with
0x3E; and ram:8120 takes the zero branch. The trace contains no execution
of the page-select output at ram:8122. At clock 186,446,640, ram:8124 has
set DE=0x4000 while page 3D remains mapped. The trace decoder classifies
the physical 0xF7FFF → 0xF4000 transition as same-page-window-wrap.
[confirmed]
This run confirms the static branch in TilEm. It does not test the physical
ASIC, the photographed Fujitsu device, or a production ROM without the
emulator-only unlock shim. The fixture and commands are documented under
“Guarded Flash-worker fixtures” in the repository’s
tools/notes/flash-fixtures.md.
Illegal byte-program failure under TilEm
A second guarded fixture calls _WriteFlashUnsafe with A=0x3D,
DE=0x7FFF, BC=1, and source byte 0xD0. The source ROM holds 0x50 at
3D:7FFF, so bit 7 requests an illegal NOR 0→1 transition. The fixture uses
the same eight-byte unlock shim guard as the page-3E probe. Its machine-code
SHA-256 is
d83208e1bbcc0f891b2bb73f7558cc521d55c37ce91c3ebdd88b5076e04c5076.
[confirmed] for the fixture construction.
TilEm f56ad63’s emu/flash.c
applies stored &= requested and enters FLASH_ERROR when the stored byte
does not equal the request. Error reads complement the requested DQ7, set DQ5,
toggle DQ6, and leave the error state active until reset. The pinned file’s
SHA-256 is
280e0e45b6e1f1ef21d779abb809eaef2d04d08db09feb87a459e079280c9545.
[standard]
The trace records this poll sequence: [confirmed] for TilEm behavior.
| Clock | Worker address | Observed result |
|---|---|---|
| 186,668,556 | ram:8149 | program 0xD0 at 3D:7FFF (0xF7FFF) |
| 186,668,575 | ram:814D | read 0x00; DQ7 differs and DQ5 is clear |
| 186,668,646 | ram:814D | read 0x60; DQ7 differs and DQ5 is set |
| 186,668,712 | ram:8159 | final read 0x20; DQ7 still differs |
| 186,668,738 | ram:815D | take the NZ branch to ram:8173 |
| 186,668,753 | ram:8175 | write array reset 0xF0 at 3D:7FFF |
| 186,668,775 | ram:817A | OR A produces AF=0x3F2C |
The bcall returns to ram:9DBE with AF=0x3F2C at clock 186,668,984. The
fixture remaps page 3D, rereads 3D:7FFF, and observes the unchanged stored
byte 0x50 at clock 186,669,043. The trace decoder uses the reset-write PC to
label this invocation worker_outcome: "failure". [confirmed]
This result confirms how the unmodified worker responds to TilEm’s persistent program-error state. It does not measure status timing, DQ bits, or failure recovery on the photographed Fujitsu device or another physical calculator.
Source-space branch and ROM callers
If source H has bit 7 clear, the worker sets (IY+0x25).1 and skips
destination-crossing logic. An exhaustive raw-bcall scan of the retail ROM
finds 20 _WriteFlashUnsafe (8087) candidates and three _WriteFlash
(80C9) candidates. Static register reduction puts every source in RAM:
[confirmed]
| Page | Bcall sites and source HL |
|---|---|
36 | 5E5C=82A5 |
3C | 630E=8000, 6AA0=82A5, 6AF5=983A |
3D | 436C=8478, 4670=9C9E, 5050=82A5, 5852=8000, 58ED=8478, 5926=8478, 5CBA=83A5, 6522=83F9, 6578=8478, 65BA=(83F3), 6A23=83FD, 6A39=8402, 6AA6=8000, 6ACE=8000, 718C=8000+offset, 71E1=8479, 7201=8000, 7ABB=8479, 7B72=983A |
The page-3C site at 3C:6AF5 is _WriteFlash (80C9h). The
flush_paged_flash_block caller at 3C:6AB1 loads HL=0x983A, B=0, and
C=(0x9834) after opening the port-0x14 gate. It accepts model-dependent
pages only after the classifier at 3C:6B79; the TI-84 Plus range is
0x08–0x29. The link receiver reaches this staging path only when the
destination loaded at 3C:42AB has bit 15 clear. RAM destinations take the
direct store at 3C:42D4. The second mode-3 owner is the USB
receive-to-memory loop at 36:40E7. It fills 0x983A through the page-35
endpoint helper at 35:4FA1, which reads port 0xA1 at 35:500E, then calls
the page-3C dispatcher at 36:415C. [confirmed]
At 3D:65BA, the source is arcInfo.dest_ptr. Setup at 07:6331 saves the
incoming data pointer from the variable lookup in that field. It is a RAM data
pointer on the RAM-to-Flash path; the Flash-to-RAM path later replaces it at
07:622E with the newly allocated RAM destination. The GC trace reaches this
bcall twice with HL=9E53; the normal archive trace reaches it once with
HL=9E21. The helper called before 3D:718C returns either 8000 or
8000+offset within its caller’s established range. The local helpers at
3D:5258 and 3D:5964 preserve source HL for their dependent sites.
[confirmed]
The copied-worker entry provides an independent runtime check. Opcode E6 at
ram:8100, with destination DE<8000, identifies block-program entries. The
GC trace contains 62: source HL is 8000 once, 83F9 twice, 83FD once,
8402 once, 8478 55 times, and 9E53 twice. The normal archive trace
contains six: 83F9 once, 8478 four times, and 9E21 once. No observed
entry takes the H<80 branch. [confirmed]
The alternate branch is not a general Flash-to-Flash copy path. The worker
selects the destination page through port 0x06 before it reads the source. A
source in the banked 4000–7FFF window therefore aliases the destination
page rather than retaining an independent source page. A source in the fixed
0000–3FFF window can still be read. The guarded low-source-cross fixture
tests that case on an unmodified ROM. It locks Flash through the protected
wrapper at 3C:66D5, confirms port 0x02 bit 2 is clear, and calls
_WriteFlashUnsafe with A=0x3D, DE=0x7FFF, BC=2, and HL=0x0068.
Source bytes 00:0068 and 00:0069 are 0x4D and 0x50. [confirmed]
| Clock | Copied-worker address | Resolved write attempt and state |
|---|---|---|
| 187,318,374 | ram:8149 | first LDI: 0x4D to locked Flash at 3D:7FFF; BC=1, DE=8000, HL=0069 |
| 187,318,708 | ram:8149 | second LDI: 0x50 to RAM 8000; BC=0, DE=8001, HL=006A |
| 187,318,824 | ram:816B | terminal 0xF0 reset write also resolves to RAM 8000 |
The bcall returns AF=0x0044, Z, with the final BC, DE, and HL values
shown above. The probe captures RAM 8000=F0, (IY+0x25).1 set,
3D:7FFF=50, and port 0x02=E3 before restoring its RAM and flag fixtures.
The Flash decoder sees one command-shaped byte-program attempt and no Flash
reset because the terminal reset resolves to RAM. The fixture ROM hash equals
the source ROM hash; the probe’s machine-code SHA-256 is
bb8159803d67bbfdc354d523db7dbe72e02bf4469a89c79d2c7d033dd660074e.
[confirmed] for pinned TilEm and the unmodified ROM.
The branch remains unused by every statically identified ROM call and both available OS write traces. Its intended external use, if any, remains unknown. The dynamic result does not establish what a physical ASIC and Flash device do with the locked command-shaped write attempt. [hypothesis] for a use outside the documented RAM-source ABI and for physical consequences.
Erase APIs and worker
_EraseFlashPage sets HL=0x4000, masks A to six bits, and rejects page
3E. Its equality comparison returns A=0x3E, Z on that no-op path. For page
00 it changes HL to 0x0000, because page 0 is fixed below the banked
window. It then falls into _EraseFlash. [confirmed]
_EraseFlash applies the same immediate-return-address check as
_WriteFlashUnsafe. A direct caller with a return address at or above
0x8000 returns NZ before worker launch. A bcall proceeds to copy the erase
worker to 0x8100. The routine does not reject page 3F; the Flash chip’s
sector protection is a later, independent gate. [confirmed]
Erase-entry trace
The read-only erase-entry-returns fixture verifies eight bytes at each of
3F:4C1E, 3F:4C2A, and 3F:4E3F. It never writes port 0x14, and every
test returns before worker launch. The trace contains zero resolved Flash
writes. [confirmed] for the fixture and TilEm execution.
| Clock | Call and trigger | Return AF | Condition |
|---|---|---|---|
| 187,400,702 | _EraseFlashPage, input page 0x7E → masked page 3E | 0x3E42 | Z |
| 187,400,886 | direct CALL 3F:4C2A from RAM, input A=0xA5 | 0xA591 | NZ |
| 187,402,383 | _EraseCertificateSector, HL=0x5000, seeded AF=0xA545 | 0xA545 | caller value |
The page rejection returns at 3F:4C25. The direct call returns at
3F:4C2E. The invalid certificate address branches from 3F:4E4E to the
common tail at 3F:4E55–3F:4E56:
POP AF
RET
[confirmed]
The erase worker issues the six-cycle AMD sector-erase command: [confirmed]
| Step | Mapped page or target | Address | Value |
|---|---|---|---|
| 1 | page 02 | 0x6AAA | 0xAA |
| 2 | page 01 | 0x5555 | 0x55 |
| 3 | page 02 | 0x6AAA | 0x80 |
| 4 | page 02 | 0x6AAA | 0xAA |
| 5 | page 01 | 0x5555 | 0x55 |
| 6 | target page | HL | 0x30 |
It polls target DQ7 until it becomes 1. If DQ5 becomes 1 first, it takes the failure path. Success forces port 0x06 to page 3F and returns A=0, Z. Failure loads A=0xF0, writes it through DE, executes OR 1, forces page 3F, and returns A=0xF1, NZ. The write through DE is present in the copied worker even though _EraseFlash documents only A and HL as inputs. [confirmed]
Failure-path DE audit
The RAM-worker launcher at 3F:48C5 saves the caller’s BC, DE, and HL,
copies the worker, restores those registers, and only then calls 0x8100.
It does not synthesize a reset pointer. The worker therefore receives whatever
DE the caller supplied. [confirmed]
The Fujitsu command table defines 0xF0 as a read/reset command accepted at any
Flash address. Writing it through a Flash pointer is consequently a valid way
to leave the status state and return to array reads. [standard] The ROM code
does not check that DE is such a pointer. If DE >= 0x8000, the same
instruction writes 0xF0 to RAM instead; the public _EraseFlash ABI does
not document DE as an input. [confirmed] for the conditional ROM behavior;
[hypothesis] for a physical test that deliberately reaches DQ5 with a RAM
pointer.
There is one raw _EraseFlash bcall sequence in the OS image, at 3D:45EA.
The wrapper at 3D:45E7 first selects the model-specific certificate page,
then calls bcall 8024. Four direct calls reach that wrapper on page 3D:
[confirmed]
| Call | DE evidence at the erase |
|---|---|
3D:40A3 | The helper at 3D:409F and boot _GetCertificateStart preserve incoming DE. Its 3D:7A68 caller leaves DE=0x1DE2, a fixed-page Flash address. The 3D:71C3 path instead carries a metadata word returned by 3D:5B17, not a pointer. |
3D:4252 | _GetCertificateStart at 3D:424D, followed by EX DE,HL, puts the active certificate-half start in DE. |
3D:60EE | The local branch toggles HL to the half being erased while preserving caller DE. Page-0 thunk 00:3EEB reaches this routine from reset at 00:0D73 before any local DE initialization, and from two page-37 call sites without an ABI constraint on DE. |
3D:6127 | The alternating scan starts with HL=0x4001, DE=0x6001; on exit DE still points into the other certificate half. |
The 3D:71C3 provenance is byte-specific. 3D:787B calls 3D:5B17, which
saves a scan-result word, fetches an OS-header subfield through
_FindOSHeaderSubField, reads its first byte, then forms returned DE from
that byte and the saved scan count. The caller requires returned D to be
nonzero, saves DE at 3D:7089, and restores it at 3D:71B2 immediately
before the erase branch. _GetCertificateStart preserves DE with explicit
push/pop pairs at 3F:486E–3F:4884. This path therefore transports archive
and OS-header metadata through the erase call; it does not establish a Flash
reset pointer. [confirmed]
The 3D:60EE entry is similarly caller-controlled. Its page-0 thunk at
00:3EEB contains this raw descriptor:
CALL 2B09
.dw 6098
.db 7D
Masking the raw page selects physical page 3D. The reset caller initializes
HL, SP, and IY at 00:0D65–00:0D6F but not DE before calling the
thunk. The page-3D
body and its Flash-byte reader preserve that inherited value through the erase
at 3D:60EE. [confirmed]
The GC trace supplies a counterexample to any broader internal convention. Its
seven entries at 3F:4C2A carry DE=0x802C, 0x802C, 0x802C, 0x6000,
0x7DF1, 0x4001, and 0x4000. The first three are RAM pointers. All seven
erases succeed, so none reaches 3F:4C83; nevertheless, the launcher would
preserve the same DE values for the failure path. [confirmed]
Thus multiple certificate-wrapper paths intentionally or incidentally provide a Flash address suitable for the reset command, but the ROM as a whole has no such invariant and the convention does not extend the public ABI. The failure path is best classified as an underspecified interface with a conditionally unsafe RAM write. [confirmed]
The 0x30 command erases a physical sector, not one logical page. _EraseFlashPage is therefore named for the page used to select a sector, not for 16 KiB erase granularity. [standard]
Certificate sectors
_EraseCertificateSector preserves AF around its work. It accepts only
H=0x40 or H=0x60; other values return without erasing. For either accepted
address, it loads A=0x3E, calls _EraseFlash, restores the caller’s AF,
and returns. The restored flags hide both the Z success and NZ failure result
from _EraseFlash. The two values select the two 8 KiB sectors within
physical page 3E. [confirmed]
Successful certificate erase under TilEm
The guarded certificate-erase-success fixture runs only on the patched ROM
copy. It unlocks Flash, seeds AF=0xA545, and calls
_EraseCertificateSector with HL=0x4000. The source image contains 0x00
at physical 0xF8000, so the post-erase read distinguishes mutation from an
already erased byte. The fixture machine-code SHA-256 is
e46ffebe8dbeb6a37ea62790744e8d758dc4772b08046b058ed4a0f351dee97e.
[confirmed] for the fixture construction.
The trace resolves all six command writes and decodes one sector erase at
3E:4000 (0xF8000) at clock 186,869,906. The selected physical sector is
0xF8000–0xF9FFF, matching the first 8 KiB certificate half. [confirmed]
for TilEm execution of the ROM command path.
The worker reads the target at ram:8138 24,497 times. Grouping the observed
A values separates TilEm’s two modeled erase phases: [confirmed] for the
trace values; [standard] for the pinned TilEm state names.
| TilEm state | Target-read value | Count |
|---|---|---|
FLASH_BUSY_ERASE_WAIT | 0x00 | 3 |
FLASH_BUSY_ERASE_WAIT | 0x44 | 3 |
FLASH_BUSY_ERASE | 0x08 | 12,245 |
FLASH_BUSY_ERASE | 0x4C | 12,245 |
| array data after completion | 0xFF | 1 |
The first reads occur at clocks 186,869,913 (0x00) and 186,869,962
(0x44). Active-erase values begin at clock 186,870,207. The final 0xFF
read occurs at clock 188,070,217. ram:8143 then takes the success path at
clock 188,070,241, and the worker returns A=0, Z at ram:8151. [confirmed]
3F:4E55 restores AF=0xA545 at clock 188,070,415. The bcall-visible result
at ram:9DBA remains 0xA545, and the fixture rereads 3E:4000 as 0xFF at
clock 188,070,584. This dynamically confirms that the certificate wrapper
hides the successful worker result. It does not establish physical erase
duration, status cadence, or wrapper behavior on another OS image.
The garbage collector uses those halves as a transactional certificate and phase-journal pair. It
erases the inactive half, copies the used tail of the active half, switches the active marker, and
later copies the tail back. This behavior is visible as separate erases at physical 0xF8000 and
0xFA000; it does not treat page 3E as one 16 KiB erase unit. [confirmed]
Erase-busy read scope under TilEm
The guarded erase-busy-range fixture issues the sector-erase command directly
for 3E:4000 (0xF8000). It waits for DQ3 before sampling the selected sector,
nearby top-boot sectors, and a distant archive page. The fixture then waits for
DQ7 and reads the same locations in array mode. Its machine-code SHA-256 is
561c424816f0dd4dbe76cba7635d2edabb433a234860e63c1c8767dab8254781.
[confirmed] for the fixture construction.
The trace decodes one sector erase at clock 187,143,123. TilEm returns its alternating active-erase values at all six sampled addresses: [confirmed] for TilEm execution.
| Sample | Relation to selected sector | Busy value and clock | Array value and clock |
|---|---|---|---|
3E:4000 (0xF8000) | selected start | 0x08 at 187,143,475 | 0xFF at 188,343,472 |
3E:5FFF (0xF9FFF) | selected end | 0x4C at 187,143,502 | 0xFF at 188,343,499 |
3E:6000 (0xFA000) | adjacent 8 KiB sector | 0x08 at 187,143,529 | 0xFF at 188,343,526 |
3D:7FFF (0xF7FFF) | preceding 32 KiB sector | 0x4C at 187,143,574 | 0x50 at 188,343,571 |
3F:4000 (0xFC000) | boot sector | 0x08 at 187,143,619 | 0x3E at 188,343,616 |
08:4000 (0x20000) | distant 64 KiB sector | 0x4C at 187,143,664 | 0xFF at 188,343,661 |
Only physical 0xF8000–0xF9FFF is erased. The final values at the adjacent,
preceding, boot, and distant samples match the source ROM. [confirmed]
Pinned TilEm handles FLASH_BUSY_ERASE before applying an address-dependent
read result. It warns when the read and erase-command addresses have different
upper 16 physical-address bits, but returns erase status after either outcome.
The run emits one reading from Flash while erasing warning for 0x20000.
The other five addresses share upper byte 0x0F with the erase target, so the
warning check does not distinguish their physical sectors. [standard] for the
pinned source; [confirmed] for the trace and warning.
The Fujitsu data sheet gives different address scopes to the status bits. DQ6
toggles on successive reads from any address, while DQ2 toggles only when read
from an erasing sector. It also requires DQ7 erase polling within a selected
sector. TilEm’s global 0x08/0x4C alternation therefore models DQ2 outside
the selected sector more broadly than the data sheet specifies. [standard]
Physical TI-84 Plus behavior at these boundaries remains unmeasured.
_SetFlashLowerBound bcall
The lower-bound bcall, its register contract, and its executable example are in Flash bcall programming guide.
Archive allocation above the hardware API
The archive manager and the raw Flash API solve different problems. The boot bcalls program an address supplied by their caller. Page-3D code chooses an archive record location, maintains record states, and invokes the boot API. [confirmed]
Dynamic archive boundary
The archive pool begins at page 08. Its upper boundary is computed around installed Flash Apps; it is not the fixed range 0x15–0x1E. [confirmed]
3D:6413 starts at a model-selected top App page returned by 3D:726E: [confirmed]
| Model branch | Top App page |
|---|---|
port 0x02 bit 7 clear | 0x15 |
port 0x21 & 3 equals zero | 0x29 |
| remaining branch | 0x69 |
At each candidate it reads the first byte at logical 0x4000. A possible App header (0x80 or 0x00) is validated through the page-3C helper reached at ram:3DC5; _FindAppNumPages at 3D:4AA3 then returns the App span in C. The routine subtracts that span and repeats. It returns the first page below the installed App run in B. [confirmed]
3D:62C2 stores that value as an exclusive upper bound, loads A=0x08, and scans archive records upward from 08:4000. Its page comparisons stop at or above the dynamic bound. With no installed Apps in the local image, the trace returns B=0x29 and selects A=0x08, HL=0x4000 for the new record. [confirmed]
The nearby selector at 3D:738B returns 0x1E, 0x3E, or 0x7E. Those are model-specific certificate pages. They do not define the archive pool’s upper endpoint. [confirmed]
Record writer
3D:64AA is the archive record writer. It unlocks Flash, checks or retires the previous record marker, writes 0xFE, programs the size, symbol header, name, and data, then changes the record status to 0xFC. It calls _WriteAByte for marker bytes and _WriteFlashUnsafe for blocks. [confirmed]
The checks at 3D:6B6D and 3D:6B9B reject pages below 08, reject pages at or above the dynamic App boundary, and require the Flash destination to be at least 0x4000. The block form at 3D:6B6D also requires its RAM-side address to be at least 0x4000. [confirmed]
Record state changes only clear bits, matching NOR programming rules: 0xFF is erased, 0xFE is in progress, 0xFC is complete, and 0xF0 is retired. Sector erase is the only operation that restores zero bits to one. See Variables, archive & unarchive for the record layout and garbage collector. [confirmed]
End-to-end archive trace
tools/macros/archive-program.macro cold-boots the calculator, creates prgmA, inserts one token, and executes Archive prgmA. The final screen is Archive prgmA followed by Done. The trace contains 4,015,092 instructions, 19,876 mapping writes, and no unresolved mappings. [confirmed]
The executed write path is: [confirmed]
07:6107 archive RAM-to-Flash path
→ 3D:61AF
→ 3D:62C2 free-record scan; selects 08:4000
→ 3D:64AA archive record writer
→ 3F:4C9F _WriteAByte, three calls
→ 3F:4CA6 _WriteFlashUnsafe, six calls total
→ 0x8100 copied byte-program worker
The calls write an initial 0xF0 marker when needed, 0xFE, a two-byte size field, an eight-byte header, a four-byte payload, and final status 0xFC. Every boot-worker call follows the successful DQ7 path and returns A=0. [confirmed]
The trace also resolves the archive-range ambiguity directly. 3D:6413 returns B=0x29; 3D:62C2 explicitly starts at page 08; and the programmed physical target is page 08. [confirmed]
The generated large-program trace uses the same record path and demonstrates that the data block is not split at a 16 KiB page boundary: its 17,002-byte worker invocation crosses contiguously from page 08 to page 09. The fixture builder, UI macro, decoder, and exact worker-point query are documented under “Cross-page Flash-programming fixture” in the repository’s tools/notes/dynamic-tracing.md. [confirmed]
End-to-end garbage-collection trace
The generated GCFLASH program archives real variables A and B, unarchives A, and runs
GarbageCollect. The macro selects 2:Yes at the confirmation prompt. Dynamic coverage reaches
gc_command at 3C:71F8, archive_gc_collect at 3C:7733, and the boot erase body at
3F:4C2A. [confirmed]
The decoded GC window contains 4,630 Flash writes. They form 1,133 AMD byte-program commands, seven sector erases, 56 array-reset writes, and no unmatched command writes. The physical erases occur in this order: [confirmed]
| Target | Physical sector |
|---|---|
3E:6000 | 0xFA000–0xFBFFF |
0C:4000 | 0x30000–0x3FFFF |
3E:6000 | 0xFA000–0xFBFFF |
3E:4000 | 0xF8000–0xF9FFF |
08:4000 | 0x20000–0x2FFFF |
3E:4000 | 0xF8000–0xF9FFF |
3E:6000 | 0xFA000–0xFBFFF |
The page-0C erase and page-08 erase each cover four logical pages. The page-3E erases cover
one 8 KiB half each. The command sequence therefore directly confirms that the collector follows
the physical top-boot geometry rather than issuing one erase per 16 KiB paging unit. [confirmed]
The collector uses page 0C as the destination for the surviving B record, retires the old
record at 08:4016, erases the old page-08 sector, and marks page 08 as the next empty scratch
sector. It copies the used certificate tail between the two page-3E halves while persistent
phase bytes advance. [confirmed] See Variables, archive and unarchive
for the record bytes, sector-header states, journal fields, and recovery dispatcher.
tools/ti84re/trace/hardware.py exposes reusable resolved-instruction and resolved-memory-write
iterators. tools/ti84re/flash/trace.py decodes AMD commands and groups adjacent program runs. Their
focused CLIs reproduce the phase timeline without parsing the binary trace in a one-off script:
[confirmed]
python3 -m ti84re.flash.analyze_trace \
/tmp/tibasic-smoke/gcflash.trace \
--clock 321347460-344829074 \
--timeline
python3 -m ti84re.trace.analyze_points \
/tmp/tibasic-smoke/gcflash.trace \
--point page_3C:7733 \
--point page_3C:7cfb
The same decoded command stream can be replayed into immutable Flash images at
active journal phases. Cold TilEm boots now exercise all six phase-dispatch
branches. The 0xFF, 0xFE, 0xFC, 0xF8, and 0xE0 replays converge
byte-for-byte with uninterrupted execution. The 0xF0 replay has identical
archive bytes but completes certificate cleanup one boot earlier; cold-booting
the uninterrupted result once produces the same stable image. See
Variables, archive and unarchive
for the input and trace hashes, command counts, controlled-topology boundary,
and deferred-cleanup result. [confirmed] for TilEm.
Pinned Wabbitemu cold boots independently execute the same six dispatcher
branches. Complete output images equal the corresponding TilEm recovery
results. The record-authentic 0xF0 input is reconstructed from eight
deterministic program records before the unmodified OS materializes its journal
phase. See Variables, archive and unarchive
for input hashes, dispatcher visits, and changed-byte counts. [confirmed] for
the emulator command-boundary runs.
Reproduce the trace
Use the repository’s Nix environment when z80dasm or another analysis utility is not installed globally. The trace itself is large, so it is generated outside the repository. [confirmed]
TILEM=~/Git/tilem-headless/result/bin/tilem2
$TILEM --headless --rom tools/rom.bin --model ti84p --normal-speed --reset \
--macro tools/macros/archive-program.macro \
--trace /tmp/tilem-archive-program-success.trace --trace-range all
python3 -m ti84re.trace.resolve /tmp/tilem-archive-program-success.trace \
--initial-mapping ti84p-reset --coverage --sort addr \
--names tools/symbols/names.txt
nix develop -c z80dasm -a -t -g 0x4000 \
/tmp/ti84-page3f.bin
See tools/notes/dynamic-tracing.md for page-resolution details and trace-format caveats.
Emulator comparison
Flash emulator comparison records where the four inspected emulators agree with the ROM’s command bytes and sector boundaries, and where they diverge on illegal transitions, completion timing, status reads, and ASIC access control.
Quirks and unresolved hardware questions
- Page-guard rejection returns Z, while an accepted-page zero-length call returns NZ. The read-only TilEm fixture captures all three cases without a Flash write. Callers cannot interpret Z as proof that programming occurred. [confirmed]
- A locked
_WriteAByterequest can return Z under TilEm without changing the target when the requested and stored DQ7 bits already agree. Port0x02and the final array read confirm that the gate remained locked and the target remained0x50. Physical ASIC behavior remains unmeasured. [confirmed] for the ROM and TilEm trace; [hypothesis] for hardware. - The internal page-
3Dcertificate programmer returns the saved port-0x06page inAafter a DQ5 failure. Saved page zero therefore produces Z, and its only direct caller ignores the flags in every case. A guarded TilEm fixture reproduces the Z failure with an unchanged target. Physical DQ5 behavior remains unmeasured. [confirmed] for the ROM and TilEm trace; [hypothesis] for hardware. _EraseFlashPagealso rejects page3Ewith Z. The certificate-sector wrapper restores callerAFafter valid and invalid inputs, so it does not expose an erase result through flags. A guarded TilEm erase confirms this with a successful worker and an unchanged callerAF. [confirmed]_WriteFlash’s page-3Ecrossing behavior is byte-confirmed and dynamically reproduced in TilEm with an emulator-only patched-ROM fixture. It remains untested on a physical calculator. [confirmed] for the ROM and emulator trace; [hypothesis] for physical consequences._EraseFlash’s failure path uses undocumentedDEas a reset-command pointer. Two internal certificate paths leave a Flash address there, while the3D:60EEreset path leaves inheritedDE, the3D:71C3path carries metadata, and the public bcall accepts arbitraryDE. A forced physical DQ5 test withDEin RAM is still required. [confirmed] for the ROM paths; [hypothesis] for physical failure behavior.- The precise physical ASIC implementation of the protected-byte recognizer is represented here by WikiTI and TilEm behavior. The calculator schematic does not expose the ASIC’s internal state machine. [standard]
- Physical tests still need to measure legal and illegal byte-program status
reads, including a requested
0→1transition. Guarded native matrices pin the differing TilEm, Wabbitemu, and MAME results. None establishes physical behavior. [confirmed] for the pinned emulator runs; [hypothesis] for hardware. - The Fujitsu data sheet bounds byte program at 300 µs and sector erase at 10 s, with 8 µs and 1 s typical values. Calculator-level duration, DQ toggle cadence, erase-suspend behavior, and top-boot busy-read boundaries remain unmeasured. [standard] for the part limits; [hypothesis] for behavior on a particular calculator.
- Physical tests have not exercised chip erase, autoselect sector-protection reads, fast programming, or erase suspend/resume. Emulator agreement cannot fill those gaps because the pinned implementations disagree with the Fujitsu command table or omit the states. [hypothesis]
- The collector’s normal sector-copy policy and persistent phase dispatcher are reconstructed. TilEm cold-restart traces exercise all six ROM-written journal phases. Active
0xFF,0xFE,0xFC,0xF8, and0xE0converge byte-for-byte with uninterrupted execution. Active0xF0has matching archive bytes and converges after the uninterrupted result performs deferred0xE0cleanup on its next boot. A deterministic eight-record constructor reproduces the record-authentic0xF0input byte for byte. Pinned Wabbitemu independently executes all six dispatcher branches and produces the corresponding complete TilEm images. Cuts during busy commands and physical power loss remain untested. [confirmed] for the emulator command-boundary runs; [hypothesis] for the remaining cases. - Pinned Wabbitemu cold recovery takes the retail startup path from
00:0D73through the protected unlock at3D:60A6,gc_check_interruptedat3C:7BC7, public Flash bcalls and copied block workers, and the relock at3D:5CEF. All six phase images take this path. [confirmed] for Wabbitemu; [hypothesis] for physical gate behavior. - A controlled
_ReceiveOS_USBrun shows that_DisplayOSProgressprecedes validation of an installer record’s page byte. Seeding the already-displayed page to0x3Eimmediately before that helper isolates the downstream page validator: page0x3Ereaches2F:49A2, runs_USBErrorCleanup, and leaves the complete Flash array unchanged. This intervention does not establish the natural progress-byte behavior of a complete OS-install session. [confirmed] for the isolated Wabbitemu-core run; [hypothesis] for physical behavior.
Sources
| Source | Use |
|---|---|
| WikiTI certificate headers | literature labels for certificate-tail offsets, kept separate from ROM-derived ownership |
Wabbitemu 83psehw.c at 48c2dc0 | independent port-0x02 family-bit implementation |
WikiTI port 0x14 | Flash command lock and certificate read protection |
| WikiTI protected ports | privileged pages and protected-byte sequence |
WikiTI _WriteFlash and _WriteFlashUnsafe | public ABI and RAM-source requirement |
WikiTI _EraseFlash | sector-erase ABI and granularity warning |
WikiTI ports 0x21, 0x22, and 0x23 | chip selection and Flash execution limits |
| Datamath TI-84 Plus hardware and March 2004 PCB photograph | Fujitsu vendor identification and photographed 29LV800TA-70PFTN marking |
| Datamath memory-component index | reported AMIC, Fujitsu, Spansion, and Macronix compatible families |
Fujitsu MBM29LV800TA/BA data sheet, DS05-20845-4E | exact part organization, suffixes, command table, autoselect IDs, status bits, polling algorithm, timing, and endurance; audited 59-page PDF SHA-256 552a0ebc1de06b64507b7226e1d5bf4cebf8f61d6b5820e0cc796b1985186b19; former DatasheetArchive download URL returned 404 on 2026-08-09 |
TilEm flash.c, calcs.c, z80.c, x4_memory.c, x4_io.c, and x4_subcore.c | pinned commit f56ad637d0524ee841dd381be6ecbaf5b8975600; flash.c SHA-256 280e0e45b6e1f1ef21d779abb809eaef2d04d08db09feb87a459e079280c9545; emulator command state, ASIC gates, sector table, full reset, and exception ordering |
Wabbitemu core.c, core.h, and 83psehw.c | pinned commit 48c2dc0e6d1d87bb5cf9611efbeb0d048b19c422; file SHA-256 values 7e7552577b9934a8e344d0bea8152e2b46ddf6840e997e478723cfde7c170c2b, 6add613d150b55ffdabc8a784e1261b1fcac6e27f0519b1da835de4064b790ec, and 3acba050bde4df46348aac703899e2980efb24b5fec83f3f0b5940a47f8327c4; command state machine, erase geometry, and ASIC gates |
MAME intelfsh.cpp, intelfsh.h, ti85.cpp, and ti85_m.cpp | pinned tag mame0287; file SHA-256 values 8fb7e74656801c7939246c9bc77dceab3b36561df33d9ef4201f786eb6713da0, 42837497b8d3dfdcf1f1119168ae87bf4583c19238acf078c0efcf5dca1e64f9, 33d77ae3ffc373088202cf79d9979d2a9b715eb1f451122cfd764d1a911d75a1, and ae9f8986a80a4ea3ee00c801787f48edb0447880099612949c3429017d1cdedf; generic AMD device behavior and TI-84 Plus mapping |
jsTIfied project 42, deployed 20170706a artifact, and readable mirror at 56246a1 | deployed artifact SHA-256 c7325a38f976f64eaa34182da17d838fe4831eece4650b92d5db710cf7a8fc5b; fourth emulator implementation of geometry, commands, protection, and immediate mutation. The mirror aids review but is not byte-identical to the deployed artifact. |
Flash bcall programming guide
TI-84 Plus OS 2.55MP — the Flash bcalls a program can call, and what each one checks.
These bcalls expose the command workers. They do not provide the allocation,
battery policy, ownership checks, transaction journal, or gate management used
by the archive subsystem. A normal program that wants to archive or unarchive
a variable should use _Arc_Unarc rather than choose a raw Flash address.
Low-level calls are appropriate only when the caller owns the target region and
also owns the surrounding recovery policy. [confirmed] for the bcall behavior;
[standard] for using the public variable API.
Shared preconditions and hygiene
Every modifying bcall below has these caller obligations: [confirmed] for the ROM behavior unless marked otherwise.
- Open the protected port-
0x14gate before the call and close it on every exit. The Flash bcalls do neither operation. Code executing from ordinary RAM cannot satisfy the privileged-fetch sequence by copying its bytes into RAM. - Check battery state before opening the gate. The OS archive path calls
_Chk_Batt_Lowbefore its own Flash transaction, but the boot workers do not. - Establish ownership of the complete physical sector. Flash programming can only clear bits from one to zero. Restoring a zero bit to one requires an erase, which affects 64 KiB for ordinary pages and the smaller top-boot sectors shown under Sector geometry. [standard]
- Keep a recovery record outside the sector being changed if interruption must be survivable. The raw bcalls have no power-loss journal.
- Use
rst 28hwith the bcall ID. Do not call the page-3Fbody address. The raw write and erase cores reject a direct caller whose immediate return address is at or above0x8000. - Keep the stack, source, and destination buffers away from
0x8100–0x817B. The launcher overwrites that range with the block-program worker. The erase worker occupies0x8100–0x8151. The launcher also writes its saved IFF state at0x82A2. - Keep
IYat the OSflagsbase for the write calls. The accepted block path clears(IY+0x25).1; its unused low-source branch can set the same unnamed scratch bit._WriteAByteadditionally overwrites the first byte ofOP1at0x8478. - Treat
A,BC,DE,HL, flags,OP1, and the scratch locations above as clobbered when their selected path uses them. The launchers preserveIXand restore the interrupt-enabled state that existed on entry. - Validate the arguments before interpreting the result. Several rejected or
no-op paths return flags that resemble success. After a validated nonempty
call, require
A=0, then read back the complete programmed span or erased sector. A locked write can returnA=0, Z in TilEm without changing Flash.
The labels “safe” and “unsafe” describe only the page-3E software guard.
Neither safe entry checks the port-0x14 gate, physical protection, archive
ownership, destination address, length, battery, or power-loss state.
[confirmed]
Choosing a write bcall
| Need | Entry | Programmer-visible differences |
|---|---|---|
| Program a RAM block outside the certificate and boot pages | _WriteFlash | Rejects starting pages 3E and 3F; still requires complete span validation. |
Program a RAM block in certificate page 3E | _WriteFlashUnsafe | Permits starting page 3E; intended only for an owner of certificate update policy. |
| Clear bits in one ordinary byte | _WriteAByteSafe | Copies B through OP1; rejects pages 3E and 3F. |
| Clear bits in one certificate byte | _WriteAByte | Copies B through OP1; permits page 3E and rejects page 3F. |
The block worker expects DE in 0x4000–0x7FFF and a RAM source with
HL >= 0x8000. The ROM does not enforce either condition. For nonzero length
$n$, validate the final target before the call:
$$ p_{final} = p + \left\lfloor \frac{(DE - 0x4000) + n - 1}{0x4000} \right\rfloor $$
Also require DE in the banked window, ensure the RAM source plus $n$ does not
wrap, and keep the source outside the worker and scratch ranges. For
_WriteFlash, require every page through $p_{final}$ to stay below 3E.
Crossing from page 3D toward 3E does not stop cleanly: the worker wraps
DE to 0x4000 but leaves page 3D mapped. [confirmed]
_WriteFlash
_WriteFlash = 80C9h is the ordinary block entry. Inputs are A=page,
DE=destination, BC=length, and HL=RAM source. It masks the page with
0x3F, rejects page 3E, and then enters _WriteFlashUnsafe, which rejects
page 3F. A validated, nonempty successful call returns A=0, Z. A worker
failure returns A=0x3F, NZ. [confirmed]
On success, BC=0, while HL and DE point one byte beyond the source and
destination spans. On a program failure, HL and DE identify the failing
bytes and BC retains the decrement already performed by LDI. The page
guards are exceptional: they return nonzero A with Z. A zero-length accepted
call returns the masked page and NZ without launching the worker. [confirmed]
Use this entry only after validating the entire span. Its initial page check
does not protect a call that begins below page 3E and later crosses a page or
sector boundary.
The executable example programs two bytes from RAM at 08:4100:
ld a,$08
ld de,$4100
ld hl,writeflash_payload
ld bc,writeflash_payload_end-writeflash_payload
rst $28
.dw $80C9
or a
jp nz,flash_failed
The guarded runner seeds both target bytes to 0xFF, requires AF=0x0044,
and verifies A5 5A in both the Flash array and a _FlashToRam buffer.
[confirmed] for pinned Wabbitemu execution.
_WriteFlashUnsafe
_WriteFlashUnsafe = 8087h has the same block ABI and worker results as
_WriteFlash. It omits only the page-3E rejection. The core still masks
A to six bits, rejects page 3F, checks the call frame, and accepts a zero
length as a no-op. [confirmed]
The guarded retail-ROM usage probe calls this entry with A=0x3E, programs
3C C3 at 3E:4100, and reads the same pair back through _FlashToRam.
The bcall returns AF=0x0044. [confirmed] for pinned Wabbitemu execution.
ld a,$3E
ld de,$4100
ld hl,writeflashunsafe_payload
ld bc,writeflashunsafe_payload_end-writeflashunsafe_payload
rst $28
.dw $8087
or a
jp nz,flash_failed
“Unsafe” does not mean that the routine bypasses physical protection. Port
0x14, the port-0x21 sector group, and the Flash chip still decide whether
the command reaches and changes the array. Its page-3E access makes this
entry suitable for OS-owned certificate work, not for ordinary archive data.
[confirmed] for the software entry; [standard] for the hardware gates.
_WriteAByteSafe
_WriteAByteSafe = 80C6h takes A=page, DE=destination, and B=byte.
It masks the page, rejects page 3E, and falls into _WriteAByte. The shared
unsafe core later rejects page 3F. Its accepted path therefore has the same
page exclusions as _WriteFlash. [confirmed]
An early page-3E rejection leaves BC, DE, HL, and OP1 untouched.
Page 3F reaches the byte wrapper first, so that rejection has already stored
B at OP1, set BC=1, and set HL=0x8478. This difference matters to a
caller that tries to infer whether scratch state changed from the flags.
[confirmed]
The guarded retail-ROM usage probe exercises the accepted path at 08:4102.
It programs 0xFE → 0xFC, returns AF=0x0044, and obtains 0xFC through
_FlashToRam. [confirmed] for pinned Wabbitemu execution.
ld a,$08
ld de,$4102
ld b,$FC
rst $28
.dw $80C6
or a
jp nz,flash_failed
_WriteAByte
_WriteAByte = 8021h takes A=page, DE=destination, and B=byte.
It stores B in OP1, sets HL=0x8478 and BC=1, then enters
_WriteFlashUnsafe. It permits page 3E and rejects page 3F. A successful
call returns A=0, Z, BC=0, HL=0x8479, and DE one byte beyond the
target. OP1 retains the programmed byte. [confirmed]
The guarded retail-ROM usage probe calls this entry on page 3E, programs
0xFE → 0xF8 at 3E:4102, leaves OP1=0xF8, and returns AF=0x0044.
_FlashToRam returns 0xF8. [confirmed] for pinned Wabbitemu execution.
ld a,$3E
ld de,$4102
ld b,$F8
rst $28
.dw $8021
or a
jp nz,flash_failed
Use the byte entries for monotonic state changes such as 0xFE → 0xFC or
0xFC → 0xF0. A request that needs any 0→1 transition requires sector
erase and reconstruction. A requested byte with the same DQ7 as the stored
byte can produce false success when the ASIC gate blocks the command, so verify
the byte after every call. [confirmed] for the ROM and pinned TilEm result;
[standard] for NOR programming direction.
_EraseFlashPage
_EraseFlashPage = 8084h takes A=page. It masks the page to six bits,
chooses address 0x4000, rejects page 3E, and enters _EraseFlash. For page
zero it changes the address to 0x0000. It does not reject page 3F.
[confirmed]
The name refers to the logical page used to select a sector. It does not limit
the erase to 16 KiB. On an ordinary archive page, the command erases the
containing 64 KiB sector. Initialize DE to a writable Flash address in the
same mapping before the call; the DQ5 failure tail writes reset byte 0xF0
through undocumented DE. [confirmed] for the worker; [standard] for erase
geometry.
A successful erase returns A=0, Z. A worker failure returns A=0xF1, NZ.
The page-3E rejection instead returns A=0x3E, Z. Prevalidate the page,
check A=0, and verify the complete physical sector. [confirmed]
The guarded retail-ROM usage probe erases through page 0C, returns
AF=0x0044, and reads 0xFF back at 0C:4000. [confirmed] for pinned
Wabbitemu execution.
ld a,$0C
ld de,$4000
rst $28
.dw $8084
or a
jp nz,flash_failed
_EraseFlash
_EraseFlash = 8024h takes A=page and HL=an address in the selected sector. It performs no page mask or page guard. The worker maps A, issues a
sector-erase command through HL, and returns the same success or failure
values as _EraseFlashPage. BC, DE, and HL are otherwise retained by
the erase path, but failure can write 0xF0 to the address in DE.
[confirmed]
Choose this entry when the target must be an address other than the page start,
including a top-boot sector boundary. Set DE=HL defensively so the failure
reset targets Flash rather than arbitrary RAM. This convention avoids the
worker’s underspecified failure write; the public ABI itself does not require
or synthesize it. [confirmed] for the write; [standard] for the Flash reset
command.
The guarded retail-ROM usage probe passes HL=DE=0x4567 on page 10.
The bcall returns AF=0x0044, and _FlashToRam reads 0xFF from the same
interior sector address. [confirmed] for pinned Wabbitemu execution.
ld a,$10
ld hl,$4567
ld de,$4567
rst $28
.dw $8024
or a
jp nz,flash_failed
_EraseCertificateSector
_EraseCertificateSector = 8060h accepts any HL whose high byte is 0x40
or 0x60. It does not require L=0. It loads page 3E and calls
_EraseFlash, selecting one of the two 8 KiB certificate sectors. Other high
bytes return without work. [confirmed]
The wrapper restores the caller’s AF after both accepted and rejected calls.
It therefore hides worker success and failure as well as its own input
rejection. Preserve the certificate through its OS-owned rebuild protocol and
verify the selected sector; flags are not a result channel for this bcall.
[confirmed]
The guarded retail-ROM usage probe seeds AF=0xA545 and passes HL=DE=0x6001.
The returned AF remains 0xA545, while _FlashToRam reads 0xFF from
3E:6001. This dynamically exercises the accepted nonzero-L path and the
second 8 KiB certificate sector. [confirmed] for pinned Wabbitemu execution.
ld hl,$A545
push hl
pop af
ld hl,$6001
ld de,$6001
rst $28
.dw $8060
_SetFlashLowerBound bcall
_SetFlashLowerBound = 80CFh takes the new bound in A. The official name is
misleading on the TI-84 Plus: the body writes port 0x23, which is the upper
end of the modeled forbidden Flash-execution interval. It does not program or
erase the Flash array. Its complete body is: [confirmed]
3F:4784 nop
3F:4785 nop
3F:4786 im 1
3F:4788 di
3F:4789 out (0x23),a
3F:478B di
3F:478C ret
The leading bytes form the protected-port sequence. Flash must already be
unlocked for port 0x23 to accept the write. The routine preserves A, the
flags, and the other general registers. It selects interrupt mode 1 and leaves
maskable interrupts disabled. A caller must restore its prior interrupt-enable
state and must already accept IM1 as the OS interrupt mode. [confirmed] for
the routine; [standard] for the write gate.
The executable probe writes the boot default upper bound:
ld a,$2A
rst $28
.dw $80CF
The guarded runner requires port 0x23 = 0x2A and IFF2 clear after the call.
[confirmed] for pinned Wabbitemu execution.
This wrapper records IFF2 through LD A,I, calls the bcall, then conditionally
restores interrupts. POP AF also restores the caller’s original AF:
ld a,i
push af ; P/V records the prior IFF2 value
ld a,0x2A
rst 0x28
.dw 0x80CF ; _SetFlashLowerBound; returns with DI
pop af
jp po,interrupts_restored ; prior IFF2 was clear
ei
interrupts_restored:
The example assumes that trusted code already opened the protected-write gate and will close it. See Execution protection for the cross-emulator boundary comparison.
Return and side-effect matrix
| Path | Returned A and flags | Other visible state |
|---|---|---|
| block or byte program succeeds | A=0, Z | BC=0; HL/DE advanced; page worker ends on page 3F before bcall mapping restoration |
| block or byte program fails DQ polling | A=0x3F, NZ | HL/DE at failing byte; BC already decremented |
safe write rejects page 3E | A=0x3E, Z | wrapper-specific scratch changes described above |
unsafe core rejects page 3F | A=0x3F, Z | no worker; byte wrapper may already have changed OP1, BC, and HL |
| accepted zero-length block | masked page, NZ | no worker; write scratch bit unchanged |
| erase succeeds | A=0, Z | caller BC, DE, and HL retained by the erase worker |
| erase fails DQ polling | A=0xF1, NZ | writes 0xF0 through incoming DE |
_EraseFlashPage rejects page 3E | A=0x3E, Z | no worker |
_EraseCertificateSector returns | caller’s original AF | accepted and rejected cases are indistinguishable through flags |
This matrix explains why jr z,success is insufficient. Validate page,
address, span, and nonzero length first. Then test A=0 and verify the array.
Checking results
The executable examples above assume that trusted OS or boot code already
opened port 0x14, checked the battery, established sector ownership and
recovery state, and will relock Flash on every exit. Copying an unlock byte
sequence into RAM does not satisfy the ASIC’s privileged-fetch rule.
After a validated nonempty program or erase call, require A=0. Save the
target page and address before the call because the write worker advances
registers. Read the programmed span through _FlashToRam and compare every
byte with the source. For erase, inspect the complete physical sector, not
only the selected address. Every failure and success path must reach the
trusted owner’s relock and recovery epilogue. [confirmed] for the return and
clobber rules; [standard] for physical erase scope.
Reading back with _FlashToRam
_FlashToRam = 5017h, body 3D:6745, copies BC bytes from Flash at
A:HL to RAM at DE. It advances the mapped Flash page when HL crosses
0x8000 and restores the previous port-0x06 mapping after its RAM worker
returns. It does not need the Flash write gate for ordinary readable pages.
[confirmed]
The call consumes BC and advances HL and DE. It uses worker RAM beginning
at 0x8100 and page scratch at 0x9868, so a verification buffer must avoid
those locations while the copier runs. Locked certificate-page reads remain
subject to the ASIC’s separate read protection. [confirmed] for the ROM
scratch and worker; [standard] for the read gate.
The executable example reads back the complete two-byte _WriteFlash vector:
ld a,$08
ld hl,$4100
ld de,writeflash_copy
ld bc,writeflash_payload_end-writeflash_payload
rst $28
.dw $5017
Executable example validation
tools/probes/emulator/flash-bcall-usage.asm is the guarded executable form of
the examples above. It invokes _WriteFlash, _WriteFlashUnsafe,
_WriteAByteSafe, _WriteAByte, _EraseFlashPage, _EraseFlash,
_EraseCertificateSector, and _SetFlashLowerBound. It reads every changed
location through _FlashToRam. The six calls with result-bearing A values
branch to a failure loop unless A=0; the probe also stores every return so
the runner can check the complete result. [confirmed]
The nine short bcall call sequences on this page carry an
executable-snippet tag. The reusable tools/ti84re/wiki/executable_snippets.py parser
requires their text to match the same tagged regions in the assembled probe
byte for byte. The tools/ti84re/wiki/check_executable_snippets.py CLI exposes that
check. This catches documentation drift; Wabbitemu execution supplies the
runtime result below.
On 2026-08-10, the hash-guarded Wabbitemu adapter booted the exact OS 2.55MP
ROM, established the retail protection state, injected the 264-byte program
into RAM, and opened only Wabbitemu’s in-memory Flash gate. The run reached
every named public entry. The shared _WriteFlashUnsafe core ran four times,
the _WriteAByte body twice, and the _EraseFlash core three times because
their safe and specialized wrappers fall through or call into them. Seven
_FlashToRam calls brought the total to 14 RAM-worker entries. No execution
violation occurred. [confirmed] for this pinned emulator run.
| Observation | Guarded result |
|---|---|
_WriteFlash return and readback | AF=0x0044; array and copied bytes both A5 5A |
_WriteFlashUnsafe page-3E return and readback | AF=0x0044; array and copied bytes both 3C C3 |
_WriteAByteSafe return and readback | AF=0x0044; array and copied byte both FC |
_WriteAByte page-3E return, scratch, and readback | AF=0x0044; OP1=0xF8; array and copied byte both F8 |
_EraseFlashPage return and readback | AF=0x0044; 0C:4000 array and copied byte both FF |
_EraseFlash return and readback | AF=0x0044; 10:4567 array and copied byte both FF |
_EraseCertificateSector return and readback | caller AF=0xA545 preserved; 3E:6001 array and copied byte both FF |
| shared write scratch | (IY+0x25).1 clear after the accepted paths |
_SetFlashLowerBound result | port-0x23 upper bound 0x2A; IFF2 clear |
The assembly source SHA-256 was
ba91fa8a4d1d7c816b742a426dbb0216f927ec209f368534a13748d4683b42e7;
the assembled machine-code SHA-256 was
8f9ca5975c418871ba831c3536cba6e7e4f9f368520e1ad37650ef9c54d9249c.
See “Retail Flash bcall usage probe” in tools/notes/emulator-probes.md for the
guarded reproduction command. This execution validates the snippets against
the original ROM bodies under pinned Wabbitemu. It does not validate the
privileged port-0x14 sequence, allocation or journaling, interruption,
timing, or behavior of a physical Flash device.
Flash emulator comparison
TI-84 Plus OS 2.55MP — pinned TilEm, Wabbitemu, MAME, and jsTIfied Flash behavior beside the ROM.
The four inspected emulators agree on the command bytes and top-boot sector boundaries used by the ROM. They differ at the points most useful for negative tests: illegal bit transitions, completion timing, status reads, and ASIC access control. [standard]
| Behavior | TilEm f56ad63 | Wabbitemu 48c2dc0 | MAME 0.287 | jsTIfied 20170706a |
|---|---|---|---|---|
| Unlock addresses | low 12 bits 0xAAA, 0x555 | low 12 bits 0xAAA, 0x555 | accepts several AMD address conventions, including the ROM’s low-12-bit form | low 12 bits 0xAAA, 0x555 |
| Byte mutation | old &= requested | old &= requested | old = requested | old &= requested |
| Successful program | 7 µs real-time timer; 42 clocks at the 6 MHz reset speed | immediate array data | immediate array data | immediate array data |
Illegal 0→1 request | error state | one transient error read | writes the requested one bit | leaves the zero bit unchanged without an error state |
| Sector erase | 50 µs command window, then 200 ms erase timer; 300 and 1,200,000 clocks at 6 MHz | immediate | immediate data mutation followed by a timer | immediate; protected sector-table entries are skipped |
| Autoselect | incomplete | modeled AMD manufacturer 0x01, device 0xDA | IDs at offsets 0/1; no compatible protection read | manufacturer 0xC2 and device 0xDA; each recognized read exits ID mode |
| Chip erase | writable sectors only; final status follows the last sector | immediate full-array fill, including boot | immediate full-array fill; stale/default busy range | immediate erase of unprotected sector-table entries |
| Fast program | command flow present; fidelity unresolved | implemented for TI-84 Plus Flash version 3 | entry accepted, but A0 excludes the AMD maker ID | absent |
| Erase suspend/resume | absent | absent | absent | absent |
| CFI query | absent | absent | absent for AMD_29F800T | absent |
| Sector-protection autoselect read | unavailable with missing autoselect | offset 4 always returns zero | no data-sheet-compatible protection read | absent |
| ASIC write gate | protected-byte sequence, lock, and sector groups | privileged-page port-0x14 gate and boot-page flags | no effective Flash-write gate | protected-byte port-0x14 gate; sector flag affects erase but not program |
The table combines source results with guarded runtime checks described below.
MAME marks the complete TI-84 Plus driver MACHINE_NOT_WORKING. None of the
divergences resolves physical behavior. [standard] for the source models;
[confirmed] for the pinned runtime observations.
The emulator autoselect rows differ from the photographed Fujitsu part’s data
sheet, which specifies 0x04/0xDA. Matching device code 0xDA establishes a
compatible top-boot command family; manufacturer 0x01 does not identify the
photographed package. [standard]
TilEm behavior and limits
TilEm implements the same command progression used by the ROM: AA, 55, then A0 for program, or 80, AA, 55, 30 for sector erase. It matches command addresses by physical low 12 bits 0xAAA and 0x555. [standard]
Its program operation computes stored_byte &= requested_byte. A requested
0→1 transition leaves the zero bit unchanged and enters the emulator’s error
state. During program busy, DQ7 is complemented and DQ6 toggles. The delay
argument is 7 µs, not seven CPU cycles. TilEm’s real-time scheduler converts it
to 42 clocks at the 6 MHz reset speed. [standard]
During erase, DQ6 and DQ2 toggle. DQ3 distinguishes a 50 µs command window from the modeled 200 ms erase operation. Those deadlines are 300 and 1,200,000 clocks at 6 MHz. The ROM’s erase worker polls DQ7 and DQ5 rather than those toggle bits. [standard]
TilEm’s source comment lists fast program among unfinished work, but the state
machine implements part of it. AA 55 20 enters fast mode, A0 selects one
program operation, and the next write calls the ordinary byte-program helper.
The state then returns to fast mode. 90, followed by F0, exits. This is an
implemented command flow with unresolved hardware fidelity. Autoselect logs
that it is unimplemented; erase suspend and CFI have no states. [standard]
TilEm’s chip-erase path iterates over the sector table and calls erase only for
sectors accepted by its protection model. On a TI-84 Plus with default override
group zero, this skips physical 0xB0000–0xBFFFF and
0xFC000–0xFFFFF. Override group one admits those sectors. Each sector erase
resets the recorded program address and timer, so the final busy status
describes only the last writable sector. This differs from a single physical
chip-erase operation. [standard]
A guarded direct-core run exercises these states through
tilem_flash_write_byte and tilem_flash_read_byte. It seeds synthetic memory
and enables TilEm’s delay model. The timer deadlines come from the scheduler;
the fixture invokes the registered Flash callback directly to cross each
deadline without executing TI-OS. [confirmed]
| Program case | State after write | Busy reads | State after callback | Later reads |
|---|---|---|---|---|
legal FF → 50 | array read, program busy, 42-clock deadline | 80, C0 | array read, idle | 50 |
illegal 50 → D0 | error, program busy, 42-clock deadline | 00, 40 | error, idle | 20, 60 repeatedly |
The illegal request stores 0x50. Program-busy status takes priority over the
error state until the callback runs. The persistent error reads then set DQ5
and toggle DQ6. A following F0 write returns to array mode and reads 0x50.
This differs from Wabbitemu’s one-read error lifetime. [confirmed]
The sector case seeds all 65,536 bytes at physical 0x20000–0x2FFFF to
zero. The command changes all of them to 0xFF immediately and changes no byte
outside that range. Erase-window reads are 00, 44; erase-busy reads after
the first callback are 08, 4C; the second callback exposes array byte
0xFF. [confirmed]
| Chip-erase override | Changed bytes | Bytes left non-FF | Last program address |
|---|---|---|---|
| group 0 | 966,656 | 81,920 | 0xFA000 |
| group 1 | 1,048,576 | 0 | 0xFC000 |
Group 0 leaves 0xB0000–0xBFFFF and 0xFC000–0xFFFFF unchanged. Both
runs finish in array state with one 300-clock erase-window deadline for the
last admitted sector. The native matrix also confirms the partial fast-program
flow and its 90 F0 exit. Autoselect logs an unimplemented-command warning;
CFI query does nothing; B0 in the erase-command window logs an undefined
command and returns to array state without changing memory. [confirmed]
The native binary SHA-256 is
31f8e15a348d15f876f103b8452340484893987e458023fd913280365db5c51d.
The build requires clean TilEm commit
f56ad637d0524ee841dd381be6ecbaf5b8975600 and Git tree
58316afe35d69e69353f0f743698144153051d4a. These results describe the
pinned emulator core, not the retail ROM worker or physical Flash. Build and
run commands are under “Flash command and status matrix” in the repository’s
tools/notes/emulator-probes.md. [confirmed]
TilEm’s full calculator reset clears the Flash unlock gate, command state, and
busy flag. It retains the last program address and byte, toggle state,
protection-override group, and delay-emulation flags. An execution-protection
exception reaches this reset only after the forbidden opcode completes. A
guarded direct-core fixture executes LD (0x8000),A from restricted Flash page
08; its RAM write of 0x5A survives the reset. This ordering is TilEm
behavior, not evidence that the ASIC executes a denied instruction. [standard]
for source; [confirmed] for the pinned run. See
TilEm reset and exception scope.
Wabbitemu behavior and limits
Wabbitemu recognizes byte program, sector erase, chip erase, autoselect, and
fast-program commands. It applies program data with stored &= requested.
Successful programming returns to array mode immediately. [standard]
An illegal 0→1 request sets an error flag. The next read returns complemented
DQ7, set DQ5, and its current DQ6 toggle bit. That same read clears the error
flag, so later reads return array data. This one-read lifetime is Wabbitemu
behavior, not the hardware data-sheet polling contract. The ROM worker tests
DQ7 and DQ5 in that same first byte. [standard] for Wabbitemu source;
[confirmed] for the ROM worker.
Wabbitemu’s CPU_reset does not reset the Flash command step, error flag,
toggle bit, write byte, delay, lock, or bounds. Its opcode-fetch path separately
ends most non-read command states after an execution violation. A seeded
FLASH_PROGRAM violation therefore returns to array mode before executing one
boot instruction. A seeded FLASH_ERROR violation retains that command step;
the boot instruction’s immediate-byte read consumes status 0xE0 and clears
only the error flag. Both cases finish the same CPU_step at PC=0x0002.
[standard] for the source paths; [confirmed] for the guarded native run. See
Wabbitemu reset scope.
A guarded native run exercises seven byte pairs through Wabbitemu’s
CPU_mem_write and CPU_mem_read entry points. Each case issues AA 55 A0,
programs page 08 offset 0x0100, and reads the target twice. The harness
unlocks the in-memory ASIC gate directly and replaces the target’s initial byte
before the command. It does not execute the retail ROM worker. [confirmed] for
the pinned Wabbitemu run.
| Initial | Requested | Initial DQ6 | Stored | First read | Second read |
|---|---|---|---|---|---|
FF | 50 | 00 | 50 | 50 | 50 |
50 | 40 | 00 | 40 | 40 | 40 |
80 | 00 | 00 | 00 | 00 | 00 |
50 | D0 | 00 | 50 | 20 | 50 |
50 | D0 | 40 | 50 | 60 | 50 |
00 | 80 | 00 | 00 | 20 | 00 |
00 | 01 | 00 | 00 | A0 | 00 |
The first three requests are legal and expose array data immediately. The four
illegal requests set the error flag after programming initial & requested.
Their first read clears that flag and flips DQ6. Their second read exposes the
stored byte. All seven cases return to FLASH_READ; the initialized adapter
adds zero T-states for these accesses, so this run provides no timing evidence.
[confirmed] for the pinned Wabbitemu run.
The native binary SHA-256 is
67077107b604e97cfb751cadf4392dca53d00d5bbc417b2f48c422eebb9ac560.
It uses pinned commit 48c2dc0e6d1d87bb5cf9611efbeb0d048b19c422 and the exact
OS 2.55MP image. The guarded CLI checks every native field against the
fixed launch expectations and the independent Python source model before
writing its manifest.
The pinned Wabbitemu source and the ROM worker produce three paths. The first ROM read consumes Wabbitemu’s error status. The worker tests both DQ7 and DQ5 in that byte. Since Wabbitemu sets DQ5, every illegal request proceeds directly to one final array read. This table models emulator source combined with the byte-confirmed ROM poll logic. It does not describe physical Flash behavior. [standard] for Wabbitemu; [confirmed] for the ROM poll logic.
| Program request | Final stored DQ7 | ROM result |
|---|---|---|
| legal | matches requested DQ7 | succeeds on the first array read |
illegal 0→1 outside DQ7 | matches requested DQ7 | succeeds after the final read even though lower requested bits remain zero |
illegal DQ7 0→1 | differs from requested DQ7 | fails after the final read |
Exhaustive enumeration of all 65,536 old/requested byte pairs gives 49,152 successes and 16,384 failures. No pair is nonterminating under this composition. The successes contain 6,561 legal pairs and 42,591 illegal requests that the ROM reports as successful. These exhaustive counts are deterministic consequences of the pinned source model, not an exhaustive Wabbitemu run or hardware observation. [standard]
A second guarded mode boots the exact retail ROM, injects a four-byte
rst 28h/8087h harness into RAM page 1, and sets the documented
_WriteFlashUnsafe ABI registers. The bcall copies the 124 bytes beginning at
flash_program_worker_code to ramCode and executes them. The harness directly opens
Wabbitemu’s in-memory ASIC gate, so it does not test the protected port-0x14
unlock sequence or an OS/UI caller. [confirmed] for the pinned native run.
| Initial | Requested | Initial DQ6 | Worker reads | Stored | Result | AF |
|---|---|---|---|---|---|---|
FF | 50 | 00 | 50 | 50 | success | 0044 |
00 | 01 | 00 | A0, 00 | 00 | success | 0044 |
20 | A0 | 00 | 20, 20 | 20 | failure | 3F2C |
50 | D0 | 00 | 20, 50 | 50 | failure | 3F2C |
50 | D0 | 40 | 60, 50 | 50 | failure | 3F2C |
All five cases enter the copied worker once and issue one program write. The
legal request takes the success reset at ram:816B. The illegal lower-bit
request also takes that path after its final DQ7 read, despite leaving bit 0
clear. Both illegal DQ7 requests take the failure reset at ram:8175,
regardless of stored DQ5. DQ6 changes the first status byte but not the return
path. [confirmed] for the pinned native run.
The cold-recovery runner exercises a separate retail path without opening the
gate through emulator state. Startup at 00:0D73 calls the bjump stub at
00:3EEB, which resolves to 3D:6098. The bytes at 3D:609C–3D:60A8
write 1 to port 0x14; Wabbitemu changes flash_locked from true to false
at the OUT at 3D:60A6. The wrapper calls 00:2BAD at 3D:6101 to enter
gc_check_interrupted at 3C:7BC7. Its return path jumps to the lock sequence
at 3D:5CE6, and the OUT at 3D:5CEF changes flash_locked from false to
true. The static gate scanner classifies both sequences and finds no
unclassified port-0x14 candidate on page 3D. [confirmed]
All six reconstructed recovery images take that protected unlock → recovery →
relock path. The observer identifies the public block-program worker by
comparing all 124 bytes at ramCode with
flash_program_worker_code. Each
_WriteFlashUnsafe visit reaches one matching worker entry and one success
tail at ram:816B; no run reaches the failure tail at ram:8175. [confirmed]
| Input phase | _WriteFlashUnsafe / worker entries | Data writes at ramCode + 0x49 (ram:8149) | _EraseFlash entries |
|---|---|---|---|
0xFF | 33 | 48 | 3 |
0xFE | 32 | 47 | 3 |
0xFC | 20 | 20 | 4 |
0xF8 | 19 | 19 | 3 |
0xF0 | 304 | 65,560 | 3 |
0xE0 | 17 | 17 | 2 |
These counts cover the exact public block-program worker. Other internal RAM
workers can issue additional Flash commands during certificate rebuilding.
The run is Wabbitemu evidence for the retail control path, not physical ASIC
or Flash evidence. The observer binary SHA-256 is
242ca0d3ecab861ce1048285258d1e13ebc18a175bccf016397692fbe0f150db.
It uses pinned Wabbitemu commit
48c2dc0e6d1d87bb5cf9611efbeb0d048b19c422. [confirmed]
Sector erase changes the complete sector to 0xFF before the next instruction
and exposes no erase-busy interval. Its sector arithmetic matches the physical
64, 32, 8, 8, and 16 KiB top-boot layout. The two 8 KiB sectors are the halves
of page 3E. [standard]
Wabbitemu also implements chip erase by filling the complete Flash array with
0xFF, including page 3F, without consulting its per-write boot-page gates.
Its TI-84 Plus profile sets Flash version 3, which enables AA 55 20, repeated
A0 program operations, and 90 F0 exit. It has no erase-suspend or CFI state.
Autoselect offset 4 always returns zero, so it reports every sector as
unprotected. [standard]
A guarded native command-family run checks these source claims through
CPU_mem_write and CPU_mem_read. The adapter loads the exact OS 2.55MP image,
opens Wabbitemu’s in-memory gate, and keeps every mutation in the allocated
Flash array. It does not execute a retail-ROM Flash routine. [confirmed] for
the pinned Wabbitemu run.
| Command path | Native observation |
|---|---|
| Autoselect | AA 55 90 enters FLASH_AUTOSELECT; offsets 0, 2, and 4 return 01, DA, and 00 |
| Array reset | F0 returns autoselect and a partial AA sequence to FLASH_READ |
| Fast program | AA 55 20 enters FLASH_FASTMODE; two A0 operations store F0 & 50 = 50 and AA & A0 = A0, returning to fast mode after each |
| Fast-mode exit | 90 enters FLASH_FASTMODE_EXIT; F0 returns to FLASH_READ |
| Sector erase | AA 55 80 AA 55 30 changes all 65,536 seeded bytes at physical 0x20000–0x2FFFF to FF; no byte outside the range changes |
| Chip erase | AA 55 80 AA 55 10 reduces 322,043 non-FF bytes to zero and changes a seeded byte at physical 0xFFFFF from 00 to FF |
| CFI query | 98 from array mode returns to FLASH_READ and changes no byte |
| Erase suspend/resume | B0 in FLASH_ERASE_55 returns to FLASH_READ; the following 30 also leaves the array unchanged |
The sector test seeds the complete 64 KiB sector and two adjacent boundary
bytes before issuing the command. The chip test counts the complete 1 MiB
array and explicitly seeds its final boot-page byte. The adapter records zero
T-states for the direct calls, so the run provides no command-timing evidence.
Its binary SHA-256 is
41304b9a760438440f60cbfeca394cd37252c929ef2043e692c0254b8d1cb52d.
[confirmed] for the pinned Wabbitemu run.
Wabbitemu accepts a port-0x14 write only while the current Flash page passes
its privileged-page predicate. Its source names pages 2F, 3C, 3D, and
3F for the TI-84 Plus path; page 3E does not pass that predicate. A separate
write-validity check requires the resulting unlocked state and applies model
flags to boot-page writes. This approximates the ASIC gates but does not model
TilEm’s byte-fetch recognizer. [standard]
MAME behavior and limits
MAME 0.287 instantiates its generic AMD_29F800T device for the TI-84 Plus.
The device has a one-megabyte array, AMD manufacturer ID 0x01, device ID
0xDA, and top-boot sector geometry. [standard]
Its AMD autoselect path returns the configured maker ID at offset 0, device
ID at offset 1, and a fixed zero at offset 2. It does not implement the
Fujitsu byte-mode offsets 0, 2, and 4, including the sector-protection
read at offset 4. It has no CFI query state for AMD_29F800T and no AMD
erase-suspend state. The 0xB0 case in this source is a Sanyo-specific
bank-select command, not erase suspend. [standard]
Its 8-bit byte-program path assigns stored = requested. It does not apply NOR
AND semantics. A request to change a stored zero to one therefore succeeds in
MAME, and the first ROM poll reads the assigned byte with matching DQ7. The
program path has no timed busy mode or DQ5 failure state. [standard]
Sector erase fills the selected sector with 0xFF immediately, then enters a
timed status mode. MAME uses 1,000 ms for a 64 KiB sector, 500 ms for the 32 KiB
and 16 KiB sectors, and 250 ms for either 8 KiB sector. In-sector reads alternate
0x4C and 0x08, toggling DQ6 and DQ2 around a base DQ3 value. DQ5 stays clear.
When the timer expires, reads return the already-erased array. [standard]
The busy-read range has a separate geometry bug. MAME tests a 64 KiB interval
from m_erase_sector even when it erased a 32, 16, or 8 KiB top-boot sector.
Erasing the first page-3E half at 0xF8000, for example, returns erase status
for every read through 0xFFFFF until the 250 ms timer ends. Only the selected
8 KiB array region is changed. [standard]
Chip erase fills the complete array with 0xFF immediately and starts the
generic 16-second AMD_29F800T erase timer. The chip-erase branch does not set
m_erase_sector. Busy reads consequently use its stale or initial value and a
64 KiB interval even though the complete array has already changed. [standard]
MAME accepts AA 55 20 and sets its fast-mode flag. Its subsequent 0xA0
handler permits fast programming only for Fujitsu and selected ST maker IDs.
The 0x90 fast-exit transition uses the same maker-ID gate. The TI-84 Plus
instance uses maker ID 0x01 for AMD, so A0 logs an unknown mode byte and
90 F0 returns to normal reads without clearing the fast-mode flag. MAME
therefore has partial, not working, unlock-bypass support for this device
configuration. [standard]
The TI driver maps every Flash bank directly to the generic device’s read and
write methods. Port 0x14 stores m_flash_unlocked and updates paging, but no
memory-write path consults that value. The driver also omits ports 0x22–0x28.
MAME therefore accepts command writes without the protected byte sequence,
sector override, or execution-protection state used by the ROM. [standard]
The stored gate is a raw byte rather than a Boolean. A guarded sweep of writes
00 01 02 3F 40 FF makes port 0x02 return C3 C7 CB FF C3 FF, following
the driver’s truncated 0xC3 | (value << 2) expression. Port 0x14 itself
reads zero. A scheduled soft reset retains write one and consequently returns
0xC7; this is MAME reset behavior, not a physical lock-retention result.
[standard]
A separate guarded run maps Flash page 08 into the CPU’s 0x4000 window and
issues commands through CPU program space while reading the gate state through
I/O port 0x02. A complete program while locked reports C3 and changes the
target from FF to 50. A prefix started while locked and completed after an
unlock reports C7 and changes it to D0. A prefix started while unlocked and
completed after relocking reports C3 and changes it to 20. CPU reads and
direct generic-device reads agree after every case. [confirmed]
The saved image differs from the source only at 0x20100 (FF → 20) and has
SHA-256
2fd21a6b139a641d40a71a0e68df492e4555e79c6f1cf44858b4dcfd9158bbeb.
This CPU/I/O-space result confirms that MAME stores and exposes the port-0x14
state without applying it to mapped Flash writes. It describes MAME 0.287, not
the ASIC gate or physical Flash. [confirmed]
A guarded MAME 0.287 run exercises the ti84pv3 machine’s mapped :membank0
Flash interface through Lua. It uses the exact OS 2.55MP image but does not
execute TI-OS Flash code. The report oracle checks every field against the
pinned source model, and the image oracle compares the complete saved 1 MiB
array against the expected command mutations. [confirmed]
| Command or read | Runtime observation |
|---|---|
| Autoselect | offsets 0, 1, 2, and 4 return 01, DA, 00, and 00 |
| Byte program | FF → 50 stores 50; the illegal 50 → D0 request stores D0 |
| Array reset and CFI | F0 after a partial unlock restores array reads; 98 leaves the programmed D0 visible |
| Unlock bypass | AA 55 20 accepts the entry, but its A0 program does not change D0; 90 exposes manufacturer ID 01, and F0 restores array byte D0 |
| 8 KiB top-sector erase | the selected 0xF8000–0xF9FFF array range changes immediately; reads at 0xF8000, 0xFA000, and 0xFC000 expose busy status, while 0xE0000 remains an array read |
| Timer completion | selected and adjacent reads return FF, boot Flash returns 3E, and 0xE0000 returns 9F at frame 20 |
The saved Flash differs from the source ROM only at 0x20100 (FF → D0),
0xF8000 (00 → FF), and 0xF9FE0–0xF9FE1 (00 → FF). Its SHA-256 is
1dc4eec678252588df24118e96603b6c80806b8b9ea8e0e12b2169ac6aae3935.
The MAME executable SHA-256 is
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91.
These identities and the retained manifest scope the result to MAME 0.287,
not the retail worker or physical Flash. [confirmed]
A separate guarded run seeds each sector boundary and its adjacent probes with
00 before issuing five sector erases. Each command changes only its selected
array range. Busy reads cover 64 KiB from the selected start, including bytes
past the 32 KiB and 8 KiB sectors. [confirmed]
| Selected range | Source timer | Completion frame | Out-of-sector read while busy |
|---|---|---|---|
0xE0000–0xEFFFF | 1,000 ms | 50 | 0xF0000 = 00 |
0xF0000–0xF7FFF | 500 ms | 75 | 0xF8000 = 08 |
0xF8000–0xF9FFF | 250 ms | 88 | 0xFA000 = 08 |
0xFA000–0xFBFFF | 250 ms | 101 | 0xFC000 = 08 |
0xFC000–0xFFFFF | 500 ms | 126 | 0xFBFFE = 00 |
The frame deltas are 50, 25, 13, 13, and 25. At 50 frames/s, they match the
source timers. The 250 ms cases appear on the next whole frame. Every seeded
byte immediately outside the selected array range remains 00 after
completion. [confirmed]
Chip erase starts at emulated second 2 after the sector matrix. It immediately
changes the complete array to FF, while reads in the last sector’s stale busy
range return 4C and 08. The periodic state probe observes array reads at
second 18, exactly 16 emulated seconds later. The saved image contains no
non-FF byte and has SHA-256
f5fb04aa5b882706b9309e885f19477261336ef76a150c3b4d3489dfac3953ec.
[confirmed]
Reproducing the comparison
tools/ti84re/flash/hardware.py contains the photographed-device specification,
reported compatible families, sector table, source-modeled program rules, MAME
erase status, and the ROM worker’s DQ7/DQ5 decision. The focused CLI separates
physical and emulator identities and exposes negative cases without modifying
an emulator:
$ python3 -m ti84re.flash.describe_hardware parts
photographed part: Fujitsu MBM29LV800TA-70PFTN
package marking: 29LV800TA-70PFTN
board evidence: Datamath March 2004 TI-84 Plus PCB photograph
data-sheet autoselect: manufacturer=0x04 device=0xDA
rated byte program: 8 us typical, 300 us maximum
rated sector erase: 1 s typical, 10 s maximum
reported compatible families: AMIC A29L800A, Fujitsu 29LV800, Spansion S29AL008D, Macronix MX29LV800
$ python3 -m ti84re.flash.describe_hardware program --old 0x00 --data 0xFF
program old=0x00 requested=0xFF
TilEm: stored=0x00 poll=error state
Wabbitemu: stored=0x00 poll=one transient error-status read
MAME: stored=0xFF poll=array data
Wabbitemu error-read values (DQ6=0/1): 0x20 0x60
The Wabbitemu/ROM composition is available for one pair or as an exhaustive
summary. The single-pair model defaults the persistent DQ6 toggle bit to clear;
--dq6 selects a set bit. DQ6 changes the first read value but not the ROM’s
DQ7/DQ5 decision.
$ python3 -m ti84re.flash.describe_hardware wabbitemu-poll --old 0x50 --data 0xD0
Wabbitemu/ROM old=0x50 requested=0xD0 stored=0x50
read 0: DQ7/DQ5 poll=0x20 -> need-final-read
read 1: final DQ7 poll=0x50 -> failure
outcome: failure
$ python3 -m ti84re.flash.describe_hardware wabbitemu-poll
all byte pairs: 65536
outcomes: success=49152 failure=16384
legal successes: 6561
illegal requests reported successful: 42591
$ python3 -m ti84re.flash.describe_hardware mame-erase 0xF9000 --reads 4
sector 0x0F8000-0x0F9FFF, timer=250 ms
busy reads 0x0F8000-0x0FFFFF
status: 0x4C 0x08 0x4C 0x08
The command-capability matrix and structural ROM scan are available as JSON:
python3 -m ti84re.flash.describe_hardware --json commands
nix develop -c python3 -m ti84re.flash.analyze_rom_commands --json
The guarded MAME runtime probe requires the exact MAME binary hash and writes its command, input identities, report, complete NVRAM image comparison, and captured logs to a new output directory:
mame_flash_parent=$(mktemp -d /tmp/ti84-mame-flash.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_flash_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_flash_parent/run" --json
The CPU-visible gate probe uses the same guarded runtime and changes the gate between AMD command phases:
mame_gate_parent=$(mktemp -d /tmp/ti84-mame-gate.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_flash_gate_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_gate_parent/run" --json
The independent erase matrix uses the same guards and output contract:
mame_erase_parent=$(mktemp -d /tmp/ti84-mame-erase.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_flash_erase_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_erase_parent/run" --json
The parts, geometry, profiles, commands, poll, and wabbitemu-poll
subcommands support --json for scripts. tools/ti84re/flash/trace.py imports the
same geometry library, so dynamic trace reports and emulator comparisons use
one sector definition.
MD5 accelerator and boot API
TI-84 Plus OS 2.55MP — ASIC round assist, streaming digest bcalls, and Rabin hash transformation.
The TI-84 Plus ASIC evaluates one MD5 compression step through ports 0x18–0x1F. Retail boot code builds the complete streaming MD5 algorithm around that operation. This page reconstructs the port transaction, all 64 table-driven compression steps, the boot bcall state machine, the local counter-width quirk, and the separate _TransformHash operation used by application-signature code.
Evidence layers
The port block is internal to the ASIC, so public descriptions and emulator code cannot prove every electrical or timing detail. Claims below keep the evidence sources separate.
| Layer | Main evidence | What it establishes |
|---|---|---|
| Retail boot ROM | 3F:68ED–3F:6BF5 and 3F:723F–3F:72EA | bcall ABI, buffers, descriptor format, exact port order, padding, length accounting, and hash transformation [confirmed] |
| Dynamic execution | tools/tibasic-samples/MD5TEST.8xp, a complete resolved TilEm trace, and guarded TilEm, Wabbitemu, and MAME probes | 64 valid operations for MD5("abc"), implementing-emulator edge semantics, and MAME’s live unmapped-port behavior [confirmed] |
| Independent calculation | tools/ti84re/hardware/md5.py | every recorded result agrees with the 32-bit operation derived from the ROM and RFC 1321 [confirmed] |
| Public hardware notes | WikiTI port 0x18 and MD5 bcall pages | historical port and ABI descriptions checked against the local ROM [standard] |
| Emulator models | TilEm f56ad63, Wabbitemu 48c2dc0, and MAME 0.287 | shift-register policy, masking, reset policy, implemented undefined reads, and MAME’s missing port block [standard] |
| Algorithm specification | RFC 1321 | MD5 state, Boolean functions, constants, rotations, padding, and test vectors [standard] |
WikiTI is used as a comparison source, not as proof. For example, its _MD5Update page describes an eight-byte length, while the local routine updates only the low four bytes. Its _TransformHash page lists the four valid selectors, while the ROM maps every other nonzero low selector byte to the same branch as selector 3. [confirmed]
What the hardware accelerates
The port block does not hash a message or compress a 64-byte block by itself. It evaluates the arithmetic for one of MD5’s 64 steps: [confirmed] for the ROM transaction; [standard] for the algorithm identity.
$$ R = B + \operatorname{ROTL}_{32}\left(A + f(B,C,D) + X + T,\ s\right) \pmod {2^{32}} $$
The six 32-bit operands are A, B, C, D, one message word X, and the additive constant T. The rotate count is s. Mode 0–3 selects the Boolean function. [standard]
| Mode | MD5 name | Function |
|---|---|---|
0 | F | $(B \mathbin{\&} C) \mathbin{\vert} ((\mathop{\sim}B) \mathbin{\&} D)$ |
1 | G | $(B \mathbin{\&} D) \mathbin{\vert} (C \mathbin{\&} \mathop{\sim}D)$ |
2 | H | $B \mathbin{\oplus} C \mathbin{\oplus} D$ |
3 | I | $C \mathbin{\oplus} (B \mathbin{\vert} \mathop{\sim}D)$ |
All additions and the rotation operate on 32-bit words. The returned word is little-endian across the four read ports. [confirmed]
Port interface
Register map
Writes to 0x18–0x1D load six independent 32-bit serial registers. Four bytes are written to one port, least-significant byte first. Reads from 0x1C–0x1F expose the calculated word rather than the values last written to those ports. [confirmed] for the ROM’s use; [standard] for the public port contract.
| Port | Write behavior | Read behavior |
|---|---|---|
0x18 | serial input for A | undefined on physical hardware |
0x19 | serial input for B | undefined on physical hardware |
0x1A | serial input for C | undefined on physical hardware |
0x1B | serial input for D | undefined on physical hardware |
0x1C | serial input for message word X | result bits 7–0 |
0x1D | serial input for constant T, called AC by WikiTI | result bits 15–8 |
0x1E | rotate count s | result bits 23–16 |
0x1F | Boolean-function selector | result bits 31–24 |
The ROM writes every operand on every step. It does not depend on power-on contents or persistence from a previous operation. It writes the mode first, then A through T, then the rotate count, and immediately reads the result. There is no busy-bit poll or delay loop. [confirmed]
Both TilEm and Wabbitemu model each operand port as a four-byte sliding register: [standard]
on write byte v to operand register r:
r = (r >> 8) | (v << 24)
Four writes b0, b1, b2, b3 therefore leave r = b0 + 2^8 b1 + 2^16 b2 + 2^24 b3. A fifth write discards the oldest low byte in both emulators. Physical behavior after fewer or more than four writes has not been measured. [standard] for emulator behavior; [hypothesis] for the physical sliding-register implementation.
TilEm and Wabbitemu also mask a write to 0x1E with 0x1F and a write to 0x1F with 0x03. Both return zero for reads from 0x18–0x1B. The ROM uses only valid rotate counts and modes and never reads those four ports, so the local image cannot verify the masks or zero values. [standard]
Guarded native TilEm and Wabbitemu runs exercise these edge cases through their initialized-core port handlers. Neither adapter executes the retail MD5 routine. The Wabbitemu adapter initializes its core with the exact OS 2.55MP image; the TilEm adapter does not load a ROM. [standard]
| Case | Native result |
|---|---|
Fresh reads from 0x18–0x1B | 00 00 00 00; the fresh calculated result is 0x00000000 |
One byte 11 written to operand A | 0x11000000 |
Three bytes 11 22 33 | 0x33221100 |
Four bytes 11 22 33 44 | 0x44332211 |
Fifth byte 55 | 0x55443322; the former low byte 11 is discarded |
Raw shift and mode writes FF, FF | 0x00000004, matching shift 31 and mode 3 for operands 1 through 6 |
Reads from 0x18–0x1B after operand loads | 00 00 00 00 |
Mutate A between result-byte reads | old result 0xD6D117B4, new result 0x343F9701, assembled read 0x343F97B4 |
The mixed result retains the old low byte B4 read from port 0x1C, then
uses the new high bytes 97 3F 34 from ports 0x1D–0x1F. Both runs
therefore exercise read-time recalculation. Direct port calls add zero modeled
CPU clocks in both implementations. [standard]
Native TilEm confirmation. The TilEm run also reads the stored control
fields as shift 31 and mode 3 after raw 0xFF writes. A seeded calculator
reset clears all six operands, both controls, and the resulting word. Two
isolated builds produce binary SHA-256
b461e9720e0c304b26ab95ca814943eddfba670dd7bd1e41b48d53a0f8c689c5.
Their canonical native JSON has SHA-256
97921226800da92b585b6d16a390355c157bf9aa5976fe47d183e87bbcbad1b8.
[standard]
TilEm computes a zero-count rotation as
(result << s) | (result >> (32 - s)). With s = 0, the second operand shifts
a 32-bit value by 32, which C99 leaves undefined. The locked GCC build produces
the one-, three-, four-, and five-write results in the table. That observation
is a property of this binary, not a portable result guaranteed by TilEm’s C
source. [standard]
Native Wabbitemu confirmation. The Wabbitemu binary SHA-256 is
e5c64ec8630b0eaa9d42632ae8f559440678a567c00d0d1ce903fc99815afe81.
Its initialized-core report matches every table row and advances zero
T-states. [confirmed] for the pinned Wabbitemu run.
MAME’s TI-84 Plus I/O map has no handlers for ports 0x18–0x1F.
The ROM transaction therefore reaches unmapped I/O instead of an MD5 assist
block. MAME cannot execute the valid hardware-assisted compression path.
Its TI-84 Plus driver is marked MACHINE_NOT_WORKING. [standard]
A guarded MAME 0.287 run reads all eight ports through the main CPU’s I/O
address space. Initial reads return eight 00 bytes. Writes of eight distinct
patterns leave the same eight-zero readback. The probe then issues the first
padded-"abc" transaction from 3F:6A0F. Independent arithmetic expects
0xD6D117B4; MAME returns 0x00000000, and a final read of all eight ports
still returns zero. Two isolated runs reproduce the same report. [confirmed]
This zero is MAME’s runtime value for the unmapped accesses. It is not a register-reset value, an MD5 result from the calculator, or evidence about an electrical open bus. The run invokes MAME’s CPU I/O address space through Lua; it does not execute the retail bcall or physical hardware. [confirmed]
One ROM transaction
md5_assist_step at 3F:6A0F consumes one ten-byte descriptor through IX. The helper routines at 3F:6B7E–3F:6BDD emit four successive bytes from RAM for A through T. The routine then writes s and reads the result into the state word selected by descriptor byte 0. [confirmed]
The I/O sequence is fixed: [confirmed]
OUT 1F mode
OUT 18 × 4 A, little-endian
OUT 19 × 4 B, little-endian
OUT 1A × 4 C, little-endian
OUT 1B × 4 D, little-endian
OUT 1C × 4 X, little-endian
OUT 1D × 4 T, little-endian
OUT 1E s
IN 1C, 1D, 1E, 1F R, little-endian
This is 30 I/O instructions per MD5 step. One compression block executes 64 steps and therefore produces 1,920 port events: 1,664 writes and 256 reads. [confirmed]
The first operation in the "abc" trace uses: [confirmed]
| Operand | Value |
|---|---|
A | 0x67452301 |
B | 0xEFCDAB89 |
C | 0x98BADCFE |
D | 0x10325476 |
X | 0x80636261 |
T | 0xD76AA478 |
s | 7 |
| mode | F |
| result | 0xD6D117B4 |
X = 0x80636261 is the first little-endian message word: ASCII 61 62 63 followed by the 0x80 padding byte. Independent evaluation of the formula returns 0xD6D117B4. [confirmed]
Immediate result and timing boundary
The boot routine reads 0x1C on the instruction following the rotate-count output. This proves that software does not initiate a separate operation or wait for completion. It does not establish whether the physical ASIC is combinational, completes within the I/O cycle, or inserts an internal wait state. [confirmed] for instruction order; [hypothesis] for the physical circuit.
TilEm and Wabbitemu recalculate the full result on every read. Mutating an operand between result-byte reads can therefore create a word assembled from different calculations in those emulators. The ROM never does this. Physical result latching has not been tested. [standard] for emulator behavior; [hypothesis] for hardware.
Boot-page MD5 API
The retail boot bcall table exposes three streaming routines. A bcall ID is the word after rst 28h; the body executes on page 3F. [confirmed]
| Bcall | ID | Body | Input | Main output |
|---|---|---|---|---|
_MD5Init | 808D | 3F:68ED | none | initial state and zero bit length |
_MD5Update | 8090 | 3F:6907 | HL data, BC byte count | buffered input and updated state |
_MD5Final | 8018 | 3F:6964 | initialized state | padded final digest |
The caller invokes _MD5Init once, _MD5Update zero or more times, and _MD5Final once. _MD5Update accepts a 16-bit byte count per call and can process many complete blocks. [confirmed]
RAM state
The API uses fixed system RAM rather than a caller-owned context structure. Two independent hashes cannot be interleaved without copying this state. [confirmed]
| Address | Size | Meaning |
|---|---|---|
0x8259 | 16 | working words copied from the current state |
0x8269 | 8 | message length in bits; only the low four bytes are updated |
0x8291 | 1 | compact-big-integer length prefix written by _MD5Final |
0x8292 | 16 | state words and final digest bytes |
0x83A5 | 64 | partial or current message block |
The official equates call these regions MD5Temp, MD5Length, MD5Hash, and MD5Buffer. _TransformHash later reuses MD5Buffer for a different compact-big-integer value. [confirmed]
The final length prefix and digest form one typed result at
compactHashLength (0x8291):
typedef struct {
uint8_t length;
uint8_t bytes[16];
} CompactHashResult;
compactHashLength.length aliases the one-byte prefix, and
compactHashLength.bytes aliases MD5Hash at 0x8292. _MD5Final may trim
the prefix, but it leaves all 16 bytes in the array. [confirmed]
Initialization
_MD5Init copies 16 bytes from 3F:6615 to MD5Hash: [confirmed]
01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10
Read as four little-endian words, these are the standard initial state: [standard]
| Word | Value |
|---|---|
A | 0x67452301 |
B | 0xEFCDAB89 |
C | 0x98BADCFE |
D | 0x10325476 |
The following eight bytes in ROM are zero, and the same LDIR sequence copies them to MD5Length. The routine preserves the caller’s HL, BC, and DE with stack saves. It does not preserve flags as a distinct API result. [confirmed]
Buffer index
md5_buffer_index at 3F:694F derives the next byte position from the low 16 bits of the bit counter: [confirmed]
$$ i = \left(\frac{\text{bitLength}}{8}\right) \bmod 64 $$
Only counter bits 3–8 affect this value, so reading two bytes is sufficient even though the nominal counter occupies eight bytes. _MD5Update computes i before adding the new call’s length. [confirmed]
Length accounting and the 32-bit wrap quirk
_MD5Update expands BC to four bytes at 0x8251, shifts that temporary left three times, and adds exactly four bytes to MD5Length with the helper at 3F:6592. The helper stops after address 0x826C. It does not propagate carry into 0x826D–0x8270. [confirmed]
The implemented update is therefore: [confirmed]
$$ L_{\mathrm{new}} = (L_{\mathrm{old}} + 8,BC) \bmod 2^{32} $$
The high four bytes remain zero after _MD5Init. Standard MD5 appends the message length modulo $2^{64}$, so this boot implementation diverges once cumulative input reaches $2^{32}$ bits, or 512 MiB. A single call cannot reach the boundary because BC is 16-bit, but repeated calls can. WikiTI describes all eight bytes as holding the length and does not identify this local implementation quirk. [confirmed]
Streaming copy and block compression
After updating the length, _MD5Update resumes with the saved HL and BC. It copies bytes into MD5Buffer+i. On reaching byte 64, it calls md5_compress_block at 3F:699A, resets the index to zero, and continues with any remaining source bytes. A call ending mid-block returns with that prefix retained for the next update. [confirmed]
A zero-length call returns from the copy loop without changing the buffer or state. It still executes the index and zero-add setup first. [confirmed]
The routine does not allocate memory and has no bounds metadata for HL. A caller that supplies a range crossing unmapped or repaged memory receives ordinary Z80 memory behavior. The public ABI’s pointer and count are the only input boundary. [confirmed]
Table-driven compression
md5_compress_block copies the current four state words from MD5Hash to MD5Temp. It then executes four loops of 16 descriptors before adding the original state into the working state. [confirmed]
| Round | Descriptor base | Mode | Message-word index | Rotate cycle |
|---|---|---|---|---|
| 1 | 3F:662D | F | $g(j)=j$ | 7, 12, 17, 22 |
| 2 | 3F:66CD | G | $g(j)=(5j+1)\bmod16$ | 5, 9, 14, 20 |
| 3 | 3F:676D | H | $g(j)=(3j+5)\bmod16$ | 4, 11, 16, 23 |
| 4 | 3F:680D | I | $g(j)=7j\bmod16$ | 6, 10, 15, 21 |
Each descriptor occupies ten bytes: [confirmed]
| Offset | Size | Meaning |
|---|---|---|
| 0 | 1 | MD5Temp offset for operand A and result destination |
| 1 | 1 | offset for B |
| 2 | 1 | offset for C |
| 3 | 1 | offset for D |
| 4 | 1 | byte offset of the 32-bit message word in MD5Buffer |
| 5 | 1 | rotate count s |
| 6 | 4 | additive constant T, little-endian |
The first descriptor is: [confirmed]
00 04 08 0C 00 07 78 A4 6A D7
It selects the working words at offsets 0, 4, 8, and 12, message word 0, rotation 7, and T=0xD76AA478. The next descriptors rotate the destination offsets through 12, 8, and 4. The table bytes reproduce all standard MD5 word schedules, rotation counts, and constants. [confirmed]
After all 64 operations, 3F:69D9 adds the four original words saved at MD5Temp into the four working words at MD5Hash. This is the MD5 compression feed-forward step. [confirmed]
Why the descriptor table matters
The table separates algorithm data from the I/O driver. The four round wrappers at 3F:69FD, 3F:6A02, 3F:6A07, and 3F:6A0C differ only in the mode written to 0x1F. md5_assist_step handles all operand selection and result placement. Changing one table row would change one message index, rotation, or constant without changing the port code. [confirmed]
The local boot page therefore supplies almost the entire MD5 control structure in software. The ASIC replaces the Boolean expression, five-word addition, rotation, and final addition for one step. It does not replace block scheduling, state rotation, feed-forward, buffering, or padding. [confirmed]
Finalization
_MD5Final obtains the current buffer index i and chooses the number of padding bytes needed to stop at byte 56: [confirmed]
$$ p = \begin{cases} 56-i, & i < 56 \\ 120-i, & i \ge 56 \end{cases} $$
The padding source at 3F:68AD begins with 0x80 and continues with zeros. Finalization enters the copy loop at 3F:692B rather than calling the public _MD5Update entry. Padding therefore does not change the saved message length. If the current index is 56 or greater, this copy compresses one block and continues padding into a second. [confirmed]
The routine then copies the original eight-byte MD5Length to MD5Buffer+56 at 0x83DD and compresses the final block. The high four length bytes are normally zero because of the 32-bit accounting quirk. [confirmed]
WikiTI warns that early boot versions mishandle messages whose byte length is 55 modulo 64. The local boot 1.03 body does not have that bug: 3F:6968 takes the short branch for index 55 and computes one padding byte. The warning remains relevant to the named older boot versions, not to this ROM. [confirmed] for the local branch; [standard] for the historical report.
Digest bytes and the compact result prefix
After compression, _MD5Final writes 16 to
compactHashLength.length (0x8291) and jumps to the compact-integer trimming
helper at 3F:7014. That helper decreases the prefix while the
highest-address digest bytes are zero. It does not move or rewrite
compactHashLength.bytes at 0x8292. [confirmed]
Consumers needing the MD5 byte string should always read all 16 bytes from MD5Hash. The byte order in RAM is the conventional digest byte order. For "abc" it is: [confirmed]
90 01 50 98 3C D2 4F B0 D6 96 3F 7D 28 E1 7F 72
Written as hexadecimal, this is the RFC 1321 vector 900150983cd24fb0d6963f7d28e17f72. The prefix exists for boot code that treats the same bytes as a little-endian integer. [confirmed]
Dynamic "abc" trace
The asmmd5 smoke case executes a short assembly payload through TI-BASIC’s Asm( command. The payload calls _MD5Init, passes three bytes at HL with BC=3 to _MD5Update, calls _MD5Final, and returns. The macro preserves a ram-logical dump after the program. [confirmed]
The smoke runner checks both visible execution and the 16 bytes at dump offset 0x0292, corresponding to logical MD5Hash at 0x8292. This prevents bcall coverage alone from counting as a successful digest test. [confirmed]
Run the fixture and then decode its accelerator operations: [confirmed]
nix develop -c python3 -m ti84re.tibasic.smoke \
--tilem /path/to/headless/tilem2 \
--case asmmd5 --keep-trace
nix develop -c python3 -m ti84re.hardware.analyze_md5_trace \
/tmp/tibasic-smoke/asmmd5.trace \
--initial-mapping ti84p-reset \
--expect-steps 64
nix develop -c python3 -m ti84re.trace.inspect_ram_dump \
/tmp/md5-abc.ram --address 0x8292 \
--expect 900150983cd24fb0d6963f7d28e17f72 \
--name MD5Hash
The decoder reports 64 complete operations, 16 in each mode. It reconstructs every operand from the actual little-endian writes and compares every read word with an independent calculation in tools/ti84re/hardware/md5.py. All 64 match. [confirmed]
| Event | Per step | Whole block |
|---|---|---|
four-byte operand writes to 0x18–0x1D | 24 | 1,536 |
| mode and rotate writes | 2 | 128 |
| result-byte reads | 4 | 256 |
| total | 30 | 1,920 |
The resolved port instructions execute at the named helpers on page 3F. The first mode write is at 3F:6BE4, operand writes span 3F:6B7F–3F:6BDB, the rotate write is at 3F:6BDF, and reads are at 3F:6A66–3F:6A72. [confirmed]
_TransformHash is a separate operation
_TransformHash = 80A5 has body 3F:723F. It performs compact-big-integer preparation for Rabin application-signature verification. It does not call _MD5Init, _MD5Update, _MD5Final, md5_compress_block, or any MD5-assist port helper. [confirmed]
Compact integer representation
The routine reads compactHashLength.length followed by the little-endian
digest bytes in compactHashLength.bytes. It constructs this value at
MD5Buffer: [confirmed]
$$ m = 256 \times \operatorname{integer}(\text{digest bytes}) + 1 $$
The output byte layout is: [confirmed]
MD5Buffer+0 = digest_length + 1
MD5Buffer+1 = 0x01
MD5Buffer+2… = digest bytes, low byte first
The leading data byte 0x01 makes the represented integer odd and nonzero. It is unrelated to MD5 padding. [confirmed]
The modulus n begins at 0x8000 in the same compact format. The selector f begins at 0x83E6. A zero length represents selector 0. For a nonzero length, the ROM reads only the first payload byte at 0x83E7. [confirmed]
Selector branches
The byte branches at 3F:7261–3F:7297 and the subtractor at 3F:7299 implement: [confirmed]
| Selector representation | Output under the valid signing preconditions |
|---|---|
| zero length | $n-2m$ |
nonzero, first byte 1 | $n-m$ |
nonzero, first byte 2 | $m$ |
| nonzero, any other first byte | $2m$ |
Valid certificate data uses selectors 0 through 3, which gives the four transformations described by WikiTI. The final ROM branch is broader than f=3: malformed values 4–255, and multi-byte values whose low byte is neither 1 nor 2, also take the 2m path. [confirmed]
The doubling paths call bigint_modular_multiply at 3F:6D2C with a compact constant 2 stored at 0x8144. That engine uses the modulus at 0x8000. For valid signature parameters, m is much smaller than n, so the modular result is the ordinary 2m used in the table. [confirmed] for the call and buffers; [standard] for the signing precondition.
transform_hash_subtract_modulus at 3F:7299 copies the intermediate value, then subtracts it byte by byte from n from low address to high address while propagating borrow. It trims high zero bytes before returning. This directly verifies the n-m and n-2m interpretation rather than relying on the bcall name. [confirmed]
Output guard and malformed inputs
After a multiply or subtraction, 3F:7270 reads the compact result length at 0x86EC. A length of 0x41 or greater returns without copying that result to MD5Buffer. Lengths below 65 are copied there. Selector 2 returns earlier because MD5Buffer already contains m. [confirmed]
The routine has no explicit error code for an oversized result, malformed selector length, or inconsistent input buffers. Its normal callers supply certificate structures constrained to the 64-byte arithmetic workspace. Callers outside that context must not treat every return as a validated transformation. [confirmed]
Relationship to _SigModR
_SigModR = 80A2 at 3F:7225 copies the same input integer into both multiplication operands, invokes bigint_modular_multiply, and returns the modular square. Signature verification can compare that square with the transformed hash. _TransformHash itself does not perform the comparison or square a signature. [confirmed]
This separation matters when tracing ports: only the earlier MD5 compression routines touch 0x18–0x1F. The signature transformation and modular square are software big-integer operations on page 3F. [confirmed]
Emulator comparison and fidelity limits
| Behavior | TilEm f56ad63 | Wabbitemu 48c2dc0 | MAME 0.287 | jsTIfied 20170706a |
|---|---|---|---|---|
Ports 0x18–0x1F | mapped | mapped | absent; live reads return 00 | mapped |
| Operand writes | six 32-bit sliding registers | same | unmapped | implemented |
| Control writes | shift masked to five bits; mode masked to two | same | unmapped | implemented |
| Result reads | recalculated on each read from 0x1C–0x1F | same | unmapped; live reads return 00 | implemented |
Reads from 0x18–0x1B | zero | zero | unmapped; live reads return 00 | modeled by the port block |
| Reset and state | fields cleared on reset and serialized | fields serialized | no MD5 state | emulator fields are reset and serialized |
| Driver status | usable implementation | usable implementation | TI-84 Plus marked MACHINE_NOT_WORKING | browser emulator source model |
TilEm and Wabbitemu agree on the implemented behaviors below: [standard]
- six 32-bit operand registers;
- byte writes implemented as right shift plus insertion at bit 24;
- rotate count masked to five bits;
- mode masked to two bits;
- result bytes read from
0x1C–0x1F; - reads from
0x18–0x1Breturning zero; - immediate calculation with no busy state or modeled latency.
TilEm explicitly clears all six operands, the rotate count, and the mode on calculator reset. Its save-state format serializes every field. The guarded direct-core run reproduces the complete reset clearing. Wabbitemu also serializes these fields, but the local ROM writes them all before use. [standard]
Agreement between two emulators is useful corroboration, not independent physical measurement. Both projects may derive edge behavior from the same public notes. The dynamic trace proves that TilEm handles the ROM’s valid transaction correctly and produces the standard digest. It does not prove invalid-write behavior on a TA2 or TA3 ASIC. [confirmed] for the exercised path; [hypothesis] for unmeasured hardware edges.
MAME’s source map and guarded runtime agree that the driver omits the block. The runtime’s all-zero reads explain why a valid assist transaction yields zero there. This evidence applies only to MAME 0.287. It does not weaken the ROM trace or imply that a physical TI-84 Plus lacks the accelerator. [confirmed] for the guarded MAME run; [standard] for the driver source.
Reusable implementation model
tools/ti84re/hardware/md5.py separates the independent arithmetic and trace decoder
from pinned emulator I/O profiles. Its shared edge-case oracle derives both
implementing-emulator reports. Md5AssistImplementation models the six
sliding registers, control masks, read-time recalculation, undefined operand
reads, and unmapped MAME writes. [standard]
The comparison CLI defaults to the first compression step for "abc" and
accepts replacement operands, mode, and rotate count. Its JSON report keeps
unmapped ports distinct from a numeric read value:
nix develop -c python3 -m ti84re.hardware.describe_md5
nix develop -c python3 -m ti84re.hardware.describe_md5 --json
nix develop -c python3 -m ti84re.hardware.describe_md5 \
--profile tilem --mode 3 --shift 0x1F --a 0x01234567
TilEm and Wabbitemu return 0xD6D117B4 for the default step. The MAME profile
reports all eight ports as unmapped rather than assigning a portable open-bus
byte. tools/ti84re/emulators/mame/md5.py separately parses and validates MAME 0.287’s observed
zero reads against the pinned I/O map and the independent arithmetic model.
The guarded TilEm CLI validates the exact source commit, Git tree, and native binary. It records the shared edge matrix, reset state, modeled clock delta, compiler-specific binary identity, and physical-scope exclusion:
tilem_md5_tmp=$(mktemp -d /tmp/ti84-tilem-md5.XXXXXX)
git clone https://github.com/debrouxl/tilem.git "$tilem_md5_tmp/tilem"
git -C "$tilem_md5_tmp/tilem" checkout \
f56ad637d0524ee841dd381be6ecbaf5b8975600
nix shell \
github:NixOS/nixpkgs/f13ff45afd1bb73e640eaa08a7066dbed07e3238#gcc \
--command python3 -m ti84re.emulators.tilem.build_probe --probe md5 \
--source "$tilem_md5_tmp/tilem" \
--output "$tilem_md5_tmp/tilem-md5-probe" --json
tilem_md5_parent=$(mktemp -d /tmp/ti84-tilem-md5-report.XXXXXX)
python3 -m ti84re.emulators.tilem.run_md5_probe \
--binary "$tilem_md5_tmp/tilem-md5-probe" \
--expected-binary-sha256 \
b461e9720e0c304b26ab95ca814943eddfba670dd7bd1e41b48d53a0f8c689c5 \
--output-dir "$tilem_md5_parent/run" --json
The guarded CLI requires the exact MAME executable hash and OS 2.55MP image. It retains the native output, error log, input identities, and parsed report:
mame_md5_parent=$(mktemp -d /tmp/ti84-mame-md5.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_md5_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_md5_parent/run" --json
Security context
MD5 no longer provides collision resistance and should not be selected for a new security design. The calculator’s boot code uses it as one component of a historical application-signature format. This page describes compatibility behavior, not a recommendation to use MD5 for modern authentication. [standard]
The accelerator does not make the hash construction stronger. It only reduces the Z80 work required for the 64 compression steps. The 32-bit length-wrap quirk further distinguishes this boot API from a general RFC-conforming implementation for very large inputs. [confirmed]
Open physical tests
The ROM, trace, and two implementing emulator models close the valid software path. MAME omits the block. These ASIC questions require a physical TI-83 Plus Silver Edition or TI-84 Plus test harness:
- read
0x18–0x1Bafter reset and after operand writes; - write one, three, four, and five bytes to one operand and recover its effect through a controlled calculation;
- test high bits in mode and rotate-count writes;
- change an operand between result-byte reads to determine whether the result is latched;
- measure whether reads or writes add wait states at 6 MHz and 15 MHz;
- determine reset and low-power retention across TA2 and TA3 revisions;
- compare port availability on standard TI-83 Plus, Silver Edition, and TI-84 Plus ASICs.
The physical hardware probe records undefined reads, a fifth operand write, high control bits, and a mid-read mutation in a versioned AppVar. Its calculator-side source and host decoder are prepared, but no physical result is recorded. [confirmed] for the probe bytes; [hypothesis] for all pending hardware results.
A calculator schematic can identify the ASIC revision and external buses, but it cannot expose this internal datapath. Logic-level tests must infer the remaining behavior through I/O instructions and cycle measurements. [hypothesis]
Sources
| Source | Use |
|---|---|
| RFC 1321 | MD5 algorithm, padding, constants, and test vectors |
WikiTI ports 0x18–0x1F | historical port register description, checked against ROM and emulators |
WikiTI _MD5Init, _MD5Update, and _MD5Final | public ABI and historical finalization-bug report |
WikiTI _TransformHash | historical Rabin transformation description, checked and narrowed against 3F:723F |
TilEm md5.c and x4_io.c | emulator arithmetic, shift registers, masks, reads, and reset |
Wabbitemu 83psehw.c | second emulator implementation of the same port block |
MAME 0.287 ti85.cpp | TI-84 Plus I/O map, absent MD5 ports, and driver status |
jsTIfied deployed 20170706a artifact and readable mirror | fourth implementation of the ports 0x18–0x1F arithmetic block |
| Datamath TI-84 Plus hardware | calculator hardware and ASIC identification context |
Flash page map
This page maps the contents of all 64 physical 16 KiB Flash pages. OS code
occupies pages 00–07 and 33–3D; page 2F contains retail USB boot
support, page 3E holds two certificate sectors, and page 3F is the retail
boot page. Pages 08–2E and 30–32 are blank in this image. [confirmed]
On a retail unit, the blank range stores archived variables and can carry Flash Apps. Flash memory describes physical erase-sector geometry and the dynamic archive/App boundary. The page roles below use resolved bcall targets and per-page function counts.
OS pages (carry bcall entry points)
| Page | Funcs | Role | Representative routines |
|---|---|---|---|
00 | 928 | Kernel — mapped at 0x0000; RST vectors, bcall dispatcher, FP core, VAT, memory, integer math | _JErrorNo, _LdHLind, _DivHLBy10, _FindSym, _FPAdd, _InsertMem |
01 | 84 | Text display / homescreen | _PutMap, _PutC, _PutS, _DispHL, _NewLine, _ClrLCDFull |
02 | 271 | Float transcendentals & advanced math | _SqRoot, _LnX, _RnFx, _RndGuard |
03 | 23 | Edit-buffer / small font | _CloseEditBufNoR, _Load_SFont, _SFont_Len |
04 | 66 | Graph drawing (pixel/line) | _DarkLine, _ILine, _IPoint, _DarkPnt |
05 | 118 | TABLE editor + Graph-Table split-screen | table_editor_main, table_recompute, table_paint_grid |
06 | 49 | Key input & edit/cursor | _GetKey, _CursorOn, _CursorOff, _PutTokString (note _GetCSC’s body is on page 00) |
07 | 44 | Archive / list & matrix ops; error messages; large-font glyph table @ 07:45FF (7-byte stride) read by put_glyph_large (07:4588) | _Arc_Unarc, _CleanAll, _RedimMat, _IncLstSize, put_glyph_large |
33 | 70 | Graph coordinate math and programmable timer API | _SetXXOP1, _UCLineS, _InitTimer, _StartTimer, timer_irq |
36 | 24 | Mode setters (Func/Param/Polar/Seq) | _SetFuncM, _SetParM, _SetPolM, _SetSeqM |
37 | 23 | Graph coordinate conversion, RTC, and date/time formatting | _XftoI, _YftoI, _getDate, _getTime, rtc_read_seconds |
38 | 277 | TI-BASIC parser / evaluator | _ParseInp, _Find_Parse_Formula, parse_init |
39 | 153 | Equation pretty-printer (2D MathPrint layout) + menus | eqdisp_render_entry, eqdisp_emit_glyph, _DispMenuTitle |
3A | 85 | Statistics (1/2-var, regressions) + TVM finance | _OneVar, reg_gauss_solve, tvm_solve_iterate |
34 | 16 | Token/parser scanning | _AHEADEQUAL, _PARSAHEADS, _PARSAHEAD, parse_scan_table |
35 | 6 | USB controller paths, memory-reset engine, factorial | usb_timeout_irq, mem_reset_dispatch, ram_reset_wipe, op1_factorial |
3B | 39 | bcall jump table + mem utils | (table data) _MemClear, _MemSet, _DrawCirc2 |
3C | 72 | Link / variable transfer | _SendAByte, _RecAByteIO, _SendVarCmd, _Rec1stByte |
3D | 61 | App management & Flash | _FindApp, _FindAppUp, _FindAppDn, _FlashToRam |
Blank, boot, and system pages
No Flash-App headers (80 0F) appear at any page boundary; the image is OS-only [hypothesis]. Byte-level notes on the empty page range and the boot/system pages (some of which, e.g. 34-39/3B/3C, also carry the bcalls listed above):
| Page | Verified contents |
|---|---|
08–2E, 30–32 | Blank or unused in this OS image — 100% 0xFF in tools/rom.bin. No app headers. |
2F | Retail USB boot support installed from the checksum- and hash-validated local D84PBE2.8Xv. This installation changes 8,615 bytes of the pinned base. The page-3F boot table maps _AttemptUSBOSReceive, _ReceiveOS_USB, _USBErrorCleanup, _InitUSB, and _KillUSB here; tools/rom.bin contains the payload and tools/symbols/bcalls8x_targets.txt records their bodies. [confirmed] |
34–39 | More OS code (parser scan, USB, graph, mode, menu, and RTC); fill 0.2–17% 0xFF. |
3B | bcall jump table — starts 99 27 00 = entry 0 (_JErrorNo → ram:2799). |
3C | Link code, archive garbage collection, and the OS version string — page starts with ASCII 32 2E 35 35 4D 50 = "2.55MP"; archive_gc_collect is at 3C:7733. |
3E | Certification page and GC journal — two physical 8 KiB sectors. _GetCertificateStart (bcall 0x8057) selects the active half; _GetCertificateEnd (0x802D) bounds it; _FindFirstCertField (0x8027) and _FindNextCertField (0x8078) walk TLV fields. GC transactionally copies the used tail through the inactive half and stores phase bytes near its end. [confirmed] for the ROM and GC trace. |
3F | Retail boot page — the pinned base already matches the checksum- and hash-validated local D84PBE1.8Xv payload byte for byte; starts 3E 07 D3 04 3E 7F D3 06 3E 03 D3 0E C3 2C 81, carries boot version 1.03, and hosts the 0x8xxx boot bcall table. Boot and hardware-version bcalls resolve to _getBootVer 3F:477C (0x80B7) and _getHardwareVersion 3F:4781 (0x80BA). [confirmed] |
The large-font glyph table is on page 0x07 (see Display and LCD). Alternate large fonts live on pages 0x01 and 0x36 (selected by IY+0x35 bits 5 and 1). Page 0x07 is the busiest data page: archive code, list and matrix code, error messages, and the large font. [confirmed]
Page specialization
The OS is page-specialized: kernel + math on page 0, one subsystem per low page. A bcall is really “run subsystem X’s routine on its page” — the page map is the subsystem decomposition, physically.
RAM pages
The TI-84 Plus maps banked RAM behind the Z80’s 16 KiB windows. This page
separates selector values from physical backing, reconciles the reported
128 KiB and 48 KiB revisions, traces OS use of selectors 80–83, and gives
restoration rules for programs that borrow banked RAM.
Resident-program traces and the safe mapping protocol are summarized in
Resident scratch RAM.
Physical integration and capacity revisions
Datamath’s March 2004 board photographs show three main integrated circuits:
the TI REF 83PLUSB/TA2 ASIC, a 29LV800 Flash device, and the LCD driver.
The accompanying board description places the Z80 core and RAM inside the
ASIC. An external SRAM package therefore is not part of that photographed
revision. [standard]
WikiTI’s hardware history reports 128 KiB in the original TI-84 Plus design
and a later reduction to 48 KiB. Its RAM-page table says units with port
0x15 >= 0x55 map selectors 82–87 to one physical 16 KiB block. These are
community hardware reports. Neither page supplies a primary TI specification,
a dated transition, or a measurement tied to a photographed board. [standard]
The two reported topologies use the same eight selector values: [standard]
| Reported capacity | Physical backing | Consequence at one page offset |
|---|---|---|
| 128 KiB | eight independent 16 KiB blocks | selectors 80–87 can retain eight different bytes |
| 48 KiB | blocks 80, 81, and one block shared by 82–87 | the last write through any selector 82–87 is visible through all six |
Port 0x15 does not appear in any statically resolved OS 2.55MP I/O
instruction. The public identity table associates 0x44 and 0x45 with
128 KiB and 0x55 with 48 KiB, while Datamath identifies TA2 and TA3 package
families without assigning their RAM capacity. Do not infer capacity from an
ASIC label alone. The restoring probe records the package-independent port
0x15 byte and the observed selector groups in one frame. [confirmed] for
the ROM scan and probe format; [standard] for the public identities;
[hypothesis] for an unmeasured calculator’s topology.
Page selectors
The public TI-84 Plus register contract uses two selector encodings. The OS trace
confirms the values it executes, but it does not confirm the complete selector
space or the physical storage behind selectors 84–87.
| Window | Port | Selector encoding | Normal TI-OS value |
|---|---|---|---|
4000-7FFF | 0x06 | Bit 7 clear selects Flash page value & 0x3F; bit 7 set selects RAM page 0x80 | (value & 7) | Banked Flash page |
8000-BFFF | 0x07 | Bit 7 clear selects Flash page value & 0x3F; bit 7 set selects RAM page 0x80 | (value & 7) | 81 |
C000-FFFF | 0x05 | The low three bits select RAM page 0x80 | (value & 7) | 00 → RAM page 80 |
TilEm implements this eight-page arithmetic. The trace confirms the executed selector values below. The complete contract is [standard]. The listed OS writes are [confirmed].
In the idle boot/home trace, the RAM-window writes are:
Ports 0x0E and 0x0F extend ports 0x06 and 0x07 only for Flash
selectors. They do not change a selector with bit 7 set for RAM. See
Paging. [standard]
OUT (port 7) <- 0x7f 8000-BFFF = page_3F
OUT (port 7) <- 0x81 8000-BFFF = RAM/0x81
OUT (port 5) <- 0x00 C000-FFFF = RAM/0x80
OUT (port 7) <- 0x80 8000-BFFF = RAM/0x80
OUT (port 7) <- 0x81 8000-BFFF = RAM/0x81
OUT (port 5) <- 0x02 C000-FFFF = RAM/0x82
OUT (port 7) <- 0x83 8000-BFFF = RAM/0x83
OUT (port 7) <- 0x81 8000-BFFF = RAM/0x81
OUT (port 5) <- 0x00 C000-FFFF = RAM/0x80
The trace restores port 0x07 to 0x81 and port 0x05 to 0x00 before normal
OS execution resumes. [confirmed]
Page map
WikiTI’s RAM pages page supplies the historical page descriptions. The trace and ROM disassembly independently support only the entries whose evidence column says [confirmed].
| RAM selector | Use | Evidence |
|---|---|---|
80 | Normal C000-FFFF RAM page | The boot/home trace restores it with OUT (5),0; WikiTI marks it execution-protected. [confirmed] for the restore; [standard] for the protection claim. |
81 | Normal 8000-BFFF RAM page | The traces access OS variables, OP registers, flags, graph buffers, the user heap, and the VAT window through this selector. [confirmed] |
82 | Temporary half of an OS bank pair | The idle trace selects it through port 0x05 as part of a paged RAM helper, then restores selector 80. No page-82 store occurs. [confirmed] |
83 | Shared OS scratch and state | OS 2.55MP maps it through port 0x06 for block copies and LCD capture, and through port 0x07 for a paged byte-store helper. Homescreen expression entry writes the previous-entry buffer at 577E. [confirmed] |
84 | No use established here | WikiTI marks it execution-protected. [standard] |
85 | No use established here | WikiTI describes it as unused under typical TI-OS execution. [standard] |
86 | No use established here | WikiTI marks it execution-protected. [standard] |
87 | No use established here | WikiTI describes it as unused under typical TI-OS execution. [standard] |
Wabbitemu has an optional ram_version == 2 branch matching the reported
48 KiB topology. Selected internal pages 3–7 read and write
ram[2 * PAGE_SIZE]; internal page 2 already addresses that block. Emulator
agreement with WikiTI does not establish a physical unit’s backing. The
restoring RAM alias probe records the
original, patterned, and restored bytes for selectors 82–87. No physical
result has been recorded. [standard] for the sources; [confirmed] for the probe
bytes; [hypothesis] for an unmeasured calculator’s topology.
Flash Editor is historical community corroboration, not the missing physical
result. Its readme says pages 84–87 can buffer a Flash sector on older
128 KiB calculators and cannot do so on newer 48 KiB units. The included source
saves port 0x06, maps selected pages through it, and uses the extra selectors
during edit and sector operations. It records no calculator identity, alias
matrix, or hardware trace, so the claim does not establish which selectors are
independent on any identified unit. The source instructions and identified
readme wording are [confirmed]; the claimed physical topology remains
[hypothesis].
The exact release is programs/flashed.zip, SHA-256
b99e56f11084f473c34bc2d7679d37b407764a698aee95f9ef8b6f663d7c8463.
Members flashed.asm, routines.asm, and flashed.txt have SHA-256
046f76fcfb173dcb80e3634adba5f804f0a961e6ba1efac48fcbe7bb9dc8781a,
8669a0c6c50f8870a59d1b6d13ef567c5d5b255fcf6985e2026763adf6bc2a9c,
and 5580d4e0d096a2ee4c6ab0b0e08ba9703a3b961fcc72c31dc770a82cfc4d69b7,
respectively. [confirmed]
Emulator implementations
The pinned source revisions implement different RAM backing rules. These results describe software behavior and do not select the correct physical ASIC contract.
| Implementation | Source-verified behavior | Limit |
|---|---|---|
TilEm f56ad637 | x4_io.c makes RAM selectors flat pages 0x40 | (value & 7); x4_memory.c addresses each page independently. This gives eight distinct 16 KiB blocks. [standard] | It has no 48 KiB alias mode in the pinned mapper. |
Wabbitemu 48c2dc0 | core.c redirects reads and writes from selected pages 3–7 to physical page 2 when ram_version == 2. memory_init_84p zeroes the context and does not enable the branch. [standard] | The selected bank page remains separate from its aliased physical backing. |
| MAME 0.287 | ti85_m.cpp retains raw RAM selectors at ports 0x06 and 0x07. ti85.cpp maps banked RAM across 0x200000–0x21BFFF, exactly seven 16 KiB blocks, so selector 87 resolves beyond the map. [standard] | It neither wraps selector 87 nor implements the six-to-one 48 KiB alias. MAME marks the TI-84 Plus driver MACHINE_NOT_WORKING. |
The JSON-capable mapper CLI can reproduce Wabbitemu’s optional alias branch:
python3 -m ti84re.hardware.describe_memory_mapping --json map \
--profile wabbitemu --ram-alias-from 2 \
--write 4=0 --write 5=7 --write 6=0x87 --write 7=0x87
The report retains selector readbacks 07, 87, and 87 while all three RAM
windows resolve to physical page 82. --ram-alias-from configures a candidate
physical topology; it does not assert that an emulator enables that topology by
default. Run the same sequence with --profile mame and no alias option to expose
MAME’s unmapped selector 87. See Paging for the complete mapper
comparison.
The alias-probe decoder reconstructs equivalence classes from the ordered patterns. Each selector in one class reads the pattern written through the highest-numbered selector in that class. The two expected endpoints and a partial-alias example are reproducible without a calculator:
python3 -m ti84re.hardware.describe_ram_topology --observed 112233445566
python3 -m ti84re.hardware.describe_ram_topology --observed 666666666666
python3 -m ti84re.hardware.describe_ram_topology \
--simulate-backings 0,0,1,1,2,3 --json
The simulated partial mapping produces 22 22 44 44 55 66 and groups
82/83, 84/85, 86, and 87. This is a decoder test case, not a
reported hardware revision. [confirmed]
Per-page trace coverage
The boot/home and 2+3 ENTER traces exercise startup, homescreen
initialization, display capture, parsing, evaluation, and previous-entry
storage. Separate traces cover graph drawing, a resident _GetKey wait
interrupted by ON, and an OS error dialog. They do not exercise APD timeout,
app launch, USB transfer, variable receive, archive cleanup,
table/statistics/program editors, or a 48 KiB ASIC. Within the baseline scope,
physical RAM-page writes for the executed selectors are:
| RAM selector | Idle trace writes | 2+3 ENTER trace writes | Interpretation |
|---|---|---|---|
80 | 256227 writes, all page addresses touched | 345702 writes, all page addresses touched | Normal high RAM selected by port 0x05; contains stack, system, and user RAM activity in C000-FFFF. [confirmed] |
81 | 62947 writes, all page addresses touched | 72638 writes, all page addresses touched | Normal 8000-BFFF RAM; contains OS variables, flags, OP registers, the heap, the VAT window, and working buffers. [confirmed] |
82 | No writes observed | No writes observed | Port 0x05 briefly selects raw value 02, but the observed store uses selector 83 in bank B. [confirmed] |
83 | 1882 writes to 43D9-44BD and 5A7E-5DF2 | 3467 writes to 4373-4390, 43D9-44BD, 577E-5790, and 5A7E-5DF2 | Shared OS scratch and state. See the range table below. [confirmed] |
The traces never select 84–87. That absence describes these scenarios; it does
not establish how the selectors behave. Under the public 48 KiB contract, selectors
82–87 share one physical block rather than six independent pages. [standard]
The graph scenario in tools/macros/graph-y1-x2.macro reaches the graph screen and
still only writes pages 80, 81, and 83. It increases normal page-80/81
activity but leaves page-83 at the same confirmed ranges as the idle trace.
It does not write through selector 82 or select 84–87. [confirmed]
Two longer direct-TI-OS scenarios add coverage without establishing a borrowable range:
| Scenario | Page-83 result | Limit |
|---|---|---|
Resident guard, _DisableApd, _DelRes, _GetKey, then ON | 3,893 writes to 83:4373–83:4390, 83:577E–83:5794, and 83:5A7E–83:5D7D | The trace starts after reset with port 0x06 unknown. The analyzer recovers each page-83 selection, but skips unrelated writes whose initial mapping is unresolved. APD is disabled, so this is ON handling during a wait, not an APD-timeout test. [confirmed] |
Cold boot, then 1/0 ENTER error dialog | 3,418 writes to 83:43D9–83:44BD and 83:5A7E–83:5DF2 | The complete trace includes boot. The error path adds no page-83 range beyond the idle baseline; this does not prove that every error path behaves alike. [confirmed] |
The guarded resident run also rechecks saveSScreen and statVars after the ON
event; both sentinels remained intact in this emulator run. That narrow result does
not make the other page-83 holes safe and is not physical-calculator evidence.
The trace SHA-256 values and initial-mapping assumptions are recorded in
tools/data/ram-page-observations.csv.
These trace files do not have complete-ROM sidecars. Their TLMT initial
Flash page 0x00 SHA-256 is
bfc698e445d98d6d0905589ec34a88c9372a90cb0ed2d1fe9aa9b6fca0962fc1,
which matches page 0x00 in both known OS 2.55MP images. [confirmed] That
page hash does not identify the boot pages or the complete image.
How to hit the confirmed paths
The useful distinction is between “page number can be selected” and “the OS uses it in a normal workflow.” These paths are confirmed or have a concrete next scenario:
| Page/path | How to hit it | Evidence |
|---|---|---|
80 high RAM | Run any cold-boot, home, expression, or graph trace. | Port 5 = 00 is the normal restore value; every current trace writes all page-80 addresses. [confirmed] |
81 normal bank-B RAM | Run any cold-boot, home, expression, or graph trace. | Port 7 = 81 is the normal restore value; every current trace writes all page-81 addresses. [confirmed] |
83 display capture | Run boot-idle.macro or graph-y1-x2.macro. | Ghidra shows _SaveDisp (39:5DD8) calls lcd_read_block (ram:1890) at the 39:5E03 call site; coverage hits both, and writes 5A7E-5D7D. [confirmed] |
83 homescreen previous-entry history | Run home-2plus3.macro. | The trace adds 577E-5790, advances lastEntryPTR from 577E to 5791, and sets numLastEntries to 01. [confirmed] |
83 expression scratch copy | Run home-2plus3.macro. | The trace adds 4373-4390 through flash_copy_block; its page-select instruction is at +0x14 (ram:187C). [confirmed] |
83 split-screen/table copy | Enter a split-screen/table workflow that calls screen_split. | Ghidra shows screen_split at 05:7712 calls flash_copy_block at 05:772A; this path is not hit by the current macros. [confirmed] |
83 edit-buffer initialization | Enter an edit-buffer workflow that reaches editbuf_init_buf. | Ghidra shows editbuf_init_buf at 03:6BC4 calls flash_copy_block at 03:6BCD; this path is not hit by the current macros. [confirmed] |
83 app-menu state restore | Open an app/menu workflow that reaches mnu_restore_app_state. | Ghidra shows mnu_restore_app_state at 39:6D96 calls flash_copy_block at 39:6DA0; this path is not hit by the current macros. [confirmed] |
84–87 independent pages | Use a forced RAM-page probe or a ROM path that passes pair index 2 or 3 to the computed bank-pair helper. | The ROM can compute these selectors, but raw immediate selector scans and current traces do not show a normal OS path selecting or writing them. [hypothesis] |
The computed bank-pair helpers use this selector formula:
LD A,B
SLA A
OUT (5),A ; pair index 0/1/2/3 -> pages 80/82/84/86 in bank C
INC A
OR 0x80
OUT (7),A ; pair index 0/1/2/3 -> pages 81/83/85/87 in bank B
Decoded callers set B = 1, selecting pages 82/83; that explains the observed
port 5 = 02, port 7 = 83 sequence. Selectors 84–87 are reachable through the
helper but are not selected on any observed OS path [hypothesis]. The B = 1
caller pattern is confirmed for the decoded callers above. [confirmed]
Page 83 use [standard]
Page 83 is the page people most often borrow as scratch, but the ROM uses it as
more than anonymous free RAM. Keep the evidence classes separate:
| Range | Use | Evidence |
|---|---|---|
4373-4390 | Expression-path page-83 scratch copy | Added by the 2+3 ENTER trace. flash_copy_block+0x16 (ram:187E) performs the LDIR; flash_copy_block+0x14 (ram:187C) maps page 83. The caller is still unlabeled. [confirmed] |
43D9-44BD | Boot/home page-83 scratch copy | Present in the idle trace. flash_copy_block+0x16 performs the LDIR, and 37:44D8 stores one additional byte. [confirmed] |
577E-5A7D | Homescreen previous-entry history | Page 33 references 577E, the 5A7E upper bound, lastEntryPTR (0x8DA7), and numLastEntries (0x8E29). The 2+3 ENTER trace writes 577E-5790, advances lastEntryPTR to 5791, and sets numLastEntries to 01. [confirmed] |
5A7E-5DF2 | LCD/home display capture area | Present in the idle trace. The _SaveDisp LCD capture (ram:1890) fills the first 0x300 bytes, 5A7E-5D7D (the 96×64 framebuffer); the 5D7E-5DF2 tail is additional page-83 writes in the same scenario. Ghidra decompiles ram:1890 as an LCD-read helper that maps page 83 through port 6 and stores bytes read from LCD port 11. [confirmed] |
4000-4080 | App base-page staging before app execution | WikiTI public note; the two traces on this page do not launch an app. [standard] |
4100-433A | USB communication buffers | WikiTI public note; the two traces on this page do not exercise USB transfer. [standard] |
flash_copy_block at ram:1868 saves the current port-6 value, writes
0x83 to port 6, runs LDIR, and restores the previous page through the
page-set helper. The two repeatedly cited instructions are offsets within this
routine rather than separate functions:
ram:1877 IN A,(6)
ram:1879 PUSH AF
ram:187A LD A,0x83
ram:187C OUT (6),A
ram:187E LDIR
ram:1880 POP AF
ram:1881 CALL 0x181C
Ghidra identifies the LCD capture helper at ram:1890. It maps page 83, waits on
the LCD, reads port 11, and stores each byte through HL:
ram:189F IN A,(6)
ram:18A1 PUSH AF
ram:18A2 LD A,0x83
ram:18A4 OUT (6),A
ram:18A6 CALL 0x0CC3
ram:18A9 IN A,(0x11)
ram:18AB LD (HL),A
The reset path on page 37 initializes the previous-entry pointers:
37:6E0D LD HL,0x577E
37:6E10 LD (lastEntryPTR),HL
37:6E13 LD HL,0x0000
37:6E16 LD (numLastEntries),HL
Page 38 has a second clear path with the same pointer reset:
38:422D LD HL,0x577E
38:4230 LD (lastEntryPTR),HL
38:4233 LD HL,0x0000
38:4236 LD (numLastEntries),HL
The homescreen entry-history code on page 33 uses the same constants and variables:
33:53D1 LD A,(numLastEntries)
33:53E2 LD HL,0x5A7E
33:53F7 LD HL,0x577E
33:5430 LD A,(numLastEntries)
33:543A LD DE,0x577E
33:5451 LD DE,0x577E
33:5459 LD (lastEntryPTR),HL
33:5462 LD HL,numLastEntries
33:5465 INC (HL)
If a program modifies the history buffer on page 83, clearing numLastEntries
at 0x8E29 prevents the homescreen from scrolling back into invalid entry data.
That is the public WikiTI recovery advice, and the ROM confirms that 0x8E29 is
the OS-visible previous-entry count. [standard]
Dynamic test scenarios
The trace analyzer maps TilEm memory-write records back to physical RAM pages. Use it with full-range traces:
ROM=/path/to/ti84plus_2.55mp_complete.rom
tilem2 --headless --rom "$ROM" --model ti84p --normal-speed --reset \
--macro tools/macros/boot-idle.macro \
--trace /tmp/page83-idle.trace --trace-range all
tilem2 --headless --rom "$ROM" --model ti84p --normal-speed --reset \
--macro tools/macros/home-2plus3.macro \
--trace /tmp/page83-2plus3.trace --trace-range all
tilem2 --headless --rom "$ROM" --model ti84p --normal-speed --reset \
--macro tools/macros/graph-y1-x2.macro \
--trace /tmp/page83-graph.trace --trace-range all
tilem2 --headless --rom "$ROM" --model ti84p --normal-speed --reset \
--macro tools/macros/page83-error-divzero.macro \
--trace /tmp/page83-error-divzero.trace --trace-range all
python3 -m ti84re.trace.analyze_ram_page /tmp/page83-idle.trace --page 0x83
python3 -m ti84re.trace.analyze_ram_page /tmp/page83-2plus3.trace --page 0x83
python3 -m ti84re.trace.analyze_ram_page /tmp/page83-graph.trace --page 0x83
python3 -m ti84re.trace.analyze_ram_page /tmp/page83-error-divzero.trace \
--page 0x83 --initial-mapping ti84p-reset
The baseline idle trace writes:
RAM page 0x83 writes: 1882
unique page addresses: 1114
range 43D9-44BD
range 5A7E-5DF2
The 2+3 ENTER trace writes:
RAM page 0x83 writes: 3467
unique page addresses: 1163
range 4373-4390
range 43D9-44BD
range 577E-5790
range 5A7E-5DF2
The division-by-zero dialog trace writes:
RAM page 0x83 writes: 3418
unique page addresses: 1114
range 43D9-44BD
range 5A7E-5DF2
The before/after RAM variables line up with the previous-entry write:
| Scenario | lastEntryPTR (0x8DA7) | numLastEntries (0x8E29) |
|---|---|---|
| Idle home screen | 577E | 00 |
After 2+3 ENTER | 5791 | 01 |
Those values come from end-of-trace RAM reconstruction. The added page-83 range
577E-5790 is exactly the bytes between the old and new lastEntryPTR values. [confirmed]
Restoring after page 83
Restore the selector for every window you changed. For code entered from normal TI-OS
state that temporarily maps page 83 into bank B (8000-BFFF) and page 82 into
bank C (C000-FFFF), restore the two RAM windows this way:
LD A,0x81
OUT (7),A ; 8000-BFFF back to RAM page 81
XOR A
OUT (5),A ; C000-FFFF back to RAM page 80
For code that maps page 83 into bank A (4000-7FFF), preserve and restore port 6:
IN A,(6)
PUSH AF
LD A,0x83
OUT (6),A ; map RAM page 83 at 4000-7FFF
; use 4000-7FFF here
POP AF
OUT (6),A ; restore previous Flash/RAM page selector
Keep the nonstandard mapping inside a short critical section. The OS helper preserves
interrupt state around the temporary RAM-page mapping so the interrupt handler does not
run with bank A or bank B pointing at page 83.
For code that may be called with nonstandard paging, preserve and restore the selectors for all touched windows:
IN A,(6)
PUSH AF
IN A,(7)
PUSH AF
IN A,(5)
PUSH AF
LD A,0x83
OUT (7),A ; map RAM page 83 at 8000-BFFF
; use 8000-BFFF here
POP AF
OUT (5),A
POP AF
OUT (7),A
POP AF
OUT (6),A
The OS’s own paged byte-store helper at 37:44AE uses the normal restore pattern:
37:44D0 OUT (5),A ; A = page index << 1, trace case A = 0x02 (→ RAM page 82)
37:44D2 INC A ; A = 03
37:44D3 OR 0x80 ; A = 0x83
37:44D5 OUT (7),A ; trace case: 0x83
37:44D7 LD A,B
37:44D8 LD (DE),A ; byte store while RAM page 83 is visible
37:44D9 LD A,0x81
37:44DB OUT (7),A
37:44DD XOR A
37:44DE OUT (5),A
The dynamic trace resolves the same sequence at instruction indices 712241-712250,
including the final port 7 = 81 and port 5 = 00 writes. [confirmed]
Sources
| Source | Use here |
|---|---|
| Datamath TI-84 Plus hardware and March 2004 board photographs | Three-IC board inventory, ASIC-integrated RAM, and photographed 83PLUSB/TA2 package |
| WikiTI hardware history, revision 10880 | Reported 128 KiB design and later 48 KiB revision |
| WikiTI RAM pages, revision 11670 | Reported selector uses and 82–87 alias threshold |
TilEm x4_io.c and x4_memory.c | Independent-page emulator mapping |
Wabbitemu core.c and 83psehw.c | Optional reduced-RAM alias and model identity behavior |
MAME 0.287 ti85.cpp and ti85_m.cpp | Seven-block backing and raw-selector behavior |
Physical hardware probes
The physical-probe suite builds small AsmPrgm programs and decodes their
result AppVars. The sources cover MD5-assist edge behavior, RAM selector
aliasing, execution-protection boundaries, repeated battery-level bcalls, raw
battery-comparator selectors, memory-bus and prefix-M1 timing, keypad-matrix
settling, programmable-timer edge behavior, and raw two-wire link readback,
plus read-only ASIC and USB register snapshots. No exported result from a
physical calculator has been recorded, so the hardware conclusions remain
open.
Measurement status
The builder, link-file containers, entry jumps, bcall ID, frame layouts, and restoration or cleanup instruction sequences have byte-level host validation. [confirmed] Physical execution of any program remains [hypothesis] until an exported result AppVar is decoded and tied to a calculator and ASIC revision.
| Probe | Program | Result AppVar | Physical status |
|---|---|---|---|
| MD5 edge behavior | HWPMD5 | HWPMD511 | Not run on a recorded unit |
| RAM selector aliasing | HWPRAM | HWPRAM21 | Not run on a recorded unit |
| ASIC register snapshot | HWASIC | HWPASIC1 | Not run on a recorded unit |
| Battery-level stability | HWBATT | HWBATT01 | Not run on a recorded unit |
| Raw battery selectors | HWBRAW | HWBRAW01 | Not run on a recorded unit |
| Raw link readback | HWLINK | HWLINK01 | Not run on a recorded unit |
| Keypad-matrix settling | HWKEYS | HWKEYS01 | Not run on a recorded unit |
| Six memory wait classes | HWBUS | HWBUS001 | Not run on a recorded unit |
| Prefixed RAM M1 placement | HWPFX | HWPFX001 | Not run on a recorded unit |
| Programmable-timer edges | HWTMR | HWTMR001 | Not run on a recorded unit |
| USB control snapshot | HWPUSB | HWPUSB01 | Not run on a recorded unit |
| Flash execution boundaries | HWEF07–HWEF2A | matching HWEF...01 names | Not run on a recorded unit |
| RAM execution boundaries | HWER81–HWER84 | matching HWER...1 names | Not run on a recorded unit |
The eight-character AppVar names are versioned fixture names. Delete an
existing result AppVar before rerunning its probe. _CreateAppVar = 4E6A does
not replace a variable with the same name. [confirmed] for the local ROM bcall
path at 00:1129–00:112F.
Build and transfer
SPASM-ng is part of the Nix development shell. Build all transfer files and a hash manifest with:
nix develop -c python3 -m ti84re.hardware.build_probes \
--output-dir /tmp/hardware-probes
The command emits the 11 snapshot, edge, and timing probes, ten single-fetch
execution probes, and manifest.json. The manifest records every target
selector and scan range. It uses repository-relative source names and output
basenames, so builds made in different checkout directories remain comparable.
The CLI refuses an existing output directory. The hashes identify exact
artifacts; they do not establish that a calculator executed them.
Transfer the .8xp files with TI Connect CE or another link program. Make
a calculator backup before the first run. Then:
- Delete the probe’s result AppVar if it already exists.
- Run
Asm(prgmHWASIC)for the read-only register snapshot. - Disconnect the 2.5 mm link port, then run
Asm(prgmHWLINK)for the raw-link sample with release-to-idle cleanup. - Run
Asm(prgmHWKEYS), release the launch key, and hold the recorded test key or chord until the program returns. - Run
Asm(prgmHWBATT)for the restoring battery-level sample. - Run
Asm(prgmHWBRAW)for the higher-risk direct-selector sample only afterHWBATTsucceeds and its result has been exported. - Run
Asm(prgmHWPUSB)for the read-only USB control snapshot. - Run
Asm(prgmHWPMD5)for the MD5 probe. - Run
Asm(prgmHWBUS)on OS 2.55MP for the guarded bus-timing measurement. Export its result before another mutating probe. - Run
Asm(prgmHWPFX)for the guarded prefix-M1 timing measurement. Export its result before another mutating probe. - Run
Asm(prgmHWTMR)for the guarded programmable-timer edge measurement. Export its result before another mutating probe. - Run
Asm(prgmHWPRAM)for the RAM probe only after the earlier transfer and run path works on that unit. - Run at most one
HWEF...orHWER...execution probe before exporting its result. A denied fetch may reset the calculator. - Export the new result AppVar to the host.
- Record the calculator model, PCB or ASIC revision, boot version, OS version, exact held keys, and artifact hashes with the exported file.
Do not treat an emulator run as a physical result. TilEm and Wabbitemu are comparison implementations for the expected edge cases. MAME 0.287 does not map the MD5 port block. [standard]
Decode a result
The decoder verifies the TI link checksum, both TI entry lengths, the AppVar’s
internal size word, the HWP1 frame version, and the payload length before it
interprets measurements. [confirmed]
python3 -m ti84re.hardware.decode_probe HWPMD511.8xv
python3 -m ti84re.hardware.decode_probe --json \
HWPASIC1.8xv HWBATT01.8xv HWBRAW01.8xv HWPUSB01.8xv \
HWLINK01.8xv HWKEYS01.8xv HWBUS001.8xv HWPFX001.8xv HWTMR001.8xv \
HWPMD511.8xv HWPRAM21.8xv
The JSON form keeps the raw payload and adds named fields. Preserve the original exported AppVar even when a report has been generated.
Result frame
The calculator stores the frame after the AppVar’s normal two-byte internal size word. Multi-byte lengths use little-endian order.
| Offset | Size | Field |
|---|---|---|
0 | 4 | ASCII magic HWP1 |
4 | 1 | format version, currently 1 |
5 | 1 | probe ID |
6 | 2 | payload length |
8 | 1 | port-0x15 ASIC identity read |
9 | 1 | port-0x02 status read |
10 | variable | probe payload |
_CreateAppVar returns DE at the internal size word. The shared assembly
routine advances DE twice before copying HWP1. The builder rejects an
artifact without the byte sequence for this advance. [confirmed] from local
ROM bytes at 00:1129–00:112F and the assembled probe listings.
MD5 edge probe
Probe ID 1 records five four-byte fields:
| Payload offset | Field | Operation |
|---|---|---|
0 | valid result | first MD5 compression step for "abc"; expected arithmetic result 0xD6D117B4 |
4 | undefined reads | direct reads from ports 0x18–0x1B |
8 | fifth-write result | four zero bytes and a fifth 0x12 byte written to operand A |
12 | high-control result | 0xFF written to mode and rotate-count ports |
16 | mixed result | operand A changed after result byte 0 and before bytes 1–3 |
The valid arithmetic vector follows the ROM’s operand order and RFC 1321. [confirmed] for the ROM transaction and arithmetic. The four edge results are physical [hypothesis]. See MD5 accelerator and boot API for the emulator comparison.
The program disables interrupts while it uses ports 0x18–0x1F. It clears
all six operand registers and both controls before restoring the caller’s
interrupt state. It does not preserve an earlier internal MD5-assist state,
which the port interface cannot read back directly. [confirmed] for the
assembled instruction sequence.
RAM alias probe
Probe ID 2 tests selectors 82–87 at bank-A address 0x7F00. Its 18-byte
payload contains six original bytes, six observed pattern bytes, and six bytes
read after restoration.
The program saves port 0x06, disables interrupts, and records the byte visible
through each selector. It writes 11 22 33 44 55 66, rereads all selectors,
restores each saved byte, verifies the restored values, and restores port
0x06. [confirmed] for the assembled instruction sequence.
An observed sequence of 11 22 33 44 55 66 distinguishes six independent
selector backings for this address. 66 66 66 66 66 66 distinguishes a shared
backing for selectors 82–87. The decoder also infers partial equivalence
classes. Because writes occur in ascending selector order, every selector in
one class must read the pattern written through the highest-numbered member.
Bytes outside 11 22 33 44 55 66, or a group whose reported writer is not its
highest member, produce mixed-or-unexpected. [confirmed]
The decoder reports restore_matches. A false value means the post-restore
reads differ from the saved bytes and invalidates a claim of successful cleanup.
The standalone CLI accepts the six observed bytes or simulates an explicit selector-to-backing assignment:
python3 -m ti84re.hardware.describe_ram_topology --observed 666666666666
python3 -m ti84re.hardware.describe_ram_topology \
--simulate-backings 0,0,1,1,2,3 --json
The pinned SPASM-ng build produces 214 machine-code bytes with SHA-256
be8e1dc12060cda657cf076196e9f27302822cf7307935b5bf4056b0c55c548d.
The packaged 508-byte HWPRAM.8xp has SHA-256
72da5a412b596161b50f03fa5d5f2018d88b26c51207208bdf63335a9c67f6e3.
[confirmed]
Execution-protection fetch probes
Probe ID 4 tests one bank-A selector per program. It scans the configured range
through data reads for an existing RET byte. It creates a result AppVar with a
pending outcome, remaps and verifies that byte, and performs:
PUSH DE
JP (HL)
A successful fetch executes RET, returns to the probe, and changes the AppVar
outcome to returned. The program never writes the selected RAM or Flash page.
[confirmed] for the assembled instruction sequence.
The existing RET is also the only target opcode. It has no memory-write or
I/O side effect, which limits risk if a protection exception is delivered after
the opcode executes. Pinned TilEm uses that ordering: it completes a forbidden
opcode and resets afterward. A pending result after reset therefore does not
distinguish a suppressed opcode from an executed RET followed by reset. The
probe measures return-versus-reset behavior, not the precise exception point.
[standard] for TilEm; [hypothesis] for the physical ASIC.
| Payload offset | Size | Field |
|---|---|---|
0 | 1 | target kind: 0 Flash, 1 RAM |
1 | 1 | port-0x06 selector |
2 | 2 | logical scan start |
4 | 2 | scan length |
6 | 2 | selected RET address, or 0xFFFF |
8 | 1 | outcome code |
9 | 7 | ports 0x04, 0x06, 0x21–0x23, 0x25, and 0x26 |
| Program | Result AppVar | Selector and scan range | TilEm x4 | Wabbitemu |
|---|---|---|---|---|
HWEF07 | HWEF0701 | Flash 07, 0x4000–0x7FFF | returned | returned |
HWEF08 | HWEF0801 | Flash 08, 0x4000–0x7FFF | violation reset | returned |
HWEF09 | HWEF0901 | Flash 09, 0x4000–0x7FFF | violation reset | violation reset |
HWEF29 | HWEF2901 | Flash 29, 0x4000–0x7FFF | violation reset | violation reset |
HWEF2A | HWEF2A01 | Flash 2A, 0x4000–0x7FFF | returned | returned |
HWER81 | HWER8101 | RAM selector 81, 0x4000–0x7FFF | returned | returned |
HWER820 | HWER82A1 | RAM selector 82, 0x4000–0x43FF | violation reset | returned |
HWER821 | HWER82B1 | RAM selector 82, 0x4400–0x47FF | violation reset | violation reset |
HWER83 | HWER8301 | RAM selector 83, 0x4000–0x7FFF | returned | returned |
HWER84 | HWER8401 | RAM selector 84, 0x4000–0x7FFF | violation reset | violation reset |
These outcomes assume the retail boot values: port 0x21 mode 0, Flash bounds
08–29, and RAM chunk bounds 10–20. They are predictions from the pinned
emulator predicates, not physical results. The decoder reports ports 0x04,
0x06, 0x21–0x23, 0x25, and 0x26 from immediately before the test.
In paired mapper mode, a port-0x06 write remaps bank B with bank A. That can
unmap the running probe. Every artifact therefore records
unsupported-paired-mapping without writing port 0x06 or attempting the
fetch when port 0x04 bit 0 is set. [standard] for the emulator predictions;
[confirmed] for the artifact guard.
The other outcomes are no-ret-found and target-changed-before-fetch.
Neither measures execution protection. A pending AppVar after an observed
reset is evidence only if the AppVar survived that reset unchanged. Export the
file before running another probe, and record whether the calculator visibly
reset. Reset retention and the physical fetch outcomes remain [hypothesis].
ASIC register snapshot
Probe ID 3 reads ports 0x04, 0x20, 0x21, 0x29–0x2C, 0x2E, 0x2F,
0x39, and 0x3A. Port 0x15 identity and port 0x02 status remain in the
common frame header. The payload preserves the listed port order. [confirmed]
The program performs no ASIC register writes. It disables interrupts across the reads so an interrupt handler cannot change the sampled configuration between fields. The result AppVar is its only intended persistent change. [confirmed] for the assembled instruction sequence; [hypothesis] for physical read values.
This snapshot establishes a starting configuration for later bus-timing and GPIO tests. It cannot measure T-state additions, GPIO direction polarity, or electrical pin levels. Those tests require controlled register writes and external timing or voltage observations. See Bus timing and wait states and ASIC status, identity, protection, and GPIO.
Battery-level probe
Probe ID 6 calls _Chk_Batt_Level = 5221h 16 times and records every value
returned in A. The result frame also saves state before the first call, after
the final call, and after cleanup: [confirmed]
| Payload range | Contents |
|---|---|
0–3 | pre-call ports 0x04, 0x39, and 0x3A, then (IY+0x18) traceFlags |
4–19 | 16 _Chk_Batt_Level results |
20–24 | post-call status, ports 0x04, 0x39, 0x3A, and traceFlags |
25–28 | readback after restoring the three ports and traceFlags |
29 | final port-0x02 status |
The decoder rejects result bytes outside 0–4. It reports a five-bin histogram,
a stable level only when all 16 samples agree, and cleanup_matches only when
the three port readbacks and complete flag byte match their saved values.
The probe restores the caller’s interrupt-enable state before creating the
AppVar. [confirmed] for source, assembled bytes, and decoder behavior.
Run the probe at a stable supply voltage, export HWBATT01, record the measured
rail voltage and load externally, and delete the AppVar before the next point.
An upward and downward sweep can locate OS-visible transitions and hysteresis.
The result is the retail bcall’s level, not a direct voltage measurement or a
raw comparator-bit trace. [hypothesis] for pending physical results.
The pinned SPASM-ng build produces 304 machine-code bytes with SHA-256
4fcb9e9052fcccad350cd3b7901235a4cb87390eeb764e78e7be0686d0da99ea.
The packaged 688-byte HWBATT.8xp has SHA-256
9d075837dc399ec0771e563c747e7498b4c260fe6f4fee128f17a57ba238fea0.
[confirmed]
Raw battery-selector probe
Probe ID 7 samples the port-0x02 comparator after each selector used by
_Chk_Batt_Level. It runs the sequence 16 times and records one four-bit mask
per sequence. The bit assignment stays in numeric selector order even though
the ROM tests the final three selectors in another order: [confirmed]
| Mask bit | Port-0x04 selector | Sample order |
|---|---|---|
| 0 | 0x06 | first |
| 1 | 0x46 | fourth |
| 2 | 0x86 | third |
| 3 | 0xC6 | second |
Each selector write executes five calls to 00:0CEB, then reads comparator bit
0 from port 0x02. The first 0x06 sample precedes the port-0x3A bit-7
enable. The probe then samples 0xC6, 0x86, and 0x46. It continues through
all four selectors even when the initial sample is zero. The retail bcall can
return early instead. [confirmed] from the assembled probe and the ROM path at
33:4EDC–33:4EE8.
After each sequence, the probe reproduces the cleanup at 33:4EEB–33:4F00:
it sets port-0x39 bit 4, pulses port-0x3A bit 4 around
CALL 00:0CED with A = 0x40, and clears port-0x3A bits 4 and 7. The common 30-byte
state layout matches the battery-level probe, with raw masks at offsets
4–19 and post-sequence state at offsets 20–24. The decoder rejects
masks above 0x0F. It reports a 16-bin histogram, a stable mask only when all
samples agree, pass counts for each selector, and cleanup_matches. [confirmed]
Run HWBATT before this probe at each voltage point. Export both AppVars and
record the externally measured rail voltage and load. Sweep upward and downward
slowly enough to distinguish threshold crossings from noise and hysteresis.
HWBATT01 records the OS-visible result; HWBRAW01 identifies which selector
comparators passed. Neither AppVar measures voltage. [hypothesis] for pending
physical results.
This probe directly manipulates battery-selection GPIO. An interruption or reset before cleanup can leave the selection state changed. Use a backed-up test calculator, stable externally current-limited power, and an independently verified voltage before running it. Do not run it as the first probe on a unit.
The pinned SPASM-ng build produces 397 machine-code bytes with SHA-256
d28548e32a53189f32c6ba7f2a4aba85278453ebcf8d1fba8f788f735f24b57c.
The packaged 874-byte HWBRAW.8xp has SHA-256
c0316a51a5262a32143fa72fe11c8ba510ee9aee1f87f8e28466777c54057586.
[confirmed]
USB control snapshot
Probe ID 5 reads ports 0x49, 0x4A–0x4D, 0x4F–0x52,
0x54–0x57, 0x5A, and 0x5B. Port 0x15 identity and port 0x02
status remain in the common frame header. The 15-byte payload preserves this
port order. [confirmed]
The program performs no I/O writes. It disables interrupts across the reads so
the OS interrupt handler cannot alter the sampled USB state between fields.
It then restores the caller’s interrupt-enable state before creating the
result AppVar. The builder verifies one direct IN instruction for every
listed port. [confirmed] for source and assembled bytes; [hypothesis] for
physical read values.
The pinned SPASM-ng build produces 174 machine-code bytes with SHA-256
8a720e21077a9cad678b20228b5f66c8c7f54a83651989da0fe75b9807dc7e7f.
The packaged 428-byte HWPUSB.8xp has SHA-256
dc2f769c4b6fc98a9b47f66b7f6acdd9523810956de131aa42079c4cfa25027c.
[confirmed]
Ports 0x49, 0x51, and 0x52 test historical transceiver and enable-timer
claims that have no confirmed OS 2.55MP transaction. Ports 0x4B, 0x4F,
0x50, and 0x5A provide readback for controls whose ROM writes are known but
whose physical meanings remain incomplete. The snapshot does not enable USB,
start a timer, or test presentation mirroring. Connected and disconnected
captures on known TA2 and TA3 units are both needed. See USB ASIC and link
assist.
Raw two-wire link probe
Probe ID 8 measures CPU-visible port-0x00 readback with the 2.5 mm connector
disconnected. For each target write 0, 1, 2, and 3, it first writes 3
to establish both-low, writes the target, waits for 0, 1, 4, or 16 NOP
instructions, and reads the complete port byte. It repeats all 16 points 16
times. [confirmed] for the assembled instruction sequence.
The public disconnected digital contract predicts complete read bytes 0x03,
0x12, 0x21, and 0x30 for target writes 0–3. The low two bits report the
line levels; bits 4–5 report the local output latch. The decoder checks those
fields separately and also reports exact-byte histograms. This distinction
allows a physical result to preserve unexpected upper bits without losing the
ROM-relevant low-line comparison. [standard] for the predicted table;
[hypothesis] for pending physical values.
| Payload range | Contents |
|---|---|
0–3 | pre-sequence ports 0x00, 0x03, 0x04, and 0x20 |
4–259 | 256 samples in write-major, trial-major, delay-major order |
260–263 | post-sequence ports 0x00, 0x03, 0x04, and 0x20 |
264 | port-0x00 read after writing zero to release both lines |
265 | final port-0x02 status |
The four NOP counts define instruction-spaced sample points, not wall-clock
times. OUT, IN, and ASIC wait states contribute additional delay. A logic
analyzer or oscilloscope is still required for voltage thresholds, pull-up
resistance, and analog rise time. Comparing stable and unstable sample bins can
nevertheless locate a digital readback change between the tested instruction
gaps. [confirmed] for the code spacing; [hypothesis] for physical settling.
Run this probe only with the link connector empty. It deliberately drives both
lines low before every sample and can leave a link-activity request pending.
It disables interrupts during the matrix, releases both lines, records
port-0x04 before and after, and restores the caller’s interrupt state before
creating the AppVar. Cleanup does not depend on the unverified bits-4–5 latch
readback. A reset or interruption before cleanup can leave a line asserted. Use
a backed-up test calculator. Do not attach another calculator, Graph Link,
TI-Keyboard, speaker, or other peripheral.
The pinned SPASM-ng build produces 482 machine-code bytes with SHA-256
394b2bc9560f277c293d7257f324439619298d5f12c3a2a16cce00cd5f28a8b2.
The packaged 1,044-byte HWLINK.8xp has SHA-256
8eb8c9e16899044384efd43a01c29d8eef4ff43c92ce106f04419089a1481025.
[confirmed]
Keypad settling probe
Probe ID 9 measures port-0x01 after keypad group-selection edges. It first
selects all groups and waits for the launch key to be released. It then waits
for any held key or chord and records that all-groups read. A 65,535-iteration
settling loop follows before the timed matrix begins. [confirmed] for the
assembled control flow.
For each group write 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF,
and 0x7F, the probe first writes 0x00 to select every group, writes the
target, waits for 0, 4, 16, or 64 NOP instructions, and reads port 0x01.
It repeats each group and delay point 16 times. The 0x7F case also tests the
otherwise unused eighth group-selection bit. [confirmed] for the assembled
instruction sequence; [hypothesis] for its physical effect.
| Payload range | Contents |
|---|---|
0–4 | pre-sequence ports 0x01, 0x02, 0x03, 0x04, and 0x20 |
5 | all-groups read that triggered the held-chord delay |
6–517 | 512 samples in group-major, trial-major, delay-major order |
518–522 | post-cleanup ports 0x01, 0x02, 0x03, 0x04, and 0x20 |
The decoder reports raw samples, histograms, stable values, active-low pressed columns, and comparisons with the same trial’s 64-NOP value. It distinguishes an early sample with additional low columns from any other mismatch. That comparison can identify a read that has not yet released a column after the all-groups precondition without assuming which keys the operator held. [confirmed] for the decoder; [hypothesis] for pending physical samples.
The fixed settling loop before measurement is 1,703,905 base T-states. This is
about 0.284 seconds at nominal 6 MHz or 0.114 seconds at nominal 15 MHz. The
probe records port 0x20 so a result retains the selected speed. These values
exclude the preceding LD DE,0xFFFF; the timed sample points remain instruction
gaps rather than wall-clock measurements. [confirmed] for the instruction
count; [standard] for the nominal clock conversion.
Delete HWKEYS01 before the run. Release the launch key, then hold the exact
test key or chord until the program returns. Record every held key with the
exported AppVar. The probe will wait indefinitely if no key is pressed. It
disables interrupts during the wait and matrix, writes 0xFF to unselect all
groups before restoring the caller’s interrupt state, and records adjacent
status, interrupt, and speed ports before and after. It does not measure switch
bounce, analog voltage, or a logic-analyzer waveform.
The pinned SPASM-ng build produces 822 machine-code bytes with SHA-256
33936def9f7844131f77b970804e7fd8af79610cc93a041afcfb9fd507555e8e.
The packaged 1,724-byte HWKEYS.8xp has SHA-256
3479ac8eb426977d088c7587da816bc88a67ded0625962334a0a723ce3424d23.
[confirmed]
Memory-bus timing probe
Probe ID 10 measures all six port-0x2E memory wait classes with programmable
timer 2. Each class has a baseline run with port 0x2E = 0 and a second run
with only that class enabled. Timer source 0x45 divides the 32.768 kHz crystal
by 16, giving a documented 2,048 Hz sample clock. [confirmed] for the assembled
setup; [standard] for the physical timer source.
The probe runs only when timer-2 source and mode ports 0x33 and 0x34 are
zero, port-0x02 bit 2 reports a locked Flash gate, all four ports
0x29–0x2C have both memory-group gates set, and fixed Flash bytes
00:0CE6–00:0CEA equal F5 23 2B F1 C9. A failed guard creates a result
with an outcome code and no measurements. [confirmed] for the control flow and
OS 2.55MP helper signature.
| Outcome | Meaning |
|---|---|
0 | all guards passed and 12 measurements completed |
1 | timer-2 source was active |
2 | timer-2 mode/status was nonzero |
3 | Flash gate reported unlocked |
4 | at least one Flash/RAM timing gate was disabled |
5 | fixed-page helper did not match OS 2.55MP |
Every measurement starts counter 0xFF, runs a fixed loop, then records the
counter, timer-2 mode/status, and port 0x04. Mode bit 2 or port-0x04 bit 6
marks an expired sample invalid. The enabled-minus-baseline counter difference
is the added number of 2,048 Hz timer ticks. [confirmed] for the result decoder;
[standard] for the timer completion bits.
| Case | Port-0x2E mask | Iterations | Wait-sensitive accesses | Timed operation |
|---|---|---|---|---|
| Flash M1 | 0x01 | 4,096 | 20,480 | five opcode fetches per call to 00:0CE6 |
| Flash read | 0x02 | 16,384 | 16,384 | one fixed-page data read |
| Flash write | 0x04 | 16,384 | 16,384 | one locked 0xF0 reset-command write |
| RAM M1 | 0x10 | 16,384 | 65,537 | four loop opcodes per iteration plus counter-read opcode |
| RAM read | 0x20 | 16,384 | 32,769 | one data read and one branch operand per iteration, plus counter operand |
| RAM write | 0x40 | 16,384 | 16,384 | one idempotent scratch-byte write |
The access counts include every fetch affected before the counter read. They do not treat the loop body as one abstract access. If a class adds one T-state per listed access, the decoder estimates the CPU frequency as
$$ f_{\mathrm{CPU}} = \frac{N_{\mathrm{wait}} \times 2048}{\Delta_{\mathrm{timer}}}\,. $$
Counter quantization makes each individual estimate coarse. Agreement across the six independent cases is stronger evidence than any single value. A zero delta means no wait was observed at this resolution; it does not prove a fractional or conditional delay is absent. [confirmed] for the arithmetic; [hypothesis] for pending physical results.
| Payload range | Contents |
|---|---|
0–12 | pre-sequence ports 0x02, 0x03, 0x04, 0x20, 0x29–0x2C, 0x2E, 0x2F, and 0x33–0x35 |
13 | outcome code |
14–49 | six baseline/enabled pairs of counter, mode/status, and port-0x04 |
50–62 | post-cleanup copy of the 13 pre-sequence ports |
The Flash-write loop targets 0x0000 with 0xF0 only. On the documented AMD
command interface, 0xF0 is a read-array reset rather than a program or erase
command. The probe also refuses an entry state that reports the protected gate
open. [confirmed] for the emitted address and byte; [standard] for the Flash
reset command; [hypothesis] for the physical gate readback.
The complete run disables interrupts for about one second at nominal 6 MHz and
less at nominal 15 MHz. Standard-timer requests can coalesce during that window,
so OS tick consumers can miss time. After every measurement the probe stops
timer 2, acknowledges its mode port, and restores port 0x2E. At the end it
restores the idle counter byte and the caller’s interrupt state. A reset during
measurement can leave port 0x2E or timer 2 changed. Use a backed-up OS 2.55MP
calculator with stable power.
The pinned SPASM-ng build produces 636 machine-code bytes with SHA-256
46c5f64f5ba720a129a4f889af5757dfb34dbc037a60172669c9ad5dcfb76017.
The packaged 1,352-byte HWBUS.8xp has SHA-256
b9f66cf6cdc3564c6a4d412c36a9a768be388f45b1eeee558d3351c3a0a4a874.
[confirmed]
Prefix-M1 timing probe
Probe ID 11 measures how the RAM opcode-wait bit treats six instruction
shapes. Each shape runs 12,288 times with port 0x2E = 0, then with only RAM
M1 bit 4 set. The program executes from user RAM and uses timer 2 at 2,048 Hz.
[confirmed] for the assembled loops; [standard] for the timer source.
| Case | Bytes | Instruction | Z80 M1 fetches per iteration | Complete-loop M1 count |
|---|---|---|---|---|
| Unprefixed | 00 | NOP | 1 | 61,441 |
| CB | CB 42 | BIT 0,D | 2 | 73,729 |
| ED | ED 44 | NEG | 2 | 73,729 |
| DD | DD 7C | LD A,IXH | 2 | 73,729 |
| Repeated DD | DD DD 7C | LD A,IXH | 3 | 86,017 |
| Indexed CB | DD CB 00 46 | BIT 0,(IX+0) | 2 | 73,729 |
The complete count includes four loop-control M1 fetches per iteration and the
final timer-counter IN opcode. The indexed-CB displacement, final opcode, and
(IX+0) data access are non-M1 reads on a Z80. Only port-0x2E bit 4 is set,
so those reads do not receive the RAM-read delay. [confirmed] for the emitted
bytes and counts; [standard] for the Z80 bus-cycle classification.
LD A,IXH is an undocumented Z80 form chosen because it leaves IX intact.
The next LD A,B overwrites its result. NEG also changes A, and each BIT
changes flags, but the common loop overwrites those values before its branch.
The indexed-CB case reads the current result slot through IX; it does not
write through that pointer. [confirmed]
TilEm counts two M1 fetches for the indexed-CB instruction. Wabbitemu applies
its opcode wait to three bytes, although it decrements R after the final
fetch. The reusable source analyzer requires TilEm commit
f56ad637d0524ee841dd381be6ecbaf5b8975600, Git tree
58316afe35d69e69353f0f743698144153051d4a, and Wabbitemu tree SHA-256
a8a4f97fc7952770bed317b4a477f80345894da38d14fad8f0bf0ee60aae71ba.
It reports no physical result. [standard]
python3 -m ti84re.emulators.describe_prefix_fetch_models \
--tilem-source /path/to/tilem \
--wabbitemu-source /path/to/wabbitemu --json
The canonical JSON report has SHA-256
ac5c618269a5a097b2f23c0ac9fc3ed5ca20b1749673b1020206e5c447fbf61c.
[confirmed] for the hash-guarded source analysis.
The exact assembled image also completed in the pinned Wabbitemu core after a
retail OS 2.55MP boot. The guarded runner injected all 587 bytes into RAM page
01 and stopped at 01:9EC2, immediately before _CreateAppVar. It
executed 737,692 probe instructions and 5,669,409 modeled T-states without an
execution-violation reset. The baseline-to-enabled timer deltas were 21, 25,
25, 25, 29, and 30 ticks in table order. The indexed-CB delta is one tick from
the repeated-DD three-wait control and five ticks from the mean of the
single-prefix two-wait controls, so the decoder selects
wabbitemu-three-m1. [confirmed] for this emulator execution.
The native adapter was built from Wabbitemu commit
48c2dc0e6d1d87bb5cf9611efbeb0d048b19c422; the build has SHA-256
3acb6a18280f9c42d6fe324188eab73f87280ee70b973e1251fcfa50f54fb14e.
This run validates the assembled loops, timer sampling, restoration path, and
the pinned emulator’s wait model. It does not execute AppVar creation, measure
host wall time or electrical timing, or establish physical ASIC behavior.
[confirmed]
The decoder compares the indexed-CB added ticks with the mean of the CB, ED, and DD rows and with the repeated-DD row. It reports whether the sample is closer to the two-M1 Z80/TilEm model or Wabbitemu’s three-wait model. Timer quantization can make the result equidistant; retain all raw triples. [confirmed] for the decoder; [hypothesis] for pending physical samples.
The entry guards require timer-2 source and mode/status to be zero and RAM gate
bit 1 to be set in ports 0x29–0x2C. Outcomes 1, 2, and 3 report an
active source, active mode, or disabled RAM gate. The payload uses the same
13-byte pre/post state and 36-byte measurement layout as HWBUS. The probe
restores port 0x2E, timer source and mode, the idle counter, and interrupt
state. A complete nominal-6-MHz run masks interrupts for about one second.
[confirmed]
Delete HWPFX001 before running Asm(prgmHWPFX). Retain the exported AppVar,
probe manifest, calculator model, PCB or ASIC revision, and OS version together.
The pinned SPASM-ng build produces 587 machine-code bytes with SHA-256
5e58ddfb1df820b79446fc10c79b681d0c58f7f16b252d18f1fef44d576a045b.
The packaged 1,254-byte HWPFX.8xp has SHA-256
724a43e82e81de096025931a1aff052de270185f4c3d7ef856f8efeae76bbc77.
[confirmed]
Programmable-timer physical probe
Probe ID 12 separates four programmable-timer edges on a physical ASIC:
- whether crystal source
0x41divides 32.768 kHz by 33, as published and modeled by TilEm, or by 32, as modeled by Wabbitemu and MAME; - whether the
0xC0source family applies the speed-selected port-0x2Fprescaler; - whether counter value zero free-runs through a 256-count period, completes immediately, or remains idle; and
- whether mode/status bit 2 appears after the first or second unacknowledged expiry.
These alternatives come from the raw WikiTI port descriptions and pinned TilEm, Wabbitemu, and MAME source. They define the discriminator, not the physical answer. [standard] for the source-specific models; [hypothesis] for the pending ASIC behavior.
The entry guards require timer-1 and timer-2 source and mode/status ports to be
zero. Port 0x04 completion bits 5–6 must also be clear. The probe records an
outcome without starting a measurement when a guard fails. Every polling loop
has a 0xFFFF iteration bound, and outcome 6 reports a measurement timeout.
[confirmed]
| Outcome | Meaning |
|---|---|
0 | all guards passed and all measurements completed |
1 | timer-1 source was active |
2 | timer-1 mode/status was active |
3 | timer-2 source was active |
4 | timer-2 mode/status was active |
5 | a programmable-timer completion bit was pending |
6 | a bounded measurement loop timed out |
The four divisor trials run sources 0x41 and 0x45 together. Source 0x45
is the common 2,048 Hz reference in every compared model. Each trial retains
both start and end counters, and the decoder aggregates the ratio instead of
depending on one quantized sample. The mode-3 matrix writes 0x4B to port
0x2F, then writes CPU-speed requests 0–3, records the readback, and counts
source-0xE0 expiries against source 0x45. The target timer uses a
250-count loop so the decoder can reconstruct
more than 255 target ticks. [confirmed] for the assembled measurement and
decoder; [standard] for the named source models.
The zero-counter case compares source 0x45 at counter zero with 31 ticks of
source 0x46. The expiry case records timer-1 mode/status and port 0x04
after an ordinary four-count loop expiry and again after the following
unacknowledged 256-count overflow period. The reference timer supplies bounded
completion windows; the program never executes HALT. [confirmed]
| Payload range | Contents |
|---|---|
0–12 | pre-sequence ports 0x02, 0x03, 0x04, 0x15, 0x20, 0x2D, 0x2F, and 0x30–0x35 |
13 | outcome code |
14–29 | four source-0x41/source-0x45 counter trials |
30–65 | four nine-byte mode-3 speed and expiry-count cases |
66–71 | counter-zero case |
72–77 | first- and second-expiry status case |
78–90 | post-cleanup copy of the 13 pre-sequence ports |
Run and decode a physical result with:
python3 -m ti84re.hardware.decode_probe --json HWTMR001.8xv
The decoder reports raw counters and status bytes alongside the nearest source model. A nearest-model label describes that sample; it does not establish an ASIC-wide rule. Retain the exported AppVar, manifest, calculator and ASIC identity, CPU-speed readbacks, and artifact hashes together.
The exact 835-byte assembled image completed in the pinned Wabbitemu core after
a retail OS 2.55MP boot. The shared injected-program runner stopped at
01:9EE4, immediately before _CreateAppVar, after 1,645,212 probe
instructions and 12,937,610 modeled T-states. It recorded no
execution-violation reset. [confirmed] for this emulator execution.
The four trials inferred divisor 3568/111, or about 32.144. The decoder
selected the Wabbitemu/MAME divisor-32 model. Speed requests 0–3 read back as
0, 1, 1, and 1. The nonzero cases inferred a port-0x2F prescaler near one,
matching Wabbitemu’s omitted prescaler. Counter zero completed with
mode/status 0x04 and port 0x04 = 0x68. Both expiry samples read
mode/status 0x05, so bit 2 was present on the first expiry. Every restoration
field compared equal. [confirmed] for the pinned Wabbitemu run.
The native adapter uses Wabbitemu commit
48c2dc0e6d1d87bb5cf9611efbeb0d048b19c422, source-tree SHA-256
a8a4f97fc7952770bed317b4a477f80345894da38d14fad8f0bf0ee60aae71ba,
and binary SHA-256
3acb6a18280f9c42d6fe324188eab73f87280ee70b973e1251fcfa50f54fb14e.
Run the same guarded path with:
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_timer_physical_probe \
--binary /path/to/wabbitemu-headless \
--expected-binary-sha256 3acb6a18280f9c42d6fe324188eab73f87280ee70b973e1251fcfa50f54fb14e \
--output-dir /tmp/wabbitemu-timer-physical --json
This run validates the assembled control flow, bounded polling, result layout,
cleanup, and decoder against one emulator implementation. It does not execute
AppVar creation, measure wall time or crystal accuracy, or establish physical
ASIC behavior. No HWTMR001 result from a calculator is available.
The pinned SPASM-ng build produces 835 machine-code bytes with SHA-256
6767caf1d714bc15e642de2f791151a060015fa0d9faebe1ebddd92d184df68a.
The packaged 1,750-byte HWTMR.8xp has SHA-256
2182a69520ad1e82e0c7b94ef96c5910ca50727b58c77173c4f428ba95cc329c.
[confirmed]
Safety boundary
The RAM alias probe is designed to restore its writes, but it has not completed a physical run. A reset, power loss, assembly defect, or unexpected exception before the restoration loop can leave a changed byte. Use a backed-up test calculator and stable power. Do not run the RAM probe on a unit whose contents cannot be replaced.
The snapshot, battery, raw-battery, raw-link, keypad, bus-timing, prefix-M1,
programmable-timer, and alias probes restore interrupt enable state before
creating the result AppVar.
Both battery probes restore ports 0x04, 0x39, 0x3A, and the complete
saved traceFlags byte.
The raw-battery probe also executes the ROM’s selector cleanup after every
sample sequence. The raw-link probe releases both link lines during cleanup. A
returned execution probe restores port 0x06 and the interrupt state. The
keypad probe normalizes port 0x01 to the OS’s all-groups-unselected value
0xFF. The bus and prefix timing probes restore port 0x2E and an initially
idle timer 2. The programmable-timer probe restores CPU speed, port 0x2F,
and the initially idle timer-1 and timer-2 triplets. It snapshots port 0x2D
but does not write it.
The bus-timing probe’s direct Flash writes are 0xF0 read-array resets, not
program or erase sequences. The prefix-M1 probe accesses only user RAM and I/O.
A denied fetch may reset before cleanup instructions. The result AppVar is the
intended persistent data write.
[confirmed] for the source and assembled bytes; [hypothesis] for unmeasured
physical execution and reset retention.
Source layout
| Path | Purpose |
|---|---|
tools/probes/hardware/common.inc | OP1 setup, _CreateAppVar, and frame copy |
tools/probes/hardware/asic-snapshot.asm | read-only ASIC, timing, and GPIO register snapshot |
tools/probes/hardware/battery-level.asm | repeated retail battery-level bcall and restoring state audit |
tools/probes/hardware/battery-raw.asm | repeated raw comparator-selector sequence and restoring state audit |
tools/probes/hardware/link-raw.asm | disconnected two-wire link readback and instruction-spaced settling matrix |
tools/probes/hardware/keypad-settle.asm | held-key and chord matrix-settling measurements |
tools/probes/hardware/bus-timing.asm | six-class Flash/RAM wait-state timing matrix |
tools/probes/hardware/prefix-m1.asm | prefixed-instruction RAM-M1 timing matrix |
tools/probes/hardware/timer-physical.asm | guarded programmable-timer divisor, prescaler, zero-counter, and expiry matrix |
tools/probes/hardware/usb-snapshot.asm | read-only low-USB control and status snapshot |
tools/probes/hardware/md5-edge.asm | calculator-side MD5 measurements |
tools/probes/hardware/ram-alias.asm | calculator-side RAM alias and restoration measurements |
tools/probes/hardware/execution-fetch.asm | parameterized read-only Flash and RAM fetch measurement |
tools/ti84re/hardware/probe.py | reusable TI container, frame, and payload library |
tools/ti84re/hardware/bus_timing.py | timing-register models and physical counter-pair decoder |
tools/ti84re/emulators/prefix_fetch_models.py | hash-guarded emulator prefix-fetch source analysis |
tools/ti84re/hardware/timer.py | reusable source, duration, RTC, and physical timer-result models |
tools/ti84re/emulators/describe_prefix_fetch_models.py | text and JSON prefix-fetch comparison CLI |
tools/ti84re/emulators/wabbitemu/run_prefix_m1_probe.py | exact-ROM guarded assembled-probe execution CLI |
tools/ti84re/emulators/wabbitemu/run_timer_physical_probe.py | exact-ROM guarded assembled timer-probe execution CLI |
tools/ti84re/hardware/battery.py | ROM decision tree and emulator threshold-region model |
tools/ti84re/hardware/describe_battery.py | text and JSON threshold/sample model CLI |
tools/ti84re/hardware/build_probes.py | SPASM runner, artifact validator, packager, and manifest CLI |
tools/ti84re/hardware/decode_probe.py | text and JSON result CLI |
Generated .8xp files are build artifacts and are not required in the
repository. A physical evidence record should retain the exact exported
AppVar, manifest, hashes, and unit metadata together.
Variables and the VAT (Variable Allocation Table)
Deep dive: Variables, Archive & Unarchive — Store/Recall, the byte-verified
_FindSymwalk, and archive/unarchive.
Every named object the user creates — reals, lists, matrices, strings, programs, pictures, appvars, groups — is catalogued in the VAT, a table in RAM that grows downward from a fixed top. The VAT stores metadata + where the data lives; the data itself sits elsewhere in RAM (or in archived flash).
Object types — TIVarType enum [confirmed]
| Val | Name | Val | Name |
|---|---|---|---|
| 0x00 | RealObj | 0x0C | CplxObj |
| 0x01 | ListObj | 0x0D | CListObj |
| 0x02 | MatObj | 0x0E | UndefObj |
| 0x03 | EquObj | 0x0F | WindowObj |
| 0x04 | StrngObj | 0x10 | ZStoObj |
| 0x05 | ProgObj | 0x11 | TblRngObj |
| 0x06 | ProtProgObj | 0x12 | LCDObj |
| 0x07 | PictObj | 0x13 | BackupObj |
| 0x08 | GDBObj | 0x14 | AppObj |
| 0x09 | UnknownObj | 0x15 | AppVarObj |
| 0x0A | UnknownEquObj | 0x16 | TempProgObj |
| 0x0B | NewEquObj | 0x17 | GroupObj |
The active object’s type byte is held in varType (0x85D0); the current var being processed in curType (0x8450). Both are typed TIVarType in the DB.
Variable lookup
The OS passes variable identity through OP1 as a “name string”: OP1[0] = type byte, OP1[1..] = the name (token/bytes). The lookup/create family all key off OP1:
| Routine | Addr | Role |
|---|---|---|
_FindSym | 00:0E65 | find the VAT entry named by OP1; its page-07 body selects fixed-token or length-prefixed scanning from the OP1 name encoding; also the RST 10h fast path |
_ChkFindSym | 00:0E60 | route AppVarObj, GroupObj, ProgObj, TempProgObj, and ProtProgObj directly to the length-prefixed scanner; other classes fall through _FindSym |
_CreateReal | 00:10B8 | make a RealObj named by OP1 |
_CreateReal/_CreateCplx/_CreateRList/_CreateCList/_CreateRMat/_CreateStrng/_CreateProg/_CreateAppVar/… | 00:10B0-00:1153 | one exported creator bcall per creatable variable class — ~13 _Create* routines covering the creatable classes, not one per TIVarType (no _CreateList/_CreateMat/_CreateStr); some object types are made only by internal routines with no public _Create* bcall (e.g. the GroupObj creator at 00:1157, called from 39:73AF) |
_DelVar/_DelVarArc | 00:1308/00:12D9 | delete (and handle archived copies) |
_InsertMem/_DelMem | 00:0F81/00:1368 | public low-level grow/shrink of a RAM region (the create path instead uses the internal gap routine at ram:0F0C) |
_CreateReal (recovered): sets the type byte and a fixed size of 9, then jumps to the common create core at 00:1011. That core stores the type, type-checks the object (chk_type_not_str at ram:2045), handles the complex-list special case (OP1.value.exp == 0x5D), applies the 6-character name limit (00:1023 CP 0x7; 00:1025 JP NC,00:2700 → LD A,0x88, E_Syntax), and carves the gap via the internal routine at ram:0F0C (00:1034). Aggregate creators (lists/matrices) instead enter through the size prelude var_alloc (00:1005), which computes count×element-size + the 2-byte header and raises E_Memory on overflow (JP C,00:2721 at 00:1008 → LD A,0x8E) before falling into the same 00:1011 core.
Variable data formats — rendered as C [confirmed]
A VAT entry points at the variable’s data, whose layout depends on the object type. Every numeric value is a 9-byte BCD TIFloat (see Floating-Point); aggregates are a small header followed by an element array or a tokenized blob. These mirror the project’s DB types (TIFloat, TIComplex, TIListHdr, TIMatrixHdr), with fields shown in ROM byte order:
/* ── numeric primitives ───────────────────────────────────────────── */
typedef struct {
uint8_t type; /* 0x00 real, 0x80 negative; 0x0C/0x8C = complex part */
uint8_t exp; /* base-10 exponent, biased by 0x80 (0x80 == 10^0) */
uint8_t mantissa[7]; /* 14 packed BCD digits, normalized d.dddddddddddddd */
} TIFloat; /* 9 bytes */
typedef struct { TIFloat re, im; } TIComplex; /* 18 bytes */
/* ── aggregate data (what the VAT entry's dataAddr points at) ──────── */
struct List { uint16_t count; TIFloat elem[/* count */]; }; /* ListObj 1; CListObj 0x0D uses TIComplex[] */
struct Matrix { uint8_t columns, rows; TIFloat elem[/* rows * columns */]; }; /* MatObj 2, row-major */
struct Tokens { uint16_t size; uint8_t body[/* size */]; }; /* EquObj 3, StrngObj 4, ProgObj 5/6 — tokenized */
struct AppVar { uint16_t size; uint8_t data[/* size */]; }; /* AppVarObj 0x15 — RAW bytes, not tokenized */
Per object type:
| Type | Val | dataAddr → | Size (bytes) |
|---|---|---|---|
RealObj | 0 | one TIFloat | 9 |
CplxObj | 0x0C | one TIComplex (re, im) | 18 |
ListObj / CListObj | 1 / 0x0D | count word + count×TIFloat/TIComplex | 2 + 9·n / 2 + 18·n |
MatObj | 2 | columns,rows bytes + row-major TIFloat[] (index math in Matrices and lists) | 2 + 9·r·c |
EquObj | 3 | size word + tokenized formula — system var, carries a selection/style byte, auto-evaluated (Graphing, Table) | 2 + size |
StrngObj | 4 | size word + tokenized text — inert (see Strings) | 2 + size |
ProgObj / ProtProgObj | 5 / 6 | size word + tokenized program (6 = edit-locked) | 2 + size |
AppVarObj | 0x15 | size word + raw bytes (any binary, not tokens) | 2 + size |
PictObj | 7 | a graph back-buffer image (plotSScreen snapshot) | 756-byte payload + 2-byte size word = 758 (_CreatePict passes payload size 0x02F4) |
GDBObj | 8 | graph database: mode byte + window vars + selected equations + styles | varies |
GroupObj | 0x17 | an archived bundle of other vars (lives in Flash) | varies |
WindowObj/ZStoObj (0x0F/0x10) hold the graph Window settings, TblRngObj (0x11) the table range, BackupObj (0x13) a full RAM image — all system, fixed-shape blobs.
Aggregate creators size their data region (= count × element-size + 2-byte header) in the var_alloc prelude (ram:1005), then fall into the common create core (ram:1011), which carves the gap via the internal routine at ram:0F0C (the create path’s own block-move, not the public _InsertMem; see Memory management). The specific _Create* routine then writes the data header after the core returns — e.g. _CreateRList writes the list count, _CreateStrng the 2-byte size word. All key off the name in OP1 (OP1.value.exp is the name’s token class — _CreateRList validates a list-name token 0x5D/0x24/0x3A/0x72).
The VAT entry [confirmed]
The VAT grows downward from symTable (0xFE66). _FindSym (00:0E65 →
findsym_scan at 07:565F) selects the scanner from the OP1 name encoding.
Fixed-token names, including reals, complex values, L-lists, [A]-matrices,
and system variables, use a short one- to three-byte comparison against
0x8479–0x847B. Length-prefixed program, AppVar, and Group names branch to
07:55D1 for a full-name comparison. _ChkFindSym takes the same named branch
earlier for object types 0x05, 0x06, 0x15, 0x16, and 0x17.
[confirmed]
TI’s public SDK directs callers to _ChkFindSym for Programs and AppVars. The
OS 2.55MP _FindSym body can still reach the length-prefixed scanner when OP1
has a matching name encoding. The public recommendation and the target ROM’s
internal dispatch are separate claims. [standard] for the SDK contract;
[confirmed] for this ROM.
On a match the scanner reads the entry metadata at fixed offsets relative to
the matched name pointer N:
Location (vs name ptr N) | Field |
|---|---|
N, N-1, N-2 | the name bytes (matched against OP1’s 0x8479–0x847B) |
N+1 | data page (B; 0 ⇒ data in RAM) |
N+2 / N+3 | data address — high byte, then low byte |
N+4 | version metadata |
N+5 | T2 metadata |
N+6 | type — low 5 bits = TIVarType class, high bits flag archive state; copied to OP1 at 0x8478 |
The fixed-token form has a forward C view when its base is the lowest name
byte, N-2:
typedef struct {
uint8_t name[3];
uint8_t dataPage;
uint8_t dataAddrHi;
uint8_t dataAddrLo;
uint8_t version;
uint8_t t2;
uint8_t typeID;
} VATEntry; /* 9 bytes */
Thus ((VATEntry *)(N-2))->dataPage is the byte at N+1, and typeID is
the byte at N+6. Named program, appvar, and group records have a
variable-length name and therefore cannot use this fixed three-byte prefix;
their six metadata bytes retain the same order relative to N. [confirmed]
Because the VAT grows downward, the type byte sits at the higher address and the name at the lower, so the scanner reads metadata upward from the matched name (this is the reverse of a forward C-struct order). _FindSym/_ChkFindSym return the page in B (0 ⇒ data in RAM). For an archived var the data address points into Flash and the page byte selects the page; the VAT entry itself always stays in RAM (only the data moves to Flash).
A fixed-token entry occupies nine bytes. 00:0E69 selects a nine-byte copy for
creation, while findsym_scan moves six bytes from the matched name token to
the type and three more bytes from that name position to the next entry’s type.
Length-prefixed program, appvar, and group names extend the name portion but
retain the six metadata bytes at N+1 through N+6. [confirmed]
Names come in two encodings:
- Token-named vars — real, complex,
L-lists (tVarLst0x5D),[A]-matrices (tVarMat0x5C), system vars, and the token-named strings (tVarStrng0xAA+ id) and equations (tVarEqu0x5E+ id) — carry a fixed name token, matched by the 1–3 byte compare above. - Length-prefixed names — programs, appvars, groups — store the name bytes with a length byte at the higher address; the scanner at
07:55D1reads the length byte, then compares the name bytes that precede it (downward, toward lower addresses — the same high-address-first ordering as the rest of the entry).
Strings (Str1–Str0) — a distinct object type [confirmed]
String variables are StrngObj (type 4) — not equation variables (EquObj = 3), although both hold tokenized byte streams. The ten strings Str1…Str0 are named by a 2-byte token: lead tVarStrng (0xAA) then tStr1…tStr0 (0x00…0x09), so Str1 = AA 00 … Str0 = AA 09.
Storage. _CreateStrng (id 0x4327, 00:1123) decompiles to create_var_entry(StrngObj) followed by writing a 2-byte word size into the data; the data area is then [word size][size tokenized bytes] — the same [size][bytes] shape programs and appvars use (above). The bytes are TI-BASIC tokens, not raw ASCII: a string stores exactly the token stream the editor renders, so "sin(A)" keeps the sin( token, the A token, and ) — which is why a string can hold any displayable token, commands included.
String vs. equation variable. Both hold tokenized byte streams, so the two are worth separating. EquObj vars (Y1–Y0, parametric, polar, sequence) are system variables that carry a selection/style flags byte and are auto-evaluated by the grapher, table, and solver (see Graphing, Table & Y= variables). A StrngObj is an inert user variable — no selection/style, never evaluated on its own; it is bytes the string commands manipulate.
Bridges between the two. Tokens convert a string’s text to/from executable form:
expr(parses a string’s token bytes as an expression and evaluates it → a value (string → number/list/…).String►Equ(/Equ►String((2-byte tokensBB 56/BB 55—t2ByteTok0xBBthentStrngToEqu0x56/tEquToStrng0x55) copy token bytes between aStrand aY=/equation variable (string ↔ equation).sub(,length((_StrLength, id0x4C3F→36:7F91), andinString(operate on the token bytes;_StrCopy(0x44E3→00:2810) is the byte mover. The"string-literal delimiter in source is its own token,tString(0x2A).
FindSym scan and VAT entry layout
The _FindSym scan loop and per-class VAT entry layout are byte-verified in Variables, Archive & Unarchive (findsym_scan@07:565F; tSymPtr1/tSymPtr2 and archived-var resolution covered there).
Floating-point engine
Deep dive: Calculation engine — ×, ÷, ^, roots, the transcendentals (sin/cos/ln/eˣ), and number formatting.
All TI-BASIC arithmetic runs through a BCD floating-point engine centered on the OP registers in RAM. The engine lives mostly on flash page 0 (it’s hot), with the RST-30 shortcut for the most common op.
Number format — TIFloat (9 bytes on disk) [confirmed]
+0 type 0x00 = real (positive), 0x80 = negative real;
0x0C/0x8C = complex (paired with the imaginary part)
+1 exp base-100? no — base-10 exponent, biased by 0x80 (0x80 = 10^0)
+2..+8 mantissa 7 bytes = 14 packed BCD digits, normalized d.dddddddddddddd
As a C struct:
typedef struct {
uint8_t type; /* +0: 0x00 real (positive), 0x80 negative; 0x0C/0x8C complex part */
uint8_t exp; /* +1: base-10 exponent, biased by 0x80 (0x80 == 10^0) */
uint8_t mantissa[7]; /* +2..+8: 14 packed BCD digits, normalized d.dddddddddddddd */
} TIFloat; /* 9 bytes on disk / in a stored var */
/* In an OP register slot the number occupies 11 bytes: the 9 above plus 2 trailing guard */
/* digit bytes (OP1EXT at +9/+10) used during math — see "OP registers" below. */
The stored value is
$$v = \pm\,(d_0.d_1d_2\cdots d_{13})\times 10^{\,e-\mathtt{0x80}}$$
where $e$ is the biased exponent byte and $d_0\ldots d_{13}$ are the 14 BCD mantissa digits. A ROM-byte scan found roughly 126 candidate BCD constants ROM-wide [hypothesis] ($\pi/180 = 1.745\ldots\mathrm{e}{-2}$, $180/\pi = 5.729\ldots\mathrm{e}{1}$, 65536, plus the FP transcendental coefficient tables on page 0x02). The table addresses below are confirmed by Ghidra disassembly and raw ROM bytes.
OP registers — 11 bytes each [confirmed]
OP1–OP6 begin at 0x8478 and occupy 11 bytes each. Their shared layout is:
typedef struct {
TIFloat value;
uint8_t guard[2];
} TIOpRegister;
The guard bytes extend the stored BCD mantissa during calculation. OP1 is
the primary accumulator; binary operations use OP2 and return in OP1.
Core operations [confirmed]
Every binary operation has the shape OP1 ∘ OP2 → OP1. Add and subtract walk the same five stages below; multiply and divide instead combine exponents (add them for ×, subtract for ÷) and multiply/divide the mantissas. Because the format is sign-magnitude BCD, the sign is settled separately — negating a value is a single XOR 0x80 on its type byte — so the digit work always runs on a non-negative 14-digit mantissa:
flowchart LR
A["clear guard digits<br/>fp_clear_guard"] --> B["align exponents<br/>shift smaller right by Δ"]
B --> C["BCD digit op<br/>add / sub / mul / div"]
C --> D["renormalize<br/>back to d.dddd…"]
D --> E["round on guards<br/>write type/exp to OP1"]
The page-0 entry points — the hottest get a one-byte RST shortcut, which is why FP code is dense with RST 30h/08h/20h:
| Routine | Addr | Shortcut | Effect |
|---|---|---|---|
_FPAdd | ram:229E | RST 30h | OP1 ← OP1 + OP2 |
_OP1ToOP2 | ram:1A2F | RST 08h | copy OP1 → OP2 (11 bytes, via copy_op11 ram:1a8e) |
_Mov9ToOP1 | ram:1B01 | RST 20h | load 9 bytes at HL → OP1 (a constant/var) |
_CkOP1FP0 / _CkOP2FP0 | ram:1DE9 / ram:1DEE | — | test OP1/OP2 == 0 (sets Z) |
_CkOP1Real | ram:1942 | — | type-check OP1 is real |
Alignment, then the worked example — _FPAdd
To combine $x=(-1)^{s_x} m_x\times 10^{e_x}$ and $y=(-1)^{s_y} m_y\times 10^{e_y}$, the engine first aligns to the larger exponent. With $e_x \ge e_y$ it shifts $m_y$ right by
$$\Delta = e_x - e_y \quad(\text{digit shifts})$$
one nibble per fp_shift_right_digit call; if $\Delta > 15$ the smaller operand falls entirely past the 14 mantissa digits plus the 2 guard digits and is dropped. It then adds the aligned mantissas when the signs match ($s_x = s_y$) and subtracts when they differ ($s_x \ne s_y$), fixing the result’s sign afterward — the essence of sign-magnitude arithmetic:
\begin{algorithm}
\caption{\texttt{\_FPAdd}: $OP1 \gets OP1 + OP2$ (sign-magnitude BCD)}
\begin{algorithmic}
\IF{$OP2 = 0$}
\RETURN $OP1$
\ENDIF
\IF{$OP1 = 0$}
\STATE $OP1 \gets OP2$ \COMMENT{incl. extended bytes}
\RETURN $OP1$
\ENDIF
\STATE $\Delta \gets \mathrm{exp}(OP1) - \mathrm{exp}(OP2)$ \COMMENT{\texttt{fp\_exp\_diff}}
\STATE shift the smaller mantissa right by $|\Delta|$ digits to align \COMMENT{\texttt{fp\_shift\_right\_digit}}
\IF{$|\Delta| > 15$}
\RETURN larger operand \COMMENT{other is negligible}
\ENDIF
\IF{$\mathrm{sign}(OP1) = \mathrm{sign}(OP2)$}
\STATE $\mathrm{mantissa} \gets$ BCD-add
\ELSE
\STATE $\mathrm{mantissa} \gets$ BCD-subtract
\STATE fix result sign \COMMENT{\texttt{fp\_sub\_mantissa}}
\ENDIF
\STATE round via the guard digits, renormalize, store exp/type in $OP1$
\RETURN $OP1$
\end{algorithmic}
\end{algorithm}
The full helper cluster is documented below.
Dynamic confirmation. Traced under headless TilEm: the
2+3run (home-2plus3.macro) enters_FPAddand — signs equal — falls through the sign test tofp_add_mantissa(ram:1cb9), while the5−2run (fpsub.macro) negatesOP2and takes the opposite-sign branch intofp_sub_mantissa(ram:1d37).fp_sub_mantissahas 0 hits in the add trace and the add path 0 hits in the subtract trace, so the pseudocode’s sign dispatch is confirmed both ways.
The FP helper cluster [confirmed]
These five page-0 primitives are shared by add/sub/mult/div and the transcendentals. All were decompiled and disassembled in this ROM; the fp_* names below are the project’s labels (in tools/symbols/names.txt). They operate on the OP-register guard region (OP1EXT/OP2EXT are 2 bytes each, at 0x8481–0x8482/0x848C–0x848D — fp_clear_guard zeroes all four) and the 7-byte mantissas of OP1 / OP2 (OP1M 0x847A / OP2M 0x8485, two bytes past the type/exponent bytes at 0x8478/0x8483).
| Helper | Addr | Role [confirmed] |
|---|---|---|
fp_shift_right_digit | ram:1bea | Mantissa shift-right by one BCD digit (one nibble). Cascades nibbles down 8 bytes (b[i] = b[i]>>4 | b[i-1]<<4) and returns the digit shifted out. Called per step to align the smaller operand. |
fp_exp_diff | ram:1fbf | Exponent difference OP1.value.exp − OP2.value.exp (signed). Drives how many fp_shift_right_digit steps are needed for alignment. |
fp_add_mantissa | ram:1cb9 | BCD add of the two mantissa+guard runs. Sets HL=0x848C (OP2 guard), DE=0x8481 (OP1 guard) and runs the shared BCD add/DAA-style adjust loop (bcd_add_pair). Used for same-sign add. |
fp_sub_mantissa | ram:1d37 | BCD subtract (OP1 − OP2) of mantissa+guard with borrow, via repeated DAA-style BCD adjust across all 7 mantissa bytes plus the guard byte. Used for opposite-sign add. (ram:1d2f, fp_sub_mantissa_fwd, is the same subtract entered with the operand pointers swapped.) |
fp_clear_guard | ram:2627 | Zero the extended guard bytes (OP1EXT/OP2EXT). |
ram:1d2f and ram:1d37 are two entry points into the same BCD-subtract body — 1d2f loads HL=0x8481 (OP1 guard), DE=0x848C (OP2 guard) and computes OP2 − OP1 into OP2 (LD A,(DE)SUB (HL)), while 1d37 enters with the pointers swapped for the reverse OP1 − OP2, before joining the common loop — so the caller picks the subtraction direction by choosing the entry. This is what lets _FPAdd produce a non-negative magnitude and then fix the sign.
Multiply/divide/transcendentals (on page 0x02) reuse the same align/normalize primitives.
Accumulator high-nibble helper [confirmed]
_ShRAcc = 0x41D4, body ram:1BCB, is a six-instruction scalar helper rather
than an OP-register operation. It executes four RRA instructions, masks with
0x0F, and returns the original high nibble of A in the low nibble. The
final AND defines the returned flags; no other register is touched.
A controlled trace passes A = 0xAB and records A = 0x0A, F = 0x1C after
the bcall returns. The result is in tools/data/community-bcall-semantics.csv.
[confirmed] under TilEm.
Floating-point stack (FPS) [standard]
FPS (0x9824) is a software stack for temporaries; _PushRealO1 (= RST 18h, ram:155C), _PushReal, _PopRealO1 through _PopRealO6, _PopReal, _AllocFPS, and _DeallocFPS manage it. Used to spill OP registers during nested expression evaluation.
Multiplication, division, and transcendentals [confirmed]
The rest of the FP op set lives alongside add on page 0, with the transcendentals banked to page 0x02:
| Routine | Addr | Role |
|---|---|---|
_FPSub | ram:2297 | OP1 = OP1 − OP2 |
_FPMult | ram:238B | OP1 = OP1 × OP2 |
_FPRecip | ram:253D | OP1 = 1 / OP1 |
_FPDiv | ram:2541 | OP1 = OP1 / OP2 |
_LnX | 02:6EFD | natural log |
_EToX | 02:705C | eˣ |
_SinCosRad | 02:733E | sin/cos (radians) |
See Calculation engine for the ×/÷/^/root algorithms and number formatting.
Transcendental method [confirmed]
The ln/e^x/sin-cos evaluators use local page 02 code and coefficient tables.
fp_mul_indexed_constant (ram:2362) calls the stub at ram:3DD1, whose
inline descriptor 1E 7D 02 selects coeff_fetch (02:7D1E). It then enters
the _FPMult body at ram:2392. The preceding LD A,n selects a coefficient;
it does not select a flash page. The actual banked-call helper is
cross_page_jump (ram:2B09). [confirmed]
The shared algorithm — digit-by-digit pseudo-division [confirmed]
The forward log and exp evaluators use a digit-by-digit pseudo-division
recurrence. logexp_digit_table (02:7181) contains the 16 values
$\log_{10}(1+10^{-k})$ for $k=0\ldots15$. Each step scales a BCD value by
$1+10^{-k}$ with one digit shift and one BCD addition. Only base conversion
uses fp_mul_indexed_constant and general multiplication. The traces also
separate the accumulator entries: _EToX uses fp_add_mantissa
(ram:1CB9), while _LnX uses its sibling at ram:1CA9. [confirmed]
Logarithm. With the exponent already split off so the mantissa is $x\in[1,10)$, the loop (02:6F80–6FEE) drives $x$ up toward $10$ by repeatedly scaling by the largest table factor that doesn’t overshoot; the number of scalings at each position is the corresponding digit of the answer, and the running sum of the table entries is the logarithm:
\begin{algorithm}
\caption{Logarithm by pseudo-division (table $c_k=\log_{10}(1+10^{-k})$ at \texttt{02:7181})}
\begin{algorithmic}
\REQUIRE reduced mantissa $x \in [1,10)$, accumulator $L \gets 0$
\FOR{$k = 0$ \TO $15$}
\WHILE{$x \cdot (1+10^{-k}) \le 10$}
\STATE $x \gets x + (x \gg k\text{ digits})$ \COMMENT{$\times(1+10^{-k})$ is a BCD shift-add}
\STATE $L \gets L + c_k$ \COMMENT{add count = the $k$-th digit of the answer}
\ENDWHILE
\ENDFOR
\RETURN $\log_{10}x = 1 - L$ \COMMENT{$x$ driven up to $10$, then $\ln x = \log_{10}x \cdot \ln 10$}
\end{algorithmic}
\end{algorithm}
The two passes split the coarse digits ($k=0\ldots7$) from the fine digits
($k=8\ldots15$). fp_constant_table (02:7D42) supplies $\ln 10$ as row 6
through fp_mul_indexed_constant; _LogX skips that final multiply.
Exponential. _EToX/_TenX (02:7066+) run the same table backwards — consuming the fractional part $y$ digit by digit, subtracting $\log_{10}(1+10^{-k})$ while building $10^{y}=\prod_k(1+10^{-k})^{d_k}$ into an accumulator, again with only shift-adds:
\begin{algorithm}
\caption{Exponential by pseudo-multiplication (same table, run in reverse)}
\begin{algorithmic}
\REQUIRE $y = $ fractional part of $x\log_{10}e$, accumulator $A \gets 1$
\FOR{$k = 0$ \TO $15$}
\WHILE{$y \ge c_k$}
\STATE $y \gets y - c_k$
\STATE $A \gets A + (A \gg k\text{ digits})$ \COMMENT{$\times(1+10^{-k})$}
\ENDWHILE
\ENDFOR
\RETURN $10^{y} = A$
\end{algorithmic}
\end{algorithm}
logexp_digit_table powers ln, log, eˣ, and 10ˣ. fp_constant_table
supplies the base-conversion and trig-reduction constants. [confirmed]
Dynamic confirmation. Traced under headless TilEm:
ln(2)(ln2.macro) drives_LnX, whose selector (02:6F94) stepsA=00…07then08…0F(the coarse/fine split at the6FAD AND 0x8/6FD3 BIT 4tests), walking successive02:7181rows with a per-step shift-add, then fetches $\ln 10$ viaLD A,6CALL ram:2362and multiplies.e^{1}(exp1.macro) drives_EToX, which consumes the same table in reverse (the inner step isfp_sub_mantissa1d37, the accumulator addfp_add_mantissa1cb9), selector sweeping00…0Funder the710A CP 0x0Fbound. On-screen results:.6931471806and2.718281828.
_SinCosRad uses the same recurrence shape on the range-reduced angle.
trig_recurrence_table_a (02:7201) and
trig_recurrence_table_b (02:7281) each contain eight rows with two
sign/phase variants selected by OP5.value.type bit 7. The exact rotation
identity encoded by each row remains open. [confirmed]
_LnX — natural log (02:6EFD) [confirmed]
_LnX first calls _CkOP1Pos (ram:1E5D) and raises a domain error for
$x \le 0$. The core at 02:6F1B splits $x$ into mantissa and exponent. Its
pseudo-division loop at 02:6F8C–6FEC steps through
logexp_digit_table. The first phase stops when the selector reaches bit 3;
the second stops at bit 4. Calls to fp_mul_indexed_constant select row 3 for
$\log_{10}e$ and row 6 for $\ln 10$. [confirmed]
_EToX — eˣ (02:705C) [confirmed]
_EToX clears the guard digits, then uses fp_mul_indexed_constant row 3
for $\log_{10}e$. It skips _TenX’s separate guard initialization and joins
the shared body at 02:7069. That body splits the integer digit shift, handles
sign and reciprocal cases, then evaluates the fractional part through
logexp_digit_table. The CP 0x0F bound at 02:7109 establishes 16 selector
slots. [confirmed]
_SinCosRad sine and cosine in radians (02:733E) [confirmed]
This one keeps its range reduction on page 0x02 and is the most fully recovered:
-
Mode/select flags.
0x8499holds the trig-op selector —0x01(sin),0x02(cos),0x04(tan) — ORed with0x80when(IY+0)bit 2 is clear (BIT 2,(IY+0)JR NZ,+2OR 0x80)._SinCosRaditself enters withA=0x81, so it stores0x81regardless.fp_clear_guardand_ZeroOP3initialize the work area. -
Exponent gate.
LD A,(0x8479)SUB 0x80JP C,02:73D4CP 0x0CJP NC— tiny arguments (negative exponent) take a fast path at02:73D4, and arguments with decimal exponent ≥ 12 are rejected to the slow/error path (_JError 0x84for out-of-range), because reduction can no longer be done accurately. -
Reduce the angle. It reduces against the stored period constants and takes the fractional part to find the quadrant. The reduction constants are the page-0x02 BCD block:
02:7D81— the 2π full-turn modulus (mantissa62 83 18 53 07 17 96=6.2831853…), copied to the OP3 work reg viaLD HL,02:7D81CALL ram:1AE2(ram:1AE2/copy7_from_8490copies 7 mantissa bytes to0x8490).02:7D8E,02:7D95,02:7D96— companion constants used in the quadrant-fixup / remainder comparisons (CALL ram:1D7Bmagnitude compare at02:73B1/02:7447). The quadrant (0–3) is accumulated inB/bStack_1(bits 0/3/6) and decides sin-vs-cos and the result sign (theXOR 0x1 / OR 0x8 / XOR 0x8flag juggling at02:7424–02:7464).
-
Per-digit evaluation. The reduced argument enters
transcendental_eval(02:7498), the shared engine used by $\ln$ and $e^x$. Forsin(1), the reduced argument is $r = \pi/2 - 1 = 0.5707963267948966$. The engine computes $\cos r$, while the quadrant bits inOP5.value.typecarry the sign and phase. The recurrence has three phases; the first two consume the trig tables:- Phase 1 — digit extraction (
02:74A4–02:74E0). For rows $k=0,\ldots,7$,02:74A8setsDE = OP2Mand calls the table-A entry at02:731D. The selected row address is0x7201 + 16*k + 8*v, where $v$ is bit 7 ofOP5.value.type.ram:1A94copies the eight bytes.fp_align_round_diffaligns the row intoOP3at scale $10^{-(k+1)}$. A non-restoring subtract/add sweep reduces the accumulator modulo 1 and stores one decimal digit inOP5.value.mantissa[k]. Forsin(1), the digits are6,3,8,8,2,4,3,6, leaving $u \approx 2.56\times10^{-9}$. [confirmed] - Phase 2 — correction product (
02:74E6–02:7528). Entry02:7312loadstrig_recurrence_table_b[0], where $b_0 = 0.9509852944837202$, intoOP2M. At each digit position $k$, the loop performs $n_k = \lfloor(11-d_k)/2\rfloor$ BCD shift-add steps of $\mathtt{OP2} \gets \mathtt{OP2} + \mathtt{OP2}\cdot10^{-2k}$. This builds $b_0\cdot\prod_k(1+10^{-2k})^{n_k}$ without general multiplication. Forsin(1), the product is $0.9704891777365256$. [confirmed] - Phase 3 — result assembly (
02:752Aonward). The engine walks the stored digits again with align/add-sub steps and exponent bookkeeping. Forsin(1), it producesOP4 = 0.8414709848078931, withcos(1) = 0.5403023058681400alongside. [confirmed]
The closed-form identity of the phase-1 digit map remains open; see Open questions.
- Phase 1 — digit extraction (
Dynamic confirmation. Traced under headless TilEm:
sin(1)in radian mode (sin1.macro) drives_SinCosRad. The flag init, the exponent gate (735D LD A,(0x8479)SUB 0x80JP C,02:73D4CP 0x0CJP NC— neither branch taken, since the decimal exponent of 1 is0), and the reduction multiply by the02:7D81constant (7372 LD HL,02:7D81CALL ram:1AE2). The trace records all three recurrence phases and the on-screen result.8414709848. It also records eight phase-1 entries at02:731D, withB = 0–7andHL = 0x7201 + 16·B + 8after eachram:1A94copy, followed by one02:7312entry for the phase-2 base row.
Coefficient tables [confirmed]
coeff_fetch zeroes OP2.value.type, indexes fp_constant_table[A], then
copies the selected constant into OP2. The only
LD A,nCALL fp_mul_indexed_constant uses in this cluster select row 3
(log10(e)) and row 6 (ln(10)). Later trig reduction constants are loaded
directly from the same block.
02:7D42 constants, 9-byte stride:
[00] 81 57 29 57 79 51 30 82 32
[01] 80 15 70 79 63 26 79 48 97
[02] 7F 78 53 98 16 33 97 44 83
[03] 7F 43 42 94 48 19 03 25 18 ; log10(e) fetch site
[04] 80 31 41 59 26 53 58 98 00
[05] 7E 17 45 32 92 51 99 43 30
[06] 80 23 02 58 50 92 99 40 46 ; ln(10) fetch site
[07] 62 83 18 53 07 17 96 31 41 ; direct trig-reduction region starts here
[08] 59 26 53 58 98 78 53 98 16
Three entry paths share the indexing tail at 02:7320. Entry 02:7301
selects logexp_digit_table and OP4M. Entry 02:7312 selects
trig_recurrence_table_b and OP2M. Entry 02:731A selects OP4M, then
falls through 02:731D to select trig_recurrence_table_a. The phase-1 path
enters at 02:731D with OP2M already in DE. The shared tail adds
16*B + 8*v to HL, where $v$ is bit 7 of OP5.value.type, then ram:1A94
copies the eight-byte row to the destination. [confirmed]
logexp_digit_table has 16 eight-byte rows:
[00] 30 10 29 99 56 63 98 12 [01] 04 13 92 68 51 58 22 50
[02] 00 43 21 37 37 82 64 26 [03] 00 04 34 07 74 79 31 86
[04] 00 00 43 42 72 76 86 27 [05] 00 00 04 34 29 23 10 45
[06] 00 00 00 43 42 94 26 48 [07] 00 00 00 04 34 29 44 60
[08] 00 00 00 00 43 42 94 48 [09] 00 00 00 00 04 34 29 45
[10] 00 00 00 00 00 43 42 94 [11] 00 00 00 00 00 04 34 29
[12] 00 00 00 00 00 00 43 43 [13] 00 00 00 00 00 00 04 34
[14] 00 00 00 00 00 00 00 43 [15] 00 00 00 00 00 00 00 04
The two trig recurrence tables hold the forward sin/cos near-unity factors.
Each row is 16 bytes: the first eight-byte variant is selected when
OP5.value.type bit 7 is clear, and the second when it is set.
02:7201:
[00] 09 96 68 65 24 91 16 20 | 10 03 35 34 77 31 07 56
[01] 09 99 96 66 68 66 65 24 | 10 00 03 33 35 33 34 76
[02] 09 99 99 96 66 66 68 67 | 10 00 00 03 33 33 35 33
[03] 09 99 99 99 96 66 66 67 | 10 00 00 00 03 33 33 33
[04] 09 99 99 99 99 96 66 67 | 10 00 00 00 00 03 33 33
[05] 09 99 99 99 99 99 96 67 | 10 00 00 00 00 00 03 33
[06] 09 99 99 99 99 99 99 97 | 10 00 00 00 00 00 00 03
[07] 10 00 00 00 00 00 00 00 | 10 00 00 00 00 00 00 00
02:7281:
[00] 95 09 85 29 44 83 72 02 | 10 52 06 69 51 89 55 92
[01] 99 94 95 10 19 99 69 80 | 10 00 50 52 03 08 13 30
[02] 99 99 94 94 95 10 20 35 | 10 00 00 50 50 52 03 05
[03] 99 99 99 94 94 94 95 10 | 10 00 00 00 50 50 50 52
[04] 99 99 99 99 94 94 94 95 | 10 00 00 00 00 50 50 51
[05] 99 99 99 99 99 94 94 95 | 10 00 00 00 00 00 50 51
[06] 99 99 99 99 99 99 94 95 | 10 00 00 00 00 00 00 51
[07] 99 99 99 99 99 99 99 95 | 10 00 00 00 00 00 00 01
The forward ln, e^x, and sin/cos paths all advance one coefficient-table row
at a time with shift-and-add. Ln/e^x use logexp_digit_table; sin/cos use the
two trig recurrence tables. The inverse-trig arctangent engine instead uses a
base-10 CORDIC iteration, documented in Calculation engine.
Calculation engine
The calculation engine evaluates arithmetic and transcendental expressions in
the OP1–OP6 BCD floating-point registers. The software stack at FPS
(0x9824) holds nested temporaries. Floating-point engine
defines the TIFloat format and _FPAdd; this page covers multiplication,
division, powers, roots, transcendentals, formatting, and errors.
Register and stack model [confirmed]
Every calculation runs through the OP registers and the software FP stack; the table gives each register’s RAM address and its role during an operation.
| Reg | Addr | Role in a calc |
|---|---|---|
OP1 | 0x8478 | primary accumulator / result. Unary ops take arg here, return here. |
OP2 | 0x8483 | second operand for binary ops (OP1 ∘ OP2 → OP1). |
OP3–OP6 | 0x848E… | scratch; sign/exponent staging, complex pairs. |
| guard | 0x8481/8482 (OP1EXT), 0x848C/848D (OP2EXT) | extended guard digits, zeroed by fp_clear_guard at the top of nearly every op. |
FPS | 0x9824 | software FP stack for spilling OP registers during nested evaluation. |
A TIFloat is type(+0) exp(+1) mantissa(+2..+8); type bit7 = sign, bits set 0x0C = complex.
exp is base-10 biased by 0x80. Sign is sign-magnitude, so negation is a single XOR 0x80.
FP-stack discipline during nested expressions [confirmed]
Binary/transcendental routines that need to preserve an operand spill it to FPS:
_PushRealO1(=RST 18h,ram:155C),_PushReal/_PushRealOn,_PushOP1(ram:1599)._PopRealO1…_PopRealO6(ram:150F…14F6),_PopReal(ram:1512)._AllocFPS/_DeallocFPS(ram:1534/1526) grow/shrink the stack frame.
For example, the complex-log core uses _PushRealO1 to save the input,
computes the magnitude, then _PopRealO2 to recover it for the angle — the canonical
“spill then restore” used everywhere the parser evaluates a nested sub-expression.
Basic arithmetic [confirmed]
All four route operands through OP1/OP2, clear the guard digits, early-out on zero
operands, then do BCD mantissa work and renormalize. Result in OP1.
| Op | Routine | Addr | Notes |
|---|---|---|---|
+ | _FPAdd | ram:229E (= RST 30h) | sign-magnitude BCD add; see floating-point.md. |
− | _FPSub | ram:2297 | flips OP2.value.type bit 7, then falls into the add path. |
× | _FPMult | ram:238B | ram:250F adds exponents (→ _ErrOverflow on carry past 0x7F), then digit-by-digit BCD multiply accumulating into OP3. |
÷ | _FPDiv | ram:2541 | _CkOP2FP0 first → _JError(0x82) DIVIDE BY 0 if divisor 0; else restoring BCD long division. |
1/x | _FPRecip | ram:253D | sets OP1=1 then enters the divide loop (same body as _FPDiv). |
Convenience / derived ops:
_FPSquareram:238A=RST 08h(OP1→OP2) then_FPMult. [confirmed]_Cuberam:237D=_FPSquarethen_FPMult. [confirmed]_Times2ram:2282=OP1+OP1;_TimesPt5ram:2382loads the constant0.5(9-byte BCD @ram:2635) into OP2 then_FPMult. [confirmed]_InvSubram:227D=_InvOP1Sthen_FPAdd⇒OP2 − OP1(reversed subtract). [confirmed]- Negation:
_InvOP1Sram:24BD(XOROP1.value.typewith0x80, guarding against −0),_InvOP2Sram:24CD,_InvOP1SCram:24BA(both)._CkOP1Posram:1E5DANDsOP1.value.typewith0x80. [confirmed]
Roots and integer parts [confirmed]
_SqRoot02:6E38:_ErrD_OP1NotPos(→ DOMAIN if negative/complex-real),fp_clear_guard,_ZeroOP3, then a digit-by-digit BCD square-root extraction loop (ram:1C9Ctrial-subtract +ram:1D4Acompare, halving the exponent up front). A classic long-hand sqrt, not Newton’s method._Int/_Intgrram:2621/2263: floor._Truncram:2279drops the fractional part (toward zero);_Intgrtruncates then subtracts 1 (_Minus1ram:2294) when the original was negative, giving true floor._Fracram:24E3: fractional part = x − trunc(x); shifts mantissa by the exponent and keeps the low digits._Round/_RndGuardram:2623/02:6A57: round to the active display-digit count;_Roundis a thincross_page_jumpwrapper (body banked off page 0).
Degree, radian, and polar conversions [confirmed]
_DToRram:236B(deg→rad): multiply OP1 by $\pi/180$ (ram:235Dloads the constant) then normalize viaram:249E._RToDram:2374(rad→deg): multiply by $180/\pi$ (ram:2361)._PToR02:50BDpolar→rectangular; pairs with the complex trig below. These constants are the BCD floatsπ/180 = 1.745…e-2and180/π = 5.729…e1noted in floating-point.md’s constant scan.
Cross-page dispatch (cross_page_jump at ram:2B09) [confirmed]
Banked ROM calls use a bcall-style trampoline.
cross_page_jump:
- saves the current page (
IN A,(6)), - builds a
RET-to-page-0 trampoline on the stack (bcallreturn frame, page restored on exit), - reads a 3-byte
{lo, hi, page}descriptor (page masked with0x1F/0x3Ffor 83+/84+ via ports 2/0x21), OUT (6),Ato bank the target page in at4000, then jumps to it.
The ln/e^x sites that look similar are local calls, not cross-page dispatches.
fp_mul_indexed_constant (ram:2362) reaches coeff_fetch (02:7D1E)
through the inline descriptor at ram:3DD1. The preceding LD A,3 or
LD A,6 selects a coefficient, not a page. _EToX falls through locally into
the _TenX body at 02:7069. logexp_digit_table (02:7181),
trig_recurrence_table_a (02:7201), trig_recurrence_table_b (02:7281),
and fp_constant_table (02:7D42) all reside on page 02. [confirmed]
Transcendentals
Logarithms [confirmed]
_LnX02:6EFD:_CkOP1Pos; non-positive real →_ErrDomain. For a positive real it calls the real-log core (_CLNpath, selectorC=2); the generic entry handles complex args._LogX02:6F16: same structure, base-10 selectorC=0, guards_ErrD_OP1_0/_ErrD_OP1NotPos._CLN02:6CCA/_CLog02:6CE7— complex log:_CAbs(magnitude) → real_LnX/_LogXfor the real part,_ATan2Rad(02:76D4) for the imaginary part (the argument/angle). Uses_PushRealO1/_PopRealO2to juggle the operand. This is whyln(-2)returns a complex result ina+bimode but raises_ErrNonReal(0x87) in real mode.
Exponentials [confirmed]
_EToX02:705C(e^x): loads thelog10(e)constant throughfp_mul_indexed_constant, then falls through into the local_TenXbody._TenX02:7066(10^x): splits exponent into integer (digit shift) + fractional (16-slot table-driven evaluation throughlogexp_digit_table). Argument too large →_ErrOverflow.
Trigonometric functions [confirmed]
_SinCosRad02:733E,_Sin7342,_Cos7346,_Tan734A. Each loads a function selector byte into0x8499(1=sin,2=cos,4=tan;0x80bit set when the rad-special mode tested byBIT 2,(IY+0)is off;_SinCosRadforces0x81).- Range reduction: reads OP1 exponent; exponent ≥ 0x0C (|x| ≳ 10^12·) →
_ErrDomain(“argument out of range”). It then reduces the angle modulo a quarter-period using the BCD constant table near02:7D81and runs the same table-driven digit recurrence as ln/eˣ over the two trig recurrence tables (one row per digit step, sign-variant picked byOP5.value.typebit 7) — the per-stepbcd_sub_op1_op2(ram:1D8A) /bcd_add_8496_8480(ram:1D26) are the shift-and-add BCD steps of that recurrence, not a fixed polynomial and not CORDIC for the forward trig. The per-row decoding of02:7201/02:7281is detailed in floating-point.md.
Inverse trig [confirmed]
_ASinRad76DA,_ACosRad76C9,_ATanRad76CF,_ATan2Rad76D4, plus the degree-mode_ASin/_ACos/_ATan/_ATan2at76F1/76DF/76E9/7749._ASin/_ACoscall domain check02:79D3; |arg| > 1 →_ErrDomain.- All inverse trig funnel into the shared arctangent CORDIC engine at
02:774B(B=0x20seeds the octant/quadrant base written to0x84A4; the core is a base-10 trial-subtract digit recurrence, not a fixed 32-step loop), with asin/acos expressed via atan2 of (x, √(1−x²)).
Hyperbolics [confirmed]
_SinHCosH7626,_TanH762A,_CosH762E,_SinH7632;_ATanH/_ASinH/_ACosHat7909/7956/7964. Same0x8499selector mechanism; built from_EToX(sinh = (e^x−e^-x)/2, visible in the_EToX+_FPDivsequence near02:6D08).
Power operator ^ [confirmed]
The general a^b lives at 02:6D08+: it computes b·ln(a) then e^() and reconstructs
with _SinCosRad for the complex case — i.e. a^b = e^(b·Ln a), with _FPDiv/_FPMult
glue and _OP2ToOP6/_OP6ToOP1 shuffles. Integer/√ special cases short-circuit to
_FPMult/_SqRoot. ^ is the most FP-stack-heavy single operator.
Number entry and display formatting
When the homescreen shows a result (or Ans), the engine converts the OP1 TIFloat to a
digit string honoring the MODE screen (Normal/Sci/Eng, Float/Fix 0–9).
_FormReal06:5ACF— real-number formatter. [confirmed]fp_clear_guard; zero →_OP1Set0; copies arg to OP5.- Reads the digit-count/mode flags from
(IY+0xc)and the byte at0x89FA(active fixed/decimal-places setting;(IX-1)local holds the effective format byte). - Exponent thresholds drive Normal↔Sci switchover: it compares
OP1.value.expagainst0x7D/0x7F(≈ the ±-exponent window) and renormalizes (ram:1BE7) to bring the value into the displayable mantissa range, bumping a digit counter. Negative sign decrements the leading column count (DEC (IX-3)).
_FormEReal06:5799— forces scientific/E notation by setting0,(IY+0xc)then calling_FormReal. [confirmed]_FormBase06:57C0— integer formatting in a base; requires_CkOP1Real(→ DATA TYPE / DOMAIN on non-real). [confirmed]_FormDCplx06:59D3— complexa+bi/r∠θformatting (calls_FormRealtwice). [standard]- Exponent ↔ ASCII helpers on page 0:
_ExpToHexram:1E4E,_OP1ExpToDecram:1E77,_DecO1Expram:1E6F(decrement exp),ram:1BCB(BCD-digit → value). [confirmed] - The formatted string is then drawn by
_DispOP1A(04:7844) / homescreen put-string routines (see display-lcd.md).
Ans is the last-result TIFloat saved in a system var and reloaded into OP1
(via _Mov9ToOP1 = RST 20h) when the token Ans is evaluated. [standard]
Error handling [confirmed]
Errors are raised by loading an error code in A and jumping to _JError (ram:2793),
which unwinds to the error context and shows the named message. The raiser cluster lives at
ram:26E8+ — exact code map read from disassembly:
| Raiser | Addr | A code | Message |
|---|---|---|---|
_ErrOverflow | ram:26E8 | 0x81 | OVERFLOW |
_ErrDivBy0 | ram:26EC | 0x82 | DIVIDE BY 0 |
_ErrSingularMat | ram:26F0 | 0x83 | SINGULAR MAT |
_ErrDomain | ram:26F4 | 0x84 | DOMAIN |
_ErrIncrement | ram:26F8 | 0x85 | INCREMENT |
_ErrNon_Real | ram:26FC | 0x87 | NONREAL ANS |
_ErrSyntax | ram:2700 | 0x88 | SYNTAX |
_ErrMode | ram:2704 | 0x9E | MODE |
_ErrDataType | ram:2708 | 0x89 | DATA TYPE |
_ErrArgument | ram:2711 | 0x8A | ARGUMENT |
_ErrDimMismatch/Dimension | ram:2715/2719 | 0x8B/0x8C | DIM MISMATCH / INVALID DIM |
_ErrUndefined/Memory | ram:271D/2721 | 0x8D/0x8E | UNDEFINED / MEMORY |
Domain pre-checks (page-0, set Z if OK else jump to _ErrDomain):
_ErrD_OP1NotPosram:2119—_CkOP1Pos; not >0 ⇒ DOMAIN (used by_SqRoot,_LogX)._ErrD_OP1Not_Rram:2120—_CkOP1Real; complex ⇒ DOMAIN._ErrD_OP1NotPosIntram:2125—_CkPosInt._ErrD_OP1_LE_0ram:212A,_ErrD_OP1_0ram:212D— zero/sign guards (e.g.ln(0)).
Where the calc engine raises what:
÷ 0,1/0:_FPDiv/_FPRecip→0x82DIVIDE BY 0.×/10^x/exponent overflow:ram:250Fexponent-add →0x81OVERFLOW.√(neg),ln/log(≤0),asin/acos(|x|>1),tan(π/2), |trig arg| ≳ 10^12:0x84DOMAIN.- Complex result requested in real mode:
0x87NONREAL ANS (the_CLN/complex paths).
Routine index
Arithmetic core (page 0): _FPAdd 229E, _FPSub 2297, _FPMult 238B, _FPDiv 2541,
_FPRecip 253D, _FPSquare 238A, _Cube 237D, _Times2 2282, _TimesPt5 2382,
_InvSub 227D, _Int 2621, _Intgr 2263, _Trunc 2279, _Frac 24E3, _Round 2623,
_InvOP1S 24BD, _InvOP2S 24CD, _InvOP1SC 24BA, _CkOP1Pos 1E5D, fp_clear_guard 2627,
fpmul_expadd 250F, _DToR 236B, _RToD 2374, cross_page_jump 2B09.
Transcendentals (page 02): _SqRoot 6E38, _LnX 6EFD, _LogX 6F16, _CLN 6CCA,
_CLog 6CE7, pow_core 6D08, _EToX 705C, _TenX 7066, _SinCosRad 733E, _Sin 7342,
_Cos 7346, _Tan 734A, _SinHCosH 7626, _TanH 762A, _CosH 762E, _SinH 7632,
_ACosRad 76C9, _ATanRad 76CF, _ATan2Rad 76D4, _ASinRad 76DA, _ACos 76DF,
_ATan 76E9, _ASin 76F1, _ATan2 7749, atan_cordic 774B, coeff_fetch 7D1E,
trig_coeff_table 7D81.
Formatting (page 06): _FormReal 5ACF, _FormEReal 5799, _FormBase 57C0, _FormDCplx 59D3.
Errors (page 0): _JError 2793, raiser table 26E8+, domain pre-checks 2119–2131.
Worked flow: 2*sin(π/6)+ln(5) [hypothesis]
- Parser pushes
2(OP1), evaluatessin(π/6): loadsπ/6into OP1,_SinCosRad/_Sin(selector0x8499), table-driven digit recurrence →OP1=0.5. ×: the saved2is inOP2(or popped from FPS) →_FPMult→OP1=1.ln(5): spill1to FPS (_PushRealO1),OP1=5,_LnX(_CkOP1Pospasses) →1.6094….+: pop1toOP2(_PopRealO2),_FPAdd→OP1≈2.6094._FormRealrenders per MODE; result stored asAns.
Statistics
The statistics subsystem reads list data, accumulates moments, solves
regressions, and writes named results such as x̄, Σx, Sx, a, b, r,
and r². This page separates the CALC, STAT-TESTS, and DISTR command
families.
The CALC paths read L1–L6 through the VAT and use the BCD
floating-point engine. The engine lives on Flash page 3A; raw disassembly
supplies indexed-bit and cross-page operations that the decompiler can
mis-render.
statVars result block [confirmed]
Every STAT-CALC result is a 9-byte TIFloat (see floating-point.md) written
into a fixed RAM table beginning at statVars = 0x8A3A (statVars EQU 8A3Ah
in ti83plus.inc). Entries are packed at the 9-byte FPLEN stride. These are the
system variables recalled by name ([2nd][STAT] ▸ VARS):
| Addr | Name (.inc) | User-facing var | Meaning |
|---|---|---|---|
8A3A | StatN | n | sample count (Σ of frequencies) |
8A43 | XMean | x̄ | mean of x |
8A4C | SumX | Σx | sum of x |
8A55 | SumXSqr | Σx² | sum of x² |
8A5E | StdX | Sx | sample std dev of x (÷ n−1) |
8A67 | StdPX | σx | population std dev of x (÷ n) |
8A70 | MinX | minX | minimum x |
8A79 | MaxX | maxX | maximum x |
8A82 | MinY | minY | minimum y (2-Var) |
8A8B | MaxY | maxY | maximum y (2-Var) |
8A94 | YMean | ȳ | mean of y |
8A9D | SumY | Σy | sum of y |
8AA6 | SumYSqr | Σy² | sum of y² |
8AAF | StdY | Sy | sample std dev of y |
8AB8 | StdPY | σy | population std dev of y |
8AC1 | SumXY | Σxy | sum of x·y |
8ACA | Corr | r | correlation coefficient |
8AD3 | MedX | Med | median of x |
8ADC | Q1 | Q1 | first quartile |
8AE5 | Q3 | Q3 | third quartile |
8AEE | QuadA | a | regression coeff a (highest order) |
8AF7 | QuadB | b | regression coeff b |
8B00 | QuadC | c | regression coeff c |
8B09 | CubeD | d | regression coeff d |
8B12 | QuartE | e | regression coeff e |
8B1B…8B50 | MedX1/2/3, MedY1/2/3 (8B1B/8B24/8B2D/8B36/8B3F/8B48) | Med-Med (×3 partitions) |
These 31 consecutive values form the typed prefix used by the Ghidra database:
typedef struct {
TIFloat StatN, XMean, SumX, SumXSqr, StdX, StdPX, MinX, MaxX;
TIFloat MinY, MaxY, YMean, SumY, SumYSqr, StdY, StdPY, SumXY;
TIFloat Corr, MedX, Q1, Q3, QuadA, QuadB, QuadC, CubeD, QuartE;
TIFloat MedX1, MedX2, MedX3, MedY1, MedY2, MedY3;
} TIStatResultsPrefix; /* 31 × 9 bytes at statVars */
This makes, for example, statVars.XMean and statVars.Corr distinct fields
rather than unrelated constants 0x8A43 and 0x8ACA. The address table remains
the byte-level evidence for the layout. [confirmed]
Continuing past the table (also .inc): PStat/ZStat/TStat/ChiStat/
FStat/DF/Phat…/MeanX1/StdX1/StatN1/MeanX2/StdX2/StatN2/StdXP2/
SLower/SUpper/SStat — these hold the inferential-stats outputs (the STAT-TESTS
menu) and are written by the test commands, not by 1/2-Var Stats. An ANOVA block
anovaf_vars (F_DF/F_SS/F_MS/E_DF/E_SS/E_MS) follows.
STAT-TESTS are separate command handlers. [confirmed] Z-Test/T-Test/χ²-Test/
2-SampFTest/ANOVA( etc. come in as their own 2-byte t2ByteTok (0xBB)-prefixed command tokens — e.g.
LinRegTTest=34h in the STAT command token map — and are not dispatched through _OneVar (whose token map is
only F2–FF). They fill the PStat…SStat/anovaf_vars block above directly. Test
handlers appear on both sides of the named stat_* accumulation, variance,
median, and regression routines within 3A:4A00–3A:7E60. See
STAT-TESTS engine. The per-test entry addresses are
not exposed as named routines and remain [hypothesis].
A scratch byte stat_calc_command (0x8A36, immediately below statVars) holds the stat-command
discriminator (the model index set from the command token) for the
duration of the computation. Working list/element pointers used by the loop live
in the OP-scratch RAM 0x84AF…0x84DB (84D3=median data ptr, 84D5/84D7=current
x/y element ptr, 84D9=sums matrix base, 84DB=freq list ptr, 84B1/84B2=loop
counters, 84B3=element count). [confirmed]
Recall by name: _Rcl_StatVar (00:2149, id 0x42DC) is a page-0 bcall
trampoline (CALL 0x3E07 → dispatcher, inline id 0xC9E7) that loads the named
statVar into OP1; the VAT-level recall (_RclVarSym/rcl_var_push, see
sub-vat-archive.md) routes the stat-var name tokens (tRegEq 0x01, tStatN 0x02,
tXMean 0x03, … tCorr 0x12, the STATVARS token group) to it. The name-token
values are in ti83plus.inc (tStatN=02h … tSumXY=11h, tCorr=12h, tMedX=13h,
regression coeffs via tRegEq=01h). [standard]
_OneVar STAT-CALC entry [confirmed]
bcall(_OneVar) is the single entry point for all STAT-CALC commands
(1-Var, 2-Var, and every regression). The parser invokes it after pushing the
list arguments; the command token (F2–FF) selects the behavior.
_OneVar (3A:6420):
SET 5,(IY+9) ; statFlags: "stat computation active"
LD B,0 ; arg counter
RES 1,(IY+0)
RES 1,(IY+1a)
LD (9817),0 ; clear a status byte
LD HL,8499
CALL 1b33 ; stage the parsed arg descriptor at 8499
LD A,0FF
LD (84af),A
CALL _CkOP1Real (1942-ish) / arg-class checks …
; ---- argument parsing (6442..64de) ----
; walks the parser argument list, accepting list-name tokens (0x24 list,
; 0x2A list-element, 0x1C/0x25/0x19 = freq/list variants); validates count;
; _JError(0x8A) ARGUMENT / 0x88 SYNTAX on a bad arg list.
; ---- set up the data pointers (64e1..6503) ----
LD HL,847a
LD DE,8d2a
CALL 1a9a ; resolve the x-list (and y/freq) → 84D3..84DB
POP AF
LD (8a36),A ; *** save the command code → model discriminator ***
LD HL,6352
CALL 27da ; install an on-error cleanup frame
CALL 6572 ; accumulation pass
CALL 2800
CALL 6345 ; tear down frame
; ---- regression coefficient region select (6506..652f) ----
LD A,(8a36)
CP 4
JR NC,.. ; A<4 ⇒ polynomial regression
LD A,16
LD HL,8aee ; coeff dest = QuadA block; … solve
…
SET 7,(IY+9) ; mark results valid
CALL 67c1 … ; finalize / median
Key facts read from the disassembly:
- The command byte is saved in
stat_calc_commandand steers everything afterward. LD HL,0x8AEE(=QuadA) is the regression coefficient destination; the solver writesa,b,c,d,ethere in descending order of power._ErrStat(00:2741, id0x44C2, code0x15“STAT”) and_ErrStatPlot(00:2759, code0x1B) are the STAT-specific error raisers; the_OneVarbody jumps to0x2741on e.g. fewer than the required data points._ErrDimMismatch(0x2715) is raised ifL1andL2/freq lengths differ (the21bblength compare at6584/658a).
STAT command token map [confirmed]
The parser passes the command token; _OneVar stores it in
stat_calc_command (0x8A36) and treats it as a model index. From
ti83plus.inc:
| Token | Value | Command | Model |
|---|---|---|---|
tOneVar | F2 | 1-Var Stats | one variable |
tTwoVar | F3 | 2-Var Stats | two variable |
tLR | F4 | LinReg(a+bx) | degree-1 (a+bx form) |
tLRExp | F5 | ExpReg | y=a·bˣ (log-linear) |
tLRLn | F6 | LnReg | y=a+b·ln x (log-x) |
tLRPwr | F7 | PwrReg | y=a·xᵇ (log-log) |
tMedMed | F8 | Med-Med | resistant line |
tQuad | F9 | QuadReg | degree-2 |
tLR1 | FF | LinReg(ax+b) | degree-1 (ax+b form) |
CubicReg/QuartReg come in as the regression tokens tCubicR=2Eh/tQuartR=2Fh;
SinReg=32h, Logistic=33h, LinRegTTest=34h are 2-byte t2ByteTok (0xBB)-prefixed
tokens (their 2Eh/2Fh/32h/33h/34h values are the second byte after 0xBB). Degree for the polynomial solver = the model index; the coefficient
fan-out into QuadA..QuartE is naturally sized by degree. [standard]
SortA(/SortD( are separate tokens (tSortA=E3h, tSortD=E4h) with their
own command handler — not _OneVar. The sort used here, stat_sort (3A:7935),
is stat-internal: its only callers are stat_median_quartile (3A:79B9) and
medmed_partition (3A:760F) (xref-confirmed), so it powers the 1-Var median/
quartile and Med-Med paths in Median, quartiles, extrema, and sorting. The SortA(/SortD( command sort is a
different routine on page 0x02 (≈02:5939, comparator _CpOP1OP2) — see
Matrices and lists.
Accumulation pass [confirmed]
This builds the power-sums for 1/2-Var Stats and the regression sum-setup. It makes a single pass over the data list(s), accumulating the power-sums needed for the mean, variance, and least-squares normal equations. Read from disassembly:
6572: CALL 6f90/6f7d ; default freq = 1 if no freq list given
6584: CALL 21bb ; if freq list present, length-check vs x-list
; → _ErrDimMismatch (2715) on mismatch
658a: LD HL,(84d3) ; HL = first element ptr; DE = element count
6590: LD A,(8a36) ; dispatch on command:
CP 8 (Med-Med) → jump to the resistant-line path (760f/75e4 → 79b9)
else compute the matrix dimension from the degree:
CP 1c/25/19/9 → dim=4
CP 5 (CubicReg) NC → dim+? ; default
65c1: A = dim
SUB 2
PUSH AF
65cd: set up x/y element pointers (84d5/84d7/84db)
65f0: ---- per-element accumulator init ----
LD DE,8a3a ; … CALL 1a92 ; StatN slot
LD DE,8a94 ; YMean/Σy slots
CALL 110f ; allocate the sums matrix (84d9 = base)
6646..66fe: ---- per-element loop ----
6f6a : fetch next x (and y) list element, advance ptr
28e4/2297 : loop bound (RST FPSub / compare)
6567 : helper = (RST 8: OP1→OP2)
LD HL,(84af)
CALL 6f7d ; _FPMult (238b)
→ forms the running power x^k · freq
238a : _FPSquare (Σx²)
238b : _FPMult (Σxy, Σx^(i+j))
RST 30: _FPAdd → accumulate into the matrix cell / Σ-slot
2999/29db/29a2 : guard-clear / OP-shuffle helpers
66fe: JP C,6655 ; loop while elements remain
So one pass builds, for a degree-d fit, the symmetric moment matrix of
power-sums Σxⁱ (i = 0 … 2d) and the right-hand side Σxⁱy, stored as a small
2-D array reached by the RAM trampoline helpers 00:3A8F/3AA1/3AA7/3AAD/3AB9
(matrix-element get/set by (row B, col C)). StatN, SumX, SumXSqr, SumY,
SumYSqr, SumXY, MinX/MaxX/MinY/MaxY are filled here directly. [confirmed]
Non-polynomial regressions transform first [confirmed]: the front-end at
658a+ checks the command code and, for ExpReg/PwrReg (ln y),
LnReg/PwrReg (ln x), pre-applies the logarithm to each element before
accumulating, then exponentiates the resulting linear coefficients off page 0x3A. The
per-element ln is in the element fetch stat_next_elem (3A:6F6A):
LD A,(8A36)
CP 4
RET NC
It then bcalls _LnX at 3A:6F72 for model codes < 4 (ExpReg/LnReg/PwrReg); the
back-transform _EToX/_TenX lives on page 02; see Transcendentals. This is the standard
“linearize, fit a line, transform back” method; r is the correlation of the
transformed data.
Mean and standard deviation [confirmed]
After the pass, _OneVar finalizes the moments (3A:6762+):
6762: LD DE,8a67
CALL 6984 ; σx (population) from Σx², Σx, n
6786: LD DE,8a5e
CALL 6989 ; Sx (sample), via _Minus1 (n→n-1) at 677c
6798: LD DE,8a55
CALL 6998 ; Σx² slot
67a7: LD DE,8aa6
CALL 6998 ; Σy² (2-Var)
The variance helpers (3A:6984/6989/6998) implement the one-pass formula
var = (Σx² − n·x̄²)/N then √:
6998: _FPSquare(x̄) ; recall Σx² (15da) ; _FPMult ; (RST 30 _FPAdd / subtract) ; …
6989: CALL _FPDiv (2541)
CALL 3939 (_SqRoot wrapper) ; store
The only difference between σx (population) and Sx (sample) is the divisor:
the population path divides by n, the sample path first does _Minus1
(00:2294, n−1) — confirmed at 3A:677C. x̄ = Σx / n via _FPDiv. [confirmed]
Regression solver [confirmed]
For a polynomial fit the moment matrix from the accumulation pass is the augmented normal-equations
matrix [ M | Σxⁱy ]. _OneVar solves it in place by Gauss-Jordan elimination
(not a closed-form determinant), then writes the coefficients to QuadA…QuartE.
67c6: build/copy the augmented matrix; 84d9 = base
67d4..67e3: scale the pivot row
67ec: LD BC,0202
CALL 3aad ; pivot element (2,2)
67f7: CALL 212d ; _ErrD check (zero pivot → SINGULAR MAT 0x83)
67fa: RST 8 ; … ; pivot reciprocal
6804: CALL 2541 (_FPDiv) ; divide row by pivot
680d..6815: elimination loop
The 3A:6845–6891 cluster, byte by byte:
6845 CALL 3939 (cross_page_jump) ; OP1 = √OP1 (page-39 _SqRoot body)
6848 RST 08h (_OP1ToOP2) ; OP2 = √…
6849 CALL 1674 (_CpyTo1FPST) ; OP1 ← FPS−9 (the saved numerator sum)
684C CALL 2541 (_FPDiv) ; OP1 = numerator/denominator = r
684F LD A,0x12 ; CALL 213D ; _Sto_StatVar(tCorr): Corr (8ACA) ← r
6854 CALL 1BA4 (_OP1Set0) ; accumulator = 0
6857 POP BC / PUSH BC ; BC = augmented-matrix row count
685B LD B,2 ; start at row 2 (first data column)
685D loop:
CALL 19EC (_OP1ToOP4); CALL 150A (_PopRealO2) ; OP2 ← popped FPS value
CALL 3AA7 (cross_page_jump) ; matrix element (col B) → OP1
CALL 238B (_FPMult) ; element · value
CALL 19FE; RST 30h (_FPAdd) ; accumulate into OP4/OP1
INC B until B = H ; walk the column
6878 CALL 2903 (fp_st_slot7_op3) ; stash the column sum
687B CALL 1DEE (_CkOP2FP0) ; denominator zero?
687E JR Z,6891 ; yes → skip r² store
6880 CALL 2541 (_FPDiv) ; ratio for r²/R²
6885 LD A,B; CP 2 ; model order == 2 (linear)?
6888 LD A,0x35 / 0x36 ; id 0x35 = r² (slot 8C05), 0x36 = R² (8C0E)
688E CALL 213D (_Sto_StatVar)
The region forms r = num/den and stores it to Corr at 0x8ACA. It then
accumulates a column-weighted residual sum over the augmented matrix. When the
denominator is nonzero, it stores r² for linear fits or R² for higher-order
fits in separate statVar slots at 0x8C05 and 0x8C0E. [confirmed]
68d6..6953: back-substitution — each coeff = (rhs − Σ known·M) / pivot
(3aa7/3aa1 matrix access, 238b _FPMult, RST 30/RST 8 accumulate,
24bd _InvOP1S to subtract, 2541 _FPDiv)
each solved coefficient is stored via 69af → CALL 3ab9 (matrix set)
then copied out to the QuadA..QuartE statVars block.
-
A zero/near-zero pivot raises
_ErrSingularMat(0x83,SINGULAR MAT), for example when all x values are equal or the degree exceeds the number of distinct points. The guard is the3A:67F7call toram:212D; the0x35/0x36calls at3A:6888–3A:688Eare stat-variable stores. [confirmed] -
The solver is dimension-generic:
LinReg(2×2) →a,b;QuadReg(3×3) →a,b,c;CubicReg(4×4) →a,b,c,d;QuartReg(5×5) →a,b,c,d,e. The coefficients land inQuadA(8AEE) downward. [confirmed] -
Correlation
randr²are computed for the linear models from the centred sums: $$r=\frac{\sum (x-\bar x)(y-\bar y)}{\sqrt{\sum (x-\bar x)^2\,\sum (y-\bar y)^2}}=\frac{n\sum xy-\sum x\sum y}{\sqrt{\big(n\sum x^2-(\sum x)^2\big)\big(n\sum y^2-(\sum y)^2\big)}}$$assembled with
_FPMult/_FPSub/_SqRoot/_FPDiv(the6845/684ccluster) and stored toCorr(8ACA). The store offset is pinned: at3A:684Fthe code doesLD A,0x12CALL 0x213D, and0x213Dis_Sto_StatVar(the store counterpart of_Rcl_StatVar 00:2149— both funnel through the0x3E07statVar dispatcher with the name id inA). Id0x12=tCorr= theCorrslot, so this single sequence is exactlyr → Corr (8ACA). The preceding3A:6845_SqRoot/_FPDivcluster forms the ratio;r²(andR²for higher-order fits) is the coefficient of determination derived by the following column-weighted pass. It is stored separately through IDs0x35and0x36, at0x8C05and0x8C0Erespectively. [confirmed] -
The fitted equation is also written to
RegEQ(theY=-style regression equation system var, recalled via tokentRegEq=0x01) soRegEQcan be pasted or graphed. [standard]
The Med-Med model (F8) takes the resistant-line branch (3A:760F/79B9):
it sorts, splits the x-sorted data into three equal partitions, takes the median
(x,y) of each (MedX1/2/3, MedY1/2/3 at 8B1B…), and fits the line through the
outer two summary points adjusted toward the middle — classic Tukey median-median. [standard]
Median, quartiles, extrema, and sorting [confirmed]
For 1-Var Stats the five-number summary needs the data sorted:
MinX/MaxXare tracked during the accumulation pass with running min/max compares.- The median/quartile path (
3A:79B9→7A0B…) sorts a working copy via the internal sortstat_sort(3A:7935), then:Med(MedX,8AD3) = middle element (or mean of the two middle for even n),Q1(8ADC) = median of the lower half,Q3(8AE5) = median of the upper half (TI’s “exclude the overall median when n is odd” convention), with frequency-weighted positions (the7B30/7B4C/7B6Ehelpers walk the cumulative-frequency index, and198d/238binterpolate the rank). The ROM path is [confirmed]. The quartile rule is [standard].
The five-number summary (minX, Q1, Med, Q3, maxX) is what the MED/box-plot
stat plot reads back out of statVars.
Worked two-variable statistics and regression flow [hypothesis]
- Parser pushes the list args, sets
A = command token,bcall(_OneVar). _OneVarparses args → x-list ptr(84D3), y-list(84D5), freq(84DB); saves the model code tostat_calc_command.- Accumulation pass: one walk of L1/L2 building
n, Σx, Σx², Σy, Σy², ΣxyandminX/maxX/minY/maxYintostatVars, plus the 2×2 moment matrix. - Moments: $\bar x=\tfrac{\sum x}{n}$, $\bar y=\tfrac{\sum y}{n}$; the sample/population spreads $S_x,\sigma_x,S_y,\sigma_y$ via the variance helper (divide by $n-1$ vs $n$).
- Solve: Gauss-Jordan on the normal equations $\left[\begin{array}{cc|c}\sum 1&\sum x&\sum y\\\sum x&\sum x^2&\sum xy\end{array}\right]$ →
b=slope,a=intercept→QuadA/QuadB;r,r²→Corr; equation →RegEQ, pasted intoY1. - Results displayed by the STAT-CALC report screen; all of x̄/Σx/…/a/b/r persist
in
statVarsfor later recall by name (_Rcl_StatVar).
Stat plots [standard]
Stat plots (Scatter tScatter=FE, xyLine FD, Histogram tHist=FC, box plots
tBoxIcon, normal-prob) are drawn by the graphing subsystem, reading the
five-number summary and the raw L1/L2 lists. _ErrStatPlot (00:2759, code
0x1B) guards an invalid/undefined plot configuration; _ZmStats (33:65DC,
id 0x47A4) is the ZoomStat routine that auto-scales the window to the plotted
list data (sets Xmin/Xmax/Ymin/Ymax from minX/maxX/minY/maxY). See
sub-graphing.md. [standard]
DISTR functions [confirmed]
normalpdf(, normalcdf(, invNorm(, binompdf(, tcdf(, χ²cdf(, Fcdf(,
etc. are parser functions (DISTR-menu tokens, the t2ByteTok (0xBB)-prefixed
two-byte tokens like tShadeNorm=35h), evaluated through the normal function
dispatch of the TI-BASIC parser, not through _OneVar. They are not
exposed as named bcalls in this OS image (a search of bcall_targets.txt finds
only _SetNorm_Vals 00:220F, a helper that copies the display “Normal mode”
default values — unrelated to the normal distribution). Their numerical cores
(error-function / incomplete-gamma / incomplete-beta continued fractions) live on
a banked flash page reached via the parser’s function table and the page-02 FP
transcendentals; they belong to the parser/sub-tibasic dispatch rather than the
STAT subsystem documented here. [hypothesis]
Negative search. [confirmed] A name search of the whole-OS image for norm/stat/distribution
cores returns no normalcdf/erf/incomplete-gamma/incomplete-beta entry points — the only
*norm* symbols are _SetNorm_Vals (00:220F, display “Normal mode” defaults),
fp_normalize/fp_norm_left (mantissa normalisation), cplx_norm_* (complex modulus) and the
eqdisp_setnorm_split layout helpers — none is a distribution. Likewise every stat_*
symbol on page 0x3A is part of the _OneVar STAT-CALC engine (accumulate / variance / median /
sort / regression), not a DISTR core. The normalcdf( evaluation path runs in the
page 39 FP core described below. The STAT-TESTS p-value approximation carries
its coefficients in a table on page 3A
(STAT-TESTS engine). The erf / incomplete-gamma /
incomplete-beta continued fractions behind the remaining DISTR tokens remain
[hypothesis]. The parser’s two-byte, 0xBB-prefixed DISTR-token function table does
not expose them as named routines in this database.
Traced normalcdf( path. [confirmed] A headless TilEm trace of
normalcdf(0,1) through the OS 2.55 interactive prompt identifies the evaluation
path (tools/macros/distr-normalcdf.macro). Coverage against boot-idle.macro
shows the parser collecting the fields on the FP stack. A cross_page_jump chain
through ram:2B09 reaches page 39 through page 01 glue. The numerical core
occupies 39:4A02–39:4F5B, with helpers at 39:5D2D–39:5E41,
39:6C63–39:6D31, and 39:57CF–39:57FC. The trace does not execute the
page 38 slot suggested by a raw token-index read (38:459F for tDNormal).
The table at 38:4000 contains parse-side argument-class stubs such as
LD B,0x29JR 4A44; it is not the execution dispatch.
STAT-TESTS engine on page 3A [confirmed]
The inferential-statistics commands execute in their own engine on page 3A, sharing the bank
with _OneVar but distinct from it. Three byte-pinned structures locate it:
Candidate PStat–SStat references. A ROM-wide byte-pattern scan
(tools/ti84re/rom/scan_stat_writers.py, immediate or absolute operands landing in
0x8B5A–0x8C37) finds about 50 opcode-shaped candidates on page 3A
(3A:4B15–3A:6BDC) plus candidates on pages 06, 35, 37, and 39.
Because the scan does not recover instruction boundaries, these hits locate a
search cluster but do not by themselves establish a writer count or exclude
references on other pages. [hypothesis]
A T-Test output stage at 3A:5500. [confirmed] The routine multiplies OP1 through
fp_mult_const (ram:2385), scales by StdPX (0x8A67), divides through fp_div_const
(ram:2532) against SStat (0x8BFC), then stores the result with
LD A,0x24CALL _Sto_StatVar. ID 0x24 is tStatT, the TStat slot. The
routine then references DF at 0x8B87. The surrounding code reads and clears
statFlags bits and dispatches on the stored model ID.
The normal p-value coefficient table at 3A:554F. [confirmed] Nine-byte TIFloat
constants, byte-verified in sequence:
| Addr | Value | Role |
|---|---|---|
3A:554F | 0.2316419 | threshold p |
3A:5558 | 1.330274429 | coefficient b5 |
3A:5561 | -1.821255978 | coefficient b4 |
3A:556A | 1.781477937 | coefficient b3 |
3A:5573 | -0.356563782 | coefficient b2 |
3A:557C | 0.319381530 | coefficient b1 |
This coefficient set matches the Zelen–Severo approximation of the standard normal tail,
$\Phi(z)\approx 1-\varphi(z),(b_1t+b_2t^2+b_3t^3+b_4t^4+b_5t^5)$ with
$t=1/(1+pz)$. The loop at 3A:551F evaluates the five coefficients in descending order by
Horner steps; LD HL,554Fh at 3A:550E pins the table start. The type bytes at 3A:5561
and 3A:5573 are 0x80, which supplies the negative signs on b4 and b2.
The STAT-TESTS handlers use the result to form PStat. [confirmed]
UI descriptor tables at 3A:7D00–3A:7E60. [confirmed] The same bank carries
the test editor’s data. It includes alternative-hypothesis strings for the
1-PropZTest and 2-PropZTest menus, plus the F-test tail strings. It also contains
SinReg and Logistic formula templates, three-byte dispatch stubs into fixed page 0
vectors, and an ascending handler-pointer array at 3A:7DF4–3A:7E1E. The mapping
from array slots to menu items remains open.
Subsystem integration
L1..L6 lists (VAT data) statVars (0x8A3A) ← results, recall-by-name
│ (element fetch 3A:6F6A) ▲
▼ │ (_Rcl_StatVar 00:2149)
_OneVar (3A:6420, id 0x4BA3) ──► per-element accumulation pass (3A:6572)
│ cmd code → stat_calc_command │ uses FP engine:
│ │ RST30 _FPAdd, 238B _FPMult,
├─ moments / Sx,σx (3A:6984..) │ 238A _FPSquare, 2541 _FPDiv,
├─ Gauss-Jordan solve (3A:67C6..) ───► │ 3939 _SqRoot, 2294 _Minus1
│ → QuadA..QuartE, Corr, RegEQ │
└─ sort + median/quartile (3A:7935/79B9) ┘
errors: _ErrStat 00:2741 (0x15), _ErrStatPlot 00:2759 (0x1B),
_ErrSingularMat 0x83, _ErrDimMismatch 00:2715 (0x8B)
The STAT subsystem is a thin data-driven front-end on page 0x3A that reads list
data via the VAT, drives the page-0/page-02 BCD FP engine to build power-sums, then
either finalizes the moments or runs an in-place Gauss-Jordan solve of the normal
equations, depositing every output as a named TIFloat in the statVars block.
Routine index
| space:addr | name | what |
|---|---|---|
3A:6420 | _OneVar | STAT-CALC entry (1/2-Var + all regressions), id 0x4BA3 |
3A:6572 | onevar_accumulate | one-pass power-sum accumulation loop |
3A:6567 | onevar_powmul | running power·freq product (OP1→OP2, ×) |
3A:6345 | onevar_frame_teardown | restore stat error frame |
3A:6352 | onevar_frame_teardown_tail | on-error tail calling onevar_frame_teardown |
3A:6984 | stat_stddev_pop | population variance/σ finalize (÷ n) |
3A:6989 | stat_stddev_samp | sample variance/S finalize (÷ n−1) |
3A:6998 | stat_var_core | (Σx²−n·x̄²) variance core + √ |
3A:67C6 | reg_gauss_solve | Gauss-Jordan solve of normal equations |
3A:69AF | reg_store_coeff | write a solved coefficient (matrix set) |
00:3A8F/3AA1/3AA7/3AAD/3AB9 | stat_mtx_index/get/set | RAM trampolines for sums-matrix element access by (row,col) |
3A:6F6A | stat_next_elem | fetch next list element, advance ptr |
3A:6F7D/6F90 | stat_freq_default | default frequency = 1 |
3A:7935 | stat_sort | stat-internal data sort (median/quartile, Med-Med) |
3A:79B9 | stat_median_quartile | median/Q1/Q3 + Med-Med medians |
3A:760F/75E4 | medmed_partition | Med-Med 3-partition setup |
3A:5500 | ttest_output_stage | T-Test result store: ×StdPX, ÷SStat, _Sto_StatVar ID 0x24 (TStat) |
3A:554F | normal_tail_coef_tbl | Zelen–Severo coefficients (p, b5…b1) for PStat p-values |
00:2385 | fp_mult_const | OP1 ×= (HL)-pointed float constant |
00:2532 | fp_div_const | OP1 ÷= (HL)-pointed float constant |
39:4A02–39:4F5B | distr_normal_core (unnamed) | traced normalcdf( evaluation core on page 39 |
00:2149 | _Rcl_StatVar | recall a named statVar into OP1, id 0x42DC |
00:2741 | _ErrStat | raise STAT error (code 0x15), id 0x44C2 |
00:2759 | _ErrStatPlot | raise STAT PLOT error (0x1B), id 0x44D1 |
00:2294 | _Minus1 | OP1 − 1 (n→n−1 for sample stddev) |
33:65DC | _ZmStats | ZoomStat — fit window to plotted data, id 0x47A4 |
00:2715 | _ErrDimMismatch | list length mismatch (0x8B) |
RAM: statVars=0x8A3A, stat_calc_command=0x8A36, work pointers 0x84AF–0x84DB
(84D3 x/median ptr, 84D5/84D7 element ptrs, 84D9 sums-matrix base,
84DB freq ptr, 84B1/84B2 loop counters, 84B3 element count).
FP engine reused: RST 30h=_FPAdd, RST 08h=OP1→OP2, 00:238B=_FPMult,
00:238A=_FPSquare, 00:2541=_FPDiv, 00:2294=_Minus1, 02:6E38/3A:3939
=_SqRoot, 24BD=_InvOP1S.
Remaining questions
- Correlation stores.
3A:684FdoesLD A,0x12CALL 0x213D(_Sto_StatVar, ID0x12=tCorr), i.e.r → Corr (0x8ACA);r²/R²is the coefficient of determination from the following column-weighted pass, stored through IDs0x35/0x36at0x8C05/0x8C0E. See the annotated3A:6845–3A:6891listing under Regression solver. [confirmed] - DISTR numerical cores. The
normalcdf(evaluation path is traced to the page39FP core (39:4A02–39:4F5Band helpers) — see DISTR functions. The erf / incomplete-gamma / incomplete-beta continued fractions behind the remaining DISTR tokens are unnamed and untraced; the page38parse-side table is not the execution dispatch. The exact algorithm in the page39core (continued fraction versus polynomial or rational fit) remains [hypothesis]. - STAT-TESTS (Z/T/χ²/F/ANOVA) fill
PStat…SStat/anovaf_varsfrom their own engine on page3A. A pinned T-Test output stage, the normal-tail coefficient table, and the UI descriptor area locate the engine. See STAT-TESTS engine. The per-test entry addresses and the slot-to-menu mapping for the3A:7DF4pointer array remain [hypothesis]. The_Sto_StatVar/_Rcl_StatVarstubs (ram:213D/ram:2149) funnel through the cross-page-jump table atram:3E07(oneCALL 2B09+ inlineaddr,pagedescriptor per ID); resolving those descriptors gives the per-ID bodies without needing a live trace. stat_sort(3A:7935) is a 49-byte setup that validates/counts the elements then dispatches the compare-swap viarst 28h(the bcall site isn’t fully analyzed in the DB). TheSortA(/SortD(command sort is a different routine (page 0x02, comparator_CpOP1OP2) — its complex-list ordering is documented in Matrices and lists.
Matrices and lists
TI-84 Plus OS 2.55MP stores lists and matrices as VAT objects and evaluates
their element, aggregate, and linear-algebra operations through page-02
routines. This page covers layout, indexing, arithmetic, sorting, determinant,
inverse, multiplication, and row reduction. Variables and the
VAT, Floating-point engine, and
Variables, archive and unarchive describe the shared
storage and arithmetic layers.
Raw disassembly supplies the banked-page operations that the decompiler does not reduce reliably.
Data model
-
A list is
word count(2 bytes) followed bycount× 9-byteTIFloatelements (18-byte complex elements if the list is complex, flagged0x0C). Element $i$ (1-based) lives at $\mathrm{addr}(L_i)=\mathrm{data}+2+(i-1)\cdot 9$. -
A matrix is
byte columnsbyte rowsfollowed bycolumns*rows× 9-byteTIFloat, stored row-major. The element offset from the start of the data area, after the two dimension bytes, is$$\mathrm{offset}=\big((\mathit{row}-1)\cdot \mathit{columns}+(\mathit{column}-1)\big)\times 9$$
-
Every element read/write routes one
TIFloatthroughOP1/OP2and the FP engine — there is no “vector unit”; matrix multiply is a triple loop of_FPMult+_FPAdd. -
The data area is found through the VAT (
_FindSym, Variables & the VAT): the VAT entry’s data pointer + page byte locate thecount/dimheader, after which all indexing is pointer arithmetic computed by_AdrLEle/_AdrMEle. -
One shared Gauss-Jordan engine (
02:42A6) implements matrix inverse[A]⁻¹(flag0x00) anddet((flag0x40) with partial pivoting.rref(/ref(are the same elimination family.
Data layouts and creator routines [confirmed]
List — _CreateRList (00:10C4), _CreateCList (00:1109)
_CreateRList(count, dataPtrOut):
reject unless OP1 name token (8478.exp) ∈ {0x5D, 0x24, 0x3A, 0x72} # list-name classes
var_alloc(1) # carve count*9 + 2 bytes via _InsertMem
store count word at data[0..1]
if list is complex (8499.type & 8): data[2] = 0x0C # element-size flag
Layout: [countLo countHi] [TIFloat e1] [TIFloat e2] …. A complex list keeps a 0x0C
flag and 18-byte elements.
Matrix — _CreateRMat (00:1115)
_CreateRMat(H=rows, L=columns, dataPtrOut):
_HTimesL() # element count = H * L
var_alloc(2) # carve H*L*9 + 2 bytes
LD (HL),C # write columns
INC HL
LD (HL),B # write rows
_HTimesL(00:1EF6) computesresult = H * L(B=HHL=Σ L, aDJNZadd loop) — it computes the element count from the two dimension bytes. [confirmed]- The header stores
columns,rows; the payload containscolumns*rowsfloats row-major.
Dimension naming.
_CreateRMatreceivesH=rowsandL=columns. The common header writer atram:10E0recovers those bytes asBandC, then storesCbeforeBatram:10EE–ram:10F1._AdrMElereads the first byte as the stride, adds itB-1times, and addsC-1. Its public register convention is thereforeB=row,C=column, with the offset(row-1)*columns+(column-1). [confirmed]
Element access and index-to-offset conversion [confirmed]
Two address-calculators turn a 1-based index into a byte
pointer, then a 9-byte move shuttles the TIFloat to/from OP1.
List element address — _AdrLEle (02:47C5)
_AdrLEle(index, listDataPtr): ; HL=index, DE=listDataPtr
INC DE
INC DE ; skip the 2-byte count header
A = (DE) & 0x1F ; element type (low 5 bits); 0x0C ⇒ complex
CALL 21C4 ; classify real vs complex element width
HL = (index − 1) ; _HLTimes9(index-1)
CALL 1930 (_HLTimes9) ; HL = (index-1) * 9
HL += DE ; final element pointer
So list element i is at data + 2 + (i−1)*9 (×18 path for complex). _HLTimes9
(00:1930) is the universal “multiply by 9” (real TIFloat size). chk_type_lt_1a (ram:21C4)
masks the type to ≤0x19 and sets carry for the complex case (drives the 18-byte width). [confirmed]
Convenience wrappers (all = _AdrLEle then a 9-byte move through OP1, complex-aware): [confirmed]
_GetLToOP1(02:47EA) — list[i] → OP1 (real or complex via two_Mov9B).rcl_list_elem_to_op1(02:47FB),rcl_list_elem_b(02:47FE) — recall to OP1 with the index pre-loaded in RAM (84AF/84D3)._PutToL(02:4829) — OP1 → list[i];_CkValidNumvalidates the float first, then copies, honoring the complex (& 0xC) element width.rcl_c_list_elem(02:49A7),rcl_c_list_elem_b(02:49B5) — complex-list element viacplx_op_arrange(splits real/imag into OP1/OP2).get_pos_list_elem(02:5BBB) — fetch by a positive-integer index with_CkOP1Posbounds (loadsA=0x15=E_Statand jumps to the error vectorram:2741on a bad index).
Matrix element address — _AdrMEle (02:4002) [confirmed]
_AdrMEle: ; B=row, C=column, DE=matrixDataPtr
if B==0 or C==0 -> LD A,0x78
JP 0x2793 ; 0-index rejected (error vector)
A = (DE) ; A = columns ; first header byte
HL = 0
repeat (B − 1) times: HL += columns ; (row-1) * columns
HL += (C − 1) ; + (column-1)
DE += 2 ; skip both dim bytes
CALL 1930 (_HLTimes9) ; HL *= 9
HL += DE ; final element pointer
The row-major address is data + 2 + ((row-1)*columns + (column-1)) * 9.
Each (B-1) addition skips one complete row, and C-1 selects an element
within that row. The 8-bit additions propagate carry into H, so the result
is a 16-bit offset for matrices up to 99×99. [confirmed]
Matrix element wrappers: [confirmed]
_AdrMRow(02:4000) — address of the start of rowB; it setsC=1and enters_AdrMEle._GetMToOP1(02:4044) —[M](r,c)→ OP1 (_AdrMElethenRST4= load 9 bytes)._PutToMat(02:406C) =mele_store_ckvalid(02:4068):_AdrMEle_CkValidNum_MovFrOP1— OP1 →[M](r,c)with validation._StMatEl(38:6C8F) — high-level “store into[M](r,c)” used by the parser: resolves the matrix name (5F45), bounds-checks indices against the dims (r≤rows && c≤cols, else_JError 0x8C=E_Dimension), unarchives if needed, then_PutToMat. [standard]
Internal index helpers reused by the algorithms [confirmed]
mele_adr_af_jp(02:403C) =_AdrMEle(currentIJ)RST4— “load[M](i,j)to OP1” (the elimination inner-loop read). Indices come from the loop state at84AF/84B3/84B4.mele_adr_to8483(02:4051) =_AdrMEle_Mov9B(→OP2@8483)— load element to OP2.mele_put_af(02:405A) /mele_put_d3(02:405E) =_AdrMEle_CkValidNum_MovFrOP1— store OP1 back to[M](i,j).list_idx_times9(35:79E9) =_HLTimes9(idx)then a small dispatch (RST4) — the list analogue used in a few list-builder paths.
List operations [standard]
Creation, resizing, insertion, and deletion
| Routine | addr | Role |
|---|---|---|
_CreateRList | 00:10C4 | new real list: count*9+2 bytes; see Data layouts and creator routines [confirmed] |
_CreateCList | 00:1109 | new complex list: count*18+2 [confirmed] |
_IncLstSize | 07:4EF4 | grow a list in place via _InsertMem; caps length at 999 (0x3E7), else E_Dimension 0x8C (07:4F00 JP Z,0x2719 → LD A,0x8C). _InsertList is the distinct sibling at 07:4F07. [confirmed] |
_DelListEl | 07:4F43 | delete element(s): _HLTimes9(index) to size the gap (×2 if complex, & 0x1F == 0x0D), then _DelMem via a cross-page jump [confirmed] |
_RedimMat/_ConvDim | 07:4D3B / 38:741F | re-dimension (shared with matrices); _ConvDim/_ConvDim00 (38:741F/7422) coerce OP1 to a real index first [confirmed] |
dim(, dim(L)→n, list↔value
dim( reads the count word straight from the list header; assigning n→dim(L) calls the
resize path (_IncLstSize/_DelListEl) to grow/shrink, zero-filling new cells. List→matrix
and matrix→list (List►matr(, Matr►list() reshape via _DataSize and a linear payload copy
(mele_copy9_d3 (02:4539)/mele_copy9_loop (02:453F), a _DataSize-counted byte copy of the float payload). [standard]
List arithmetic L1+L2, scalar broadcast
Binary list ops are element-wise folds: the parser walks both lists by index, loads
L1[i]→OP1, L2[i]→OP2, applies the FP RST shortcut (RST 30h _FPAdd, _FPSub, _FPMult,
_FPDiv), stores into a freshly _CreateRList’d result. Length mismatch ⇒ E_DimMismatch
(_ErrDimMismatch 00:2715, 0x8B); a list⊕scalar broadcasts the scalar across every element. [standard]
sum(, prod( — higher-order folds over a list [confirmed]
Tokens 0xB6=sum(, 0xB7=prod( load a combiner function pointer and fold the
list (dispatcher 02:6104):
sum( : HL = 0x3A83 (cross-page → FP add-accumulate), seed via _OP1Set0
prod( : HL = 0x49B9 (seed accumulator = 1.0, _PushOP1), combine with _FPMult
CALL 0x64B7
...
JP (HL) # apply the combiner across e1..eN
The fold seeds the accumulator (0 for sum, 1 for prod), then for each element does
acc = combine(acc, L[i]) through OP1/OP2. Works on real and complex lists (type 1/0xD
both route to 02:6140). [confirmed]
Sequence, cumulative, sorting, and statistics operations
seq(expr,var,lo,hi[,step])evaluatesexprforvar = lo..hi, pushing each result and finally_CreateRList-ing the collected floats;_SetSeqM 36:7D1Fis the sequence-graph variant. A trace ofseq(X²,X,1,5,1)produced{1 4 9 16 25}and mapped the collection path (tools/macros/list-seq-eval.macro). Each element enters throughcross_page_jumpat37:6E87. The parser setup at38:5B3Cevaluates the expression, with34:5AA1and34:5BD8computing X². The append path runs through02:69BCand37:4260–37:4285; it addresses list elements through00:150Fand00:154Fand compares them at00:198D. Page07VAT routines at07:565F,07:5662, and07:5683grow the storage. After the last element,37:70DCcalls_CreateRListat00:10C4. The trace contains one collection cycle per element, with a period of roughly 2,000 instructions repeated five times. [confirmed] The02:5E14–02:5F5Dspan is the command-executor dispatch shared by every evaluated command; it is not theseq(collection loop.cumSum(is a running_FPAddwriting back each partial sum (the sum-fold with the accumulator stored every step). [hypothesis]SortA(/SortD(— list sort in place (SortA(co-sorts dependent lists); the comparator and per-element sort key are detailed in the next subsection. [confirmed]- Stats (
mean/median/sum/stdDev/variance) are list folds layered onsum(/sort. [hypothesis]
SortA( and SortD( list sorting [confirmed]
SortA( (tSortA 0xE3) and SortD( (tSortD 0xE4) sort a list in place — ascending and
descending respectively; SortA(L1,L2,…) co-sorts the trailing lists by the same permutation. This
is the command sort, distinct from the stat-internal stat_sort (3A:7935) that backs median/
quartile/Med-Med (see Statistics).
The command dispatch is byte-pinned in list_fold_dispatch on page 02.
CP 0xE3 at 02:6529 (SortA() and CP 0xE4 at 02:657A (SortD() converge
on the shared setup at 02:652F. Register A carries the direction: 0x0E for
ascending and 0x10 for descending. The executor chain checks arguments at
ram:38BB, registers the list through 02:5DFB, and saves the element pointer
from 0x84AF to 0x84B1. It then resets the pointer to 1 and enters the
compare/store loop through 02:6A12. The engine at 02:5939 compares each
element with _CpOP1OP2 (00:198D).
_CpOP1OP2 compares two TIFloats as real numbers [confirmed]: it tests the
sign (type byte bit 7), then the exponent, then the mantissa digits, and returns the
ordering. It does not compute a magnitude and does not read an imaginary part. Each comparison
therefore orders elements by the single 9-byte TIFloat the sort holds in OP1/OP2:
| List element | Sort key |
|---|---|
| real | the value (sign → magnitude) |
| complex | the real part only; the imaginary part is not read, and elements with equal real parts keep their input order |
No element type is ordered by magnitude/modulus (_CAbs is never on this path). [comparator and
its real-number semantics confirmed; the per-element sort key follows from them — the unanalyzed
sort body’s element-load is not byte-traced]
Traceable list sample
The tools/tibasic-samples/data.* fixture drives the list paths above with a
small end-to-end TI-BASIC program:
{3,1,4,1,5}->L1
SortA(L1)
cumSum(L1)->L2
sum(L1)->S
Disp L1
Disp L2
Disp S
It exercises list literal creation, list variable tokens (5D 00/5D 01),
in-place sorting, a running cumulative sum, a folded sum, and list display. The
generated DATA.8xp was run under headless TilEm: the screen showed sorted
L1={1 1 3 4 5}, cumulative L2={1 2 5 9 14}, and sum 14; the trace hit
list_fold_dispatch (02:6104) plus the page-38 list parse/store helpers. [confirmed]
Matrix operations [confirmed]
dim(, redim, identity, copy
dim([M])reads the two header bytes → a 2-element list{rows,cols};{r,c}→dim([M])reallocates via_RedimMat(07:4D3B), preserving overlapping cells and zero-filling new ones. [standard]identity(n)(token0xB4→identity_build(02:4108)) [confirmed]: allocaten×n, then walk every cell writing1.0whenrow==col(theexp==typetest) and0otherwise:_OP1Set1 ; for each (i,j): if i==j -> store 1.0 (mantissa[0]=0x10) else 0Fill(value,[M])/randM(stamp a constant / random values across all cells via a per-cell loop over the whole matrix. The02:62D4branch (CP 0xB5) isdim((0xB5=tDim), which creates ther×cresult (5DBB→_CreateRMat 110F) and stores the dims (631B/631C/4825) but performs no fill. For the decodedrandM(fill see TherandM(cell fill.- Matrix copy/reshape =
_DataSize-counted byte copy of the float payload (mele_copy9_d3(02:4539)/mele_copy9_loop(02:453F)). [confirmed]
The randM( cell fill [confirmed]
randM(rows,cols) builds its r×c result through _CreateRMat (00:110F). It fills
each cell with $\operatorname{int}(19\cdot\operatorname{rand})-9$, matching the
documented integer range $[-9,9]$. The loop is byte-pinned at
02:5CC1–02:5CE6. A headless TilEm trace of randM(3,3) executes this path
(tools/macros/matrix-randm.macro):
02:5CC1 loop:
PUSH BC / PUSH DE ; save cell counter and element pointer
5CC3: CALL ram:392D ; banked-call stub -> _Random (36:7DC9); OP1 = uniform [0,1)
LD A,0x13 ; 19 decimal
CALL ram:389D ; banked-call stub -> 33:5F83; load small int A as FP operand
CALL _FPMult (238B) ; OP1 = 19·rand
CALL _Intgr (2263) ; truncate -> {0..18}
LD A,0x09 ; 9 decimal
CALL ram:389D ; second operand = 9
CALL _FPSub (2297) ; OP1 = int(19·rand) - 9 in [-9, 9]
POP DE ; advance element pointer by one float cell
CALL 1B0C ; store OP1 into the matrix element
LD HL,-18 / ADD HL,DE ; step to the next 9-byte cell
POP BC / DEC BC ; cells remaining--
JR NZ,loop
The loop reaches _Random (0x4B79 → 36:7DC9) through a page 0 banked-call
stub table. It does not use an RST 28h bcall site, so a ROM-wide scan for
RST 28h.dw 0x4B79 finds no match. The stub at ram:392D contains CALL 2B09
followed by the inline descriptor .dw 0x7DC9.db 0x76. The trampoline writes
the descriptor’s page byte to port 6. Bit 7 clear selects flash, and the low six
bits select the page, so 0x76 selects page 36. Static descriptor scans must
mask the page byte with 0x3F. The small-integer loader stub at ram:389D
targets 33:5F83 through the same mechanism. [confirmed]
[A] + [B], [A] - [B], scalar·[A] — element-wise [standard]
Binary matrix add/sub apply the FP operation through a nested walk:
for each column:
for each row:
load [M](r,c) -> OP1
apply the FP operation
store the result
The operation requires equal dimensions (_ErrDimMismatch 0x8B). The nested two-counter cell
walk at 02:412A is the transpose copy (§ transpose); the add/sub element-loop driver is a
sibling in the same 412A–414E family and is inferred here. [standard]
[A] * [B] — matrix multiply [confirmed]
The multiply body is at 02:40BA. It is not a defined function in the disassembly (so the
decompiler/MCP can’t reach it), so this was decoded from rom.bin directly with z80dasm,
cross-checked against a routine Ghidra does define. The body is called from 02:5FFF (the
* operator handler, in the 02:5FE6 region) and reused from 02:4605 and 02:5B39. (0x40BA
is also the _SinCosRad bcall ID in ti83plus.inc — a hex coincidence, unrelated to this page-02
address.)
40BA is a classic O(n³) triple loop with an FP accumulator:
for each result cell (i,j): # counters at 84B7, 84B4
for k = 1 .. inner: # inner counter at 84AF
load [A](i,k) (403C mele_adr_af_jp)
multiply by [B](k,j) (47B9 / 0166F FP multiply)
accumulate (479F)
store acc -> [C](i,j) (4064 / 405A)
The three dec (hl) counters (84AF inner, 84B4, 84B7) each have a jr nz back-edge
(40E5, 40F9, 4100); an inner-dim mismatch (A.cols ≠ B.rows) raises _ErrDimMismatch. An
n×n product is n³ TIFloat multiply+add steps. [confirmed] The body comes
from direct rom.bin decoding; callers 02:5FFF, 02:4605, and 02:5B39
are byte-verified.
Transpose [A]ᵀ — 02:412A, dispatched from the ᵀ token 0x0E [confirmed]
The transpose operator ᵀ is the postfix token tTrnspos = 0x0E. The page-02 command
dispatcher handles it at 02:60E9 (CP 0x0E). It requires one matrix operand by testing
CP 0x02 followed by JR NZ. At 02:60F5, it swaps the two dimension bytes for the result
header with LD A,H, LD H,L, and LD L,A, then
allocates the transposed-shape matrix (5DBB/5DE0), runs the per-cell copy body at 02:412A,
then stores via JP 0x5F89. 02:412A has exactly one caller, 02:60FE (byte-verified CD 2A 41).
02:412A is the transpose copy [confirmed]. It walks every source cell and writes the value into the
destination whose _AdrMEle stride is the swapped dimension, so dst(c,r) = src(r,c):
412A: LD HL,(84AF) ; loop counters = dims
412E: CALL 403C ; load src [M] (B=row,C=column) from (84D3) → OP1
4131: LD HL,(84AF)
LD B,L
LD C,H
4136: CALL 4068 ; store OP1 → dst [M] via dest ptr (84D7)
4139: DEC (84AF)
JR NZ,412E ; inner counter
4141: LD (HL),C
INC HL
DEC (HL)
JR NZ,412E ; outer counter
4146: POP HL
LD B,L
LD C,H
RET
403C reads from the source data pointer (84D3); 4068 writes to the destination pointer
(84D7). The destination header carries the dimensions swapped by 02:60F5.
The row-major _AdrMEle calls therefore place src(r,c) at dst(c,r). [confirmed]
02:4178 (mat_fill_type1) is a separate single-counter fill/apply in the 414A–4178 block,
not the transpose body. [confirmed]
augment(, dim(, List►matr(, Matr►list( — per-function drivers [standard]
These are dispatched from the page-02 function-token evaluator (list_fold_dispatch, the
CP immJR/JP chain that runs 5E46/60C8–63xx, keyed on the token byte). Each command’s
body and its single caller are byte-verified below.
| Command | dispatch site | body | what the disassembly shows |
|---|---|---|---|
Matr►list( | 0x8D @ 6388 | 02:4773 (2-arg), 02:49E3 (1-arg list copy) | [confirmed] The 0x8D branch splits on argument count (638D: CP 0x02). The column-extract engine is 02:4773 (2-arg path: 639D: CALL 5DD8CALL 4773; only caller 63A0, byte-verified CD 73 47). It holds one column in C while B walks the rows, reading through the _GetMToOP1 setup at 02:4040 and writing through mele_store_ckvalid at 02:4068. It then copies the completed column into the destination list through 02:4051/02:479F. The 1-arg/list path uses 02:49E3 (6397: CALL 0x49E3), a list-element copy-until-length-match (47E6 recall, 4825 store, 21BB compare vs (84AF), RET Z). |
transpose ᵀ | 0x0E @ 60E9 | 02:412A | [confirmed] Swaps the dim header (60F5), allocates the transposed shape, then 412A copies dst(c,r)=src(r,c) over every cell (403C read from (84D3), 4068 write to (84D7)); only caller 60FE. See the transpose subsection above. |
augment( | 0x91 @ 02:635B | 02:6238 copy [confirmed]; 02:4663 engine entered but carry-gated [confirmed] | The branch requires two operands, reads the dimensions at 02:5D98, and compares the row counts with LD A,HCP L. Equal rows fall through; H>L raises E_Dimension. 02:6238 allocates the result and copies the row-major float payload through 02:4539. The branch then calls 02:4663. Carry is set at 02:6361 and restored at 02:6378; JR C,46EF at 02:46DC skips elimination. The statistics regression path enters the same dispatcher through 3A:6398 with carry clear. The augment(L1,L2) sibling at 02:637F also shares the setup at 02:6362 with carry clear. [confirmed] |
dim( (matrix create/set-dims) | 0xB5 @ 62D4 | create + dim setup (5DBB/5DEB) [confirmed] | The compare at 62D4 is CP 0xB5, and 0xB5 = tDim (dim(), not randM( — so this is the →dim( matrix create/resize handler. It splits on argument count (62D9: CP 0x02): a 2-arg path (62DD) and a 1-arg path (630A). Both create the result and set its dims through 02:5DBB (CALL 5CEB registers the variable by name, stores the data pointer to 84D3, reads and zero-rejects the dim bytes OR LJP Z,2719, stores dims to 84AF) and 02:5DEB/02:631E. There is no per-cell fill loop here — consistent with dim(, which only sets dimensions. 02:5264 (cplx_swap_dispatch) is reached only from the 0xBD complex-operand branch (62D0), not here. randM( is a separate two-byte token (tRandM = 0xBB20) whose decoded fill loop is documented under The randM( cell fill. [confirmed] |
List►matr( | 0x8E @ 61C1 | 02:7D19 + copy | reshapes the argument lists into a matrix (_DataSize-counted float copy 4539/453F). [standard] |
The matrix-element kernels these drivers share are _AdrMEle/_AdrMRow (4002/4000) for indexing,
4068 (mele_store_ckvalid) for validated stores, and 4539 (mele_copy9_d3) for the bulk
row-major payload copy. Each command’s dispatch site and body is
[confirmed]. The randM( cell fill and the carry-gated role of 02:4663
inside augment( are also [confirmed].
Determinant, inverse, and row reduction [confirmed]
det( and [A]⁻¹ share the Gauss-Jordan elimination engine with partial pivoting —
matrix_gauss_engine @ 02:42A6 — the entry flag in A selecting behaviour; only two
direct call sites exist (byte-verified — CD A6 42 appears exactly twice). rref(/ref( are a
separate driver and do not call 42A6 (see below):
| Token / op | site | flag A | meaning |
|---|---|---|---|
[A]⁻¹ (^ token 0x0C, operand = matrix) | 02:5F80 | 0x00 | inverse; singular ⇒ error |
det( (token 0xB3) | 02:5FC0 | 0x40 | determinant; bit6 set ⇒ singular tolerated (returns 0) |
det(’s handler at 02:5FA3 (not a defined function in the disassembly; address
unverified) first type-checks the operand is a matrix (chk_op_is_matrix (02:69B7):
type==2 else E_DataType 0x89), then LD A,0x40CALL 0x42A6.
The engine (42A6) [confirmed]
matrix_gauss_engine(A = mode flags):
HL = dims (84AF)
if H != L -> _JError(0x8C) # must be square for det/inverse
if 1x1: handle scalar directly (inverse = _FPRecip)
461C: scan |all elements| -> max magnitude (pivot-tolerance baseline)
init permutation/pivot vector at (84D5): perm[k] = k # identity permutation
for each pivot column 'col' (84AF loop):
41D0/41C1: PARTIAL PIVOT — scan the column for the largest |element|,
compare |OP1| vs |best| via _AbsO1O2Cp
remember the row
43B9 -> 414E: SWAP the pivot row into place (full physical row swap)
4259 swaps the matching entries in the permutation vector,
and (for det) toggles the running sign
normalize pivot row: load pivot, _FPRecip / _FPDiv so pivot -> 1
4473 / 426D: ELIMINATE — for every other row, row_r -= factor * pivot_row
(4473 = load-load-_FPSub element step, 426D/426F = dot-product /
back-substitution accumulate with _FPMult + RST6 _FPAdd)
accumulate determinant = product of pivots (× sign from swaps)
SINGULAR handling (43A5): if a pivot is ~0:
BIT 6,A
JP Z, 0x26F0 (_ErrSingularMat, E_SingularMat 0x83)
-> inverse (flag 0, bit6=0) ERRORS
-> det (flag 0x40, bit6=1) returns 0
Key sub-routines (all page_02; names are the live Ghidra DB labels): [confirmed]
461Cmat_max_abs— compute the matrix’s max-abs element (numeric scale for the near-zero pivot test).41C1abs_cmp_op1op2—|OP1|vs|pivot|compare (1A0F/1987abs+compare);41D0— scan a column for the largest-magnitude pivot (partial pivoting), calling43B9to swap rows as it goes.43B9/414Emrow_swap_loop/_AdrMRow— physical row swap / row scale (whole-row moves;414Eloads the column-count stride and swaps two complete rows via_AdrMRow×2 +1DDA).4259— swap two entries in the permutation vector at84D5.4473ele_sub_ref— the elimination element step ([M](i,k) − factor*[M](pivot,k):RST8CALL 403CJP 2297= load +_FPSub).426Dcol_dot_accum/426Fcol_dot_accum_from— column dot-product / back- substitution accumulate (_FPMult+RST6).- Pivot normalize uses
_FPRecip/_FPDiv; sign/inverse use_InvOP1S.
det( therefore = forward elimination with partial pivoting, return the signed product
of the pivots (each row swap flips the sign); a zero pivot ⇒ det = 0 (no error).
[A]⁻¹ = full Gauss-Jordan (reduce to identity, the augmented identity becomes the
inverse); a zero pivot ⇒ ERR:SINGULAR MAT.
Determinant sign and pivot-product bytes (02:43D8–02:4470) [confirmed]
The determinant sign comes from the permutation parity, not a separate sign cell. Each
physical row swap (43B9) calls 4259 to swap the matching pair in the permutation
vector at 84D5; the determinant magnitude is the running product of the diagonal pivots
formed during back-elimination. The tail that closes the det/inverse pass:
43D8 (det branch, bit6 = det):
43D9: BIT 6,A ; det mode?
43DE: CALL 151B ; pop pivot
43E3..43F6: PUSH AF ; (RST 8 _CpyToOP2)
CALL 403c (load [M](i,j)) ;
CALL 238b (_FPMult)
DEC pivot/row counters (84B0) ; loop
→ multiply the running determinant by each pivot
43F8: POP AF
AND 1
JP NZ,24bd ; *** DET SIGN *** low bit of the
; permutation-swap count → conditional _InvOP1S (negate)
43FF (inverse branch): re-walk for the augmented-identity columns,
4410..446F: per-column back-substitution (4428/445B = _FPMult-accumulate,
442B/24bd = _InvOP1S sign flips), then JP 0x420F to undo the
column permutation (4259-pairs) so the inverse comes out in the
original row/col order.
So the sign byte is the LSB of the swap-count applied via _InvOP1S (00:24BD) at
43FB/442B; the pivot product is the 238B/RST 30h accumulate over the diagonal in
43E3-43F6. The permutation undo (420F/4259) restores element order for the inverse. [confirmed]
Separate rref( and ref( driver [standard]
rref(/ref( do not re-enter the 42A6 Gauss-Jordan engine. A function-xref shows
matrix_gauss_engine (02:42A6) has exactly two callers — mat_inverse_entry (02:5F80,
flag 0) and det_entry (02:5FC0, flag 0x40); there is no third call site (byte-confirmed
above: CD A6 42 appears exactly twice). So det(/[A]⁻¹ are the only consumers of that
square-only, partial-pivoting driver. [confirmed]
rref( (BBh,A6h) and ref( (BBh,A5h) are 2-byte 0xBB-lead function tokens. On the
page-38 statement/expression evaluator (eval_expr_inner 38:59A4), token 0xBB is detected
and parse_advance consumes the prefix; the second byte is then dispatched through the
evaluator’s six-entry leaf_production_handler_table at 38:7175.
The selector at 38:701A–7026 chooses grammar_handler_table, the
38:478C code family, or this leaf table; 703A: CALL 0x0033 = _LdHLind
jumps to the resolved handler. Their reduced-row-echelon elimination is therefore a distinct,
non-square-tolerant driver reached through that table — a separate routine from 42A6, using
the same per-element FP primitives (_FPDiv/_FPMult/_FPSub) but with its own pivot loop
that tolerates rectangular matrices and rank deficiency (zero rows left in place, no
SINGULAR MAT). The concrete rref/ref body sits behind the two-byte entries in
leaf_production_handler_table; the table is now named and typed in the rebuilt
database, but the two tokens’ exact handler selection has not yet been isolated.
The two-caller xref establishes that it is a
separate driver from 02:42A6 [confirmed]. Its exact body address remains
[standard].
Floating-point and VAT integration [confirmed]
- Every element is a
TIFloat(Floating-point). Indexing produces a pointer; the value is then moved intoOP1/OP2(RST4= load-9,_Mov9B,_MovFrOP1) and all arithmetic is the FP engine’sRST 30h(_FPAdd)/_FPMult/_FPDiv/_FPSub/_FPRecip. There is no SIMD; a matrix multiply makes thousands of these calls. Complex elements (lists/[i]) carry a0x0Cflag and use 18-byte (two-float) elements, split viacplx_op_arrange. - Where the data lives: the parser resolves the list/matrix name through
OP1→_FindSym/_ChkFindSym(Variables & the VAT/sub-vat) → VAT entry → data pointer (+ flash page if archived). Thecount/dimheader is read first; then_AdrLEle/_AdrMEledo pointer math. A store into an archived matrix/list unarchives to RAM first (_Arc_Unarc; Flash cannot be written in place). - Scratch RAM used by the algorithms (verified operands):
84AF(current dims / i,j loop state),84B0/84B3/84B4(pivot, k, row counters),84B7(dims copy),84D3/84D5/84D7(data pointers + the permutation vector base),8478=OP1,8483=OP2,8499=OP4,84AF=OP6 region = the matrix-op loop frame.
Errors [confirmed]
The list/matrix routines raise these _JError codes; each row gives the code, its name,
and the routine and condition that triggers it.
_JError code | name | raised by |
|---|---|---|
0x78 | 0-index reject (via ram:2793) | _AdrMEle/_AdrMRow on a 0 row/col index |
0x83 | E_SingularMat (ERR:SINGULAR MAT) | 42A6 inverse on a zero pivot (_ErrSingularMat 00:26F0) |
0x85 | E_Increment | _ErrIncrement 00:26F8 (bad seq/loop step) |
0x89 | E_DataType | det(/matrix ops on a non-matrix operand (chk_op_is_matrix (02:69B7)) |
0x8B | E_DimMismatch (ERR:DIM MISMATCH) | add/sub/multiply with incompatible dims (_ErrDimMismatch 00:2715) |
0x8C | E_Dimension (ERR:INVALID DIM) | non-square det/inverse, out-of-range element store (_ErrDimension 00:2719, _StMatEl) |
0x15 | E_Stat (via ram:2741) | get_pos_list_elem bad index (_CkOP1Pos) |
Routine index
| space:addr | name | what |
|---|---|---|
00:10C4 | _CreateRList | new real list (count*9+2) [confirmed] |
00:1109 | _CreateCList | new complex list (count*18+2) [confirmed] |
00:1115 | _CreateRMat | new matrix (H*L*9+2, header columns,rows) [confirmed] |
00:1EF6 | _HTimesL | element count = H*L (dims multiplied) [confirmed] |
00:1930 | _HLTimes9 | ×9 (real TIFloat stride) [confirmed] |
02:4000 | _AdrMRow | address of matrix row start [confirmed] |
02:4002 | _AdrMEle | matrix element address: ((row-1)*columns+(column-1))*9 [confirmed] |
02:4044 | _GetMToOP1 | [M](i,j) → OP1 [confirmed] |
02:406C | _PutToMat | OP1 → [M](i,j) (validated) [confirmed] |
02:40BA | matrix-multiply body | O(n³) triple loop, decoded from rom.bin (not a defined function in the disassembly); called from 02:5FFF/4605/5B39. 0x40BA in ti83plus.inc is the unrelated _SinCosRad bcall ID. [confirmed] |
02:4108 | identity_build | identity(n): diagonal-1 fill (token 0xB4) [confirmed] |
02:412A | mat_transpose | transpose [A]ᵀ body (token 0x0E, dispatched 60E9/called 60FE): per-cell copy dst(c,r)=src(r,c) via the swapped dest header [confirmed] |
02:414E | mrow_swap_loop | row swap/scale (elimination) [confirmed] |
02:4178 | mat_fill_type1 | live DB name; single-counter per-cell fill/apply loop in the 414A–4178 block — not transpose [confirmed] |
02:4539 | mele_copy9_d3 | bulk row-major float-payload copy (skip 2 dim bytes, LDIR); used by augment(/reshape [confirmed] |
02:4663 | mat_gauss_engine | live DB name; min(H,L) partial-pivoting elimination engine; only caller is the augment( 0x91 branch (6379). Its role inside plain augment( is the one open item [standard] |
02:4773 | mat_to_list_cols | Matr►list( 2-arg column-extract engine (only caller 63A0): nested col×row walk copying matrix columns into list element(s) [confirmed] |
02:5264 | cplx_swap_dispatch | live DB name; complex OP-pair arrange/swap (5344/52D3) reached only from the 0xBD branch (62D0) — not the 0xB5/dim( matrix-create branch [confirmed] |
02:6238 | mat_augment_copy | augment( column-concat: allocate result (5DE0) + 4539 payload copy + re-point 84D3 [confirmed] |
02:49E3 | lele_copy_until_eq | live DB name; list-element copy-until-length-match (21BB, RET Z); inner copy of the Matr►list( 1-arg/list path (6397) [confirmed] |
02:41C1 | abs_cmp_op1op2 | absolute-value compare: OP1 vs pivot [confirmed] |
02:41D0 | pivot_col_scan | partial-pivot: find largest absolute value in column [confirmed] |
02:4259 | perm_swap | swap two entries of the permutation vector (84D5) [confirmed] |
02:426D/426F | col_dot_accum/col_dot_accum_from | column dot-product / back-substitution accumulate [confirmed] |
02:42A6 | matrix_gauss_engine | inverse(flag 0)/det(flag 0x40) Gauss-Jordan + partial pivot; square-only (H==L guard) [confirmed] |
02:4473 | ele_sub_ref | [M] − factor*pivot element step (_FPSub) [confirmed] |
02:461C | mat_max_abs | maximum absolute element (pivot tolerance) [confirmed] |
02:47C5 | _AdrLEle | list element address: data+2+(i-1)*9 [confirmed] |
02:47EA | _GetLToOP1 | list[i] → OP1 (complex-aware) [confirmed] |
02:47FB | rcl_list_elem_to_op1 | recall list elem to OP1 [confirmed] |
02:47FE | rcl_list_elem_b | recall list elem (B-indexed) [confirmed] |
02:4829 | _PutToL | OP1 → list[i] (validated, complex-aware) [confirmed] |
02:49A7 | rcl_c_list_elem | complex-list element → OP1/OP2 [confirmed] |
02:49B5 | rcl_c_list_elem_b | complex-list element (B-indexed) [confirmed] |
02:5BBB | get_pos_list_elem | list element by positive index (bounds) [confirmed] |
02:5E46 | func_eval_dispatch | single-byte function-token evaluator (0xB0–0xCD) [confirmed] |
02:5F80 | mat_inverse_entry | [A]⁻¹: flag 0 → matrix_gauss_engine [confirmed] |
02:5FC0 | det_entry | det(: flag 0x40 → matrix_gauss_engine [confirmed] |
02:6104 | list_fold_dispatch | sum(/prod( higher-order list fold [confirmed] |
02:69B7 | chk_op_is_matrix | require operand type==2 else E_DataType [confirmed] |
ram:21C4 | chk_type_lt_1a | classify element type width: AND 0x1FCP 0x1ACP 0x18CCF — real-vs-complex (0x0C) element width [confirmed] |
35:79E9 | list_idx_times9 | list index ×9 + dispatch [confirmed] |
07:4D3B | _RedimMat | re-dimension matrix/list [confirmed] |
07:4F07 | _InsertList/_IncLstSize | grow a list in place [confirmed] |
07:4F43 | _DelListEl | delete list element(s) [confirmed] |
38:6C8F | _StMatEl | parser store into [M](r,c) (bounds-checked) [confirmed] |
38:741F/7422 | _ConvDim/_ConvDim00 | coerce a dim/index to real [confirmed] |
00:26F0 | _ErrSingularMat | E_SingularMat 0x83 [confirmed] |
00:26F8 | _ErrIncrement | E_Increment 0x85 [confirmed] |
00:2715 | _ErrDimMismatch | E_DimMismatch 0x8B [confirmed] |
00:2719 | _ErrDimension | E_Dimension 0x8C [confirmed] |
Resolved behavior and remaining questions
rref(/ref(use a separate driver, not42A6. Xref proves42A6has exactly two callers (inverse5F80, det5FC0); rref/ref are 2-byte0xBB-lead function tokens dispatched via the page-38 evaluator’sleaf_production_handler_table(38:7175). Theref(execution dispatch is byte-pinned in the page02command chain. It comparesCP 0x2Dat02:609Aand, with arguments present, executesRST 28h.dw 0x4B85. Bcall ID4B85hresolves through the page3Btable to35:7995; its port-encoded page byte0x75selects page35.35:7995is an iterative FP reduction loop (_Minus1/_FPMult/OP-exchange primitives, back edge at35:79C4) consistent with the row-reduction driver. [confirmed] Therref(execution dispatch lives on page38, where two entry stubs (38:514Fwith carry set andB=1;38:5157with carry clear andB=0) converge onRST 28h.dw 0x4B88at38:515D. The ID resolves through the page3Btable to02:7C23, a per-element driver that walks the pushed matrix data from the FPS pointer (LD HL,(9824)then aDJNZloop), validates dimensions against the header bytes (8479/847Aexponent checks raising through26F4on failure), and stores results back per cell. NoCP 0x2Esite exists on page02, so the parser normalizes therref(token before this dispatcher. The role ofBand carry in distinguishingrref(from related calls remains [hypothesis]. The parse-side signature descriptors remain distinct (38:431E/0x5108forref(vs38:4323/0x510Cforrref().- det sign / pivot-product (
42A6tail43D8-4470) and dimension labeling. The det sign = LSB of the permutation-swap count applied via_InvOP1S(24BD) at43FB/442B; the magnitude is the238B/RST 30hdiagonal-pivot accumulate (43E3-43F6);420F/4259undo the column permutation for the inverse. Matrix storage is [confirmed]: the first header byte is the column count, the second is the row count, and_AdrMEletakesB=row,C=column. See Data layouts and Element access. - transpose,
Matr►list(, and theaugment(column-concat bodies. Each command’s page-02dispatch site and body are byte-confirmed, every body having exactly one caller:- transpose
[A]ᵀ(token0x0E@60E9) →02:412A(only caller60FE): the dim header is swapped (60F5) and412Acopiesdst(c,r)=src(r,c)over every cell.02:4178is a separate single-counter fill/apply, not transpose. [confirmed] Matr►list((0x8D@6388) →02:4773(2-arg column-extract engine, only caller63A0) with02:49E3as the 1-arg/list inner copy. [confirmed]augment((0x91@635B) → equal-rows guard (CP LJP NC,2719) + column-concat copy at02:6238(5DE0allocate +02:4539LDIRpayload copy). [confirmed]dim((0xB5@62D4;0xB5=tDim, notrandM() → creates the result and sets its dims (5DBB/5DEB).02:5264(cplx_swap_dispatch, only caller62D0in the0xBDbranch) is reached only from that complex branch, not here. [confirmed]List►matr(0x8Ebranch (61C1) →02:7D19+_DataSizecopy (4539/453F) is unchanged [standard].
- transpose
- The
augment(call to02:4663performs pivot-column setup but skips elimination because the engine tests the carry set by02:6361. The statistics regression path enters the same dispatcher with carry clear. [confirmed] - The
randM(fill loop at02:5CC1–02:5CE6computes $\operatorname{int}(19 \cdot \operatorname{rand}) - 9$ per cell. It calls_Random(36:7DC9) through the page 0 banked-call stub atram:392D; noRST 28hbcall site is involved. See TherandM(cell fill. [confirmed] seq(/SortA(/SortD(/stats list-builders: confirm the collect-then-_CreateRListloop and the in-place float sort/compare. (Residual — comparator_CpOP1OP2confirmed; the unanalyzed page-02 sort body’s element-load is still not byte-traced.)
Solver and numerical methods
The numeric solver paths implement root finding, numerical differentiation, integration, and time-value-of-money calculations. Each path repeatedly evaluates an expression through the calculation engine and the TI-BASIC interpreter.
Raw opcode checks supply banked-page evidence where Ghidra does not recover a complete function body.
Solver errors [confirmed]
The numerical routines raise four dedicated errors. Each has a tiny page-0 raiser stub
that loads an error code into A and tail-jumps to _JError (ram:2793); most banked
pages also keep a local copy of each stub so the iteration loop can reach it with a
cheap relative jump.
| Error | bcall | page-0 stub | code | Message |
|---|---|---|---|---|
_ErrSignChange | 0x44C5 | ram:2749 → _JError(0x98) | 0x98 | NO SIGN CHNG |
_ErrIterations | 0x44C8 | ram:274D → _JError(0x99) | 0x99 | ITERATIONS |
_ErrBadGuess | 0x44CB | ram:2751 → _JError(0x9A) | 0x9A | BAD GUESS |
_ErrTolTooSmall | 0x44CE | ram:2755 → _JError(0x9C) | 0x9C | TOL NOT MET |
error_name_table (07:6B81) is indexed by
(code − 0x88), so codes 0x88…0x9C map to consecutive strings:
07:6B81 SYNTAX(88) DATA TYPE(89) ARGUMENT(8A) DIM MISMATCH(8B) INVALID DIM(8C)
UNDEFINED(8D) MEMORY(8E) INVALID(8F) ILLEGAL NEST(90) BOUND(91)
WINDOW RANGE(92) ZOOM(93) LABEL(94) STAT(95) SOLVER(96) SINGULARTY(97)
NO SIGN CHNG(98) ITERATIONS(99) BAD GUESS(9A) STAT PLOT(9B) TOL NOT MET(9C)
SOLVER=0x96 is the context name shown on the Solver app’s error screen;
SINGULARTY=0x97 is raised when a step lands on a pole. [confirmed]
Equation Solver and solve( root finder [confirmed]
The interactive Equation Solver app and the numeric solve( token share one
root-finding engine living on flash page 0x39. (The Solver app’s UI — the
EQUATION SOLVER / eqn:0= / bound= / left-rt= screen — is drawn from strings at
06:6ABB, loaded by code at 06:6286/06:62EA/06:66F3.)
Function-value evaluator f(x) [confirmed]
A callback, given the trial value in OP1, returns f(x) = left − right
of the equation. Located around 39:468F:
-
_CkValidNum(ram:1E9B), then_MovFrOP1(ram:1B0C) stores the current guess into the solve variable (its data pointer is loaded from(9306), in the expression-stack region — the bytes areED 5B 06 93=LD DE,(9306)). -
It installs the error handler at
39:46C7, then re-evaluates the stored equation throughparse_inp_current_state_bjump(ram:391B). The stub’s inline descriptor targetsparse_inp_current_state(38:5992), an interior entry in_ParseInpthat preserves the already selected parser state. -
The error filter at
39:46C7inspects the error code inA: codes below0x86(OVERFLOW/DIV BY 0/SINGULAR MAT/DOMAIN) and0x87(NONREAL ANS) are swallowed by these comparisons:CP 0x87 JR Z CP 0x86 JP NC,0x2799This
xis treated as a point wherefis undefined, so the solver can step past it, while0x86(BREAK) and codes≥ 0x88are re-raised via_JErrorNo(JP 2799). Before returning a swallowed error,fix_temp_count_bjump(ram:327F) dispatches to_FixTempCnt(07:4FEC) to repair temporary-object accounting. This is whysolve(can skip singularities inside the bracket without aborting. [confirmed]
The sign test 39:463A reads OP1.value.type (8478) and OP2.value.type (8483), masks 0x80
and XORs them: Z = same sign, NZ = opposite sign — the bracket sign-change predicate. [confirmed]
Iteration loop [confirmed]
Setup (39:43AD…4410) evaluates f at the two user bounds, records their signs, and
seeds the bracket. The main loop runs from 39:4413:
- Loop / iteration counter is carried in
A,INC Aeach pass (39:44BF), pushed on the stack. Two caps are compared withSBC HL,…:LD HL,0x01F3(=499) at39:4479/39:458B→ exceeding it jumps to39:45A0 LD A,0x99 … JP 2793= ITERATIONS, and the earlyLD A,0x9Apath (39:45AD) = BAD GUESS (raised when the initial bracket is unusable).- A small count (
CP 0x04,39:44C3) gates the early Illinois/secant correction.
- Bisection midpoint:
_InvSub(ram:227D, = b−a) then_TimesPt5(ram:2382, ×0.5) give the half-width $\tfrac{1}{2}(b-a)$ at39:443C/443F; adding $a$ yields the midpoint $m=a+\tfrac{1}{2}(b-a)$. [confirmed] - Secant / regula-falsi step:
_FPMult(238B),_FPSub(2297),_FPDiv-class and_InvOP1S(24BD) around39:4488…44F2compute the linear-interpolation step $x_{n+1}=x_n-f(x_n)\,\dfrac{b-a}{f(b)-f(a)}$. The result is compared against the bisection bound; the algorithm keeps the secant guess only if it stays inside the bracket, otherwise it falls back to the midpoint — a classic bisection ⊕ secant (Illinois/regula-falsi) hybrid, the documented TI behavior. [standard] - Sign-change bookkeeping: the byte at
0x84AF(OP6 area) holds the running sign offat the bracket ends;XOR 0x80toggles it (39:44AB…44B3). If the two bounds never bracketed a sign change, the path at39:45CD…45DA JP 2749raises NO SIGN CHNG. [confirmed] - Convergence / tolerance test:
_AbsO1O2Cp(ram:1987, compares |OP1| vs |OP2|) is used repeatedly (39:446F,44D7,44F8,45C7) to test the bracket width / residual against tolerance.const_solver_tol_1e13(39:46EA) stores the1.0e-13tolerance;const_solver_floor_1e99(39:46E1) stores the1.0e-99(00 1D 10 …). On reaching tolerance the solver exits through the39:4540 → 4553branch (dynamically traced on anX²−2 = 0solve that converged to √2 ≈ 1.41421356);39:4547is aCALL, not the converged return, and the observed path bypassed it. The tolerance tests at446F/44D7/44F8run under that trace;45C7is reached only on other convergence sub-paths. [confirmed]
\begin{algorithm}
\caption{Solver root-finder --- bracketed secant / regula-falsi (page 0x39)}
\begin{algorithmic}
\REQUIRE bracket $[a,b]$ with $\mathrm{sign}(f(a)) \neq \mathrm{sign}(f(b))$ \COMMENT{else \textsc{no sign change} (0x98)}
\FOR{$k = 0$ \TO $499$}
\STATE $m \gets a + \tfrac{1}{2}(b-a)$ \COMMENT{bisection midpoint: \texttt{\_InvSub}, \texttt{\_TimesPt5}}
\STATE $s \gets a - f(a)\,\dfrac{b-a}{f(b)-f(a)}$ \COMMENT{secant: \texttt{\_FPMult/\_FPSub/\_FPDiv}}
\STATE $x \gets s$ \textbf{if} $s \in [a,b]$ \textbf{else} $m$ \COMMENT{fall back to bisection}
\STATE $f_x \gets \mathrm{eval\_equation}(x)$ \COMMENT{re-parse, error-trapped (39:468F)}
\IF{$\mathrm{sign}(f_x) = \mathrm{sign}(f(a))$}
\STATE $a \gets x$ \COMMENT{keep the sign change in the new bracket}
\ELSE
\STATE $b \gets x$
\ENDIF
\IF{$|b-a| < 10^{-13}$}
\RETURN $x$ \COMMENT{converged, exits via 39:4540 -> 4553}
\ENDIF
\ENDFOR
\STATE \textbf{raise} \textsc{iterations} (0x99) / \textsc{bad guess} (0x9A)
\end{algorithmic}
\end{algorithm}
Dynamic confirmation. Traced end-to-end under headless TilEm by driving the built-in Equation Solver to solve
X²−2 = 0(solver-sqrt2.macro). It converged on screen toX = 1.4142135623…(√2) withleft-rt = 0. The mem-write records show the guess at0x8478climbing1.40898 → 1.41421335 → 1.4142135623645 → 1.4142135623731(|err| ≈ 4.9e-15, crossing below the1e-13tolerance on the final step).solver_iterate(39:4413) ran 808×; the per-iteration re-parse (parse_eval_expr38:5AB3) ran 834×; the secant-in-bracket-else-bisect test (39:44F8), the499-cap compare (39:4479 LD HL,0x01F3), and the1e-13/1e-99constants (39:46EA/46E1) all executed as the pseudocode describes.
left-rt shown on the Solver screen is the final residual f(root) (the
left-side − right-side value the evaluator computed). [standard]
TVM finance solver [confirmed]
The five-variable time-value-of-money solver (N, I%, PV, PMT, FV, plus
P/Y, C/Y, and the PMT:END/BEGIN flag) lives on flash page 0x3A. Each variable is a
named system FP var; the routine loads them via small accessors:
3A:7F02loads the pointer at(84D3)(iMathPtr1;ED 5B D3 84=LD DE,(84D3)),3A:7F0Fthe one at(84D5)(iMathPtr2), etc. ((D?5B)are the finance sysvar VAT slots).(84D3)=iMathPtr1,(84D9)=iMathPtr4,(84AF)=OP6,(84D3)/(84D9)/(84D3)hold the iteration state. [confirmed]
TVM equation
The solver evaluates the standard cash-flow identity (rate $i = \tfrac{I\%}{100}\big/\tfrac{C}{Y}$, with $S=0$ for END / $1$ for BEGIN):
$$0 = PV + (1+iS)\,PMT\,\frac{1-(1+i)^{-N}}{i} + FV\,(1+i)^{-N}$$
Implemented with _FPRecip (ram:253D, for (1+i)^(−N) via reciprocal/power),
_FPMult (238B), _FPDiv (2541), _FPAdd (RST 30h), _InvSub/_FPSub
(227D/2297) around 3A:70D6…7140. The compound factor (1+i)^N is built with the
power/exp helpers. [standard]
Iteration [confirmed]
Solving for I% (the only variable with no closed form) uses Newton’s method on the
rate:
-
Iteration state is allocated as a small FPS frame at
3A:70A2:LD HL,0x0005 bcall(_AllocFPS)The loop counter is
B = 0x40(=64iterations max),3A:70AB. -
Each pass recomputes the TVM residual and its derivative, takes a Newton step, and tests the exponent of the correction against
CP 0x74(3A:71F4) — i.e. converged when the update is ≤ ~10⁻¹². The new estimate is written back via(84D9)→(84D3)(3A:71F9…71FE). [confirmed]
_SinH call in the TVM rate loop [confirmed]
At 3A:710B the TVM body contains EF CF 40:
RST 0x28
.dw 0x40CF
The bcall
table maps 0x40CF to _SinH (_SinHCosH=0x40C6, _SinH=0x40CF, _ASinH=0x40ED
are three consecutive distinct entries). A scan of the whole loop body (3A:70A0…7210) finds
three bcalls: _SinH (0x40CF, 3A:710B), an unmapped helper 0x462A (adjacent to
_AdrLEle 0x462D — a list/element accessor for the finance sysvar slots), and _SetXXOP2
(0x478F, 3A:71C5). The _SinH call carries the math: the surrounding
_FPMult/_OP1ToOP2/_FPSub sequence (CD 8B 23 … CD D4 16 CD 51 16 EF CF 40 CD 3F 16)
evaluates the annuity / compound-growth factor in hyperbolic form — the numerically
stable way to form (1+i)^N − 1 and [1−(1+i)^-N]/i for small rates i, avoiding
catastrophic cancellation. This is the only transcendental call in the rate-Newton loop. [confirmed]
- Exhausting the 64-iteration
DJNZ/DEC Bbudget falls to3A:7206 JP 274D= ITERATIONS (0x99). Solving forN/PV/PMT/FVis closed-form (algebraic rearrangement) and does not iterate. [standard]
The amortization helpers (ΣPrn, ΣInt, bal(, Pmt_End/Pmt_Bgn) and the finance
function tokens (tFinNPV 0x00, tFinIRR 0x01, tFinBAL 0x02, tFinPRN 0x03,
tFinINT 0x04, tFinPV 0x2D, tFinPMT 0x2E, tFinFPMT 0x20, tFinPMTend 0x4B,
tFinPMTbeg 0x4C; all 0xEF-prefixed 2-byte tokens) are dispatched into this page.
IRR( internally uses the same rate-Newton iteration and can also raise ITERATIONS. [hypothesis]
nDeriv( and fnInt( numeric calculus [hypothesis]
The numeric-calculus engine is on flash page 0x33 (the graph-math page — appropriate,
since both operate on a Y= expression). The function tokens are 0xBB-prefixed:
tRoot 0x22 (the solve(-style root token), tFnInt 0x24 (fnInt), and
tNDeriv 0x25 (nDeriv). They are recognised by the 0xBB-group scanners
(33:504E CP 0xBB, also 38:4E3F).
nDeriv( symmetric difference quotient [standard]
nDeriv(expr, var, value [,ε]) computes the centered difference
(f(x+ε) − f(x−ε)) / (2ε) with default ε = 1e-3. The setup region 33:4C80…4D00
stores/restores the variable, evaluates f at x±ε, and divides by 2ε using
_FPSub/_FPDiv (2297/2541) and _TimesPt5. The (97E7)/(97E9) counters at
33:4C80/33:4CB4 track the two/three sub-evaluations. The finite-difference
and variable save/restore flow is [confirmed]. The default ε is [standard].
fnInt( adaptive numeric integration [confirmed]
fnInt(expr, var, a, b [,tol]) is an adaptive iterative quadrature. The body is the
Ghidra function fnint_body at 33:4D00 (extent 33:4D00…4E91):
-
builds interval midpoints and half-widths:
_FPSub(2297),_TimesPt5(2382, ×0.5),_FPDiv(2541). The bytes at33:4D18are executable code —33:4D18 21 83 84(LD HL,0x8483),33:4D1B 3E 60(LD A,0x60),33:4D1D CD 65 1B(CALL fp_set_digit1B65; not_OP2SetA, whose body is1B24) — loading the scalar0x60 = 96(a working digit/scale count), not a quadrature weight. [confirmed] -
maintains a working set of partial sums in an FPS frame (
_AllocFPS 1534,_PopRealOx 14F6/150F/1505,_DeallocFPS 1526, with slot offsetsDE=0x15/0x1B/0x24) — endpoint values, the running estimate, and the previous estimate for the error test. [confirmed] -
iterates, refining the partition by interval bisection. The ×0.5
_TimesPt5halving refines the interval, the97E7/84AFdepth counters track subdivision depth, and the loop tail includes33:4E81 LD DE,0x0024 … C3 CB 45, while33:4E8C 3D F5 C2 57 4Ddecodes as:DEC A PUSH AF JP NZ,0x4D57It converges when the change in the estimate has exponent
≤ CP 0x74(~10⁻¹²,33:4E74). Exhausting the refinement budget falls through to33:4E8F JP 274D= ITERATIONS (0x99). [confirmed]
Quadrature rule. A full byte scan of 33:4D00…4F00 finds exactly one
floating-point constant in the body: const_ln10x100 (33:4E92)
(00 82 23 02 58 50 92 99 40 = 2.30258509…×10², i.e. ln(10)·100). It is referenced at
33:4E5D:
LD HL,0x4E92
CALL 0x1982
This is immediately after the only transcendental bcall in
the body, 33:4E56 EF AB 40 = bcall _LnX (0x40AB). So ln(10)·100 is used purely to
convert the requested significant-digit tolerance into a decimal error bound via
ln — it is a tolerance scaler, not a quadrature node or weight. There is no node/weight table anywhere in
the body (the data after 33:4E92, FD CB 18 AE …, decodes as code: RES 5,(IY+0x18)
followed by LCD/keypad port I/O DB 3A / D3 3A). A Gauss–Kronrod rule would require a fixed
block of ~7–15 irrational node and weight constants stored as TIFloats; their complete
absence, together with the explicit ×0.5 interval bisection and the coarse-vs-fine estimate
comparison, rules out Gauss–Kronrod. The rule is an adaptive Newton–Cotes-style scheme
with recursive interval bisection (Simpson-class), not Gauss–Kronrod.
[confirmed] The only constant present is the ln-based tolerance scaler; no
quadrature node table exists.
Both nDeriv( and fnInt( evaluate the user’s f by storing the running argument into the
integration/derivative variable and re-running the parser, exactly like the Solver’s
f(x) callback — the same “store var → parse_eval → read OP1” loop. [standard]
Parser feedback loop [standard]
Every routine above shares this inner cycle, which is the whole reason they are slow:
- Place the trial value in
OP1(_Mov9ToOP1/ arithmetic result). _MovFrOP1(ram:1B0C) store it into the named variable the expression mentions (the solve var, thenDeriv/fnIntintegration var, or the TVM var).- Re-evaluate the expression through the TI-BASIC parser (
_ParseInp 38:5987/parse_eval_expr 38:5AB3/_Find_Parse_Formula 38:758A; the Solver uses its ownparse_inp_current_stateentry at38:5992). The parser walks the same stored token stream each pass. - Read the numeric result back from
OP1, form the residual / difference, decide the next step. The error handler at39:46C7, its(IY+7).2state bit, and the_FixTempCntcleanup catch a DOMAIN/NONREAL error at one sample. The solver treats that point as undefined instead of aborting, as described under Function-value evaluator.
Because the expression is re-tokenised and re-evaluated on every iteration, a solve(
with a 499-iteration cap can parse the equation up to ~499 times, and a fnInt over a fine
adaptive partition can parse it thousands of times — the dominant cost.
Parser routing for tFnInt, tNDeriv, and tRoot [confirmed]
These three are 2-byte tokens with the t2ByteTok = 0xBB lead byte (ti83plus.inc:
tRoot = 0x22, tFnInt = 0x24, tNDeriv = 0x25), so in the token stream they appear as
BB 22 / BB 24 / BB 25. The routing is a generic paged command call, not an inline
bjump, and goes through the page-0x02 command-execution layer:
-
The evaluator hands the operand token to the page-0x02 dispatcher, which recognises the
0xBBgroup and the second byte:tFnIntat02:68F3(CP 0x24),tNDerivat02:6904(CP 0x25),tRootat02:58AD/02:69BC(CP 0x22). [confirmed] -
The page-0x02 handler parses the comma-separated argument list and sets defaults. For example, the
nDeriv/fnIntprologue at02:6AF6does:LD A,0x7D LD (0x8479),AThis seeds the default tolerance exponent
0x7D(=1e-3, the documented nDeriv ε) before the call. [confirmed] -
It then performs a paged call into page 0x33. The page-0x33 entry re-validates the token through the
33:504Ebb_token_scanner(CP 0xBB, thenCP 0x68 / 0xCF / 0xDB / 0xF6to assign a small class index inCandCALL 0x50AC) and dispatches into the numeric bodiesnderiv_body(33:4C80) /fnint_body(33:4D00). Because the call crosses pages through the bcall/app-call trampoline, no static xref to these bodies survives in the Ghidra database — the mark of a generic paged call rather than an inline bjump. [confirmed]
Routine index [confirmed]
Equation Solver / solve( (page 0x39):
39:43AD solver_root_setup (eval f at both bounds, seed bracket)
39:4413 solver_iterate (bisection+secant hybrid main loop)
39:463A solver_sign_test (OP1/OP2 sign-change predicate; Z=same sign)
39:468F solver_eval_fx (store guess -> reparse equation -> f=left-right)
39:46C7 solver_eval_errfilter (swallow <0x86 and 0x87/NONREAL; re-raise 0x86/BREAK and >=0x88 via _JErrorNo)
ram:391B parse_inp_current_state_bjump (cross-page stub to 38:5992)
38:5992 parse_inp_current_state (interior _ParseInp entry used with selected state)
ram:327F fix_temp_count_bjump (cross-page stub to _FixTempCnt at 07:4FEC)
39:46EA const_solver_tol_1e13 (convergence tolerance, TIFloat 00 73 10..)
39:46E1 const_solver_floor_1e99 (residual-zero floor, TIFloat 00 1D 10..)
39:45A0 ->ITERATIONS(0x99) 39:45AD ->BAD GUESS(0x9A) 39:45DA ->NO SIGN CHNG(0x98)
TVM / finance solver (page 0x3A):
3A:70A2 tvm_solve_iterate (Newton on I%, 64-iter FPS-framed loop)
3A:7F02 tvm_load_var (iMathPtr1) 3A:7F0F tvm_load_var (iMathPtr2) (finance var accessors)
3A:7206 ->ITERATIONS(0x99)
Numeric calculus (page 0x33):
33:4C80 nderiv_body (centered difference (f(x+e)-f(x-e))/2e, e=1e-3)
33:4D00 fnint_body (adaptive bisection integrator; extent 4D00..4E91)
33:4E56 ->bcall _LnX (0x40AB) (digit-tolerance -> decimal error bound)
33:4E8F ->ITERATIONS(0x99)
33:4E92 const_ln10x100 (TIFloat 00 82 23 02 58 50 92 99 40 = ln(10)*100; the
ONLY FP constant in fnint_body -- no node/weight table)
33:504E bb_token_scanner (CP 0xBB then class-index 0x68/0xCF/0xDB/0xF6 -> CALL 50AC)
33:4381 ctrlflow_handler_table (13-entry jump table for For/While/Repeat/End/Return, etc.)
33:435F ctrlflow_dispatch (entry from bcall 0x5140/0x513D; SUB 0x20; index the table)
Page-0 FPS register save/restore + active-frame bookkeeping cluster (the “solver helper
cluster” — these are generic FPS slot accessors used by the solver, fnInt/nDeriv and other
FPS-framed routines; each slot is 9 bytes = one TIFloat, offset -(9*slot) from the frame
base pointer (9302)):
ram:2800 fps_swap_active_frame (swaps the active FPS frame pointer at (86DE) -- the
bracket/scope bookkeeping primitive)
ram:2895/28C3/28D8/28E9/2903/2908/2914/291B fp_st_slotN_opX
(store OP1/OP3 into FPS slot 2/4/5/6/7/7/8/9)
ram:29CF/29D7/29DB/2A0B/2A0F/2A13/2A17 fp_ld_op1_slotN
(load OP1 from FPS slot 5/7/8/10/11/12/13)
Error stubs / table (page 0 & 0x07):
ram:2749 _ErrSignChange(0x98) ram:274D _ErrIterations(0x99)
ram:2751 _ErrBadGuess(0x9A) ram:2755 _ErrTolTooSmall(0x9C)
ram:2793 _JError 07:6B81 error_name_table (indexed by code-0x88)
Shared FP/parse helpers (page 0): _FPAdd 229E, _FPSub 2297, _FPMult 238B,
_FPDiv 2541, _FPRecip 253D, _InvSub 227D, _TimesPt5 2382, _InvOP1S 24BD,
_AbsO1O2Cp 1987, _OP1ToOP4 19EC, _OP4ToOP2 19FE, _CkValidNum 1E9B,
_MovFrOP1 1B0C, _AllocFPS 1534, _DeallocFPS 1526, _PopRealOx 14F6/150F/1505.
Parser entries (page 0x38): _ParseInp 5987, parse_eval_expr 5AB3,
_Find_Parse_Formula 758A.
Resolved behavior and remaining questions
Summary of the four sub-results:
-
fnInt(quadrature rule. Not Gauss–Kronrod. The body has no node or weight table; its sole FP constant isconst_ln10x100, used with bcall_LnXto convert digit-tolerance to a decimal error bound. With explicit ×0.5 interval bisection and a coarse-vs-fine estimate comparison, it is an adaptive Newton–Cotes / Simpson-class bisection integrator.33:4D1Bis executable code:LD A,0x60 CALL fp_set_digit -
TVM
_SinH(id0x40CF). The TVM rate loop calls_SinHat3A:710B(0x40C6/0x40CF/0x40EDare three distinct hyperbolic bcalls); it evaluates the annuity / compound factor in hyperbolic form for numerical stability at small rates. -
Class-3 routing of
tFnInt,tNDeriv, andtRoot. The parser route isBB-token → page-0x02 dispatcher (02:68F3/6904/58AD) → arg-parse + default-tol (02:6AF6, exp 0x7D = 1e-3) → paged call → page-0x33 bodies, re-validated bybb_token_scanner(33:504E). The trampoline hides the static xref, confirming it is a generic paged call. -
Page-0 helper cluster. The routine index identifies generic FPS slot save/restore (9-byte TIFloat slots at
-(9*slot)from frame base(9302)) plus the active-frame swapper atram:2800, renamedfps_swap_active_frame; the store/load stubs arefp_st_slotN_opX/fp_ld_op1_slotN.
Residual (genuinely unverified, would need deeper paged tracing):
- The exact byte layout of the For/While/Repeat loop-control record pushed by the
page-33 control-flow handlers (
ctrlflow_handler_table) is not yet field-mapped; only the dispatch path is confirmed. See the TI-BASIC execution pipeline. - bcall
0x462Ain the TVM body is unmapped (adjacent to_AdrLEle; likely a finance-sysvar list/element accessor).
Tokenizer and TI-BASIC tokens
TI-BASIC source is stored as a sequence of one- and two-byte tokens, not as the characters shown in the editor. Tokenization chooses that encoding; execution walks it; detokenization turns it back into display text.
Encoded width
Most tokens occupy one byte. _IsA2ByteTok (00:1FE8) decides whether a byte
introduces a two-byte token by searching two_byte_token_lead_table
(00:1FF6), an 11-byte list of lead bytes:
| Lead | Group |
|---|---|
5Ch | Matrices |
5Dh | Lists |
5Eh | Equation variables |
60h | Pictures |
61h | Graph databases |
62h | Output/Y-variable group |
63h | System variables |
7Eh | Graph-format group |
BBh | General extended commands |
AAh | String variables |
EFh | TI-84 Plus extensions |
The first byte selects a group and the second selects an entry in that group.
For example, 5D 00 is L1, while BB 6A is Asm(. The complete second-byte
maps are in the token tables. [confirmed]
Encoded width and displayed width are different questions:
_IsA2ByteTokanswers whether the stored token uses one or two bytes._GetTokLen(01:66E5) returns the length of the token’s displayed name._Get_Tok_Strng(01:66EA) returns that display string.
The editor uses the latter two operations to paint source. Parser scanners use the first operation so they never mistake the second byte for a command or delimiter. [confirmed]
Editor conversion and insertion [confirmed]
The token editor tracks four little-endian pointers in one fixed block:
#pragma pack(push, 1)
typedef struct {
uint16_t top; /* +0x00, editTop at 0x96F4 */
uint16_t cursor; /* +0x02, editCursor at 0x96F6 */
uint16_t tail; /* +0x04, editTail at 0x96F8 */
uint16_t bottom; /* +0x06, editBtm at 0x96FA */
} EditorBufferState; /* 8 bytes */
#pragma pack(pop)
_BufClear = 0x4936, body ram:222E, sets editCursor = editTop and
editTail = editBtm. It does not wipe every byte between those pointers.
_bufInsert = 0x4909, body 06:42E5, accepts a token in DE. It first checks
for room at the cursor. When D = 0, it inserts the one-byte token from E;
when D != 0, it performs a second room check and writes the two bytes in
D,E order. Success advances editCursor and returns NZ. A full buffer or
failed second-byte check returns Z without reporting success. A controlled
trace calls the clear body, calls the insert body with DE = 0xBB6A (Asm(),
calls the clear body again, and returns from every call. The pointer mutations
above are confirmed from the ROM body; that return-path reducer does not
snapshot the transient buffer contents. [confirmed] under TilEm.
_ConvKeyToTok = 0x4A02, body 07:44DE, converts a cooked key in A to a
token in DE. Input 0x05 has the dedicated result DE = 0x003F. Ordinary
inputs subtract 0x5A and select a byte from the table beginning at
07:4000; special inputs 0xFB, 0xFC, and 0xFE use keyExtend at
ram:8446 and the tables rooted at 07:4426, 07:422C, 07:4099, or
07:4102. The latter paths can return two-byte tokens. A controlled trace
confirms A = 0x05 → DE = 0x003F; the special-table cases remain confirmed
from ROM control flow, not exhaustively traced. Reduced results are in
tools/data/community-manual-bcall-traces.csv and
tools/data/community-bcall-semantics.csv.
One source line, three representations
The statement
cumSum(L1)->L2
is stored as:
BB 29 5D 00 11 04 5D 01 3F
└───┘ └───┘ │ │ └───┘ └─ EOL
cumSum( L1 ) → L2
No source spaces or character count are preserved. The editor reconstructs
the spelling from token tables. The interpreter sees typed operations and
names immediately, without reparsing the visible word cumSum. [confirmed]
Token streams and execution
The tokenizer and interpreter meet at the parse cursor. The current byte is
fetched at 38:72DA; 38:4180 skips logical tokens while respecting two-byte
leads and quoted strings. The expression evaluator then maps the token to a
grammar class and selects a recursive production. [confirmed]
This division explains several behaviors:
3Fhends a stored line even though no newline character is displayed in the token stream.- bytes resembling
ThenorEndinside a quoted string are data because the quoted-string scanner consumes the whole region. - a second byte following
5Dh,BBh, or another lead cannot be interpreted independently. - display names can change in width without changing encoded width.
The pointer table at 38:4000 belongs to expression dispatch, not tokenization.
It contains little-endian handler addresses selected after token classification.
See TI-BASIC execution for
the evaluator and TI-BASIC dynamic tracing for the
bounded token-width and scan models.
Reproducible samples
tools/ti84re/tibasic/samples.py generates readable .bas, raw .tok, and loadable
.8xp forms from one definition. Treat .bas as the review form, .tok as
the exact interpreter input, and .8xp as the calculator fixture. A compact,
diverse subset is used for dynamic coverage; the generator retains the broader
fixture library for targeted subsystem investigations.
python3 -m ti84re.tibasic.samples --write-dir tools/tibasic-samples
The generator’s byte assertions catch accidental changes between the readable source and the token body. Calculator execution is still required to establish runtime behavior.
TI-BASIC execution
TI-BASIC is a token-stream interpreter built around three kinds of state: a
cursor over program bytes, a recursive expression evaluator whose result lives
in OP1, and control records that remember where loops and subprograms resume.
This page follows one statement through those layers before describing the
special cases.
The addresses and byte-level decisions below refer to TI-84 Plus OS 2.55MP. Claims marked [confirmed] are tied to ROM bytes or calculator traces. A [hypothesis] marks the remaining interpretation of an incompletely decoded structure.
The execution pipeline
A stored program is a VAT object of type ProgObj (05h) or ProtProgObj
(06h). Its data begins with a little-endian size word followed by exactly that
many token bytes. There are no stored line numbers: 3Fh separates lines, and
3Eh separates colon-delimited statements. [confirmed]
Execution can be read as a pipeline:
flowchart LR
V["VAT program object"] -->|"size selects token body"| C["parse cursor<br/>nextParseByte … basic_end"]
C -->|"fetch and classify"| H["statement or expression handler"]
H --> E["recursive expression"]
H --> M["variable or list access"]
H --> D["command or control transfer"]
E --> R["OP1 result"]
M --> R
D --> R
R -->|"advance or replace cursor"| C
The page-38 evaluator owns the cursor and grammar. Statement commands can cross to page 02, control-flow bodies to page 33, display code to pages 01/03/37, and variable lookup to the VAT routines. Those page changes are continuations of one interpreter, not separate parsers. [confirmed]
The token cursor
TIBasicParserState (0x9652) keeps the program identity and cursor interval
contiguous in RAM: [confirmed]
#pragma pack(push, 1)
typedef struct {
uint8_t basic_prog[9];
uint16_t basic_start;
uint16_t next_parse_byte;
uint16_t basic_end;
uint8_t num_arguments;
} TIBasicParserState;
#pragma pack(pop)
next_parse_byte is the current token position. basic_start begins the
current body, and basic_end is its inclusive parser/refill boundary.
The small page-38 helpers are the useful way to reason about cursor movement:
| Routine | Address | Operation |
|---|---|---|
parse_cur_tok | 38:72DA | Fetch the current byte and classify 00h, 3Eh, and 3Fh |
parse_advance | 38:7248 | Increment nextParseByte, compare it with basic_end, and refill when needed |
parse_expect_or_err | 38:5CD8 | Require one token or restore the fault position and raise syntax error |
parse_scan_tokens | 38:4180 | Scan to a statement delimiter without splitting a two-byte token or quoted string |
parse_init | 38:5B7B | Reset parser-position bytes and parser flags |
Encoded width matters whenever the interpreter skips rather than evaluates.
_IsA2ByteTok (00:1FE8) searches the 11-byte
two_byte_token_lead_table (00:1FF6); a match
means the lead and following byte must move together. The scanner also treats a
quoted string as one region, so Then, Else, or End bytes inside a string
cannot terminate an outer scan. [confirmed]
scan_to_delimiter():
loop:
token = current_token()
if token is end, colon, or EOL:
return
if token is quote:
advance through the closing quote or EOL
else if token is a two-byte lead:
advance once for the second byte
advance to the next token
This routine does not know the grammar of an expression. Its job is narrower: preserve token boundaries while another routine searches for a statement or block delimiter.
Expressions are nested productions
_ParseInp (38:5987) initializes parser state for a homescreen expression or
formula and enters the shared evaluator. Stored-program execution arrives with
the program body and parser frame already selected. Both converge on the
recursive expression machinery around parse_eval_expr (38:5AB3) and the
statement loop at 38:59C5. [confirmed]
The evaluator is not a flat “token to function” switch. It first maps the
current token to a grammar class, selects a production family, and lets that
handler recursively consume tighter-binding operands. The selector at
38:7010 chooses among three bases:
Selector C | Handler family | Role |
|---|---|---|
Other than 02h or 03h | grammar_handler_table (38:4000) | Main grammar productions |
02h | code at 38:478C | Postfix/power production |
03h | leaf_production_handler_table (38:7175) | Six leaf-production offsets |
For the main family, the grammar class in A indexes
grammar_handler_table. Its 87 page-local offsets contain 84 valid pointers and 81
distinct handler destinations. The bytes there are data — beginning 9F 41 F0 45 1C 42 ...—not executable Z80. The selector doubles the class, reads the
pointer, and calls the chosen production. [confirmed]
evaluate_production(class, level):
if level == 2:
return postfix_production_478C(class)
else if level == 3:
handler = leaf_production_handler_table[class]
else:
handler = grammar_handler_table[class]
return handler(parse_cursor, OP1)
This nesting is what gives ^, multiplication, and addition their precedence.
Binary productions move operands through the OS floating-point stack and apply
operations such as _FPAdd, _FPMult, or _BinOPExec; the completed value is
left in OP1. 38:6FB7–6FC2 also folds token classes F2h and above by adding
12h before dispatch. [confirmed]
The other indirect jumps in the declared interpreter graph also have bounded ROM-owned destinations:
| Jump | Selector source | Valid destinations |
|---|---|---|
38:4390 | 14 entry wrappers load literal continuations | 14 |
38:7244 | 49-class table at 38:4FDB; five rows are zero/invalid | 27 distinct |
02:5675 | Five preceding token comparisons load literal targets | 5 |
33:4380 | Bounds-checked 13-row table at 33:4381 | 13 |
The CFG follows those destinations without treating adjacent table bytes as instructions. The bounds describe valid interpreter state, not arbitrary register or stack corruption. [confirmed]
The shared statement loop can then do one of three things with the result:
- store it through a parsed variable name;
- write it to
Ansthrough_StoAns(38:6251); or - pass it to a command or control-flow continuation.
_AnsName (38:74B7) constructs the internal name with class byte 72h;
_RclAns (38:679F) recalls it through the ordinary variable machinery.
[confirmed]
Variable identity and value payload are separate
The parser first builds a variable-name descriptor in OP1. The descriptor’s
type byte selects a VAT object class and the following bytes encode the token or
name. findsym_scan (07:565F) resolves that identity to a VAT entry and data
pointer; only then does the recall path copy or address the value payload.
[confirmed]
| Object class | Type | Payload used by BASIC |
|---|---|---|
| Real | 00h | One 9-byte TIFloat |
| Real list | 01h | 2-byte length, then 9-byte elements |
| Matrix | 02h | Two dimensions, then 9-byte elements |
| String | 04h | 2-byte length, then token/character bytes |
| Program | 05h | 2-byte length, then token bytes |
| Protected program | 06h | Program payload with protected edit semantics |
| Complex list | 0Dh | 2-byte length, then complex elements |
flowchart LR
T["name token"] --> N["OP1 name descriptor"]
N --> V["07:565F<br/>VAT scan"]
V -->|"recall"| P["typed payload"]
P --> R["OP1 value or element address"]
R --> A["FP/list/matrix operation"]
A -->|"store"| S["create, resize, or replace VAT payload"]
Scalar arithmetic copies a 9-byte value into the OP registers. List and matrix
access instead checks the container type and dimensions, computes one element
address, and moves that element through OP1. Stores can therefore fail before
arithmetic runs: name lookup, type compatibility, dimensions, and allocation
are distinct boundaries. [confirmed]
Statements end locally; blocks scan structurally
A false single-line If only has to skip one statement. A false If ... Then,
While, Repeat, or For( must find a matching structural boundary without
executing the intervening tokens. That is the purpose of
blockmatch_end_else (38:4130). [confirmed]
find_matching_boundary():
depth = 0
loop:
token = current_token()
if token == Else and depth == 0: return Else
if token == End and depth == 0: return End
if token == End: depth -= 1
if token in {For, While, Repeat}: depth += 1
if token == If:
scan_to_delimiter()
if current_token() == Then: depth += 1
scan_to_delimiter()
The distinction between If condition and If condition:Then is structural:
only the latter opens a block. Nested Else tokens are ignored until the depth
returns to zero. The comparisons and counter changes are visible at
38:4137–417E; the counter is the 16-bit DE register. [confirmed]
Natural loops use a page-38 OPS record
The public page-33 routine behind bcall grf_435f = 5140h subtracts 20h,
accepts 13 indices, and jumps through the table at 33:4381. Three ABI probes
confirm both bounds outcomes at 33:436D and 33:4372, but natural stored
programs do not enter it. It is not the For(/End transition. [confirmed]
Natural For( execution reaches parse_for_production (38:41E5). Natural
End execution reaches parse_end_ops_record (38:4200), which consumes a
5-byte loop record from the operator stack at OPS + 1. All three structured
loops share the shape — sentinel byte, continuation word, state word: [confirmed]
#pragma pack(push, 1)
typedef struct {
uint8_t sentinel; /* 00h */
uint16_t continuation; /* where the End token jumps back to */
uint16_t state; /* varies per fixture; meaning open */
} TILoopOpsRecord;
#pragma pack(pop)
A shadow-memory replay of the headless trace records the loop state for a
program that runs For(θ,1,3), While θ<3, and Repeat θ≥2 in sequence. The
reproduction macro is tools/macros/run-loops-typed.macro.
| Loop | Record at End | Continuation | State |
|---|---|---|---|
For( first End | 00 36 58 07 00 | for_first_update (38:5836) | 0007h |
For( later Ends | 00 7D 58 07 00 | for_steady_update (38:587D) | 0007h |
Repeat End | 00 E7 57 23 00 | 38:57E7 | 0023h |
The continuation field selects the loop mechanics. Observed state words include
0012h and 0007h, so the field is not a fixed constant; its exact role is
still open. [confirmed]
While and Repeat push their records in a parse-time form with continuation
38:5AC1. Three observed pushes carry marker bytes F0 58, 11 58, and
2A 58. The first condition evaluation rewrites the record to the runtime form
above. That evaluation runs through
38:41CC or 38:41D9 — each guards with CALL 7203, calls 71B4, pops the
saved value into HL, and jumps to 38:57A8 or 38:57E1 respectively;
38:57E1 sits immediately before the Repeat continuation 38:57E7.
[confirmed]
A single trace does not establish which loop re-enters through
parse_end_ops_record on each iteration and which jumps directly from its
continuation. [hypothesis]
flowchart LR
F["For( token"] --> P["parse_for_production<br/>create production state"]
P --> B["execute loop body"]
B --> E["End token<br/>parse_end_ops_record"]
E --> O["pop sentinel + continuation + state word"]
O --> I["for_first_update"]
O --> S["for_steady_update"]
I --> B
S --> B
The continuation path resolves the loop variable through the VAT, applies the floating-point increment, compares the updated value, and either revisits the body or removes the record. The paired trace confirms the record bytes and the two continuations. The complete layout of the associated limit, step, and temporary floating-point values is not yet decoded. [confirmed]
The optional closing ) in For( changes marker-to-marker work and parser
buffer state. Neither spelling reaches the page-02 finalization gate in the
paired trace, so the exact causal transition remains open. The measured effect
is covered in the For( parenthesis trap.
Labels rescan instead of indexing
goto_lbl_name_scanner (38:4870) reads the label name after Goto or Lbl.
The search path at 38:7600 rescans the program body for a matching Lbl, then
moves nextParseByte to it. This explains both the linear cost of Goto and
why jumping out of structured loops can bypass normal loop cleanup. The token
and name-scanning path is [confirmed]; the complexity and stack consequence are
standard TI-BASIC behavior.
Program calls share data but preserve parser control
prgmNAME resolves another ProgObj, saves the caller’s interpreter state,
and evaluates the callee body. It does not create a local variable frame.
Scalars, lists, strings, and Ans remain global, so they form the practical
calling convention. [confirmed]
| Event | Effect |
|---|---|
prgmNAME | Enter the callee with a nested parser/control frame |
Return | Unwind one BASIC program frame and resume the caller |
| End of body | Return through the same program-frame machinery |
Stop | Terminate the whole BASIC program chain |
The run-confirmed callee transition passes through 38:6910, calls at
38:6914, and enters the body evaluator at 38:778F. The public parser bcalls
are not substitutes for that prepared state: calling _ParseInpLastEnt or
_Find_Parse_Formula from an arbitrary AsmPrgm reaches parser setup but not a
working BASIC call frame. The negative fixtures end at ERR:INVALID and
ERR:UNDEFINED, respectively. [confirmed]
For source-level conventions and the Ans/scalar/list calling fixture, see
TI-BASIC examples and ASM interop.
Commands parse arguments, then hand off
Commands use the same expression evaluator for arguments and then cross to a specialized subsystem. The important boundary is between argument parsing and the operation itself:
| Command | Parse/dispatch evidence | Downstream operation |
|---|---|---|
Disp | page-38 statement handler | _Disp at 37:51D3, then _NewLine |
Output( | 38:6AE6, page-02 handler | _OutputExpr at 03:4AF2 |
Input | 02:54EF | entry editor, _ParseInp, variable store |
Prompt | 02:562F | repeated named-variable entry and store |
Menu( | 02:555D | _DispMenuTitle at 39:4D21, then label transfer |
Pause | 02:55E7 | display and key-wait loop |
getKey | expression token ADh | non-blocking _GetKey bcall 4972h |
Input accepts either an optional prompt string or a row/column prefix before
one store target. Prompt loops over comma-separated variables and generates
the NAME= labels itself. Menu( parses a title followed by option-string and
label pairs. These argument-order boundaries are [confirmed]; the entry
editor’s internal cursor and redraw state are not yet mapped.
getKey is an expression value, not a statement. The table at 37:6700 is a
token-attribute table; returned key codes come from _GetKey on page 06. This
distinction prevents a common false inference from the nearby CP ADh bytes.
[confirmed]
Errors unwind saved relative stack state
The error entries first select an error code, then join the common path at
00:270A. For example, _ErrDivBy0 at 00:26EC selects 82h, while the
syntax entry at 00:2700 selects 88h. Natural Disp 1/0 and Disp 1+
traces reach those entries, respectively. [confirmed]
The entry identifies the error message, but its incoming guard identifies the cause. Twelve natural programs separate all six numeric error entries into 12 guard paths. This table groups related paths so the mechanism stays visible:
| Program | Originating guard | Predicate | Error shim |
|---|---|---|---|
Disp 1/0 | 00:2548–254B | divisor in OP1 is zero | _ErrDivBy0 at 00:26EC |
Disp 10^100 | 02:7076–7078, then 02:7053–7059 | positive exponent argument is at least 100 | _ErrOverflow at 00:26E8 |
Disp 1E99*1E99 | 00:2513–251D | adjusted sum of biased decimal exponents overflows | _ErrOverflow at 00:26E8 |
Disp ln(0) | 02:6F1E, then 00:212D–2131 | logarithm operand in OP1 is zero | _ErrDomain at 00:26F4 |
Disp sin⁻¹(2) / cos⁻¹(2) | 02:76F1–76F5 / 02:76DF–76E2 | operand lies outside $[-1,1]$ | _ErrDomain at 00:26F4 |
Disp (-1)! / (-1) nCr 1 | 35:79CF–79D2 / 02:4FC8, then 00:2125–211D | operand fails the operation’s sign or integer check | _ErrDomain at 00:26F4 |
Disp sqrt(-1) | 00:1B8F–1B93 | a complex result reaches the real-mode guard | _ErrNon_Real at 00:26FC |
Disp [[1,2][2,4]]⁻¹ | 02:439C–43A5 | the pivot helper rejects the rank-deficient matrix | _ErrSingularMat at 00:26F0 |
For(I,1,3,0) / For(I,1E99,1E99) | 37:4268–426B / 38:586D–5876 | the step is zero / adding it makes no progress | _ErrIncrement at 00:26F8 |
The two OVERFLOW rows reach the same shim through different predicates. A trace
that records only 00:26E8 therefore merges distinct numeric behavior. The
compact numeric-error report retains the ordered guard path, the register state
after each guard instruction, and the final error code in A. [confirmed]
A whole-ROM direct-reference scan finds 114 candidate CALL or JP operands
to the six shims. The natural corpus reaches 11 distinct direct callers:
| Error entry | Direct-reference candidates | Witnessed callers |
|---|---|---|
_ErrOverflow at 00:26E8 | 9 | 2 |
_ErrDivBy0 at 00:26EC | 2 | 1 |
_ErrSingularMat at 00:26F0 | 3 | 1 |
_ErrDomain at 00:26F4 | 91 | 4 |
_ErrIncrement at 00:26F8 | 6 | 2 |
_ErrNon_Real at 00:26FC | 3 | 1 |
These 114 sites are linear-disassembly candidates, not 114 established predicates. The scan can decode data as instructions. It also omits indirect transfers and helpers that load an error code before entering the common path. The report preserves that distinction. [confirmed]
flowchart LR
S["TI-BASIC expression"] --> E["operator evaluator"]
E --> G["numeric guard<br/>zero, range, or exponent"]
G --> R["shared error shim<br/>00:26E8–2708"]
R --> C["00:270A<br/>common error path"]
C --> U["restore OPS, FPS,<br/>error SP, and page"]
The shared context wrapper at 00:27DA does not save absolute FPS and OPS
pointers. It saves each pointer as a delta from the corresponding base at
0x9822 or 0x9826, together with the previous error stack and mapped page.
The unwind path at 00:27BB–27D9 restores them in reverse order. [confirmed]
save_error_context(target):
push current_flash_page
push previous_error_stack
push FPS - word_at(0x9822)
push OPS - word_at(0x9826)
error_stack = SP
jump target
unwind_error(error_code):
SP = error_stack
OPS = word_at(0x9826) + pop_word()
FPS = word_at(0x9822) + pop_word()
error_stack = pop_word()
restore_flash_page(pop_word())
return error_code
flowchart LR
E["error entry<br/>82h or 88h"] --> C["00:270A<br/>common error path"]
C --> U["00:27BB<br/>load saved error SP"]
U --> O["restore OPS delta"]
O --> F["restore FPS delta"]
F --> P["restore previous error SP and page"]
P --> H["error UI / caller continuation"]
The traces confirm the entry, common unwind, and pointer restoration. The
error-screen Goto editor and every nested-error caller remain outside the
current interpreter model.
What the coverage model establishes
tools/ti84re/tibasic/analyze_coverage.py ties eight finite models to byte signatures
in the pinned ROM. It exhausts 591,360 states across token width, delimiters,
one scan step, one block-depth transition, the extended-class fold, precedence
family selection, command finalization, and page-33 table bounds. Z3 proves a
minimum representative set for the semantic outcomes. [confirmed]
Dynamic evidence is a separate layer. Natural programs reach 38 of 52 outcomes at 26 selected branch sites. Public-bcall and internal-entry probes bring the declared outcome set to 52 of 52 while preserving provenance. The report records only trace hashes and compact counts; raw traces remain outside the repository. See TI-BASIC dynamic tracing for commands and exact boundaries. [confirmed]
The broader saturation audit starts at all 81 grammar-handler destinations and selected command, control-flow, value-storage, and numeric-error entries. It reaches 8,490 ROM instructions and 1,351 conditional branches, or 2,702 possible branch outcomes. The retained traces observe 924 outcomes; natural TI-BASIC programs account for 898. [confirmed]
This is deliberately not a claim of complete interpreter coverage. The four
declared computed jumps are expanded over their ROM-defined valid domains, but
corrupted or otherwise out-of-domain dispatch state is not modeled. Calls into
display, graphing, and other ROM pages leave the declared regions, and arbitrary
token streams, recursion depths, VAT layouts, and floating-point values remain
open. The compact tools/oracles/tibasic/tibasic-saturation.json report records those
boundaries explicitly.
Address map
| Address | Role |
|---|---|
00:1FE8 | _IsA2ByteTok |
38:4000 | Grammar-handler pointer table |
38:4130 | Matching End/Else scanner |
38:4180 | Token-aware skip scanner |
38:41E5 | Natural For( production entry |
38:4200 | Natural End record consumer |
38:4870 | Goto/Lbl name scanner |
38:5987 | _ParseInp |
38:5AB3 | Recursive expression evaluator |
38:6251 | _StoAns |
38:6910 | Stored-program statement-body entry |
38:6FB7 | Grammar-class validation and high-token fold |
38:7010 | Production-family selector |
38:7248 | Cursor advance/refill |
38:72DA | Current-token fetch and delimiter classification |
38:758A | _Find_Parse_Formula |
38:7600 | Store/label name scanning region |
38:778F | Nested stored-program body evaluator |
02:5676 | Command finalization gate |
33:435F | Bounded control-flow command dispatcher |
The local finite-model evidence is tools/oracles/tibasic/tibasic-coverage.json. The broader
direct-CFG evidence is tools/oracles/tibasic/tibasic-saturation.json. The selected backward
error slices are in tools/oracles/tibasic/tibasic-numeric-errors.json. All three generators
verify the pinned ROM before producing a report.
TI-BASIC programming patterns
TI-BASIC performance depends on parser work, floating-point transfers, VAT lookups, and display calls. This page collects the source-level decisions. The full programs, traces, and BASIC/ASM fixtures are in TI-BASIC examples and ASM interop.
Choose work with fewer interpreter crossings
The statement loop at 38:59C5 dispatches every source statement. Structured
loops also maintain FPS and OPS records, and each variable access crosses the
VAT/value boundary. [confirmed]
| Source-level choice | Interpreter cost | Example |
|---|---|---|
| Keep loop bodies short | Fewer statement dispatches and parser scans per iteration | Text animation |
| Prefer list or matrix primitives | One parsed command can run an internal ROM loop | Trace-backed list fixture |
| Cache repeated list elements in scalars | Avoid repeated VAT lookup and list-element address calculation | DFS list stack |
| Keep graph drawing in the graph buffer | Avoid repeated home-screen formatting and LCD updates | Graph-buffer visualization |
Use structured loops instead of hot Goto paths | Avoid repeated label rescans through 38:7600 | Loop behavior |
Include the optional For( closing parenthesis | Avoid the documented implicit-close parser trap | For( parenthesis trap |
These are interpreter-cost rules, not cycle counts. Exact timing depends on the program, data, display mode, and calculator speed.
Preserve the BASIC caller for callbacks
TI-BASIC subprograms share variables and Ans, but preserve parser control in
a private frame. Use a small input/output convention and let BASIC perform the
prgmNAME call. [confirmed]
| Boundary | Supported pattern |
|---|---|
| BASIC → BASIC | Store inputs, call prgmNAME, then read shared variables or Ans. |
| BASIC → ASM | Call Asm(prgmNAME) and require the payload to return normally. |
| ASM → BASIC callback | Store a result or signal in Ans, return to BASIC, and let the wrapper call prgmNAME. |
The compiled launcher, negative public-bcall probes, and private-frame comparison are documented under BASIC and ASM interop.
TI-BASIC examples and ASM interop
These trace-backed examples connect source-level choices to the interpreter paths documented in TI-BASIC execution. The page also records the supported BASIC/ASM boundary and the tested failures around direct ASM-initiated BASIC execution.
Trace-backed examples
| Pattern | Trace evidence | Practical rule |
|---|---|---|
Straight-line display (HELLO) | page-38 statement parse plus _Disp | Fine for status text; avoid using Disp as a frame loop. |
Prompted arithmetic (FACTOR) | loop-body reseed, FP multiply, display | Keep loop bodies short; store loop-invariant values before For(. |
List built-ins (DATA) | sum( reaches list_fold_dispatch | Prefer built-ins when one parser setup can cover many elements. |
Text animation (ANIMTXT) | Output( plus LCD text paths on every loop | Precompute positions/strings and update the smallest region possible. |
Graph drawing (GRAPHV) | primitives draw into plotSScreen, then _PDspGrph | Batch graph primitives before DispGraph. |
Graph visualization (GRAPHDFS, GRAPHLST) | window stores plus repeated Line(/Circle(/Text( reach _StoSysTok, _ILine, _IPoint, graph_pixel_op, _PDspGrph, and small-font paths; GRAPHLST also reaches list indexing in draw arguments | Store graph topology in lists; draw the whole view in one graph-buffer pass. |
BASIC subprogram (CALLSUB, CALLABI) | page-38 program-body evaluator and shared VAT variables | Treat globals/lists/Ans as the calling convention. |
List algorithms (BIGADD, BIGMUL, DFS) | VAT lookup, element address, OP-register move per access | Preallocate lists; cache dimensions and reused elements in scalars. |
The table is intentionally selective. The complete fixture and evidence list lives on the dynamic tracing page, where it can be audited without interrupting the programming guidance.
Patterns tied to interpreter cost
Text animation with Output(
ClrHome
For(I,1,8)
Output(1,I,"X")
End
Disp "DONE"
Observed run: ANIMTXT.8xp leaves DONEXXXX on the first row, then Done. The
trace hits page-38 parser paths, page-33 loop/math helpers, _OutputExpr
(03:4AF2), _Disp (37:51D3), and LCD text routines. [confirmed]
The performance lesson is that animation is expensive twice: the interpreter
parses each Output( call, then the display stack updates text/LCD state. For a
real animation, keep loop bodies tiny and avoid recomputing strings or indexes
inside the drawing loop.
Graph-buffer visualization
ClrDraw
0->Xmin
94->Xmax
0->Ymin
62->Ymax
Line(0,0,94,62)
Line(0,31,94,31)
Line(47,0,47,62)
Circle(47,31,10)
Text(0,0,"DFS")
DispGraph
Observed run: GRAPHV.8xp ends on the graph screen with DFS, axes, a circle,
and the diagonal line visible. The trace hits _GrBufClr, _StoSysTok,
_ILine (04:4029), graph_pixel_op, _IPoint, _PDspGrph (04:7904), and
the page-38 argument parser. [confirmed]
The performance lesson is to draw several primitives into the graph buffer, then
display the graph buffer once. Repeated home-screen Output( calls give you
more text-layout overhead and less control over redraw timing.
Text animation and graph-buffer animation have different costs. Output( keeps
the home/text display model active and pays row/column formatting on every
iteration. Graph-buffer animation pays coordinate conversion, pixel primitive
work, and a display-buffer copy at DispGraph. For visible motion, batch one
frame in plotSScreen, call DispGraph, then compute the next frame; avoid
alternating graph primitives with home-screen output inside the same hot loop.
Graph visualization of DFS topology
GRAPHDFS.8xp draws the same four-node graph traversed by DFS.8xp:
ClrDraw
0->Xmin
94->Xmax
0->Ymin
62->Ymax
Line(10,44,35,54)
Line(10,44,35,14)
Line(35,54,55,29)
Circle(10,44,3)
Circle(35,54,3)
Circle(35,14,3)
Circle(55,29,3)
Text(16,8,"1")
Text(6,33,"2")
Text(46,33,"3")
Text(31,53,"4")
DispGraph
The graph data from DFS.8xp maps to graph pixels through fixed coordinate
lists:
| Node | DFS value | Pixel center | Label position |
|---|---|---|---|
| 1 | root | (10,44) | Text(16,8,"1") |
| 2 | first edge target | (35,54) | Text(6,33,"2") |
| 3 | second edge target | (35,14) | Text(46,33,"3") |
| 4 | child of 2 | (55,29) | Text(31,53,"4") |
The edge lists L1={1,1,2} and L2={2,3,4} become the three line segments
1-2, 1-3, and 2-4. The fixture stores window variables first so these
pixel-like coordinates cover the visible graph area.
Observed run: the final graph screen shows four labeled nodes with edges
1-2, 1-3, and 2-4. The trace hits _ILine (04:4029),
graph_pixel_op, _IPoint, _PDspGrph (04:7904), small-font glyph
rendering, window variable stores through _StoSysTok, _RestoreDisp, and
page-38 statement evaluation. [confirmed]
The performance lesson is to separate graph data from graph drawing. Keep edge lists and traversal state in lists, but convert them to pixels in a single draw phase instead of interleaving traversal, display, and recalculation.
GRAPHLST.8xp makes that separation explicit. It stores edge endpoint
coordinates in L1–L4 and node centers in L5/L6, then draws edges and
nodes with loops:
{10,10,35}->L1
{44,44,54}->L2
{35,35,55}->L3
{54,14,29}->L4
{10,35,35,55}->L5
{44,54,14,29}->L6
For(I,1,3)
Line(L1(I),L2(I),L3(I),L4(I))
End
For(I,1,4)
Circle(L5(I),L6(I),3)
End
Observed run: GRAPHLST.8xp renders the same four-node topology as
GRAPHDFS.8xp; the smoke runner checks the same node and edge crop regions.
The trace additionally hits list_var_index and _GetLToOP1, proving that the
draw arguments came through list element recall rather than hard-coded
coordinates. [confirmed]
Subprogram interfaces
Caller:
0->A
prgmSUBRT
Disp A
Callee:
Disp "SUB"
A+1->A
Return
Observed run: loading CALLSUB.8xp and SUBRT.8xp displays SUB, then 1,
then Done. This confirms the practical TI-BASIC calling convention for
scalars: arguments and return values live in shared global variables; Return
exits the callee and resumes the caller. The trace hits the
page-38 statement interpreter, VAT/name resolution (findsym_scan), parser
entry/refill paths, the program-body evaluator call at 38:6914 into
eval_eqn_recursive (38:778F), _StoSysTok, _StoAns, _RclVarSym, and
_Disp. [confirmed]
The full smoke trace also hits _ParseInpLastEnt/_ParseInp once while the
homescreen evaluates the initial prgmCALLSUB command selected by the macro.
That launch parse is not the same as the callee transition. The repeated
subprogram body path is the private 38:6910 → 38:6914 → 38:778F
sequence, reached after TIBasicParserState (0x9652) and the adjacent stack
pointers have been populated:
| RAM state | Address | Role in the private parser frame |
|---|---|---|
TIBasicParserState.basic_prog | 0x9652 | current OP1-style program/object name |
TIBasicParserState.basic_start | 0x965B | first token byte after the stored program size word |
TIBasicParserState.next_parse_byte | 0x965D | current parser cursor |
TIBasicParserState.basic_end | 0x965F | parser end pointer |
TIBasicParserState.num_arguments | 0x9661 | argument count/state byte used by parser helpers |
chkDelPtr3 / chkDelPtr4 | 0x981C / 0x981E | temporary VAT/data pointers used during name and object setup |
FPS / OPS / pTemp / progPtr | 0x9824 / 0x9828 / 0x982E / 0x9830 | live FP/temp/program storage bounds |
There is no local variable frame for BASIC programs. A subprogram that uses A
modifies the caller’s A. For reusable routines, document which variables are
inputs, scratch, and outputs.
| ABI part | Practical convention | Trace evidence |
|---|---|---|
| Inputs | Scalars, lists, and Ans are shared across caller and callee. The caller stores them before prgmNAME. | CALLSUB stores A; ABICALL seeds L1 and Ans. |
| Outputs | The callee stores results back to globals, list elements, or Ans. | SUBRT increments shared A; ABISUB writes A, L1(3), and Ans. |
| Scratch | No automatic save/restore exists. Routines must document scratch variables. | The VAT and parser state are shared across caller and callee. |
| Return/Stop | Return exits the callee and resumes the caller. Stop terminates the whole program chain. | SUBRT returns to CALLSUB, which then runs Disp A; STOPSUB stops CALLSTOP before caller text AFTER can display. |
| Parser state | prgmNAME runs with private parser/FPS state already set up by BASIC. | The callee path reaches 38:6910 → 38:6914 → 38:778F. |
ABICALL.8xp broadens that scalar-only case:
{2,4,6}->L1
7
prgmABISUB
Disp A
Disp L1
Disp Ans
with callee:
Ans+L1(2)->A
9->L1(3)
A
Return
Observed run: ABICALL.8xp and ABISUB.8xp display 11, {2 4 9}, 11,
then Done. The callee reads the caller’s Ans=7 and L1(2)=4, stores 11
in shared scalar A, mutates shared L1(3) to 9, evaluates A as the final
callee expression so Ans is also 11, and returns. The smoke runner checks
the rendered scalar, list, Ans, and Done regions, and the trace hits
stmt_eval_body_entry, call_eval_eqn_recursive, eval_eqn_recursive,
_AnsName, and store_list_elem. [confirmed]
CALLSTOP.8xp and STOPSUB.8xp cover the non-returning branch:
Disp "BEFORE"
prgmSTOPSUB
Disp "AFTER"
with callee:
Disp "STOP"
Stop
Observed run: CALLSTOP.8xp and STOPSUB.8xp display BEFORE, then STOP,
then Done; AFTER never appears. The smoke runner checks the BEFORE,
STOP, and Done regions and also checks a low-pixel region where AFTER
would be drawn if the caller resumed. The trace reaches stmt_eval_body_entry,
call_eval_eqn_recursive, and _Disp. This confirms that Stop in a callee
terminates the whole BASIC program chain instead of returning to the caller. [confirmed]
Arbitrary-precision decimal addition
BIGADD.8xp uses lists of base-10 digits in little-endian order. 12345 is
{5,4,3,2,1}, 98765 is {5,6,7,8,9}, and the result is the list
{0,1,1,1,1,1} for 111110.
{5,4,3,2,1}->L1
{5,6,7,8,9}->L2
{0,0,0,0,0,0}->L3
0->C
For(I,1,5)
L1(I)+L2(I)+C->S
int(S/10)->C
S-10C->L3(I)
End
C->L3(6)
Disp L3
Disp L3(6)
Observed run: the list line begins {0 1 1 1 1 ...}, the explicit carry line is
1, and the program ends with Done. The trace hits list element address and
store paths (list_var_index, _AdrLEle, _GetLToOP1, _PutToL,
store_list_elem*) plus fnint_body, _FPDiv, _FPAdd, _FPSub, and
_FPMult. [confirmed]
Performance notes: this is intentionally simple, but it is parser-heavy. For a
general routine, cache dim(L1) and dim(L2) before the loop, avoid repeated
list indexing when a digit is reused, and use a larger base only if you can
tolerate more carry and display conversion work.
For a reusable arbitrary-precision add routine, treat L1 and L2 as
little-endian digit arrays and compute the loop bound from list lengths:
dim(L1)->N
If dim(L2)>N
dim(L2)->N
0->C
For(I,1,N)
0->A
0->B
If I<=dim(L1)
L1(I)->A
If I<=dim(L2)
L2(I)->B
A+B+C->S
int(S/10)->C
S-10C->L3(I)
End
If C
C->L3(N+1)
The invariant after iteration I is that L3(1..I) contains the low I
digits of L1+L2, and C is the carry into digit I+1. Base 10 is easy to
display and debug. A larger base reduces loop count but adds conversion and
larger carry values; on TI-BASIC, that tradeoff only helps when display is not
part of the hot path.
Arbitrary-precision decimal multiplication
BIGMUL.8xp uses the same little-endian digit convention for schoolbook
multiplication. The example multiplies 123 ({3,2,1}) by 45 ({5,4}), so
the expected result is 5535, represented as {5,3,5,5,0}.
{3,2,1}->L1
{5,4}->L2
{0,0,0,0,0}->L3
For(I,1,3)
For(J,1,2)
L3(I+J-1)+L1(I)*L2(J)->S
int(S/10)->C
S-10C->L3(I+J-1)
L3(I+J)+C->L3(I+J)
End
End
Disp L3
Disp L3(4)
Observed run: BIGMUL.8xp displays {5 3 5 5 0}, then 5, then Done.
The trace hits nested For( loop parsing, list element reads/stores, _FPMult,
_FPAdd, _FPSub, _GetLToOP1, and _PutToL. [confirmed]
The invariant is that each inner-loop step normalizes one result cell
L3(I+J-1) and carries into the next cell. This is still base-10 arithmetic,
so it favors trace readability over speed. A larger base reduces the number of
digits but makes the carry path and display conversion heavier.
DFS with a list stack
DFS.8xp uses two edge lists (L1 source, L2 destination), a visited list
(L3), and an explicit stack (L4) to traverse this graph:
1 -> 2
1 -> 3
2 -> 4
{1,1,2}->L1
{2,3,4}->L2
{0,0,0,0}->L3
{1,0,0,0}->L4
1->P
While P
L4(P)->V
P-1->P
If L3(V)=0
Then
1->L3(V)
Disp V
For(E,1,3)
If L1(E)=V
Then
P+1->P
L2(E)->L4(P)
End
End
End
End
Disp L3
Observed run: traversal order is 1, 3, 2, 4 because the stack is LIFO and
node 3 is pushed after node 2. The final visited list is {1 1 1 1}. The
trace hits blockmatch_end_else, parse_scan_tokens, eval_stmt_entry,
parser refill/advance paths, _Disp, and the same list read/write helpers used
by BIGADD. [confirmed]
Performance notes: this version scans all edges for every visited node, so it is
easy to understand but O(VE) in BASIC-level work. For larger graphs, keep an
offset table of edge ranges per node, avoid augment( in hot loops, and
preallocate stack/visited lists with scalar pointers as this sample does.
The loop maintains three invariants:
L3(V)=1means nodeVhas already been displayed and expanded.L4(1..P)is the pending stack, withL4(P)popped next.- Edges are scanned from left to right, so pushing node
2and then node3makes node3display before node2.
The trace cost follows those invariants. Every While and nested If Then
forces the interpreter to scan for block boundaries (blockmatch_end_else,
parse_scan_tokens), and every L1(E)/L2(E) access goes through VAT lookup
and list-element address calculation. Precomputed adjacency ranges reduce both
the number of edge scans and the number of interpreted branch scans.
BASIC and ASM interop
BASIC to ASM
The validated smoke test is:
Asm(prgmASMRET)
with:
AsmPrgm
C9
Asm( is token BB 6A; AsmPrgm is BB 6C; prgm is token 5F. The
Asm( command handler parses the following prgmNAME token stream, then
bcalls _ExecutePrgm (4E7C, target 07:5758). The trace shows that path
compile or copy the AsmPrgm body and hand off through 07:57B4, execute the
payload byte at ram:9D95 with opcode C9h, and return to BASIC. [confirmed]
tools/asm_execution.py byte-pins the complete setup and cleanup path:
[confirmed]
| Address | Operation |
|---|---|
07:5758 | Query application restriction selector 3; reject a disallowed caller. |
07:5762 | Resolve the program named by OP1; reject an archived data page. |
07:5766 | Distinguish compiled marker BB 6D from hexadecimal AsmPrgm source. |
07:577B | Reject a machine image larger than 0x2000 bytes. |
07:5785 | Insert an exact-sized gap at ram:9D95 and copy compiled bytes. |
07:57D4 | For source form, call _GetAsmSize, allocate the result, and call _SquishPrgm. |
07:5791 | Store the allocation length in asm_prgm_size (ram:89EC). |
07:57B1 | Install the error cleanup at 07:5800. |
07:57B4 | Call the JP ram:9D95 trampoline at 07:57FD. |
07:57C4 | Clear the length and delete the allocation after a normal return. |
07:5800 | Restore speed state, delete the allocation, and resume error handling. |
BB 6C is the hexadecimal source token. It takes the source/squish path at
07:57D4; BB 6D identifies an already compiled body. The source pointer is
adjusted after _InsertMem because the source object moves when the gap opens.
Normal and error exits therefore delete the recorded allocation length rather
than a fixed-size region. [confirmed]
Practical convention: pass data through OS variables or known RAM locations,
validate inputs on the BASIC side, and make the ASM payload return normally with
RET unless it intentionally transfers control elsewhere.
Cooperative ASM-directed BASIC callback
The run-confirmed way to let ASM choose a BASIC continuation is to keep BASIC in
charge of the program call. ASMSIG.8xp sets Ans to 1 and returns:
RST 28h
.dw 419Bh ; _OP1Set1
RST 28h
.dw 4ABFh ; _StoAns
RET
The BASIC wrapper then branches on Ans and performs the ordinary prgmNAME
call:
Disp "BEFORE"
Asm(prgmASMSIG)
If Ans
prgmZZBASIC
Disp "AFTER"
with target:
Disp "CALLED"
Observed run: ASMBRIDG.8xp, ASMSIG.8xp, and ZZBASIC.8xp display
BEFORE, CALLED, AFTER, then Done. The trace hits the AsmPrgm payload
at userMem, _OP1Set1 (00:1B38), _StoAns (38:6251), _AnsName
(38:74B7) while evaluating If Ans, and then the normal BASIC program-body
path for prgmZZBASIC (38:6910 → 38:6914 → 38:778F). [confirmed]
This is a callback convention, not a direct jump from ASM into a BASIC body.
The ASM side communicates a return code through Ans; BASIC owns the parser
state, performs the prgm call, and resumes after the target returns.
For a numeric return value without a BASIC callback, ASMVAL.8xp stores 2 in
Ans:
RST 28h
.dw 41A7h ; _OP1Set2
RST 28h
.dw 4ABFh ; _StoAns
RET
The wrapper consumes it as an ordinary BASIC value:
Asm(prgmASMVAL)
Ans+3->A
Disp A
Observed run: ASMRTN.8xp and ASMVAL.8xp display 5, then Done. The trace
hits userMem, _OP1Set2 (00:1B50), _StoAns (38:6251), _AnsName,
_FPAdd, and _Disp; the smoke runner also checks the final-frame result and
Done regions. [confirmed]
| Direction | Confirmed mechanism | Caveat |
|---|---|---|
| BASIC → ASM | Asm(prgmNAME) parses prgmNAME, bcalls _ExecutePrgm, copies the AsmPrgm payload, then jumps through userMem. | The payload runs in the calculator OS process; a bad payload can corrupt interpreter state. |
| BASIC → BASIC | prgmNAME enters the page-38 parser/VAT/body evaluator path and Return resumes the caller. | There is no local frame; variables, lists, and Ans are shared. |
| ASM → BASIC callback | ASM stores a signal/result such as Ans=1, returns, and the BASIC wrapper conditionally runs prgmNAME. | BASIC must own the actual prgm call; this is cooperative, not an arbitrary ASM bcall into BASIC. |
| ASM → BASIC value return | ASM stores a numeric result in Ans with _StoAns; BASIC resumes and evaluates Ans. | This returns data to BASIC, not control into a BASIC program body. |
| ASM → VAT lookup | ASMFIND builds OP1={ProgObj,"ZZBASIC"} and bcalls _ChkFindSym. | Lookup is not execution; the wrapper returns and ZZBASIC does not display CALLED. |
| Direct ASM → BASIC | No working public bcall sequence is proven in this repo. | ASMPARSE reaches _ParseInpLastEnt/_ParseInp and then ERR:INVALID; ASMFORM reaches _Find_Parse_Formula and then ERR:UNDEFINED; ZZRUN reaches the private evaluator and then ERR:SYNTAX; forced-command/edit-buffer probes did not call the target BASIC program successfully. |
ASM to BASIC
Direct ASM-initiated BASIC program execution is not yet run-confirmed in this repository. Two apparent candidates are not that entry point:
_ExecutePrgmis theAsmPrgmexecutor reached byAsm(prgmNAME), not a general “run a BASIC program” entry._ExecuteNewPrgm(4C3C, target00:265F) is not a drop-in BASIC runner from an arbitraryAsmPrgmeither. It expects OS state beyond a name pointer._ParsePrgmName(4E82, target38:40D4) only consumes aprgmNAMEtoken from the current parser cursor and builds the name object used byAsm(.
The confirmed BASIC subprogram path is different: the CALLSUB/SUBRT trace
does not hit _ParsePrgmName, _ExecutePrgm, _Find_Parse_Formula, or
_SetParseVarProg. It resolves the program name through the page-38
parser/VAT path, enters the program-body evaluator at 38:6914 →
38:778F, and lets Return unwind to the caller. Calling that same machinery
from arbitrary ASM requires more than loading OP1 and bcalling a single public
entry; it needs the same parser cursor, stack, error, and run-state setup that a
live BASIC caller already has. [hypothesis]
A typed two-program trace of prgmPP calling prgmOO captures the live parser
frame at each 38:6914 entry through shadow-memory replay. The reproduction
macro is tools/macros/run-callsub-typed.macro.
| Field | Observed value at callee entry |
|---|---|
basic_prog (0x9652) | 05h, two encoded name bytes, six zeros |
basic_start (0x965B) | callee body start (first token) |
next_parse_byte (0x965D) | equals start before execution; end when finished |
basic_end (0x965F) | start + body size |
state byte (0x9661) | 01h |
FPS / OPS pointers | valid live pointers (0x9E94 / 0xFCB1 on one entry) |
| gate bits | BIT 0,(IY+28h) and BIT 7,(IY+48h) are both zero (IY=0x89F0; bytes at 0x8A18 and 0x8A38) |
Both gate bits read zero in the working path. The private callee transition at
38:6910 executes XOR ACALL 6A15 before entering the evaluator. These
observations suggest that an ASM payload must locate the target through
_ChkFindSym into OP1, copy the name header to 0x9652, point
start/cursor at _ChkFindSym’s data pointer plus two (past the size word),
set end = start + size, store 01h at 0x9661, ensure the FPS/OPS bounds
are sane, bank page 38 into port 0x06, and enter 38:6910. No identified
public bcall performs this setup. This proposed hand-built state transplant
also accounts for the parser-frame failures in the negative probes
(ZZFIND/ZZFORM/ZZPARSE). [hypothesis]
The generated negative probe consists of OO.8xp, ZZRUN.8xp, and
ZZRUNWR.8xp. ZZRUN is an 81-byte payload targeting prgmOO; it returns
immediately if _ChkFindSym sets carry. ZZRUNWR contains the one-line
Asm(prgmZZRUN) launcher.
A link-loaded run resolves OO through _ChkFindSym with DE=0x9E76, then
sets the parser interval to 0x9E78–0x9E7F and enters 38:6910. The trace
reaches 38:6914 and 38:778F, walks the target body, and terminates at
_ErrSyntax (ram:2700) with the parser cursor at 0x9E7C. The final frame
shows ERR:SYNTAX. An otherwise equivalent 80-byte layout without the
_ChkFindSym carry guard instead ended at _ErrArgument (ram:2711). The
layout-sensitive error indicates that the copied name and cursor interval do not
reproduce the native BASIC call frame. [confirmed]
ASMFIND.8xp and ZZFIND.8xp make the VAT lookup boundary reproducible. The
wrapper displays BEFORE, runs Asm(prgmZZFIND), and displays AFTER. The
payload builds OP1={ProgObj,"ZZBASIC"} and bcalls _ChkFindSym (42F1):
LD HL,name
LD DE,8478h ; OP1
LD BC,0009h
LDIR
RST 28h
.dw 42F1h ; _ChkFindSym
RET
name: .db 05h,"ZZBASIC",00h
Observed run: ASMFIND.8xp, ZZFIND.8xp, and ZZBASIC.8xp display BEFORE,
AFTER, and Done; ZZBASIC’s CALLED text does not display. The trace hits
userMem and findsym_scan, and the smoke runner checks the wrapper output
and a low-pixel region where an unexpected third line would appear. This proves
ASM-side VAT lookup from an AsmPrgm context, not BASIC program execution. [confirmed]
Generated negative fixtures make the execution boundary sharper.
ASMFORM.8xp and ZZFORM.8xp make the _Find_Parse_Formula negative probe
reproducible. The payload is the same OP1-name setup as ZZFIND, but it bcalls
_Find_Parse_Formula (4AF2, target 38:758A) instead of _ChkFindSym.
Observed run: the trace reaches userMem, _Find_Parse_Formula,
parse_init_findsym, findsym_scan, and eval_stmt_entry; the final screen is
ERR:UNDEFINED with 1:Quit and 2:Goto. ZZBASIC never displays CALLED.
That failed run confirms _Find_Parse_Formula is not a drop-in BASIC program
executor from an arbitrary AsmPrgm context. [confirmed]
ASMPARSE.8xp and ZZPARSE.8xp make the _ParseInpLastEnt negative probe
reproducible. The payload is the same OP1-name setup as ZZFIND, but it bcalls
_ParseInpLastEnt (4B07, target 38:5984) instead of _ChkFindSym.
Observed run: the trace reaches _ParseInpLastEnt, _ParseInp (38:5987),
parseinp_find_setup (38:5B2B), findsym_scan, parse_init, and
eval_stmt_entry; the final screen is ERR:INVALID with 1:Quit and
2:Goto. ZZBASIC never displays CALLED. Static disassembly explains the
mismatch: after resolving the OP1-named object, _ParseInp continues through
parser setup that expects a live parser/FPS call-frame shape. It is not a
general “run this token stream” ABI for an arbitrary AsmPrgm. [confirmed]
The homescreen command/edit-buffer route is also not a safe callable ABI. A payload that did only:
LD A,05h ; kEnter
RST 28h
.dw 402Ah ; _JForceCmd
RET
entered _JForceCmd (00:0747) but never returned to the BASIC wrapper’s
Disp "AFTER" statement. The final screen showed repeated BEFORE/Done
lines, and the trace hit ram:0747 and userMem repeatedly. The disassembly
explains why: _JForceCmd reloads SP from 85BC before dispatching the
forced key, discarding the AsmPrgm caller’s stack. [confirmed]
Two edit-buffer variants narrow that path further. A payload that bcalls
_PutTokString (4960, target 06:46FD) for the token bytes
5F 5A 5A 42 41 53 49 43 (prgmZZBASIC) returns to the wrapper and reaches
Disp "AFTER", but it only renders/inserts token text; ZZBASIC does
not run. Combining those _PutTokString calls with _JForceCmd(kEnter) hits
both _PutTokString and _JForceCmd, then repeats the wrapper/inserted text
through the command loop; it still never displays CALLED from ZZBASIC.
_rclToQueue (49B4, target 06:5F29) is a related editor queue helper, but
its ROM path depends on an already-open edit buffer (editCursor/editTail)
and the rclFlag.enableQueue state; it does not create a BASIC program call
frame. [confirmed]
_ExecuteNewPrgm (00:265F) is not a public ASM-to-BASIC entry — a payload
that sets OP1 to ProgObj (05), points HL at the zero-terminated name
ZZBASIC, and bcalls 4C3C enters it and findsym_scan, then ends at
ERR:SYNTAX [confirmed]; ZZBASIC never displays CALLED. Repeating the test with
ZZBASIC loaded as ProtProgObj (06) and OP1=06 gets farther: the trace
hits _ExecuteNewPrgm, the copy tail at 00:268A, and the jump at 00:268F.
It still ends at ERR:SYNTAX and never runs the target body. That makes
_ExecuteNewPrgm another stateful OS helper, not a standalone program executor
ABI for AsmPrgm payloads. [confirmed]
The bounded public-candidate search covers _ExecutePrgm, _ExecuteNewPrgm,
_ParsePrgmName, _ParseInpLastEnt, _Find_Parse_Formula, _JForceCmd,
_PutTokString, and _rclToQueue. None is a standalone direct-call ABI.
[confirmed] This is a scoped negative result, not proof that no private state
construction can execute a token stream.
Private-frame comparison
The T042 probe compares an incomplete direct call with the ordinary caller in
one reset-origin TilEm run. ZZFRAME saves four state groups before the
experiment: [confirmed]
| Saved group | Range | Contents |
|---|---|---|
| Parser and name | 0x9652–0x9662 | basic_prog, parser pointers, and numArguments |
| VAT and temporary stacks | 0x981C–0x9831 | VAT scratch, FPS, OPS, pTemp, and progPtr |
| Parser flags | 0x89F0–0x8A39 | IY flags read by the page-38 parser |
| Error state | 0x86DD | prior errNo value |
The payload installs a caught-error frame through _pushErrorHandleR at
ram:27DA, substitutes a token stream containing prgmZZGOOD, and calls
eval_stmt_entry at 38:59C5. It reaches the statement entry once, then
raises E_DataType before 38:6910, 38:6914, or 38:778F. _JError
transfers through ram:27BB; the payload restores every saved group before it
stores error code 9 in Ans. The combined frame SHA-256 matches before and
after the failed call. [confirmed]
The BASIC wrapper then calls prgmZZGOOD normally. That path reaches
38:59C5 twice and 38:6910, 38:6914, and 38:778F once each, returns
value 2, and resumes at AFTER. The final screen reads BEFORE, 9, 2,
AFTER, and Done. [confirmed]
The comparison pins the missing boundary for this construction. Parser
pointers and copied RAM blocks do not create the OPS grammar/type record, FPS
baselines, VAT adoption, and return record consumed before 38:6914. The
ordinary prgmNAME handler creates them. Reproducing all of that state would
duplicate the BASIC caller rather than expose a separate supported ABI.
[confirmed]
tools/probes/scratch-guard/asm-basic-frames-tilem.json records the pinned TilEm run,
screen contract, ROM spans, SPASM-ng output, and source/program hashes. The
supported application pattern remains Asm( → _ExecutePrgm → ram:9D95,
return a value through Ans, and let BASIC perform prgmNAME.
TI-BASIC dynamic tracing
TI-BASIC coverage combines exhaustive local models, natural calculator traces, and provenance-labeled probes. The models classify bounded decisions. Natural traces establish program reachability. Probes distinguish remaining outcomes without presenting prepared state as a natural language path. None of these layers is whole-interpreter coverage.
Evidence layers
| Layer | Establishes | Does not establish |
|---|---|---|
| ROM signature | The modeled instructions are the expected OS 2.55MP bytes | Meaning of every surrounding routine |
| Finite model | Every state in one declared finite domain has an outcome | Arbitrary streams or caller-owned RAM and stack state |
| Natural trace | A stored TI-BASIC program reaches an outcome with ordinary parser state | Feasibility of an unobserved outcome |
| Public-bcall probe | Exact ROM execution reaches a public ABI boundary | Natural TI-BASIC reachability of the supplied register value |
| Internal-entry probe | Exact ROM execution distinguishes a selected internal state | A supported ABI or natural caller for that state |
| RAM or LCD assertion | The fixture produces its expected machine or visible result | Which internal path is uniquely responsible |
tools/ti84re/tibasic/analyze_coverage.py refuses a ROM whose SHA-256 differs from
the pinned OS 2.55MP image, then verifies short byte signatures at every modeled
decision family. [confirmed]
Exhaustive finite models
The checked report exhausts 591,360 states and 45 semantic outcomes:
| Model | Exhausted states | Outcomes | Boundary |
|---|---|---|---|
| Encoded token width | 256 | 2 | Lead-byte membership, not second-byte validity |
| Statement delimiter | 256 | 4 | Byte classification, not refill faults |
| Token scan step | 256 | 4 | One step, not arbitrary stream length |
| Block matcher transition | 524,288 | 10 | Every 16-bit depth over eight decision-equivalent token classes |
| Extended grammar fold | 256 | 2 | CP F2h/ADD 12h, not later handlers |
| Precedence handler family | 65,536 | 3 | Grammar class × selector byte, not recursive handler state |
| Command finalization gate | 256 | 5 | First page-02 gate only |
| Control-flow table bounds | 256 | 15 | Index validation, not the 13 handler bodies |
The block model uses token equivalence classes because the ROM performs the
same comparisons for every non-control byte. It still enumerates all 65,536
values of the 16-bit DE depth, including the zero and increment-wrap boundaries. This
is exhaustive over the stated local transition, not a depth limit of 255.
Z3 minimizes one representative per semantic outcome after exhaustive enumeration establishes the partition. Z3 is not being presented as a proof of the entire Z80 routine or of arbitrary token streams.
Coverage by provenance
The report declares 26 branch sites and both outcomes at each site. Natural programs reach 38 of those 52 outcomes. Public-bcall probes add the four page-33 bounds outcomes. Internal-entry probes add the remaining 10 outcomes. The union reaches 52 of 52, but only the first number describes natural TI-BASIC reachability. [confirmed]
flowchart LR
N["Natural TI-BASIC<br/>38 / 52 outcomes"] --> U["Declared outcome union<br/>52 / 52"]
B["Public bcall probes<br/>4 additional outcomes"] --> U
I["Internal-entry probes<br/>10 additional outcomes"] --> U
F["Eight finite models<br/>591,360 states"] --> R["Compact report"]
U --> R
R --> Z["Exact Z3 set cover<br/>15 outcome traces"]
The per-provenance counts in the JSON are 38, 8, and 18 because wrappers share ordinary grammar outcomes. Those counts overlap. The additional-outcome counts in the diagram describe what each probe layer contributes after the preceding layer. No successor at a declared branch is unclassified. [confirmed]
That 52-outcome matrix is a regression test for eight local models. It is not
the interpreter denominator. The broader CFG audit seeds all valid destinations
from grammar_handler_table (38:4000) and the 13-entry
ctrlflow_handler_table (33:4381),
then follows direct control flow through five bounded components. [confirmed]
Expanded CFG saturation
The expanded graph contains 8,490 reachable instructions and 1,351 conditional branches. Its 2,702 possible outcomes produce this trace breakdown:
| Component | Possible | All evidence | Natural programs |
|---|---|---|---|
| Parser core | 1,956 | 625 | 619 |
| Command arguments | 162 | 54 | 48 |
| Page-33 control flow | 174 | 13 | 0 |
| Value storage | 154 | 112 | 112 |
| Numeric and error checks | 256 | 120 | 119 |
| Total | 2,702 | 924 | 898 |
Natural factorial and dfs traces identify the page-38 loop path:
For( reaches parse_for_production (38:41E5), End reaches
parse_end_ops_record (38:4200), and the loop continuations are
for_first_update (38:5836) and for_steady_update (38:587D). They do not
enter the page-33 probe dispatcher. [confirmed]
flowchart LR
T["81 parser handlers<br/>plus subsystem entries"] --> G["8,490-instruction<br/>direct CFG"]
G --> B["2,702 outcomes"]
N["Natural programs"] --> O["898 observed"]
P["ABI and entry probes"] --> A["924 observed total"]
O --> A
B --> A
B --> U["1,778 unobserved"]
The exact outcome cover retains 30 of 33 traces. hello, callstop, and the
natural syntax-error trace remain useful semantic examples, but they do not add
a branch outcome to the larger graph. The report therefore separates the
minimum outcome corpus from the selective documentation corpus. [confirmed]
Natural programs
Seven successful fixtures cover distinct interpreter behaviors:
| Case | Distinct behavior | Oracle |
|---|---|---|
hello | straight-line statement, quoted string, Disp | LCD text |
factorial | Prompt, scalar stores, For(/End, FP multiplication | LCD result 120 |
data | two-byte list tokens, literal/store, built-in list fold | lists and sum on the LCD |
dfs | nested While, If ... Then, For, and list-backed stack | traversal and visited list |
callabi | nested BASIC call, shared scalar/list/Ans, Return | returned scalar and list state |
callstop | nested BASIC call and nonlocal Stop | absence of the post-call line |
branchmatrix | Else, Repeat, nested blocks, and an omitted string quote | A5h at plotSScreen (0x9340) |
missingend and terminalif add natural end-of-input structural boundaries.
They exercise carry returns that closed blocks do not reach, then finish through
page-38 cleanup and display Done; they do not raise an OS error. The report
marks both traces with termination: completed. [confirmed]
The syntax and divide-by-zero fixtures provide baseline unwind witnesses.
syntaxerr executes Disp 1+ and reaches the syntax entry at 00:2700.
divzero executes Disp 1/0 and reaches _ErrDivBy0 at 00:26EC. The expanded numeric corpus
raises the natural local-matrix result from 34 to 38 outcomes. The 15-trace
outcome minimum omits the error fixtures because other traces cover those local
branches; the semantic corpus retains their distinct causes. [confirmed]
Twelve selected numeric-error fixtures also retain the path before the shared
error shim. The reducer restarts a candidate slice whenever it sees the guard’s
first instruction. It accepts the slice only when the remaining guard and shim
addresses occur in order and the shim leaves the expected error code in A.
This prevents an unrelated earlier call to _FPDiv, _FPMult, or the zero
checker from being attached to a later error. [confirmed]
| Case | Ordered causal boundary | Result |
|---|---|---|
divzero | 00:2548 → 00:254B → 00:26EC | divisor-zero guard, code 82h |
overflow | 02:7076 → 02:7078 → 02:7053 → 02:7056 → 02:7059 → 00:26E8 | 10^x range guard, code 81h |
muloverflow | 00:2513 → 00:2516 → 00:2517 → 00:2519 → 00:251B → 00:251D → 00:26E8 | exponent-add overflow, code 81h |
lndomain | 02:6F1E → 00:212D → 00:1DE9 → 00:2130 → 00:2131 → 00:211D → 00:26F4 | logarithm zero guard, code 84h |
increment | 37:4268 → 00:1DE9 → 37:426B → 00:26F8 | zero loop step, code 85h |
asindomain | 02:76F1 → 02:76F4 → 02:76F5 → 00:26F4 | inverse-sine range guard, code 84h |
acosdomain | 02:76DF → 02:76E2 → 00:26F4 | inverse-cosine range guard, code 84h |
sqrtnonreal | 00:1B8F → 00:1B93 → 00:26FC | real-mode result guard, code 87h |
singular | 02:439C → 02:439F → 02:43A1 → 02:43A2 → 02:43A3 → 02:43A5 → 00:26F0 | matrix-pivot guard, code 83h |
lateincrement | 38:586D → 38:5870 → 38:5873 → 38:5876 → 00:26F8 | loop no-progress guard, code 85h |
negfactdomain | 35:79CF → 35:79D2 → 00:26F4 | factorial sign/integer guard, code 84h |
ncrdomain | 02:4FC8 → 02:4FA1 → 00:2125 → 00:1DFD → 00:1E00 → 00:1E02 → 00:2128 → 00:211C → 00:211D → 00:26F4 | combination left-operand guard, code 84h |
All 12 paths come from stored TI-BASIC programs. They cover all six numeric error codes, 12 causes, and 11 distinct direct caller sites. [confirmed]
The report separately inventories 114 whole-ROM direct-reference candidates:
9 overflow, 2 divide-by-zero, 3 singular-matrix, 91 domain, 6 increment, and 3
non-real. Linear disassembly can decode data as instructions, so each candidate
still needs CFG or dynamic reachability evidence. Indirect transfers and helpers
that load A before entering 00:270A remain outside that inventory.
[confirmed]
Probe outcomes
Three public-bcall probes call grf_435f = 5140h with an input below the table,
inside the table, and at its upper boundary. They cover both outcomes at
33:436D and 33:4372. These are public ABI executions, not stored-program
loop transitions. [confirmed]
Eight internal-entry probes cover four command-finalization classes and four
grammar states. They map the required ROM page, enter the selected routine from
RAM, and let the exact ROM execute the branch. cmdbad combines the safe
implicit-end case with the invalid class, which removes one redundant trace.
These probes establish branch behavior only; they do not establish a natural
caller or a supported interface. [confirmed]
Across all 33 traces, the saturation report records 132,634,495 instructions. The raw files total about 6.08 GiB. Only SHA-256 digests, counts, outcomes, provenance, and the 47 KB report are checked in. Exact outcome-only set cover retains 30 traces. The three omitted traces remain useful semantic examples. [confirmed]
Reproduce the report
Generate the source/token/link fixtures first:
python3 -m ti84re.tibasic.samples --write-dir tools/tibasic-samples
The TilEm binary must support loading command-line .8xp files before the
macro starts. Run the natural cases while retaining their temporary traces:
TILEM=/path/to/patched/tilem2
python3 -m ti84re.tibasic.smoke \
--tilem "$TILEM" --rom tools/rom.bin \
--out-dir /tmp/tibasic-coverage --keep-trace \
--case hello --case factorial --case data \
--case dfs --case callabi --case callstop \
--case branchmatrix --case missingend --case terminalif \
--case syntaxerr --case divzero \
--case overflow --case muloverflow --case lndomain --case increment \
--case asindomain --case acosdomain --case sqrtnonreal --case singular \
--case lateincrement --case negfactdomain --case ncrdomain
Run the probe cases in the same output directory:
python3 -m ti84re.tibasic.smoke \
--tilem "$TILEM" --rom tools/rom.bin \
--out-dir /tmp/tibasic-coverage --keep-trace \
--case cflowlow --case cflowhigh --case cflowvalid \
--case cmdclose --case cmdopen --case cmdunit --case cmdbad \
--case gramlow --case gramhigh --case gramflag --case gramnonzero
The natural branch matrix uses a RAM marker instead of an image crop. The probe cases use resolved trace anchors. Existing user-facing samples retain LCD oracles where the displayed result is part of the behavior.
Build the compact report through the Nix shell so z80dasm and Z3 are pinned.
The checked command passes all 33 LABEL=PATH pairs; the complete ordered label
list is the dynamic.traces array in tools/oracles/tibasic/tibasic-coverage.json.
set --
for label in \
hello factorial data dfs callabi callstop \
branchmatrix missingend terminalif syntaxerr divzero \
overflow muloverflow lndomain increment \
asindomain acosdomain sqrtnonreal singular \
lateincrement negfactdomain ncrdomain \
cflowlow cflowhigh cflowvalid \
cmdclose cmdopen cmdunit cmdbad \
gramlow gramhigh gramflag gramnonzero
do
set -- "$@" --trace "$label=/tmp/tibasic-coverage/$label.trace"
done
nix develop -c python3 -m ti84re.tibasic.analyze_coverage "$@" \
--output tools/oracles/tibasic/tibasic-coverage.json
Export exact instruction boundaries from the rebuilt Ghidra database, then reuse the same trace arguments for the expanded report:
ghidra-analyzeHeadless "$PWD" ti84 \
-process ti84_page00.bin -noanalysis -readOnly \
-scriptPath "$PWD/tools/ghidra" \
-postScript ExportTiBasicInstructionStarts.java \
/tmp/tibasic-instruction-starts.tsv
nix develop -c python3 -m ti84re.tibasic.analyze_saturation \
--instruction-list /tmp/tibasic-instruction-starts.tsv \
"$@" --output tools/oracles/tibasic/tibasic-saturation.json
Capture and reduce the selected numeric-error paths separately. This keeps their semantic provenance without adding redundant traces to the branch-only minimum corpus:
python3 -m ti84re.tibasic.smoke \
--tilem "$TILEM" --rom tools/rom.bin \
--out-dir /tmp/tibasic-numeric-errors --keep-trace \
--case divzero --case overflow --case muloverflow \
--case lndomain --case increment --case asindomain \
--case acosdomain --case sqrtnonreal --case singular \
--case lateincrement --case negfactdomain --case ncrdomain
nix develop -c env PYTHONPATH=tools \
python3 -m ti84re.tibasic.analyze_numeric_errors \
--trace divzero=/tmp/tibasic-numeric-errors/divzero.trace \
--trace overflow=/tmp/tibasic-numeric-errors/overflow.trace \
--trace muloverflow=/tmp/tibasic-numeric-errors/muloverflow.trace \
--trace lndomain=/tmp/tibasic-numeric-errors/lndomain.trace \
--trace increment=/tmp/tibasic-numeric-errors/increment.trace \
--trace asindomain=/tmp/tibasic-numeric-errors/asindomain.trace \
--trace acosdomain=/tmp/tibasic-numeric-errors/acosdomain.trace \
--trace sqrtnonreal=/tmp/tibasic-numeric-errors/sqrtnonreal.trace \
--trace singular=/tmp/tibasic-numeric-errors/singular.trace \
--trace lateincrement=/tmp/tibasic-numeric-errors/lateincrement.trace \
--trace negfactdomain=/tmp/tibasic-numeric-errors/negfactdomain.trace \
--trace ncrdomain=/tmp/tibasic-numeric-errors/ncrdomain.trace \
--output tools/oracles/tibasic/tibasic-numeric-errors.json
Delete the temporary traces after regeneration. They are reproducible evidence, not source assets.
Reading gaps honestly
Full coverage of the small matrix means both outcomes at 26 selected sites. The expanded report gives the more useful denominator: 924 of 2,702 outcomes across five declared components, with 898 reached naturally. Neither number means whole-interpreter coverage.
The graph expands all four declared computed jumps over their valid domains:
14 literal parser continuations at 38:4390, 27 nonzero destinations from the
49-class table used by 38:7244, five literal command targets at 02:5675,
and 13 bounds-checked rows at 33:4380. This does not establish behavior for a
corrupted class, stack, or pointer outside those domains. Other open dimensions
include arbitrary token-stream length, every nested error context, full OPS/FPS
record layout, arbitrary VAT and list shapes, floating-point path classes, and
display or graph subsystem continuations.
The next useful coverage expansion starts with the unresolved caller census:
- reject linear-disassembly candidates that are data or unreachable code;
- backward-slice one remaining executable caller to its input predicate;
- construct the smallest natural program and a RAM or value oracle;
- retain its trace only when it adds a guard path or CFG outcome; and
- update the relevant interpreter model with the established transition.
Lower-level trace formats and memory-write decoding are documented in
tools/notes/dynamic-tracing.md.
TI-BASIC For( parenthesis trap
The closing ) in For( is optional syntax, but the two spellings can produce
different parser-buffer behavior. A paired OS 2.55MP trace with a false
single-line If as the first body statement measures the difference without
including boot, link transfer, menu navigation, or final display work.
Reproduced pair
The fixtures differ by one token byte:
Asm(prgmZMARK)
For(I,1,25)
If 0
1
End
Asm(prgmZMARK)
If I=26
Asm(prgmZPASS)
Asm(prgmZMARK)
For(I,1,25
If 0
1
End
Asm(prgmZMARK)
If I=26
Asm(prgmZPASS)
ZMARK contains one Z80 instruction:
AsmPrgm
C9
C9 is RET. Both traces execute it twice at userMem (0x9D95). The analyzer counts
instructions and clocks from the first marker to the second. After the second
marker, each program calls ZPASS only when I=26; ZPASS writes A5h to
plotSScreen at 0x9340. The smoke runner asserts that RAM byte directly.
[confirmed]
The headers differ only by tRParen = 11h:
explicit: D3 49 2B 31 2B 32 35 11 3F CE 30 ...
implicit: D3 49 2B 31 2B 32 35 3F CE 30 ...
Measured work
| Form | Instructions | Clocks | Trace ID |
|---|---|---|---|
Explicit ) | 145,748 | 1,698,162 | d8348851f6ba… |
| Implicit close | 157,052 | 1,790,338 | eef08147e170… |
The implicit form adds 11,304 instructions, or 7.76%, and 92,176 clocks, or
5.43%. These values describe this N=25, false-If pair. They do not imply the
same ratio for other bodies or trip counts. [confirmed]
The compact JSON report retains each complete SHA-256 digest.
Parser-buffer state
The trace records writes to nextParseByte (0x965D) and basic_end
(0x965F). The analyzer selects temporary states where the two pointers are
equal and at or above 0x9E80.
flowchart LR
E["Explicit close"] --> ES["one equal high state<br/>0x9ECB"]
I["Implicit close"] --> IS["25 equal high states<br/>0x9EC8–0xA018<br/>stride 0x0E"]
The explicit trace reuses one equal-pointer high state. The implicit trace
advances through 25 states from 0x9EC8 to 0xA018 in 0x0E-byte steps.
This is direct RAM-state evidence that the two token streams manage temporary
parse space differently. [confirmed]
The FPS pointer distinguishes the forms at every End visit:
| Form | First FPS | Last FPS | Distinct values |
|---|---|---|---|
Explicit ) | 0x9F02 | 0x9F02 | 1 |
| Implicit close | 0x9EFF | 0xA04F | 25 |
The implicit sequence advances by 0x0E per iteration. The explicit sequence
keeps one FPS value after the first body setup. This matches the temporary
cursor/end stride without using an LCD image as an oracle. [confirmed]
The natural loop record
Both forms reach parse_for_production (38:41E5) and
parse_end_ops_record (38:4200). At each End, parse_end_ops_record
consumes one five-byte TIForOpsRecord beginning at OPS + 1:
flowchart LR
R["OPS + 1 … OPS + 5"] --> Z["00h<br/>sentinel"]
R --> C["36 58 or 7D 58<br/>continuation"]
R --> S["12 00<br/>state word"]
C --> I["for_first_update"]
C --> T["for_steady_update"]
The first continuation prepares the loop update. The steady continuation
re-enters the update path on later iterations. The trace observes 25 End
visits and the same two record variants in both spellings. [confirmed]
The pair does not enter the command-finalization gate at 02:5676 or the
page-33 dispatcher at 33:435F; those routines do not explain the difference.
The exact branch between For( argument parsing and temporary FPS allocation
that selects reuse versus 0x0E-byte growth remains [hypothesis].
Reproduce the evidence
Generate the programs, run both cases, and retain their traces:
python3 -m ti84re.tibasic.samples --write-dir tools/tibasic-samples
TILEM=/path/to/patched/tilem2
python3 -m ti84re.tibasic.smoke \
--tilem "$TILEM" --rom tools/rom.bin \
--out-dir /tmp/tibasic-for-paren --keep-trace \
--case forparen --case forimplicit
Reduce the traces to the checked compact report:
PYTHONPATH=tools python3 -m ti84re.tibasic.analyze_for_paren \
--explicit /tmp/tibasic-for-paren/forparen.trace \
--implicit /tmp/tibasic-for-paren/forimplicit.trace \
--output tools/oracles/tibasic/tibasic-for-paren.json
tools/oracles/tibasic/tibasic-for-paren.json stores the hashes, marker intervals, pointer-write
counts, high-state sequence, FPS summary, and decoded OPS record variants.
The smoke check reads the completion marker from a logical-RAM dump. Raw traces
remain outside the repository.
Practical rule
Write the closing ) when a For( body begins with a single-line guard:
For(I,1,N)
If condition
statement
End
The explicit form avoids the measured advancing temporary-buffer sequence in
this pattern. Use the trace pair as evidence for this case, not as a general
claim that every implicit For( close is slower.
Display and LCD
The display subsystem turns text, graph buffers, menus, and equation layouts into the 96×64 monochrome image scanned from LCD-controller video RAM. This page maps the OS-facing render paths and software buffers; LCD controller and display bus reconstructs commands, addressing, timing, initialization, reads, power, and controller-revision behavior.
Display paths
The OS uses direct rendering for homescreen text and an explicit RAM back buffer for graphs. [confirmed]
flowchart LR
TEXT["text and menus"] --> PUT["_PutMap / _VPutMap"]
PUT --> LCD["controller video RAM"]
GRAPH["graph rasterizers"] --> BUF["plotSScreen · 0x9340"]
BUF --> CPY["_GrBufCpy"]
CPY --> LCD
LCD --> PANEL["96×64 panel"]
LCD --> SAVE["_SaveDisp → saveSScreen"]
SAVE --> RESTORE["_RestoreDisp"]
RESTORE --> LCD
| Path | Main entry | Behavior |
|---|---|---|
| Large text | _PutMap at 01:5A98 | draws one large-font glyph directly into controller RAM |
| Cooked character | _PutC at 01:5B4C | calls _PutMap, advances curCol, and handles newline/wrap |
| String | _PutS at 01:5C39 | emits a null-terminated large-font string |
| Small text | _VPutMap/_VPutS | renders variable-width glyphs using penCol/penRow |
| Graph clear | _GrBufClr at 04:6071 | clears 768 bytes of plotSScreen; does not touch the LCD |
| Graph blit | _GrBufCpy at 04:60A3 | copies selected graph-buffer rows to controller RAM |
| Physical clear | _ClrLCDFull at 01:60E4 | writes zero to all 768 visible controller bytes |
| Save/restore | _SaveDisp/_RestoreDisp | captures and restores the displayed image |
Software display state
| Address | Name | Size | Role |
|---|---|---|---|
0x8447 | contrast | 1 byte | OS contrast level used to build controller command 0xC0–0xFF |
0x844A | curTime | 1 byte | timer-driven cursor blink countdown |
0x844B/0x844C | curRow/curCol | 2 bytes | 16×8 homescreen character cursor |
0x845A–0x8461 | lFont_record | 8 bytes | current large-font render record |
0x8508–0x8587 | textShadow | 128 bytes | 16×8 homescreen character shadow |
0x86EC–0x89EB | saveSScreen | 768 bytes | saved display image |
0x9340–0x963F | plotSScreen | 768 bytes | graph/back buffer, 12 bytes × 64 rows |
Both 768-byte buffers use the MonoFramebuffer layout: [confirmed]
typedef struct {
uint8_t rows[64][12];
} MonoFramebuffer;
Each rows[y][byte_column] byte holds eight pixels, most-significant bit first.
Controller video RAM is a third image store outside Z80 RAM. Direct text output
can change it without changing plotSScreen, while graph drawing can change
plotSScreen without changing the panel until _GrBufCpy runs. [confirmed]
LCD-ready wait [confirmed]
_lcd_busy = 0x4051, body ram:0CC3, preserves AF while polling port
0x02 bit 1 until the LCD reports ready. If bit 3 of IY + 0x41 is set after
that poll, it calls the short delay helper at ram:0CE6 three times before
restoring AF and returning. The ROM has callers in the fixed page and on
pages 0x01, 0x04, 0x06, 0x07, 0x39, and 0x3B; it is the common
serialization point used before direct LCD operations.
A source-built fixture reaches the body and returns to its marker. That trace
confirms the callable path in TilEm, while the port-poll latency and optional
three-delay branch remain dependent on emulator or hardware LCD state. The
result is pinned in tools/data/community-manual-bcall-traces.csv.
Large-font text
_PutMap clamps character code zero and codes at or above 0xF8 to replacement code 0xD0. It computes character × 8 and bjumps to put_glyph_large at 07:4588. [confirmed]
The page-7 blitter adjusts that offset to a seven-byte packed stride:
$$ \text{glyph address} = \texttt{07:45FF} + 7c $$
where $c$ is the character code. It then copies eight bytes into lFont_record, so the eighth byte overlaps the first byte of the next packed glyph. [confirmed]
Two flags at IY+0x35 select alternate font-hook sources before the page-7 table read. Bit 5 calls 3B:7BFB with selector A=0x01; bit 1 calls 3B:7B9C with selector A=0x76. With neither flag set, _PutMap reads the built-in table. [confirmed]
The renderer positions the controller from curRow and curCol. Glyph edges use controller read-modify-write with the required dummy data read before the real byte. The complete bus sequence is in LCD controller and display bus.
Cursor and indicators
_CursorOn and _CursorOff reload curTime with 50. Standard hardware timer
1 reaches cursor_blink_tick at 06:7C45, which toggles the cursor every 50
ticks. See Clock, timers, and power.
[confirmed]
The run indicator uses indicCounter and indicBusy at 0x8476/0x8477. _RunIndicOn seeds it, and run_indicator_tick at ram:027B advances it from the same standard-timer interrupt. _ClrLCDFull temporarily clears and then restores the indicator-enable bit around the physical clear. [confirmed]
Numeric and string output
_DispHL at 01:5BF6 converts HL to five decimal positions with repeated _DivHLBy10, stores digits backward in scratch RAM, replaces leading zeroes with spaces, and prints through _PutC. [confirmed]
_PutC wraps when curCol reaches 16 and calls the newline/scroll path. _PutS and related bounded-string entries repeat _PutC over character data. These APIs target the character-oriented homescreen state, not the graph buffer. [confirmed]
Graph and equation rendering
Graph rasterizers write plotSScreen and call _GrBufCpy or _PDspGrph to expose the result. Coordinate transforms, clipping, line/circle algorithms, and graph state are covered in Graphing.
MathPrint uses a separate layout engine before its glyphs reach the display primitives. Its descriptors, box tree, cursor geometry, and runtime gaps are covered in Equation display.
The table editor and Y= screens build text-grid state through their own context handlers. See Table and Y= variables.
Related deep dives
- LCD controller and display bus — ports, status, commands, addressing, waits, initialization, clear/blit/read paths, contrast, power, dynamic I/O traces, and cross-emulator fidelity.
- Graphing — graph buffer, transforms, pixels, lines, circles, and graph display.
- Equation display — MathPrint layout and compositing.
- Table and Y= variables — table grid and function editor.
LCD controller and display bus
TI-84 Plus OS 2.55MP — Controller commands, video RAM, bus timing, initialization, blits, reads, contrast, and emulator fidelity.
The TI-84 Plus drives a 96×64 monochrome panel through an external LCD controller on ports 0x10 and 0x11. This page separates the visible panel from controller video RAM, reconstructs the OS command and transfer paths, traces initialization and clearing, and identifies behavior that varies across Toshiba and Novatek controller revisions.
Evidence layers
The local ROM establishes what OS 2.55MP sends to the controller. Public hardware tests establish controller behavior that the ROM does not expose. TilEm, Wabbitemu, MAME, and jsTIfied supply executable models whose choices are identified separately.
| Layer | Main evidence | What it establishes |
|---|---|---|
| TI-OS page 0 | ram:0CC3–ram:0CEA, ram:1890–ram:18D1, and ram:20BF–ram:20CD | ASIC-side wait, block reads/writes, and movement commands [confirmed] |
| TI-OS display code | 01:5A59–01:5B4B, 01:60E4–01:612D, and 01:6934–01:6955 | byte I/O, text drawing, and full-screen clearing [confirmed] |
| TI-OS graph code | 04:6071–04:620A | graph-buffer clear and LCD transfer loops [confirmed] |
| TI-OS initialization | _LCD_DRIVERON at 06:4D02–06:4D3A | mode, enable, power, and contrast command sequence [confirmed] |
| Dynamic execution | resolved home-2plus3 trace filtered to ports 0x10–0x13 and 0x2F | exact initialization and clear transactions in TilEm [confirmed] |
| Datamath module photographs | March 2004 TI-84 Plus LCD module and controller attribution | source-attributed Toshiba T6K04 identity; the die itself is hidden under epoxy [standard] |
| Toshiba T6K04 data sheet | exact controller block diagram, command table, and timing specification | 128×64 display RAM, 80-series bus, counters, read latch, busy formula, reset state, and analog-drive controls [standard] |
| Toshiba T6A04A data sheet | compatible earlier controller documentation | 120×64 display RAM and family comparison [standard] |
| Public hardware notes | WikiTI ports 0x02, 0x10–0x13, and 0x2F | status bits, command meanings, controller variants, transfer timing, and hardware quirks [standard] |
| Emulator model | Upstream TilEm lcd.c, x4_io.c, and x4_init.c at commit f56ad63 | implemented video RAM, latches, delays, aliases, and fidelity limits [standard] |
| Emulator comparison | Wabbitemu lcd.c and 83psehw.c at 48c2dc0; MAME t6a04.cpp, ti85.cpp, and ti85_m.cpp at mame0287 | row strides, pointer bounds, busy handling, ASIC waits, and unsafe edge cases [standard] |
Panel, controller, and video RAM
The panel exposes 96 horizontal pixels and 64 vertical pixels. In 8-bit transfer mode, one controller byte holds eight adjacent horizontal pixels, so the visible row occupies 12 bytes. The visible image therefore contains:
$$ 12 \times 64 = 768\text{ bytes} $$
The controller can contain more video RAM than the panel displays. Toshiba’s data sheets specify 15 bytes per row, or 120 pixels, for T6A04A and 16 bytes per row, or 128 pixels, for T6K04. The T6K04 contains 8,192 bits of display RAM and exposes 128 column outputs plus 64 row outputs. Later Novatek replacements can differ. [standard]
Datamath attributes the LCD module photographed from a March 2004 TI-84 Plus
to Toshiba T6K04. The photograph shows the controller area under opaque
epoxy, so it does not expose a readable die marking. The identification is a
source-attributed module record rather than a marking read from the image.
[standard]
The local ROM uses only visible columns 0–11 in its full-screen clear and
graph blit loops. It does not distinguish the 15-byte T6A04A row from the
16-byte T6K04 row. Datamath’s attribution supplies revision-specific external
evidence that the ROM cannot. [confirmed] for the ROM access range; [standard]
for the March 2004 controller attribution.
Coordinate vocabulary
Toshiba documentation names the pixel row X and the byte column Y. Software often uses the opposite convention. This page uses row for 0–63 vertically and byte column for the horizontal group selected by commands 0x20–0x3F.
| Quantity | Command or transfer | Visible range |
|---|---|---|
| Row | command 0x80 + row | 0x80–0xBF |
| Byte column | command 0x20 + column | 0x20–0x2B |
| Pixel within a byte | data bit 7 through bit 0 | left to right [standard] |
| Visible storage | 12 byte columns × 64 rows | 768 bytes |
The Z-address command 0x40 + shift changes which controller row appears at the top of the panel. It does not copy video RAM. Rows wrap modulo 64 when displayed. [standard]
Ordinary 8-bit addressing. The TilEm skin locates the controller model on the calculator face. The visible 12-byte row and OS command ranges are [confirmed]; the T6K04’s 16-byte controller row, read latch, and off-screen storage are [standard].
Port interface
| Port | Direction | Role |
|---|---|---|
0x10 | read | controller status |
0x10 | write | controller command |
0x11 | read/write | latched video-RAM data at the current address |
0x12 | read/write | second-chip-select mirror of 0x10 on documented ASIC revisions [standard] |
0x13 | read/write | second-chip-select mirror of 0x11 on documented ASIC revisions [standard] |
0x02 bit 1 | read | ASIC LCD-wait timer ready at high CPU speed |
0x2F | read/write | duration selector for the ASIC LCD-wait timer |
The OS paths decoded here use 0x10 and 0x11. The resolved calculator trace contains no port-0x12 or port-0x13 LCD transaction. [confirmed] for the trace; [standard] for the physical mirrors.
Controller-side signals
The T6K04 exposes an 8-bit DB0–DB7 bus for an 80-series processor. Its
D/I input distinguishes command bytes from display-data bytes, /WR selects
reading or writing, and /CE strobes the transfer. /RST resets the
controller. /STB stops its oscillator, rejects commands and data, and drives
the LCD supply outputs to VDD. The calculator ASIC turns port instructions
into these controller-side operations, so Z80 code does not toggle the
individual signals. [standard]
The T6K04 includes 128 column outputs, 64 row outputs, display RAM, an oscillator, contrast control, five LCD-supply operational amplifiers, and a DC–DC converter with doubler, tripler, and quadrupler modes. Its logic supply range is 2.7–5.5 V, and Toshiba specifies a tape-carrier package (TCP). Replacement controllers can expose compatible port behavior without reproducing every internal analog block or off-screen RAM cell. [standard]
Status read
Reading port 0x10 returns the controller state: [standard]
| Bit | Meaning |
|---|---|
| 0 | increment direction when set; decrement when clear |
| 1 | movement affects the byte column when set; row when clear |
| 2 | fixed zero in the T6K04 status definition |
| 3 | LCD-supply operational amplifier enabled when set |
| 4 | controller reset state |
| 5 | display enabled |
| 6 | 8-bit transfer mode when set; 6-bit mode when clear |
| 7 | controller busy on controllers that implement a reliable busy flag |
Late replacement controllers can move the internal video-RAM pointer when software reads status. Busy-poll loops that work on Toshiba controllers can therefore corrupt addressing on those units. WikiTI recommends a fixed delay or the ASIC delay mechanism for cross-revision code. [standard]
OS 2.55MP avoids this incompatibility in its common path: lcd_wait at ram:0CC3 reads port 0x02, not port 0x10. [confirmed]
Command set
The table separates public controller behavior from the subset used by this ROM.
| Command | Meaning | OS 2.55MP use |
|---|---|---|
0x00 | select 6-bit transfers | used by some text read/modify/write paths at 01:5AD3 [confirmed] |
0x01 | select 8-bit transfers | _LCD_DRIVERON and _PutMap [confirmed] |
0x02 | disable display output while retaining video RAM | lcd_disable at ram:0CD9; _PowerOff calls it [confirmed] |
0x03 | enable display output | _LCD_DRIVERON [confirmed] |
0x04 | decrement row after each data transfer | documented controller mode [standard] |
0x05 | increment row after each data transfer | OS vertical byte loops [confirmed] |
0x06 | decrement byte column after each data transfer | documented controller mode [standard] |
0x07 | increment byte column after each data transfer | OS horizontal row blits [confirmed] |
0x08–0x0B | select the duration of enhanced LCD-supply amplifier drive | _LCD_DRIVERON selects 0x08 or 0x0B [confirmed]; T6K04 OPA2 behavior [standard] |
0x0C–0x0F | mirroring controls reported on newer controllers | absent from the T6K04 command table and unused by this ROM [standard] |
0x10–0x17 | control LCD-supply amplifier state and ability; 0x14–0x17 keep it on | _LCD_DRIVERON selects 0x16 or 0x17 [confirmed]; T6K04 OPA1 behavior [standard] |
0x18–0x1F | T6K04 test-mode select | Toshiba says not to use this range; WikiTI separately reports 0x18 as test-mode exit and warns that high-drive modes can damage the panel [standard] |
0x20–0x2F | set T6K04 byte column in 8-bit mode | visible OS range 0x20–0x2B [confirmed]; 16-column limit [standard] |
0x20–0x35 | set T6K04 six-pixel group in 6-bit mode | OS selects 6-bit mode in an edge-rendering path [confirmed]; 22-entry limit [standard] |
0x40–0x7F | set displayed top-row offset | _LCD_DRIVERON writes 0x40 [confirmed] |
0x80–0xBF | set row | full 64-row range [confirmed] |
0xC0–0xFF | set controller contrast 0–63 | _LCD_DRIVERON derives the command from contrast [confirmed] |
The T6K04 defines contrast command 0xC0 as brightest and 0xFF as darkest.
The power and test commands affect analog drive circuitry and vary across
controller revisions. TilEm ignores them except for display enable, display
disable, and contrast. [standard]
Reset and standby states
Driving T6K04 /RST low selects 8-bit transfers, byte-column increment, row
and byte-column address zero, displayed top-row offset zero, display off,
LCD-supply amplifier on at minimum ability, minimum enhancement, and minimum
contrast. The status reset bit remains set while the controller is held in
reset. Toshiba’s reset-state list does not state that display RAM is cleared.
[standard]
Driving /STB low stops the oscillator, prevents command and data acceptance,
reduces controller power, and drives VLC1–VLC5 to VDD. The OS’s display
disable command 0x02 is a separate operation and does not clear display RAM.
[standard]
Data transfers and address movement
Port 0x11 transfers one unit at the current row and byte column, then applies command 0x04, 0x05, 0x06, or 0x07. [standard]
In 8-bit mode, all eight data bits map to adjacent pixels. In 6-bit mode, only bits 0–5 are significant and the controller packs six-pixel groups. The OS initializes 8-bit mode for ordinary full-screen work but temporarily selects 6-bit mode in large-font edge handling at 01:5AD3. [confirmed]
Read latch and dummy reads
Controller reads are one transfer behind the addressed video-RAM byte. After a row or column command, the first port-0x11 read returns the old output latch; the read loads the newly addressed byte into that latch. Software must discard one dummy read and use the second. Auto-increment or auto-decrement does not require another dummy read between sequential bytes. [standard]
lcd_read_data at 01:5A60 performs exactly two reads: [confirmed]
01:5A60 call ram:0CC3
01:5A63 in a,(0x11) ; discard stale output latch
01:5A65 call ram:0CC3
01:5A68 in a,(0x11) ; addressed byte
The routine then restores an OS-tracked row command from 0x8451 and selects row-increment mode. lcd_write_data at 01:5A59 is the matching wait plus OUT (0x11),A. These routines transfer arbitrary pixel data; neither routine sets or reads contrast. [confirmed]
Bounds and wrap behavior
The T6K04 data sheet defines byte-column commands 0x20–0x2F in 8-bit mode
and six-pixel group commands 0x20–0x35 in 6-bit mode. Its address counter
wraps across 16 or 22 entries respectively. Toshiba says not to issue a byte
column beyond 15 in 8-bit mode. [standard]
WikiTI reports that some Toshiba command decoders accept the wider
0x20–0x3F field, while transfers outside implemented RAM do not change RAM.
That out-of-range behavior is outside the T6K04 data-sheet contract and varies
across controllers and emulators. [standard]
The row coordinate wraps across 64 rows. The controller-specific byte-column width is one reason software should not use off-screen RAM as portable storage. [standard]
ASIC-side wait timing
At high CPU speed, each access to ports 0x10–0x13 clears port-0x02 bit 1 for a programmable interval. Port 0x2F selects that interval in nominal 64-T-state steps with values 48, 112, 176, 240, 304, 368, 432, or 496 T-states. CPU-speed value 1 uses bits 0–1; value 2 uses bits 2–4; value 3 uses bits 5–7. CPU-speed value 0 leaves port-0x02 bit 1 set. [standard]
Ports 0x29–0x2C separately add T-states to the LCD-port instruction and
gate the Flash/RAM waits in port 0x2E. Bus timing and wait states
reconstructs the complete joint register block and both emulator models.
The retail boot page writes 0x4B to port 0x2F at 3F:41D3. With the OS’s normal CPU-speed value 1, the low field is 3, selecting 240 T-states. At nominal 15 MHz this interval is 16 µs. [confirmed] for the writes and trace; [standard] for the hardware timer interpretation.
The T6K04 data sheet specifies its internal busy interval as $2/f_{OSC} \leq T \leq 4/f_{OSC}$. For a 35 Hz common drive, its four frequency-select examples use 28.56, 57.12, 228.48, and 456.96 kHz. They imply the following controller-busy ranges: [standard]
| Example $f_{OSC}$ | T6K04 busy range |
|---|---|
| 28.56 kHz | 70.03–140.06 µs |
| 57.12 kHz | 35.01–70.03 µs |
| 228.48 kHz | 8.75–17.51 µs |
| 456.96 kHz | 4.38–8.75 µs |
Toshiba rates the oscillator input from 20 to 500 kHz and warns that mounting conditions affect an external-resistor oscillator. The module photograph does not resolve the frequency-select wiring or resistor value. The OS’s 16 µs ASIC wait therefore cannot identify the fitted oscillator or prove worst-case controller margin by itself. [standard] for the T6K04 limits; [hypothesis] for the board-specific oscillator and margin.
The controller’s /CE switching limits are a separate timing layer. Toshiba
specifies the following values at 25 °C: [standard]
| Logic test condition | Minimum /CE cycle | Minimum /CE pulse | Minimum address setup | Minimum write-data setup | Maximum read-data delay |
|---|---|---|---|---|---|
| 3.0 V ± 10% | 1,000 ns | 450 ns | 100 ns | 280 ns | 350 ns |
| 5.0 V ± 10% | 500 ns | 220 ns | 60 ns | 60 ns | 160 ns |
These limits constrain the ASIC-to-controller strobe. The port-0x2F interval
instead prevents the next access while the controller’s internal operation can
remain busy. Z80 instruction timing does not reveal the /CE waveform, so the
physical strobe still requires a bus capture. [standard] for the controller
limits; [hypothesis] for the ASIC waveform.
lcd_wait preserves AF and spins on that ASIC-ready bit: [confirmed]
ram:0CC3 push af
ram:0CC4 in a,(0x02)
ram:0CC6 and 0x02
ram:0CC8 jr z,ram:0CC4
ram:0CCA bit 3,(iy+0x41)
ram:0CCE call nz,ram:0CE6
ram:0CD1 call nz,ram:0CE6
ram:0CD4 call nz,ram:0CE6
ram:0CD7 pop af
ram:0CD8 ret
The three optional calls add fixed instruction delay when the OS flag at IY+0x41 bit 3 is set. That byte is shared with USB state in the published equates; the local code establishes the delay effect but does not establish an LCD-specific public name for the flag. [confirmed]
lcd_write_command_a at ram:0CDB repeats the port-0x02 wait and writes A to port 0x10. lcd_disable at ram:0CD9 loads command 0x02 and enters that helper. [confirmed]
Controller initialization
_LCD_DRIVERON = 4978 has body 06:4D02. It sends every command through lcd_write_command at 06:4D35, which calls lcd_wait before writing port 0x10. [confirmed]
| Order | Command | Effect |
|---|---|---|
| 1 | 0x40 | top displayed row = controller row 0 |
| 2 | 0x05 | increment row after data transfers |
| 3 | 0x01 | select 8-bit transfer mode |
| 4 | 0x03 | enable display output |
| 5 | 0x16 or 0x17 | select one of the upper LCD-supply amplifier abilities |
| 6 | 0x08 or 0x0B | select amplifier-enhancement duration |
| 7 | 0xC0 OR (contrast + 0x18) | program contrast |
Calls to the hardware test at ram:1837 choose between the two power values. In the resolved TI-84 Plus TilEm trace, the sequence is:
40 05 01 03 17 0B EF
The final 0xEF means controller contrast 0x2F. The RAM byte contrast at
0x8447 was 0x17, and _LCD_DRIVERON added 0x18. Toshiba defines larger
T6K04 contrast arguments as darker, from brightest 0xC0 to darkest 0xFF.
[confirmed] for the ROM arithmetic; [standard] for the controller direction.
The trace records this sequence twice during cold startup before the homescreen clear. This is OS behavior under the traced startup path, not a requirement that user code initialize the controller twice. [confirmed]
Full-screen clear
_ClrLCDFull = 4540 has body 01:60E4. It temporarily clears the run-indicator flag, then invokes _ClearRow at 01:6934 for row bases 0xB8, 0xB0, …, 0x80. Each call clears an eight-row band. [confirmed]
For each byte column 0x20–0x2B, _ClearRow performs: [confirmed]
- send command
0x07throughlcd_mode_column_increment; - restore the band-base row command;
- send command
0x05throughlcd_mode_row_increment; - select the current byte column;
- write eight zero bytes while the row auto-increments;
- advance to the next byte column.
The arithmetic covers every visible byte exactly once:
$$ 8\text{ bands} \times 12\text{ columns} \times 8\text{ rows} = 768\text{ writes} $$
The resolved trace shows the first band as row command 0xB8, column commands 0x20 through 0x2B, and eight zero data writes after every column command. The next band begins at 0xB0; the final band begins at 0x80. [confirmed]
Dormant boot-page display test
Retail page 3F contains boot_lcd_keypad_diagnostic at 3F:4658. Its only
incoming branch, boot_diagnostic_gate at 3F:4615, follows XOR A,
OUT (0x05),A, and CP 0x09, so it is constant-false. The MODE boot path
can execute boot_ram_test at 3F:461A, but it cannot continue into this LCD
routine under Z80 semantics. [confirmed]
The dormant code fills all 768 visible bytes through boot_lcd_fill_pattern at
3F:46EF and overwrites individual full rows through boot_lcd_write_row at
3F:472E. It presents six patterns. The sequence includes solid 0xFF and
0x00, alternating 0x55/0xAA and 0x00/0xFF rows, a solid 0xAA
pattern, and a bordered 0x81 pattern. It then sweeps raw contrast commands
0xFF through 0xD9 and
restores the OS contrast byte through boot_lcd_restore_contrast at
3F:74F5. [confirmed]
An explicit direct-entry Wabbitemu harness validates the actual retail helpers,
including 768 data writes from boot_lcd_fill_pattern (3F:46EF), 12 data
writes from boot_lcd_write_row (3F:472E), and the 0xFF output from
boot_lcd_write_contrast (3F:74F8).
This is emulator agreement with executed ROM,
not evidence that retail boot reaches the code or that a physical panel
produces the modeled image. [confirmed] for pinned Wabbitemu commit 48c2dc0;
[hypothesis] for unmeasured physical output. See
Retail boot hardware initialization
for the stage and keypad flow.
Graph-buffer transfer
plotSScreen at 0x9340 is the 768-byte row-major graph buffer. _GrBufClr at 04:6071 clears it with one zero store followed by LDIR of 0x02FF bytes; it does not access the LCD. [confirmed]
_GrBufCpy at 04:60A3 enters the controller-transfer core at 04:6176. The TI-84 Plus direct-RAM path performs these operations for each selected pixel row: [confirmed]
- command
0x07selects byte-column auto-increment; - a command in
0x80–0xBFselects the row; - command
0x20selects visible byte column 0; - 12 sequential data writes copy one row from the RAM buffer;
- the source pointer and row command advance.
The caller adjusts the starting row and row count for full-screen and split-screen states. The controller loop therefore treats the transfer extent as state, while the row width remains 12 bytes. [confirmed]
The alternative path calls lcd_write_block at ram:18B1 when the source lives in banked RAM. That helper temporarily maps RAM page 0x83 through port 0x06, writes B bytes to port 0x11, restores the prior mapping, and restores the caller’s interrupt-enable state. [confirmed]
Text drawing and read-modify-write
Homescreen text does not render through plotSScreen. _PutMap at 01:5A98 loads an eight-byte large-font record and writes the controller directly. It selects row and byte-column commands from curRow and curCol, then emits the glyph through port 0x11. [confirmed]
When a glyph overlaps an existing byte boundary, _PutMap reads controller RAM, combines glyph bits with the retained pixels, and writes the result back. The read helpers at 01:5A70 and 01:5A7A perform the required dummy plus real reads before restoring the row and movement mode. [confirmed]
This creates two independent software representations:
| State | Address | Role |
|---|---|---|
| Controller video RAM | external LCD controller | currently scanned panel image |
plotSScreen | 0x9340–0x963F | graph/back buffer; copied explicitly |
saveSScreen | 0x86EC–0x89EB | saved 768-byte display image |
textShadow | 0x8508–0x8587 | 16×8 homescreen character shadow |
lFont_record | 0x845A–0x8461 | current eight-byte large-font render record |
Changing one RAM buffer does not update the panel until a routine copies or renders it. Direct text writes can likewise change controller RAM without changing plotSScreen. [confirmed]
Screen reads and saved displays
lcd_read_block at ram:1890 reads B bytes from port 0x11 into banked RAM. It temporarily maps RAM page 0x83, preserves the previous port-0x06 value, and restores the caller’s interrupt state. The caller must establish the controller address and consume any required dummy read before a sequential block. [confirmed]
_SaveDisp at 39:5DD8 uses this helper to capture controller video RAM into the saved-display RAM page. Dynamic RAM-page traces record writes across the 768-byte capture extent. _RestoreDisp later copies the saved image back through the display paths. [confirmed]
Contrast and power-off
The OS stores its user-facing contrast level at 0x8447. _LCD_DRIVERON adds 0x18, forces command bits 0xC0, and sends the result. Code that writes a raw controller contrast command without updating 0x8447 can cause the next OS contrast adjustment or driver initialization to jump to a different level. [confirmed] for OS state; [standard] for direct-hardware callers.
Display-disable command 0x02 blanks the panel but leaves controller video RAM available. The ASIC’s low-power transition is separate. _PowerOff calls lcd_disable at ram:0CD9, performs OS cleanup, then uses port 0x03 plus HALT to enter low power. See Clock, timers, and power. [confirmed]
Dynamic I/O trace
The trace resolver can print decoded I/O instructions after resolving every banked program counter:
nix develop -c python3 -m ti84re.trace.resolve \
/tmp/tilem-validation-home2plus3.trace \
--initial-mapping ti84p-reset --names tools/symbols/names.txt \
--io-ports 10-13,2f --io-count 360
The trace captures three load-bearing sequences: [confirmed]
- boot writes
0x4Bto port0x2Fat3F:41D3; _LCD_DRIVERONwrites40 05 01 03 17 0B EFat06:4D38;_ClrLCDFullwrites the eight-band, 12-column, eight-byte clear pattern through01:5A95,01:6945, and01:694B.
The trace is an emulator execution record. It proves the ROM path and values but does not prove analog power behavior, physical busy duration, or controller-revision quirks.
Emulator comparison
All four emulators implement the commands and visible 12-byte rows used by OS 2.55MP. Their hidden-column, busy, and ASIC-timer behavior differs. These differences are emulator test cases, not physical-controller evidence. [standard]
| Area | TilEm f56ad63 | Wabbitemu 48c2dc0 | MAME 0.287 | jsTIfied 20170706a |
|---|---|---|---|---|
| Controller RAM | 1,024 bytes, 16 × 64 | 1,024 bytes, 16 × 64 | 960 bytes, 15 × 64 | 960 bytes, 15 × 64 |
| 8-bit column increment | accesses 0–15, then normalizes to 0 | accesses 0–14, then wraps to 0 | counts modulo 32 without a RAM bound | accesses 0–14 around a 120-pixel row |
| Controller busy | 50 or 70 cycles; early transfers rejected | 60-T-state guard from the last accepted write | absent; busy status is always zero | randomized 25–47-emulator-cycle interval |
| ASIC ready | port-0x2F timer starts on every LCD read or write | interval measured from the last accepted write | port-0x02 bit 1 is always set | status derives from the emulator’s LCD timer |
Ports 0x12/0x13 | aliases | absent | aliases | aliases |
| Data-read latch | modeled | modeled | modeled | modeled |
| Analog power and test modes | ignored | ignored | values partly stored; no analog effect | drive fields and grayscale display physics modeled |
| Driver status | active TI-84 Plus trace target | source model | MACHINE_NOT_WORKING | browser emulator source model |
TilEm behavior and fidelity gaps
TilEm models the controller and the ASIC wait timer as separate mechanisms. [standard]
| Area | TilEm behavior | Fidelity consequence |
|---|---|---|
| Video RAM | fixed 1,024-byte array, 16 bytes × 64 rows | capacity matches T6K04, but not 120-pixel or no-extra-RAM variants |
Ports 0x12/0x13 | aliases command/status and data | models the documented second-chip-select mirrors |
| Controller busy | 50 emulated cycles after an accepted access | direct too-fast accesses are ignored when delay emulation is enabled |
| ASIC wait | port-0x02 bit 1 remains clear for the port-0x2F interval | reproduces the OS wait loop independently of controller busy |
| I/O overhead | adds five emulated CPU cycles per LCD-port access | emulator policy, not a physical bus measurement |
| Read latch | returns nextbyte, then loads the addressed byte | reproduces the dummy-read requirement |
| 6-bit mode | packs six-pixel writes into the internal byte array | supports OS edge-rendering paths |
| Z address | displays (row + shift) mod 64 | reproduces vertical display rotation |
| Power/test/mirror commands | mostly ignored | does not model analog drive, blue test modes, or newer-controller mirroring |
| Status-read pointer quirk | not modeled | cannot reproduce late Novatek corruption from busy polling |
| Out-of-range columns | wraps against the fixed 16-byte stride before transfer | differs from documented behavior on narrower controllers |
| Low power | frame output blanks when the LCD is inactive or the halted ASIC powers down | approximates visible power state rather than electrical retention |
TilEm reset initializes controller contrast to 32, 8-bit mode, byte-column increment, row and column zero, and display disabled. It does not clear the LCD backing array. A guarded direct-core reset retains a seeded display byte while rebuilding every controller field above. OS initialization then replaces the controller values. [standard] for source; [confirmed] for the pinned run.
Wabbitemu behavior and fidelity gaps
Wabbitemu allocates 16 bytes for each of 64 controller rows and displays the first 96 pixels. It decodes the same command groups as TilEm. Power and test commands have no controller effect. Its rendering queue uses repeated frames to approximate grayscale, which is a frontend policy rather than monochrome controller behavior. [standard]
The 8-bit column-increment path wraps when the next column reaches 15. It
therefore visits columns 0–14; TilEm visits 0–15. A direct command can
still select column 15. Wabbitemu indexes that direct coordinate modulo its
16-byte row. Commands 0x30–0x3F therefore alias columns 0–15 instead of
following the documented out-of-range behavior. [standard]
Wabbitemu rejects command and data transfers made within 60 T-states of the
last accepted LCD write. An early status read returns 0x80; an early data
read or write is discarded. Accepted data reads advance the pointer and output
latch but do not update the guard’s timestamp. [standard]
Its separate port-0x02 ready calculation also measures from the last accepted
write. It applies the port-0x2F field as $48 + 64n$ T-states. The speed-selected
ports 0x29–0x2C add their shifted instruction delay before either LCD port
handler. This agrees with the register arithmetic, but the triggering event
differs from TilEm’s every-access timer. [standard]
Wabbitemu registers ports 0x10 and 0x11 for this model but not the documented
0x12 and 0x13 aliases. It subtracts a model-specific base level of 24 from
contrast commands before its grayscale renderer consumes the value. This
display calibration is not controller voltage evidence. [standard]
A guarded initialized-core run at 48c2dc0 confirms these Wabbitemu-specific
edges dynamically: [standard]
| Case | Native observation |
|---|---|
| Controller guard | a status read at 59 T-states returns busy 0x80; one at 60 T-states is accepted |
| Early write | a data write at 59 T-states leaves the cell and pointer unchanged |
| Increment from column 14 | writes visit columns 14, 0, 1, and 2; column 15 remains unchanged |
| Direct hidden columns | command column 15 accesses column 15; command column 31 aliases the same cell and wraps the pointer to 0 |
| Read latch | three accepted reads return 0x00, 0x12, and 0x34 from cells containing 0x12, 0x34, and the following byte |
| Read timestamp | three same-T-state reads advance the pointer without changing the last-successful-write timestamp |
| Port map | reads of absent ports 0x12 and 0x13 are rejected and produce adapter fallback 0xFF |
Wabbitemu’s low-level CPU_reset leaves the complete LCD object unchanged.
The frontend calc_reset then invokes the LCD reset callback. A guarded
frontend-equivalent call disables output; zeros x, y, z, the last-read
latch, display RAM, and the grayscale queue; selects 8-bit words; and sets
contrast 32. It retains the last-access T-state and lcd_delay field. This is
frontend policy, not physical controller retention. [standard] for source;
[confirmed] for the pinned initialized-core run.
The LCD reset callback stores word_len = 8, while its status expression
shifts the field as though it were Boolean. The reset-state controller transfers
eight-bit data, but the first accepted status after display enable is 0x23,
with bit 6 clear. Sending command 0x01 stores Boolean one, after which the
same status is 0x63. This is a Wabbitemu state-representation defect, not
evidence for a physical reset status. [standard]
MAME behavior and fidelity gaps
MAME 0.287 attaches a generic T6A04 device to ports 0x10–0x13. Its 960-byte
array matches a 15-byte by 64-row controller. The OS-visible 12 columns, address
movement, Z shift, display enable, 6-bit packing, and dummy-read latch are
implemented. [standard]
The five-bit column pointer is not checked against the 15-byte stride before a
data read or write. Column 15 on row 0 indexes byte 15, which is row 1 column 0
in the backing array. Column 31 on row 63 computes index 976, beyond the
960-byte array. The OS stays within columns 0–11, so its ordinary clear and
blit loops do not trigger this C++ out-of-bounds path. [standard]
The device source lists busy and contrast among its TODO items. The busy flag never becomes one. Contrast commands update a field, but screen rendering does not consume it. MAME stores the two power-control fields without modeling their analog effect. [standard]
The TI-84 Plus driver returns port-0x02 with bit 1 permanently set and does
not map ports 0x29–0x2F. The ROM’s lcd_wait loop therefore exits on its
first read, and no speed-selected LCD instruction delay is applied. The driver
is marked MACHINE_NOT_WORKING; these omissions do not describe the ASIC.
[standard]
Native MAME confirmation. The guarded CPU-I/O-space probe reads the untouched controller startup state before seeding later independent cases. Ports 0x10 and 0x12 both return status 0x43. All 960 backing bytes are zero, and the row, column, Z address, output latch, and display-enable fields are zero. Eight-bit mode, column movement, and increment direction are selected. Port 0x02 returns 0xC3. [standard]
Four immediate status reads after display enable all return 0x63; busy bit 7 remains clear. Movement commands 0x04–0x07 produce statuses 0x60, 0x61, 0x62, and 0x63. Word-length commands change the status between 0x23 and 0x63. A display-off write through port 0x12 is visible at port 0x10, and a display-on write through port 0x10 is visible at port 0x12. Contrast 0xEF, OPA1 0x17, OPA2 0x0B, and Z-address 0x7F store 0x2F, 3, 3, and 0x3F. [standard]
Four incrementing writes from row 0, column 14 store A0 A1 A2 A3 at array indices 14–17 and leave column 0x12. A direct row-0 column-15 write reaches index 15, which is row 1 column 0 in the 15-byte stride. Row-0 column 31 reaches index 31, which is row 2 column 1, then wraps the pointer to zero. Sequential reads over bytes 0x12, 0x34, and 0x56 return 0x00, 0x12, and 0x34. Two 6-bit writes of 0x3F and 0x15 pack into bytes 0xFD and 0x50. [standard]
Ports 0x29–0x2F return seven zero bytes before and after patterned writes. Two isolated runs produce byte-identical native reports with SHA-256 d6930650a96383710be7ebb772675b5a494cba2450827b12a535c963fa464bfc. The probe does not execute row 63, column 31; the source-computed index 976 remains an unexecuted unsafe case. [standard]
Reproducing pointer differences
tools/ti84re/hardware/lcd_controller.py provides the source-attributed T6K04 specification,
vendor busy-time calculation, command decoding, status composition, the
dummy-read latch, and source-modeled pointer walks. The hardware report keeps
the module attribution and its photographic limit together:
$ python3 -m ti84re.hardware.describe_lcd_controller hardware
reported controller: Toshiba T6K04
calculator evidence: Datamath caption for a March 2004 TI-84 Plus module
limit: controller die is hidden under epoxy; no marking is visible
data sheet: 128x64 pixels, 8192 bits, 16 8-bit pages
interface: 8-bit 80-series MPU; logic supply=2.7-5.5 V; package=TCP
3 V bus: cycle>=1000 ns pulse>=450 ns read-delay<=350 ns
5 V bus: cycle>=500 ns pulse>=220 ns read-delay<=160 ns
The exact data-sheet formula can be evaluated at Toshiba’s four example oscillator choices:
$ python3 -m ti84re.hardware.describe_lcd_controller busy
fOSC=28.56 kHz: 70.028-140.056 us
fOSC=57.12 kHz: 35.014-70.028 us
fOSC=228.48 kHz: 8.754-17.507 us
fOSC=456.96 kHz: 4.377-8.754 us
The pointer CLI compares an increment starting at hidden column 14:
$ python3 -m ti84re.hardware.describe_lcd_controller walk --row 0 --column 14 --movement 7 --count 3
TilEm
0: requested=(0,14) access=(0,14) index=14 next=(0,15)
1: requested=(0,15) access=(0,15) index=15 next=(0,16)
2: requested=(0,16) access=(0,0) index=0 next=(0,1)
Wabbitemu
0: requested=(0,14) access=(0,14) index=14 next=(0,0)
1: requested=(0,0) access=(0,0) index=0 next=(0,1)
2: requested=(0,1) access=(0,1) index=1 next=(0,2)
MAME
0: requested=(0,14) access=(0,14) index=14 next=(0,15)
1: requested=(0,15) access=(0,15) index=15 next=(0,16) [column-out-of-range]
2: requested=(0,16) access=(0,16) index=16 next=(0,17) [column-out-of-range]
The hardware, busy, decode, profiles, status, and latch subcommands
accept --json for scripts. Transfer reports distinguish a controller column
outside the modeled row from an index outside the complete backing array.
tools/ti84re/emulators/wabbitemu/run_lcd_edge_probe.py runs the guarded dynamic matrix. It
requires the exact OS 2.55MP ROM, records the ROM and native-binary hashes, and
checks every field through tools/ti84re/emulators/wabbitemu/lcd_probe.py. The ROM is only an
initialized-core fixture in this mode; no TI-OS instruction executes.
tools/ti84re/emulators/wabbitemu/run_lcd_diagnostic_probe.py is a separate direct-entry mode.
It boots the exact ROM to its protection baseline, then executes
boot_lcd_initialize, boot_lcd_fill_pattern, boot_lcd_write_row, and
boot_lcd_write_contrast from an injected RAM harness.
Its manifest labels the run as direct entry and retains compact counters and
screen hashes rather than an instruction log.
tools/ti84re/emulators/mame/lcd.py parses the MAME state, pointer, latch, packing, and port-map
matrix against tools/ti84re/hardware/lcd_controller.py. The guarded CLI retains the exact
MAME, ROM, Lua-adapter, output, and evidence-scope identities:
mame_lcd_parent=$(mktemp -d /tmp/ti84-mame-lcd.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_lcd_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_lcd_parent/run" --json
Resolved findings and open hardware questions
- [confirmed] OS 2.55MP waits through port-
0x02bit 1 and does not busy-poll port0x10inlcd_wait. - [confirmed]
01:5A59and01:5A60write and read pixel data; they are not contrast helpers. - [confirmed]
_LCD_DRIVERONemits0x40,0x05,0x01,0x03, hardware-dependent power commands, and a RAM-derived contrast command. - [confirmed]
_ClrLCDFullcovers all 768 visible bytes with eight vertical bands. - [confirmed]
boot_lcd_keypad_diagnosticcovers all 12 visible columns, butboot_diagnostic_gateis constant-false. - [confirmed]
_GrBufClrchanges only RAM, while_GrBufCpyperforms the controller transfer. - [confirmed]
_PowerOffdisables display output before the ASIC enters low power. - [standard] Datamath attributes the photographed March 2004 module to Toshiba
T6K04, whose primary data sheet specifies 128×64 RAM. The die is hidden under epoxy, so the photograph does not independently expose its marking. - [hypothesis] The exact controller and off-screen RAM behavior still require per-calculator identification or measurement outside that source-attributed March 2004 module.
- [hypothesis] The late-controller status-read pointer mutation, power-command analog effects, and off-screen retention need physical tests across TA2/TA3 board revisions.
- [hypothesis] TilEm’s five-cycle LCD I/O overhead and 50-cycle controller busy period should be compared with bus captures rather than treated as hardware constants.
- [standard] Wabbitemu’s 15-column increment cycle and write-based ready timer, plus MAME’s unchecked 15-byte row, are emulator limits rather than hardware results.
- [standard] A guarded MAME run verifies its startup state, status and command decode, permanent busy-clear state, mirror ports, safe hidden-column aliases, dummy-read latch, 6-bit packing, stored analog fields, constant ASIC-ready bit, and missing delay ports.
- [hypothesis] Physical tests should sweep hidden columns and time readiness after reads and writes independently.
Sources
| Source | Used for |
|---|---|
WikiTI port 0x02 | ASIC LCD-ready bit and high-speed behavior |
WikiTI port 0x10 | status bits, commands, addressing, controller variants, busy-poll incompatibility, and power/test cautions |
WikiTI port 0x11 | pixel data, output latch, dummy reads, 6-bit transfers, and transfer delay |
WikiTI ports 0x12 and 0x13 | second-chip-select mirrors |
WikiTI port 0x2F | ASIC wait-duration fields and defaults |
| Datamath March 2004 TI-84 Plus module and LCD photograph | source-attributed T6K04 identity and the epoxy-covered module construction |
| Toshiba T6K04 data sheet, 2001-03-13 | exact 128×64 RAM, command and status tables, counter bounds, dummy reads, reset state, bus timing, oscillator choices, busy formula, supply range, and package; PDF SHA-256 e53bcf3f12c1cba2011b886ab196b6e1827aea4c1a2d9fb28c3d6d501d986577 |
| Toshiba T6A04A data sheet | compatible earlier 120×64 controller and family comparison |
TilEm lcd.c, calcs.c, x4_io.c, and x4_init.c | emulator video RAM, command decode, latches, port aliases, wait timers, and reset state |
Wabbitemu lcd.c, 83psehw.c, and calc.c | controller RAM, pointer movement, transfer guard, ASIC-ready calculation, port registration, and frontend reset scope |
MAME t6a04.cpp, ti85.cpp, and ti85_m.cpp | controller array and commands, TI-84 Plus port map, fixed ready bit, and driver status |
jsTIfied deployed 20170706a artifact and readable mirror | fourth controller model, 120-pixel storage, randomized busy interval, latch, and display physics |
Graphing
The graphing subsystem maps real coordinates to pixels, draws into
plotSScreen, and copies that buffer to the LCD. This page connects the
Y=, WINDOW, GRAPH, TRACE, and DRAW paths to their window
state, equation storage, and drawing primitives.
The interactive demo shows the confirmed sampling, coordinate
mapping, discontinuity break, and Circle( schedule while labeling its
browser-arithmetic boundary.
Window variables [confirmed]
All graph window state lives in a contiguous block of 9-byte TIFloats starting at 0x8F50.
These are the values the WINDOW editor writes and the grapher reads.
| Addr | Name | Meaning |
|---|---|---|
0x8F50 | Xmin | left edge real X |
0x8F59 | Xmax | right edge real X |
0x8F62 | Xscl | X tick spacing |
0x8F6B | Ymin | bottom edge real Y |
0x8F74 | Ymax | top edge real Y |
0x8F7D | Yscl | Y tick spacing |
0x8F86 | ThetaMin / 0x8F8F ThetaMax / 0x8F98 ThetaStep | polar/parametric range |
0x900D | XresO | Xres (pixel step between plotted columns) |
0x9151 | Xres_int | integer copy of Xres |
0x9152 | deltaX | (Xmax−Xmin)/94 — real width of one pixel column |
0x915B | deltaY | (Ymax−Ymin)/62 — real height of one pixel row |
0x9164 | shortX | reciprocal X scale used as a multiplier |
0x916D | shortY | reciprocal Y scale used as a multiplier |
0x913F | XFact / 0x9148 YFact | ZOOM IN/OUT factors |
There is a second “u” copy block at 0x8E7E (uXmin…uXres at 0x8F3B) — the
uVar window set used in the alternate (split/table) graph context, and a working/temp
float pair around 0x8E6A/0x8E73 used by the transform code. [confirmed]
Both contiguous 23-float blocks share one structural layout. The active copy is
activeWindow at 0x8F50; the alternate copy is userWindow at 0x8E7E:
typedef struct {
TIFloat x_min, x_max, x_scale, y_min, y_max, y_scale;
TIFloat theta_min, theta_max, theta_step;
TIFloat t_min, t_max, t_step, plot_start;
TIFloat n_max, u0, v0, n_min, u02, v02, w0;
TIFloat plot_step, x_resolution, w02;
} GraphWindowValues;
For example, activeWindow.x_min is the established Xmin address
0x8F50, while userWindow.x_min is uXmin at 0x8E7E. This member
notation distinguishes which window copy a routine uses without dropping the
official RAM labels. [confirmed]
deltaX and deltaY are the per-pixel steps used by graph sampling and DRAW
routines. The forward transform instead multiplies by shortX or shortY.
The LCD is 96×64, but the full graph scale spans columns 0–94 and 62 vertical
intervals; _YftoI represents those as bottom-up coordinates 1–63. Hence the
window setup’s divisions by 94 and 62. [confirmed]
Coordinate-to-pixel transforms
Forward: real coordinate → pixel index
_XftoI (37:41EB) and _YftoI (37:41DF) take a pointer in DE to a
9-byte TIFloat and return a pixel index in A. Both select operands for the
shared engine at 37:41F2: [confirmed]
_XftoI (37:41EB): BC = 0x8E6A, HL = shortX (0x9164), SCF → 37:41F2
_YftoI (37:41DF): BC = Ymin, HL = shortY (0x916D), OR A → 37:41F2
INC A
The core loads *DE, subtracts the selected base through ram:228F, and
multiplies by the selected reciprocal through ram:2385. The X path then adds
the origin term at 0x8E73. In compact form: [confirmed]
scaled = (*DE - *base) * *reciprocal_scale
if axis == X:
scaled += *0x8E73
A = graph_round_coordinate_magnitude(scaled)
if axis == Y:
A = (A + 1) & 0xFF
The finishing routine at 37:4229 is sign-agnostic. Magnitudes below 0.5
become zero; values from 0.5 through the two-digit range round half upward by
packed-BCD addition. Three- and four-digit values receive an out-of-frame bias
before conversion. A mantissa carry writes the canonical 10 00 prefix and
increments the exponent. _ConvOP1 (38:7433) then converts up to four
integer digits into DE, returns E in A, and raises a dimension error for
an exponent above 0x83. [confirmed]
tools/js/graph-coordinate.js translates this operand order and finishing path.
Its test pins the three ROM spans and compares 220,000 packed-BCD OP1 states
against an independent transcription. [confirmed]
The Y result is a 1-based, bottom-up graph coordinate. A reset-origin Y1=X²
trace maps $y=8.872793118$ to A=60 with Ymin=-10 and shortY=3.1.
_IOffset later mirrors it for the LCD controller with 0x3F - y.
[confirmed]
A function value y at sample x becomes a (column, row) pair through these
subtract-and-multiply transforms. The same conversion serves graph plotting
and TRACE coordinate display. [confirmed]
Inverse: pixel index → real coordinate
_SetXXOP1 (33:5F7E) and _SetXXOP2 (33:5F83) take an integer pixel value in A
and build a real TIFloat in OP1 / OP2 (0x8478 / 0x8483). [confirmed]
CALL 1BA7zeroes the destination mantissa,CALL 5F6Aconverts the binary value to packed BCD by repeatedADD A,0x16 / DAA(binary→decimal nibble accumulation), looping A times,- the exponent byte is set so OP1 holds the integer;
_SetXXXXOP2(33:5F9E) is the 4-digit (up to 9999) variant for larger pixel/coordinate counts.
These are used to turn a pixel column/row (e.g. under the TRACE cursor) back into the real X/Y shown at the bottom of the screen, and by DRAW commands that take pixel arguments.
Graph buffer and pixel addressing
plotSScreen=0x9340, 768 bytes = 96×64/8. Monochrome, 1 bit/pixel, 12 bytes per scanline (8 pixels per byte). This is the back buffer everything draws into. [confirmed]saveSScreen=0x86EC, 768 bytes — a saved copy (e.g. for redrawing the graph after a menu covers it). [confirmed]
_GrBufClr (04:6071): clears the whole 0x300-byte buffer to 0 (a
LD (HL),0 + 0x2FF-byte propagate copy). [confirmed]
_IOffset (04:42B5) computes the LCD controller address bytes for a pixel (inputs B=x, C=y):
(0x844F) = (x >> 3) | 0x20 # LCD byte-column command for the horizontal group
(0x8451) = (0x3F - y) | 0x80 # LCD row command, vertically mirrored
returns (table_42E4)[x & 7] # the 1-of-8 bit mask within the byte (bit = x mod 8)
HL = 3 * ((4 * display_row) & 0xFF) + (x >> 3)
This maps a (x,y) pixel to a byte+bit in the buffer and produces the matching LCD
command bytes. Adding HL to 0x9340 addresses plotSScreen; adding it to
0x9872 addresses appBackUpScreen. [confirmed]
_IPoint (04:4157) applies one of four byte operations selected by D: 0
clears the mask, 1 sets it, 2 XORs it, and 3 tests it without writing.
_PointOn (04:4155) fixes D=1. The style path at 04:4173 can emit adjacent
points before the byte operation; the drawing hook at 04:415A can replace the
normal path. [confirmed]
The normal path routes the byte through (IY+3Ch) and plotFlags.1 at
(IY+02h). Routing bit 3 selects appBackUpScreen without LCD I/O. Bit 0
selects the corresponding direct plotSScreen route. When both are clear,
the routine reads and writes the LCD controller. plotFlags.1 preserves
plotSScreen and uses the LCD byte as the source; clearing it uses and rewrites
the RAM byte. Routing bit 2 also stores the result in appBackUpScreen.
[confirmed]
_PixelTest (04:79E7): the pxl-Test( command — validates the row/col against the
current graph dimensions lcdTallP (0x8DA3) and pixWide_m_1 (0x8DA5) — 63 and 95 on a
full screen, smaller when split — maps the split-screen offset, and returns whether that buffer
pixel is on. _ErrDomain on out-of-range. [confirmed]
Drawing primitives
Lines
_ILine (04:4029) — integer pixel line via Bresenham. [confirmed]
It computes dx=|x2−x1|, dy=|y2−y1|, picks the major axis, sets the error term
(dy−dx)*2/dy*2, then loops _IPoint for each step, advancing the minor axis when the
error crosses zero. graph_chk_flag20 (04:4316) is the step-along-major-axis helper. The endpoint and
draw-mode (set/clear/xor) are passed in. _DarkLine (04:4025) is _ILine with
the “draw/dark” mode forced. [confirmed]
_CLine/_CLineS (33:6028/33:6034) and _UCLineS (33:6010) — coordinate
line: take real-coordinate endpoints, run them through the X/Y transforms (the SetXX/ftoI
path), then call the integer line. The S variants take an explicit style/mode byte; the
mode bit comes from (IY+0x35) & 0x80 (hookflags3 bit 7, the drawing-hook-active flag —
not a split-screen flag). These back the Line(
DRAW command at the math layer. [confirmed]
Circle
_CircCmd (33:74CE) is the parser-facing Circle( command wrapper. Its
dispatch at 33:74DF tests (IY+0x3C).4 and selects one of two segment
generators. [confirmed]
A reset-origin TilEm trace of Circle(0,0,5) observed the flag clear at
0x8A2C. That state selected the page-33 generator at 33:74E9; its loop at
33:7506 ran 61 times and its emit point at 33:7561 called _CLine exactly
60 times. All 59 adjacent endpoint pairs matched byte for byte. The first
segment went from $(5,0)$ to approximately
$(4.9726094768414,0.52264231633825)$, a six-degree step. The final
plotSScreen contained 306 set pixels. [confirmed]
The trace did not enter _GrphCirc (33:758D), _DrawCirc2
(3B:7171), or the coefficient lookup at 35:79E9. It therefore establishes
the clear-flag page-33 path only. [confirmed]
The _GrphCirc body allocates a 0x5A-byte floating-point frame. It preserves
the working coordinate and window values, prepares the circle state, and calls
the same dispatch at 33:74DF. The user-visible state that sets the tested
flag and selects _DrawCirc2 remains open. [confirmed]
The separate _DrawCirc2 body allocates 0xA2 bytes, or 18 TIFloats. Its
seven-iteration loop makes eight calls per iteration to the point-pair helper
at 3B:72F3, followed by four closing calls at 3B:730E: 60 _CLine calls
in total. The helper preserves the old point in OP3/OP4, stores the new OP1/OP2
point into the frame, calls _CLine at 33:6028, and advances the frame
pointer by 18 bytes. [confirmed]
The loop consumes seven consecutive constants at 35:79F5, alternating sine
and cosine values for 6°, 12°, 18°, and finally sine 24°. The adjacent
cosine 24° constant is present but is not consumed by this loop. The static
schedule and helper ABI are checked in tools/ti84re/graphing/circle.py; the latter is
compared with the pinned helper bytes for all 65,536 16-bit test seeds.
[confirmed]
DRAW menu commands (page 0x04 handlers)
Each DRAW menu command has a page-04 bcall handler that draws into plotSScreen:
| bcall | Addr | Command |
|---|---|---|
_HorizCmd | 04:793E | Horizontal y — draws a full-width horizontal line at real Y. See note below. |
_VertCmd | 04:7955 | Vertical x — draws a full-height vertical line at real X. See note below. |
_LineCmd | 04:796A | Line(x1,y1,x2,y2) — _PDspGrph, optionally draws via page 33, then JP 0x152A = _DeallocFPS1(0x24) frees the coord frame (the alloc happens upstream). |
_UnLineCmd | 04:797C | Line(…,0) — erase variant (same path, clear mode). |
_PointCmd | 04:79B2 | Pt-On/Pt-Off/Pt-Change( — reads style from OP1.value.mantissa[0] & 0x20, dispatches set/clear/toggle. |
_DrawCmd | 04:7B8B | top-level DRAW dispatch — grabs the pending count and cross-jumps to the per-command handler. |
draw_zero_op1 | 04:620B | seeds OP3=0 then draws (used for axis / DrawF zero baseline). |
Note: _HorizCmd/_VertCmd both CALL 7933 first, which allocates a 0x24-byte FPS frame
(LD HL,0x24 / CALL 1537 / SBC HL,DE) and returns a pointer to it. _HorizCmd then builds the
line’s two endpoints in that frame: it copies Xmin (0x8F50) and Xmax (0x8F59) — the window’s
X range — with _Mov9B (00:1A92, which reads a window float into the frame), interleaving the
line’s Y (OP1) via _MovFrOP1 (00:1B0C), so the endpoints are (Xmin, y) and (Xmax, y).
_VertCmd does the same with Ymin (0x8F6B)/Ymax (0x8F74) and the line’s X. It renders with
_PDspGrph, then _DeallocFPS1(0x24) frees the frame — the window variables are read only,
so the line just spans the current window edges. [confirmed]
Rendering the graph to the LCD
_PDspGrph (04:7904, “possibly-display graph”) decides whether to copy the buffer to the
screen and whether a full re-plot is needed first. [confirmed]
- Clears the “need redraw” flag at
(IY+2), - if the graph-dirty bit
(IY+3)&1is set (graphFlags.graphDraw, incgraphFlags=3/graphDraw=0;1=redraw needed — this is thegraphFlagsbit atIY+3, distinct fromgrfDBFlagsatIY+4and SmartGraph atIY+0x17), calls_Regraphto recompute the whole plot, - otherwise checks the split-screen flag (
_Bit_VertSplit) and copies the buffer to the LCD (graph_redraw_buf04:607F).
_GrBufCpy (04:60A3) blits plotSScreen to the LCD: handles split-screen
(_CheckSplitFlag, _Bit_VertSplit), draws the split divider line (_DarkLine/_ILine at
column region 0x2F), sets normal display vals, and walks the rows. [confirmed]
_RestoreDisp (04:6176) is the actual row-blit loop: for each of the up-to-64 rows it
issues the row and byte-column LCD commands, then streams pixel bytes to port_lcdData (0x11)
through lcd_wait, and pokes port_lcdCmd (0x10). This is where the buffer physically
reaches the panel. [confirmed]
_Regraph (04:6764) begins by enabling interrupts and calling the relocated
bjump thunk at ram:3F27. Its observed bytes, CD 09 2B 18 65 01, target
_RunIndicOn at 01:6518. _Regraph then prepares the window state, clears
plotSScreen, dispatches the active graph mode, and finishes at 04:6985.
SmartGraph (grfModeFlags.smartGraph) can bypass this work and copy the
existing buffer when the graph state is still valid. [confirmed]
The function-mode path has this observed shape:
flowchart LR
A[Regraph setup] --> B[Clear plotSScreen]
B --> C[Select equation]
C --> D[Prepare sample X]
D --> E[Parse and evaluate Y]
E --> F{Point is drawable?}
F -->|yes| G[X/Y transforms]
G --> H[ILine or IPoint]
F -->|no| I[Break the segment]
H --> J[Advance curInc by Xres]
I --> J
J -->|more columns| D
J -->|done| K[Next selected equation]
K -->|done| L[Return with plotSScreen]
graph_advance_sample_column (04:69CF) compares curInc (0x8E67) with
pixWide_m_2 (0x8DA6). It returns without carry at the edge; otherwise it
adds Xres_int (0x9151), stores the next column, and returns with carry.
[confirmed]
Y= equation storage and evaluation
Storage [confirmed]
Y= functions are ordinary equation variables (EquObj), stored in the VAT as tokenized
byte streams — the same token encoding the homescreen uses. Y1…Y0 (and r1…, X1T/Y1T,
u/v/w) are system equation vars. Each holds the tokens you typed after Y1=. The
equation’s flags byte is 0x23 when selected (plotted) and 0x03 when deselected, so
the selection bit is bit 5 (0x20). The per-equation style byte holds the line style:
0=line, 1=thick, 2=shade above, 3=shade below, 4=trace/path, 5=animate, 6=dotted
(curGStyle 0x8D17 is the current-equation copy). [confirmed] The
selection/style byte values also match the
TI link-protocol guide.
Variable-version scan
_GetVarVersion (33:5023) walks a tokenized variable through
_SetupPagedPtr/_PagedGet, recognizes two-byte tokens with _IsA2ByteTok,
and raises the returned compatibility tier for particular 0xBB and 0xEF
token ranges. This compatibility scan is not evidence for a graph-mode
graphability pre-scan. [confirmed]
Evaluation → points
The function-mode loop prepares each sample at 04:710F. Its traced parser
entry is parse_init_findsym (38:5975), which initializes parser state and
joins the shared evaluator tail at 38:59A4. Evaluation passes through
eval_eqn_recursive (38:778F) and eval_eqn_finish_typecheck (38:77C2).
Official _ParseInp at 38:5987 is a sibling entry with additional state
cleanup; neither graph trace executes it. [confirmed]
tools/oracles/graphing/graph-regraph.json records ROM and TilEm provenance, raw-trace hashes,
and final-buffer hashes for two reset-origin TilEm traces: [confirmed]
| Function-mode observation | Y1=X² | Y1=X⁻¹ |
|---|---|---|
post-entry _Regraph instruction span | 3,951,185 | 4,316,730 |
sample advances, curInc=0–94 | 95 | 95 |
parse_init_findsym entries | 190 | 190 |
| completed recursive evaluations | 190 | 188 |
divide-by-zero entries at ram:26EC | 0 | 2 |
post-dispatch _ILine calls | 30 | 94 |
| pixel-byte writes after the 768-byte clear | 296 | 402 |
set pixels in final plotSScreen | 261 | 266 |
The reciprocal trace reaches the divide-by-zero entry with curInc=46 and
again with curInc=47. The left segment ends at column 46; drawing restarts
with a zero-length seed at column 48, and no _ILine call bridges column 47.
The two missing evaluator completions therefore correspond to a visible break,
not a line across the asymptote. [confirmed]
These traces cover line style 0, Xres=1, and one selected equation. They do
not establish the thick, shade, trace, animate, or dotted paths; Xres>1;
multiple selected equations; or other graph modes. tools/ti84re/graphing/analyze_regraph.py
regenerates the compact report from raw TLMT traces, which remain outside the
repository.
Graph databases (GDB) [confirmed]
_StoGDB2 (33:71AC) / _RclGDB2 (33:72D9) store/recall a GraphDataBase
(GDBObj, type/exp marker 0x61) — the bundle of window vars + mode + selected equations
that the StoreGDB/RecallGDB commands save. _JError(0x89) on a type mismatch.
Indexed pointer helpers [confirmed]
_PUT_INDEX_LST (33:7066) and _GET_INDEX_LST (33:707A) store and load
2-byte slots at iMathPtr4 + 2n; _HEAP_SORT (33:7097) sorts a
caller-supplied indexed range. Their bodies do not establish a selected-equation
list or show that Regraph and TABLE share one iterator.
Graph, home-screen, and TRACE paths
- The home screen uses the large font and
curRow/curColtext cursor (see display-lcd.md). The graph screen is the pixel bufferplotSScreenrendered by the routines above; small-font labels (coords, TRACE readout) go through_VPutMap/penCol(0x86D7)/penRow(0x86D8). [confirmed] - TRACE moves a cursor along a selected function: it steps the column, evaluates the
function for that X, maps the point with
_XftoI/_YftoI, draws the cross-cursor, and uses_SetXXOP1/_SetXXOP2to convert the cursor pixel back to the real X/Y it prints at the bottom. The exact TRACE-side evaluator entry has not yet been traced. [confirmed] - A
DRAWcommand (_DrawCmd) orLine(/Circle(/Pt-On(draws straight intoplotSScreenover the current plot and persists across a SmartGraph redraw (it is not re-evaluated) untilClrDrawis issued. [confirmed]
Evidence summary and open items
- The forward transforms, coordinate rounding, and
_ConvOP1boundary are byte-pinned and differentially tested. Reset-origin X and Y witnesses confirm the pointer ABI and returned indices. [confirmed] - Reset-origin function-mode traces cover
Y1=X²andY1=X⁻¹with line style 0,Xres=1, and one selected equation. They do not cover the other styles,Xres>1, multiple selected equations, or alternate graph modes. [confirmed] - The page-33
Circle(generator is dynamically observed._DrawCirc2has a byte-pinned static schedule, but no reset-origin trace has selected it. [confirmed] _HorizCmdand_VertCmdbuild their endpoints from the live window edges and the command coordinate; they do not modify the window variables. [confirmed]- The Y= selection bit (
0x20; flags byte0x23selected /0x03deselected) and style values0–6agree with the TI link-protocol variable guide. [confirmed]
Table and Y= variables
The table subsystem stores equations from Y=, reads settings from TBLSET, evaluates selected equations row by row, caches their values, and paints the TABLE grid. This page covers automatic and prompted independent and dependent values as well as split graph-table mode.
The subsystem shares equation storage with Graphing, parser entry points with TI-BASIC execution, VAT objects with Variables and the VAT, and text output with Display and LCD.
Subsystem components
flowchart TB
TBLSET["TBLSET screen · page 37 + 02<br/>TblMin 92B3 / TblStep 92BC<br/>tblFlags IY+19: autoFill / autoCalc / reTable"]
YEQ["Y= equations in VAT<br/>EquObj tokens · tY1..tY0"]
PARSER["parser · page 38<br/>_ParseInp / parse_eval_expr"]
subgraph GEN["TABLE generator · page 05 — per X row"]
direction TB
S1["1 · X = TblMin + k·TblStep"]
S2["2 · running-X staged in OP registers"]
S3["3 · evaluate each selected Y= → OP1"]
S4["4 · format OP1 → cell string"]
S5["5 · write into table data cache"]
S6["6 · paint cache as text grid on LCD"]
S1 --> S2 --> S3 --> S4 --> S5 --> S6
end
TBLSET -->|settings| S1
YEQ -->|_Find_Parse_Formula| S3
PARSER --> S3
The TABLE feature reuses the Y= storage and the same page-38
recursive-descent evaluator the grapher and homescreen use; it adds only
(a) the running-X driver from
TblMin/TblStep, (b) a RAM value cache so scrolling doesn’t recompute, and
(c) a text-grid renderer. [confirmed]
TABLE settings
System variables (RAM TIFloats) [confirmed]
| Addr | Name | Meaning | Token |
|---|---|---|---|
0x92B3 | TblMin (a.k.a. TblStart) | first independent value in the table | tTblMin/TBLMINt = 0x1A |
0x92BC | TblStep (ΔTbl) | increment between successive rows | tTblStep/TBLSTEPt = 0x21 |
Both are 9-byte floats. They are ordinary system token variables: read/written
through _RclSysTok (38:683E) / _StoSysTok (38:623B) using the token bytes
above (the page-38 system-var token table lives around 38:61F1). ΔTbl’s token
is the list-step token 0x21; TblStart uses 0x1A. [confirmed]
Mode flags — tblFlags (IY+19 = IY+0x13) [confirmed]
From ti83plus.inc and verified by the bit-ops below:
| Bit | Name | Meaning |
|---|---|---|
4 (0x10) | autoFill | Indpnt: 0 = Auto (fill X from TblStart/ΔTbl), 1 = Ask (prompt for each X) |
5 (0x20) | autoCalc | Depend: 0 = Auto (compute Y immediately), 1 = Ask (compute a cell only on request) |
6 (0x40) | reTable | 0 = cached table valid, 1 = must recompute the table |
TBLSET key and edit handler [confirmed]
The page-02 command/mode handler that backs the TBLSET screen edits the two
floats and the two mode rows. A retired helper label at 02:7B31 is not a live function in the current DB, but the byte sequence there decodes as:
02:7B31 RES 4,(IY+0x13) ; default Indpnt = Auto (autoFill=0)
02:7B35 SET 6,(IY+0x13) ; reTable = 1 → table is now dirty
RET
02:7B3A BIT 4,(IY+0x13) … ; toggle helpers for the menu rows:
SET 4,(IY+0x13) ; Indpnt = Ask
RES 5,(IY+0x13) ; Depend = Auto
SET 5,(IY+0x13) ; Depend = Ask
So changing any TBLSET field (TblStart, ΔTbl, Indpnt, or Depend) sets
reTable, forcing a full recompute next time the TABLE is shown. [confirmed]
TBLSET display and validation context [confirmed]
The TABLE-setup screen logic lives on page 37 (the menu/editor display page).
37:5F10 reconciles the on-screen Indpnt/Depend selection against the stored
tblFlags: it compares tblFlags bit4 (autoFill) vs a UI-selection bit
(IY+0x16 & 0x40) and bit5 (autoCalc) vs IY+0x11 & 0x40, and when either
differs it sets reTable (SET 6,(IY+0x13)). It also zeroes the table-top
row index 0x91E0 when Indpnt flips to Ask. Companion sites: 37:5F2B
(BIT 5 autoCalc), 37:5F59/37:5F94 (re-reads). [confirmed]
Y= equation storage, selection, and style
Storage [confirmed]
Y1…Y9, Y0 are system equation variables, VAT objects of type EquObj = 3
(ti83plus.inc: EquObj EQU 3; NewEquObj=0x0B, UnknownEquObj=0x0A). Each holds
word size + size bytes of the tokenized formula you typed after Y1= — the
same token encoding the homescreen and program editor use (see sub-tibasic.md).
The equation name in OP1 is the 2-byte token sequence tVarEqu (0x5E) + the
Y-token:
| Var | Token | Var | Token | |
|---|---|---|---|---|
Y1 | 0x10 | Y6 | 0x15 | |
Y2 | 0x11 | Y7 | 0x16 | |
Y3 | 0x12 | Y8 | 0x17 | |
Y4 | 0x13 | Y9 | 0x18 | |
Y5 | 0x14 | Y0 | 0x19 |
(Parametric X1T/Y1T=0x20/0x21…, polar r1…, and u/v/w sequences share the
same EquObj/tVarEqu machinery.) [confirmed]
Selection and style flags [confirmed]
Each equation’s flags byte is 0x23 when selected (plotted / tabulated) and
0x03 when deselected — i.e. the selection bit is bit 5 (0x20). The separate
per-equation style byte encodes the line style: 0=line, 1=thick, 2=shade above,
3=shade below, 4=trace/path, 5=animate, 6=dotted. The TABLE iterates the
same selected set the grapher plots, so deselecting Y2 in the Y= editor (or
clearing its = highlight) removes its column from the table. curGStyle
(0x8D17) holds the in-progress style; sGrFlags bit g_style_active
(IY+20 bit5) enables per-equation styles. The graphing doc covers the plot side; the
table only reads the selection bit to decide which columns exist. [confirmed]
The values also match the
TI link-protocol var guide.
Indexed pointer helpers — iMathPtr4 (0x84D9) [confirmed]
Two official bcalls address a RAM array of two-byte values based at iMathPtr4
(0x84D9). A third official bcall sorts a caller-supplied range:
| bcall | Addr | Role |
|---|---|---|
_PUT_INDEX_LST | 33:7066 | store a value in slot n at 0x84D9 + 2n |
_GET_INDEX_LST | 33:707A | load the value in slot n through _LdHLind |
_HEAP_SORT | 33:7097 | sort an indexed caller-supplied range |
The helper behavior is [confirmed], but the bodies do not identify what every
caller stores in the array. _HEAP_SORT does not discover selected equations.
The builder and consumer for the TABLE editor’s selected-Y set remain
[hypothesis].
Resolving and evaluating a Y-var [confirmed]
_Find_Parse_Formula (bcall ID 4AF2h) is the universal “find a named var and
parse/evaluate its stored formula” entry in TI-BASIC expression evaluation. For a Y-var it
_FindSyms the EquObj, points the parse cursor at its token body, and runs the
page-38 evaluator, leaving the result in OP1. The 38:758A entry seen here is a
thin RST2 bcall trampoline; the body switches on var type (Window 0x0F /
ZSto 0x10 / TblRng 0x11 special-cased) before the cross-page parse — i.e.
the table range is itself handled as a special “formula” type by this resolver.
It is bcalled from 03:67C0 (the Y= equation editor) and 33:7720 (graph setup).
Homescreen Y1(2) evaluates through this same path: the parser sees tVarEqu tY1,
resolves the EquObj, substitutes the argument as X, and evaluates. [confirmed]
Table generation
Page 0x05 is the TABLE editor / Graph-Table subsystem. All references to
TblMin/TblStep and to the table column-data pointers (XOutDat 0x918E,
YOutDat 0x9192) concentrate on page 05, and the page’s tblFlags bit-ops
(reTable, autoFill, autoCalc) drive the recompute/scroll logic. The TABLE
editor is installed as a context (cxTableEditor = 0x4A, ti83plus.inc),
selected from [2nd][GRAPH] via the key→context router (11-boot-contexts);
its handler vectors run on page 05.
Editor main display — table_editor_main (05:5D0D) [confirmed]
if (tblFlags & 0x40 /* reTable */) recompute = table_recompute() ; 05:5DD7
else use_cache = table_use_cache() ; 05:78CF
if (graphFlags & 1) { redraw helpers ... } ; split-graph case
paint_grid(...) ; 05:7771
So on every entry to the TABLE the editor checks reTable: if dirty it runs the
recompute driver, otherwise it repaints from the cached values. [confirmed]
Recompute driver — table_recompute (05:5DD7) [confirmed]
05:5DD7 XOR A
LD (0x8E63),A ; reset table column/row state
CALL 0x3411
RET Z ; window/mode gate
CALL 0x7704 ; init column descriptors (see below)
CALL 0x774B ; seed running-X = TblMin
CALL 0x65D2
CALL 0x65C8 ; clear per-column flags
CALL 0x5EE1 ; FILL the value cache (the row loop)
CALL 0x6014
CALL 0x5FFC ; lay out / size the columns
RES 6,(IY+0x13) ; reTable = 0 (cache now valid)
CALL 0x76BA
After a successful recompute it clears reTable, so subsequent scrolls reuse
the cache until something marks it dirty again. [confirmed]
Seeding the independent value [confirmed]
05:774B initialises the two-float table_x_work array. It first clears
table_x_work[0] (0x8622), then at 05:7751 copies TblMin into
table_x_work[1], the running-X slot:
05:7751 LD HL,0x92B3 ; TblMin
LD DE,0x862B ; running-X destination
JP 0x1A92
i.e. the first row’s independent value is TblStart. The row index is bounds-checked at
05:65DC by comparing the current row with the last row:
LD A,(0x91E0)
LD HL,CurTableRow
CP (HL)
RET
The per-row X is computed as TblStart + k·TblStep rather than by an
incremental add:
05:65DC LD A,(0x91E0)
LD HL,0x91DC
CP (HL)
RET ; row-index bound check
05:6359 LD A,(0x91DD) … LD DE,0x9221 / 0x91E2 (cell buffers)
LD HL,(0x91DC /* row idx */)
ADD
CALL _LdHLind
ADD HL,DE
So row $k$ uses $X=\mathrm{TblMin}+k\cdot\mathrm{TblStep}$. (In Indpnt = Ask mode this driver is bypassed and the user types each X; see Auto and Ask modes.) [confirmed]
Per-row evaluation [confirmed]
For each visible row the recompute fills the cache:
- Store the row’s X value in a cache slot and stage it through OP1/OP2. The
running X remains in OP registers and FPS slots rather than passing through
_StoXfor each row. - Evaluate each selected equation against the current X through bcall ID
4741h. Its body at35:7C7Cdrives the page38parser cluster:parse_init(38:5B7B),fps_alloc_to_9652(38:5B10),38:5ADA, and the_ParseInpregion at38:5987. The result remains in OP1. - Format OP1 and store the result in the row’s cache slot.
The fill driver at 05:6205 loads B = 7 for the visible rows and increments
CurTableRow (0x91DC) on each iteration. It pushes a cleanup handler through
ram:27DA, calls the evaluator once per selected equation, and stores results
through 05:6284 and 05:629B. A headless TilEm trace of Y1=X² with default
TBLSET executes the _ParseInp region seven times, once per row. Between
consecutive rows, execution passes through parse_init and fps_alloc_to_9652.
The trace does not execute _StoX (38:62A3) during the fill. [confirmed]
The cache-clearing preamble is table_fill_cache_loop (05:5EE1): it strides
table_value_cache.band[0] at 0x91E2 in 9-byte (TIFloat) steps for up to 7
visible columns (LD C,0x07), keyed off the top-row index 0x91E0. The X
column itself is written from the running-X; the Y columns from the evaluated
OP1. [confirmed]
Value cache and scrolling [confirmed]
The table keeps the visible window of computed values in a RAM cache so that scrolling is instant (no recompute):
| Addr | Role |
|---|---|
0x918C XOutSym / 0x918E XOutDat | X column: symbol + data pointer |
0x9190 YOutSym / 0x9192 YOutDat | active Y column: symbol + data pointer |
0x9194 inputSym / 0x9196 inputDat | the “Ask”/input column descriptor |
0x9198 prevData | previous-column data pointer |
0x91DB | unnamed Ask-mode row state |
0x91DC / 0x91DD | CurTableRow / CurTableCol |
0x91E0 | table-top state; exact role remains open |
0x91E2 | table_value_cache, the per-cell computed-value bands |
The three contiguous 63-byte regions at 0x91E2, 0x9221, and 0x9260
form one typed cache:
typedef struct {
TIFloat value[7];
} TableCacheBand; /* 0x3F bytes */
typedef struct {
TableCacheBand band[3];
} TableValueCache; /* 0xBD bytes at table_value_cache */
Thus 0x9221 is table_value_cache.band[1], and 0x9260 is
table_value_cache.band[2]. This notation captures both the 9-byte element
stride and the 63-byte scroll-copy stride. [confirmed]
05:6014 performs the scroll:
LD HL,0x9221
LD DE,0x9260
LDIR
This copies
table_value_cache.band[1] to band[2] (a 0x3F-byte block) and performs an
LDDR shift of a 0xB4-byte region. When the cursor moves above or below the
cached window, it slides the cache and computes only the one new row
(or recomputes if reTable). [confirmed]
Auto and Ask modes [confirmed]
05:6D40/05:6D51 read the mode bits to branch:
05:6D40 … CALL 0x74BE
JR NZ
BIT 4,(IY+0x13)
RET ; Indpnt (autoFill) test
… CALL 0x74BE
JR NZ
BIT 5,(IY+0x13)
RET ; Depend (autoCalc) test
LD A,(0x91DB) … LD A,(0x91DC) … ; Ask-mode row state
- Indpnt = Auto (bit4=0): the driver auto-fills X from TblStart/ΔTbl as described under Seeding the independent value.
- Indpnt = Ask (bit4=1): the X column starts empty. The per-row prompt body
at
05:6DFFcalls the Indpnt test at05:6D4Cand invokes the entry-line editor at05:7303. The editor pushes continuation05:7329onto the OPS stack throughram:27DAand enters setup at05:5F64and05:5F51. On success,05:6032shifts thetable_value_cacheband and enters row evaluation at05:615C. [confirmed] - Depend = Auto (bit5=0): Y cells compute immediately during the fill.
- Depend = Ask (bit5=1): the gate at
05:6DD1tests bit 5 through05:6D67and05:6D56. In Ask mode,05:69D2checks cell state at0x91CEand0x8D1B, then calls05:637Cfor one deferred evaluation. That routine pushes continuation05:644Eonto the OPS stack throughram:27DAand runs the cell expression through the standard OPS machinery. [confirmed]
The mode tests at 05:6D4C and 05:6D56 first call 05:74BE. A nonzero result
bypasses the (IY+0x13) bit tests. This override behavior is [confirmed], but
the condition detected by 05:74BE remains [hypothesis].
Grid rendering [confirmed]
The table is a text grid (not the pixel graph buffer): up to 8 visible rows ×
columns, drawn with the large font through the home-screen text primitives
(_PutMap/_PutC, display-lcd.md). The paint loop:
05:7E45 loop over visible rows:
CALL 0x7E7C ; position/clear the cell (selects buffer
; 0x9221 for one column or 0x91E2 for the other)
CALL 0x7E9D
LD DE,(0x9192 YOutDat) ; the Y-column value pointer
CP 2 / CP 5 ; column-kind dispatch (X col vs Y col vs input)
CALL 0x7E7C
CALL 0x7E98 ; render the cached value into the cell
CALL 0x65DC ; row-index bound check (current row vs last)
INC row
…
LD (0x91DC),A
05:7E7C chooses the destination cache band (table_value_cache.band[1] at
0x9221 versus band[0] at 0x91E2) based on the
column index, and writes 0xFF/blank sentinels for empty (Ask) cells. The bottom
status line and the highlighted-cell full-precision readout reuse the same value
cache. [confirmed]
Split graph-table mode [confirmed]
The G-T mode (graph on the left half, table on the right) is set up by
screen_split (bcall 0x5227): it checks the split flag, calls _Bit_VertSplit,
then 05:7544 and the table-init 05:773F (seed running-X from TblMin), and
cross-jumps to redraw. G-T mode shares the table cache and running-X
driver, rendered into the right columns alongside the plot. [confirmed]
Table invalidation through reTable [confirmed]
Anything that could change a tabulated value sets tblFlags bit6, forcing the
next TABLE view to recompute:
| Site | Trigger |
|---|---|
02:7B35 bytes | editing TblStart/ΔTbl/Indpnt/Depend in TBLSET |
37:5F3D | toggling Indpnt or Depend on the setup screen |
38:6340, 38:4809, 38:54CD | the parser storing into a Y= equation or a relevant var (editing Y1=…, →Y1, or changing X/window) |
boot / reset (RAM clear) | initialises the table as dirty (reTable set); the exact init site is not pinned here (00:4105 is the “Resetting All…” message string, not the setter) |
Conversely only the recompute driver clears it (05:5DD7, 05:62FD,
05:64DE → RES 6,(IY+0x13)). [confirmed]
End-to-end example: tabulating Y1=X² + 1
- Y=: types
X²+1afterY1=. The editor tokenizes it and stores the bytes as theEquObjY1(token5E 10) in the VAT, with its flags byte’s select bit set (the=is highlighted). The parser store path setsreTable. - TBLSET (
2nd WINDOW): setsTblStart=0(TblMin0x92B3),ΔTbl=1(TblStep0x92BC),Indpnt:Auto,Depend:Auto. Each edit setsreTable(the setup bytes around02:7B35). - TABLE (
2nd GRAPH): enters contextcxTableEditor(0x4A) on page 05.table_editor_main(05:5D0D) seesreTable=1→table_recompute(05:5DD7):- seed running-X ←
TblMin(05:774B), - walk the selected equation set — here only
Y1; the exact builder and iterator remain open, - per row: stage the running-X through OP1/OP2, evaluate
Y1’s tokens via bcall ID4741h→35:7C7Cand the page38parser cluster (_Find_Parse_Formula/_ParseInp) → OP1 =X²+1, format and stash intotable_value_cache.band[0]/band[1], - advance to the next row (bound-checked at
05:65DC; X =TblStart + k·TblStep) and repeat, - clear
reTable.
- seed running-X ←
- The grid paints (
05:7E45) the cachedXandY1columns as large-font text; scrolling (05:6014) slides the cache and computes only newly exposed rows. - Deselecting
Y1(or editing the formula, or changingΔTbl) setsreTableagain and the next view recomputes.
Routine and state index
; --- TABLE setup settings & flags ---
RAM 92B3 TblMin / TblStart ; first independent value (sys float)
RAM 92BC TblStep / ΔTbl ; row increment (sys float)
IY+19 b4 tblFlags.autoFill = Indpnt Auto/Ask
IY+19 b5 tblFlags.autoCalc = Depend Auto/Ask
IY+19 b6 tblFlags.reTable = table-dirty
02:7b20 tblsetup_handler ; TBLSET key/edit handler
02:7b35 (retired label; no live function in the current Ghidra DB)
37:5f10 tblset_cx_display ; TBLSET screen reconcile → reTable
; --- TABLE editor / generator (page 05) ---
05:5d0d table_editor_main ; reTable? recompute : use cache; paint
05:5dd7 table_recompute ; seed X, fill cache, clear reTable
05:774b table_seed_runX_from_TblMin ; runningX(0x862B) ← TblMin
05:773f table_seed_runX_from_TblMin2 ; same, split-graph path
05:65dc table_row_bound ; row-index bound check (91E0 vs (91DC))
05:5ee1 table_fill_cache_loop ; fill table_value_cache.band[0]
05:6014 table_scroll_cache ; slide cell cache on scroll (LDIR/LDDR)
05:6d40 table_mode_test ; BIT autoFill/autoCalc (Auto vs Ask)
05:7e45 table_paint_grid_loop ; render cached cells as text columns
05:7e7c table_cell_select_buffer ; pick cache band 1/0
05:7712 screen_split ; Graph-Table split-screen setup
05:62fd table_recompute_clear_reTable ; another RES6 recompute exit
; --- table value-cache RAM ---
RAM 918C/918E XOutSym / XOutDat ; X-column symbol + data ptr
RAM 9190/9192 YOutSym / YOutDat ; Y-column symbol + data ptr
RAM 9194/9196 inputSym / inputDat ; Ask-input column descriptor
RAM 9198 prevData ; previous-column data ptr
RAM 91DC/91DD CurTableRow / CurTableCol
RAM 91E0 table-top state; exact role remains open
RAM 91E2 table_value_cache (three bands × seven TIFloats)
RAM 8622 table_x_work[2]; running independent-value scratch
; --- Y= equations, selected list, evaluation ---
EquObj = 3 (VAT type) ; Y1..Y0 stored as tokenized formulas
tokens: tVarEqu=0x5E + tY1=0x10 … tY0=0x19 ; Y-var name encoding
RAM 84D9 iMathPtr4 ; indexed-list base; contents depend on caller
33:7097 _HEAP_SORT ; sort caller-supplied indexed range
33:707a _GET_INDEX_LST ; fetch slot n from 0x84D9+2n
33:7066 _PUT_INDEX_LST ; store slot n at 0x84D9+2n
38:758a _Find_Parse_Formula ; FindSym Y-var + parse its formula → OP1
38:5987 _ParseInp ; parse/eval a formula against current X
38:62a3 _StoX ; store OP1 → X system var (not on the fill path)
35:7c7c equation-eval dispatcher ; bcall 0x4741 target: per-row Y evaluation
38:67ae _RclX / 38:67a4 _RclY / 38:626c _StoY
33:5023 _GetVarVersion ; classify extended tokens by version tier
; --- reTable (dirty) setters ---
38:6340 / 38:4809 / 38:54cd parser sets reTable on Y=/var edit
(boot/RAM-clear) sets reTable (init site not pinned; 00:4105 is a message string)
Evidence summary and open items
- TblMin/TblStep addresses + tokens, the
tblFlagsbit layout, and which sites set/clearreTable: [confirmed] (equates + byte-verified bit-ops). - Page 05 = TABLE subsystem, the recompute→clear-reTable structure, the running-X
seed from TblMin and
+TblStepadvance, the cell-cache buffers, the scroll (LDIR/LDDR), and the text-grid paint loop: [confirmed] from byte disassembly; the dense Z80 bodies don’t fully reduce in the decompiler but the CALL/buffer structure is byte-pinned. - The per-row driver
05:6205(seven-row loop, bcall ID4741h→35:7C7Cequation dispatcher → page-38 parser cluster) and the once-per-row_ParseInpexecution are [confirmed] by a headless TilEm trace ofY1=X²._StoXdoes not execute during the fill; the running X moves through OP registers and FPS slots._PUT_INDEX_LST,_GET_INDEX_LST, and_HEAP_SORTare generic indexed-list helpers; their bodies do not prove that TABLE usesiMathPtr4for its selected equations. - Y= selection bit (
0x20) — flags byte0x23selected /0x03deselected — and thestylebyte values (0=line …6=dotted) are [confirmed] against the TI link-protocol var guide. - Ask-mode prompting flow is [confirmed]. Indpnt=Ask prompts through the
entry-line editor at
05:7303, with OPS continuation05:7329. Depend=Ask evaluates individual cells at05:637C, with OPS continuation05:644E. See Auto and Ask modes. _Find_Parse_Formula’sTblRng(type 0x11) special-case is [confirmed] at two byte sites:38:734D(CP 0x11CALL NZ, 38:72DA— validates the range variable’s data layout via38:7260before accepting it) and38:7056(CP 0x11/CP 0x12distinguishing TblRng from the following type in the header switch).- The validation body at
38:72DAperforms generic parse-boundary checking. It calls38:7260, which reads the parse stream through the parser cursor block and accepts statement delimiters as valid terminations. The companion filter at38:72FFrejects token classes that cannot follow:0xB5,0xAB,0xEB,0xAA, and the0x41–0x64range except for a0x21second byte. Classification side effects land at0x8479and0x847A. TheTblRngspecial case therefore requires a legal statement boundary and reuses the validator called by the other parse stubs. [confirmed]
Equation display (MathPrint)
MathPrint turns a tokenized expression into a two-dimensional screen layout for the home-screen entry line, the Y= editor, the Solver equation line, and the template menus. It consumes the token stream described in Tokenizer and TI-BASIC tokens and preserves the OP registers described in Floating-point engine.
The token stream, record graph, and editor state coexist while an expression is being edited; together they emit a transient drawing stream. MathPrint does not repeatedly flatten the equation to pixels and parse it back. [confirmed]
The table below separates those three stored representations from their output:
| Representation | What it preserves | Main code |
|---|---|---|
| Native token stream | Calculator tokens and the active gap-buffer split. | Page 06 editor helpers |
| Live record graph | Expression nesting, child order, active child, and per-record geometry. | Page 34 construction and traversal |
| Editor layout state | Token classes, handler rows, argument slots, and focused cells. | Page 39 |
| Drawing stream | Positioned glyphs, points, lines, and accepted LCD writes. | Pages 01, 04, and 07 |
Page 39 is a cell-grid typesetter for the editable template view. It
classifies a token, selects a compact handler record, walks rows and argument
slots, and turns cells into positioned output. Page 34 constructs, measures,
and redraws the live record graph. Both paths eventually use the services in
Display and LCD. [confirmed]
flowchart TD
token["Token or template action"] --> dispatch["39:4A74<br/>class selection"]
dispatch --> table["39:4C27<br/>class table 39:5E45"]
table --> record["handler record<br/>rows, actions, cells"]
record --> operand["39:5167<br/>recursive operand walker"]
record --> cell["39:4E8E<br/>cell emitter"]
record --> geom["39:69C8<br/>descriptor/fraction geometry"]
operand --> dispatch
geom --> coord["39:683D<br/>cell to pixel coordinate"]
coord --> cell
cell --> glyph["07:4588 / 01:6293<br/>glyph output"]
geom --> rules["39:6ABF / ram:3555<br/>rules and rectangles"]
This first diagram follows the page 39 cell path. The record graph in the
next section is a separate, longer-lived representation. [confirmed]
Two companion pages continue this one. MathPrint live editor and settled drawing follows an edit from the gap buffer through the record graph to pixels and pins the settled-drawing traces. MathPrint validation and browser model lists the verification stack behind the standalone renderer.
Editor state and record graph
MathPrint keeps native token bytes in an editor gap buffer and maintains a live
arena of numbered records. Page 39 treats the active expression as token
classes, handler rows, argument slots, and packed D:E display cells.
eqdisp_handler_table (39:5E45) contains 68 entries. Sixty-six entries point
to decoded handler records; the class-0x00 pointer does not decode as a page 39
handler, and class 0x13 has a null pointer. [confirmed]
Page 34 allocates the record arena while the editor is active. A leaf record
contains a token program. A structural record contains a fixed header followed
by child record IDs. eqdisp_find_structural_record (34:4ACE) walks the
structural region, and eqdisp_find_leaf_record (34:4A83) walks the leaf
region. eqdisp_substitute_active_leaf (34:4AAF) substitutes the active
gap-buffer payload when the leaf pointer equals
mathprintArenaState.active_leaf at 0x8DC2 (base mathprintArenaState at
0x8DAF). The record graph therefore preserves
the editable equation tree before evaluation; page 39 row and cell state is
the transient layout view of that live equation. [confirmed]
eqdisp_allocate_record (34:4900) commits a prepared arena record.
eqdisp_render_leaf_program (34:660A) later executes the settled leaf’s
token-and-marker payload. [confirmed]
flowchart LR
tokens["Native token bytes"] --> gap["Editor gap buffer<br/>active leaf bytes"]
gap --> editor["Page 39 layout state<br/>classes, rows, slots, D:E cells"]
gap --> scan["34:58F9 / 34:5A99<br/>token and argument scans"]
scan --> build
build["eqdisp_allocate_record<br/>record allocation"] --> recordArena["Live record arena<br/>leaf programs + structural child IDs"]
gap --> substitute["eqdisp_substitute_active_leaf<br/>active-leaf substitution"]
substitute --> recordArena
recordArena --> metrics["34:7393 / 34:7609<br/>metrics and geometry"]
metrics --> render["record and leaf rendering<br/>eqdisp_render_leaf_program"]
render --> primitive["Page 1 / 4 / 7<br/>glyphs, points, and lines"]
primitive --> lcd["Accepted LCD data writes"]
The record graph decodes as an expression tree because structural child IDs preserve argument order. The handler records describe how an editable token class is laid out; they do not by themselves encode one whole equation tree. [confirmed]
Core state
The layout engine keeps most of its state in 0x85DE–0x85F2. The table below names the
fields that matter for reading the page 0x39 code. [confirmed]
| RAM | Role | Meaning |
|---|---|---|
0x85DE | mode / class | Caller mode at entry, then the current layout class. |
0x85DF | row index | Current row inside the selected handler or template. |
0x85E0 | slot index | Current argument or cell slot. |
0x85E1 | row count | Number of rows in the current handler record. |
0x85E2 | slot count | Number of cells or arguments in the active row. |
0x85E3–0x85E6 | saved display state | Snapshot of shared display flags while the engine redraws. |
0x85E7 | OP scratch | Saved OP1 slot used while recursing into operands. |
0x85E8 | template kind | Low nibble selects descriptor-backed template UI. |
0x85E9/0x85EA | descriptor origin | Packed pixel base used by descriptor cell mapping. |
0x85EB | row height | Pixel height for the current descriptor row. |
0x85EC/0x85ED | cell pointer | Pointer to descriptor cell data. |
0x85EE/0x85EF | fraction geometry | Measured numerator/denominator cell counts for fraction templates. |
0x85F2 | OP scratch | Second saved OP1 slot. |
0x86D7/0x86D8 | pen coordinate | Pixel coordinate staged before graph/small-font output. |
0x844B/0x844C | text row/column | Shared OS cursor row and column; 844C also participates in overflow. |
0x984A | baseline row | The row restored around recursive operand emission. |
0x9D27 | saved geometry | Copy of the measured fraction geometry used by the template handoff. |
The main draw/measure distinction comes from (IY+0x36) bit 6. Clear means the engine is
measuring or preparing state; set means it may emit pixels. Several other IY flags bias
class selection: (IY+0x09) bit 0 selects fraction/argument context, while (IY+0x02)
bits 4, 5, and 6 select exponent and alternate edit forms. [confirmed]
Page 39 handler recipes
MathPrint uses two formats that are easy to confuse. A page 39 handler is a
shared layout recipe selected by token class. A page 34 arena record is one
node in the current expression: it stores the operands and geometry for that
particular occurrence. The recipe says how to arrange a class; the arena
record says what this expression contains. [confirmed]
A visible expression is driven by handler recipes reached through
eqdisp_handler_table. Each class has one word entry:
handler = eqdisp_handler_table[class]
Most entries point to compact data, not executable code. The common record format is a variable-length tail:
typedef uint16_t EqDispCell; /* high byte D, low byte E */
typedef struct {
uint8_t row_count;
uint8_t cell_count[]; /* row_count entries */
/* uint8_t row_action[row_count]; */
/* EqDispCell cell[sum(cell_count[0..row_count - 1])]; */
} EqDispHandlerRecord;
row_action[] bytes are row labels or control actions. They are separate from the cell
stream. The row-cell pointer routine at 39:4DCA skips the row count, the per-row cell
counts, and the row-action bytes before it reaches the packed two-byte cells. The cell
emitter at 39:4DE6 then walks the selected row and calls 39:4E8E for each D:E cell. [confirmed]
Examples:
| Class | Record | Meaning |
|---|---|---|
0x08 | 39:608B | Numeric-calculus operator row, including nDeriv( and fnInt(. |
0x0D | 39:60F9 | Fixed structural glyph rows, including direct Lintegral cells. |
0x29 | 39:6546 | Group/root-family control row. |
0x2A | 39:654D | Root/power row containing the 00 10 payload cell. |
0x30 | 39:6030 | Fraction-context variant of the class-0x08 operator row. |
0x31 | 39:6433 | Stacked root/power row with a degree row. |
The display cell 00 C8 is the visible fnInt( name. It appears in class 0x08 and
class 0x30; it is distinct from the fixed Lintegral glyph cells in class 0x0D. [confirmed]
Page 34 expression records
Every settled record begins with this 20-byte header. The word names remain address-based where different render types assign different meanings. [confirmed]
#pragma pack(push, 1)
typedef struct {
uint16_t id; /* +00h: arena ID */
uint8_t type; /* +02h: leaf/object or structural render type */
uint16_t word03; /* +03h: parent ID in captured constructed records */
uint16_t word05; /* +05h: leaf height or structural child selector */
uint16_t word07; /* +07h: type-specific height or width */
uint16_t word09; /* +09h: type-specific width */
uint16_t word0B; /* +0Bh: local x origin for recursive entry */
uint16_t word0D; /* +0Dh: local y origin or type-specific anchor */
uint16_t word0F; /* +0Fh: type-specific flags or depth state */
uint16_t word11; /* +11h: payload length or dimensions/depth */
uint8_t byte13; /* +13h: first payload byte or type-specific data */
} SettledRecordHeader;
#pragma pack(pop)
Leaf types below 0x1F store word11 payload bytes beginning at +0x13.
Structural types 0x1F–0x2B retain the complete header and append
little-endian child IDs at +0x14. eqdisp_resolve_child (34:6CCD) resolves
a child ID through 34:4B05 and eqdisp_find_leaf_record; the child words are
not pointers. Captured construction
writes place the parent record ID at +3. [confirmed]
A leaf payload is also a small record program. The sequence
EF type id_lo id_hi invokes a structural record. EF 2D closes or separates
that embedded object without drawing a glyph. Ordinary native token bytes stay
in program order around those markers. A power record of type 0x2A binds the
preceding leaf run as its base and child 1 as its exponent. [confirmed]
Construction tables
Construction is table-driven rather than a switch over complete expressions:
\begin{algorithm}
\caption{Construct one structural arena record}
\begin{algorithmic}
\STATE $t \gets \operatorname{LookupRenderType}(sourceToken)$ \COMMENT{eqdisp\_source\_type\_table}
\STATE $g \gets \operatorname{LookupAllocationGeometry}(t)$ \COMMENT{eqdisp\_allocation\_geometry\_table}
\STATE $record \gets \operatorname{AllocateArenaRecord}(g)$
\STATE $\operatorname{ReserveChildIds}(record, g)$
\STATE $s \gets \operatorname{LookupChildScan}(t)$ \COMMENT{eqdisp\_child\_scan\_table}
\STATE $children \gets \operatorname{ScanSourceArguments}(s)$
\STATE $\operatorname{StoreChildrenInRenderOrder}(record, children)$
\end{algorithmic}
\end{algorithm}
Three ROM table families supply those steps. eqdisp_source_type_table
(34:594D) maps 16 source-token pairs to render types.
eqdisp_child_scan_table (34:59AC) gives one five-byte scan row for each type
0x1F–0x2B. eqdisp_allocation_geometry_table (33:4F82) gives the
corresponding allocation geometry. The
metric and geometry passes dispatch the same 13-type domain through 34:739F
and 34:7611. [confirmed]
Capacity gate
33:4F6D also decodes the three-byte rows in
eqdisp_allocation_geometry_table. It returns
the workspace request in DE, the child-slot count in BC, and the record
size in HL. For example, the type-0x22 integral row returns 112 workspace
bytes, four child slots, and 28 record bytes. Type 0x2B derives all three
values from its matrix element count at 33:4F42–33:4F6C. [confirmed]
34:4869 passes that workspace request to the capacity gate at
34:4B7C. eqdisp_capacity_remaining (34:4B86) starts with the word at
0x8DB1. When
(IY+2Dh).0 is clear, it subtracts the conditional reserve at 0x8DF8; when
the bit is set, it skips that subtraction. It then subtracts the record tail
at 0x8DBE. Each subtraction follows OR A, so it starts with carry clear and
wraps as a 16-bit word. A borrow from the record-tail subtraction makes
34:4B80 skip the request comparison. Otherwise 34:4B82 subtracts the
requested bytes. Either carry returns from the allocator caller at 34:486F
with A=0x02; an exact fit continues with zero bytes remaining. [confirmed]
The finite capacity model partitions all $2^{65}$ combinations of four input
words and the reserve-gate bit into six paths. A 524,287-state raw-byte
differential basis covers each word value at the range and request boundaries.
The initial values of 0x8DB1, 0x8DBE, and 0x8DF8 still depend on the
calling editor state, so this gate alone does not define one source-character
limit for every home-screen expression. The gate and projected model are
[confirmed]. A context-independent character limit remains [hypothesis].
settledRecordAllocationCheck() translates eqdisp_allocate_record_checked
(34:4862):
it obtains the workspace request from the type/matrix geometry row and passes
that request to the capacity gate, retaining the allocator’s A=02h carry
return. The arena words remain explicit because their producers walk the
record list at 34:4A83/34:4ACE; this boundary is therefore a stateful input
rather than a fabricated free-space estimate. [confirmed]
The structural children preserve semantic argument order after the source scan
applies the metadata permutation. For example, the integral metadata is
04 03 04 01 02: scan kind 4 reads four source arguments and assigns them to
children 3, 4, 1, and 2. The settled graph then stores lower endpoint, upper
endpoint, body, and variable in the order consumed by the type-0x22 renderer.
[confirmed]
Token classification
eqdisp_dispatch_token (39:4A74) turns an incoming token or action byte into a layout
class. It first handles the special 0x3D template handoff, then applies context bias. [confirmed]
\begin{algorithm}
\caption{Class selection}
\begin{algorithmic}
\REQUIRE incoming byte $a$
\IF{$a = \mathtt{0x3D}$}
\STATE jump to the template handoff at \texttt{39:672E}
\RETURN
\ENDIF
\STATE $c \gets a - \mathtt{0x2A}$
\IF{exponent/edit-context flags select an alternate form}
\STATE bias $c$ into the alternate class family
\ENDIF
\IF{fraction or argument context is active and $c \in \{3,4,5,6,7,8\}$}
\STATE $c \gets c + \mathtt{0x28}$
\ENDIF
\STATE $\mathtt{85DE} \gets c$
\STATE $HL \gets \mathrm{word}(\mathtt{39:5E45} + 2c)$
\end{algorithmic}
\end{algorithm}
This is why the same token can render differently in ordinary and stacked contexts. For
example, class 0x08 and class 0x30 share the fnInt(/nDeriv( operator family, but
0x30 is selected after the fraction-context bias. [confirmed]
Argument composition
The high-level loop is:
- Save display and OP state.
- Classify the current token into
0x85DE. - Load the handler record from
39:5E45. - Measure row and slot counts into
0x85E1/0x85E2. - Recurse into argument slots when a handler cell represents an operand.
- Restore the baseline row and emit visible cells during the draw pass.
The static caller graph assigns multi-argument walking to 39:5167. When
selected, it keeps the argument index in 0x85E0 and uses 0x85E2 as the
argument count. Forward paths pass saved OP1 state through 39:59E0; reverse
paths use 39:59F9. These routines dispatch _FindAlphaUp and _FindAlphaDn
on page 7, respectively; they do not dispatch a parser-stream scanner.
[confirmed] _FindAlphaUp and _FindAlphaDn scan the physical VAT and retain
the nearest alphabetic successor or predecessor. The result does not depend on
the physical order of VAT records. [confirmed]
For fnInt(expr,var,lower,upper[,tol]), the visible MathPrint fields preserve parser
order: slot 0 is the integrand, slot 1 is the variable, slot 2 is the lower endpoint,
slot 3 is the upper endpoint, and slot 4 is the optional tolerance. The evaluator on
pages 02 and 33 consumes the same order. [confirmed]
The same routine implements tall-template row composition. eqdisp_layout_main reaches
39:5167 from the action-0x08 window-advance path at 39:50A4 and the action-0x04
single-step path at 39:52B3. 39:5167 calls 39:5949 to decide whether the next argument
consumes one or two display rows, adjusts 0x844B, emits slot markers through 39:4E0A,
and emits the saved operand through 39:5B10 or 39:5B1D. These bytes define
row composition around fixed structural cells. The filled and nested-integral
traces below do not select this entry. [confirmed]
The action byte chooses how that argument window advances:
| Action | Decision | Result |
|---|---|---|
0x03 at 39:51F1 | Argument index is nonzero. | Walk backward through 39:523B. |
0x03 at 39:51F1 | Index is zero and (IY+1Dh).0 is set. | Emit the row-token tail. |
0x03 at 39:51F1 | Index is zero, the flag is clear, and count is below eight. | Call 39:5167 once per byte count, then emit the visible suffix and final row-7 argument. |
0x03 at 39:51F1 | Count is at least eight. | Begin the visible window at count - 8 + baseline. |
0x04 at 39:52A5 | uint8((count - 1) - index) is nonzero. | Walk once through 39:5167, then emit the row-token tail. |
0x04 at 39:52A5 | The difference is zero and (IY+1Dh).0 is set. | Emit the same row-token tail. |
0x04 at 39:52A5 | The difference is zero and the flag is clear. | Lay out argument zero through 39:513E. |
All count arithmetic is byte-sized. In particular, an initial zero in the
action-0x03 do-while loop at 39:50A1 wraps to 0xFF and makes 256 calls.
39:4DCA locates the handler row, 39:4CA4 emits its visible suffix, and
39:4E14 emits the final argument on row 7. The action-0x04 delegated return
passes through 39:5447 and 39:52A2. [confirmed]
Action bytes are TI key codes
The action byte that reaches eqdisp_layout_main (39:4F9A, entered with the
code in A) is a raw key code from the editor’s key-dispatch loop. The
dispatcher compares it directly. CP 2 (kLeft) at 39:5048 opens the
backward-walk path. CP 8 (kAlphaDown in the ti83plus.inc keypress
equates) at 39:507C opens window advance. [confirmed]
The window-advance body computes
count(0x85E2) - index(0x85E0) + baseline(0x844B). For values below nine, it
stores 6 at 0x844D and loops over CALL 39:5167 with the DEC (HL)
counter at 39:50A4–39:50AB. Otherwise, ADD A,7 repositions the index
before the jump to 39:5132. [confirmed]
Two traces constrain which inputs select it:
- A trace of a 20-digit integrand does not reach
eqdisp_layout_mainwhile the content scrolls. Its template dispatchers run during insertion transitions. In-slot horizontal scrolling uses a separate page-39scroll set (39:530A–39:539F,39:53A1–39:53FE,39:5500–39:5563,39:5605–39:5632,39:5709–39:572C,39:57AC–39:57FC, and39:5955–39:599B). Character scrolling inside that slot is not a compositor event. [confirmed] - Inserting a nested radical into the integrand re-enters the layout dispatcher
at
39:507C. The action is notkAlphaDown, so the relayout jumps directly to39:5112. This structural insertion does not select window advance. [confirmed]
The translator at 39:53A1 converts a specific incoming key code into layout
actions. It first calls bcall ID 4A68h, whose body at 07:59F1 is a context
test rather than a key fetch. The body copies the entry A to B and calls
the helper at 07:59E5. That helper compares cxMain at 0x858D with
context ID 0x5B53. A mismatch returns NZ. When the context matches, the body
returns Z only for codes 0x41–0x59 that equal the context byte at 0x859A.
[confirmed]
The translator then compares the preserved A with 0xFB. On a match, it
reads the template ID at 0x8446: 0xC7 yields action 7, and 0xC8 yields
action 8, before the jump to 39:4F9A. Register captures show 0xC8
(kFnInt) at 0x8446 when fnInt( is inserted, including a nested insertion
inside another integral’s integrand. The ti83plus.inc equates identify
0xC7 as kNDeriv, 0xC8 as kFnInt, and 0xFB as kwnA. Plain template
insertion therefore does not pass the CP 0xFB gate. The editor state that
sends 0xFB to this handler remains open. [confirmed]
Five more jumps to 39:4F9A occur on page 35, at 35:4DAE, 35:4E73,
35:4F1D, 35:4F70, and 35:5052. The jump at 39:53D7 is another entry.
These entries have not yet been attributed to specific editor events.
[confirmed]
Cell coordinates
Descriptor-backed templates use a fixed ABI. A descriptor is:
typedef struct {
uint16_t base_yx; /* packed base y/x coordinate */
uint16_t box_yx; /* packed box y/x coordinate */
uint8_t row_height;
uint16_t cols_rows; /* packed column/row count */
uint16_t cell_pointer; /* pointer to descriptor cells */
} EqDispTemplateDescriptor;
The mapper at 39:683D converts a descriptor cell to pixels. Its index names
are transposed relative to conventional screen coordinates: the descriptor
row advances LCD $x$, while the descriptor column advances $y$. The +7 loop
builds the packed high byte:
DEC B
ADD A,7
The rowHeight + 2 loop builds the low byte. The caller stores HL to penCol
(0x86D7, low to $x$) and penRow (0x86D8, high to $y$):
$$ \begin{aligned} x &= \mathit{base}_x \\ &\quad + \mathit{row},(\mathit{rowHeight}+2). \end{aligned} $$
$$ y = \mathit{base}_y + 7,\mathit{col}. $$
The known descriptors are:
| Descriptor | Kind | Use |
|---|---|---|
39:686F | 0x10 | Fraction menu descriptor. |
39:6880 | 0x11 | Root/function template menu descriptor. |
39:6893 | descriptor family | Two-row template descriptor. |
39:689C | descriptor family | Two-row, six-column descriptor. |
39:68A5 | descriptor family | Two-row, three-column descriptor. |
For kind nibbles 3 and above, 39:69C8 adds 0x10 and calls
ram:025E:
BIT 6,(IY+2)
RET
A set bit selects 39:689C. Otherwise it adds 0x10 again and calls
ram:0254:
BIT 5,(IY+2)
RET
A set bit selects 39:68A5, and a clear result selects 39:6893. The JavaScript
selectDescriptor translation takes this caller-owned flag02 byte explicitly
for the family branches and reports the byte that 39:69FC stores back at
0x85E8. [confirmed]
Descriptor 39:6880 contains FE09, FB C8, 00 C7, 00 C8, and FB C7 in one row.
That places fnInt( as a menu/template cell, not as a structural integral glyph. [confirmed]
Fractions
Fractions are the most completely recovered dynamic template. The kind-2 fraction path
uses 0x85EE and 0x85EF as measured numerator and denominator widths. It draws a fixed
template box, emits the row/column labels, and updates the focused numerator or denominator
cell. [confirmed]
The rectangle helper at 39:6ABF handles the focus rectangle. Its endpoint helper at
39:6B1C uses:
$$x_\text{left} = \mathtt{0x1B} + 7n$$
$$x_\text{right} = x_\text{left} + 4$$
Static callers of 39:6ABF, 39:6B1C, and the box wrapper 39:6AF5 are all in this
fraction-template UI path. The emitter for the visible bar in a generic
expression remains unidentified. [confirmed]
Exponents and raised rows
Superscripts are represented as row placement, not as a font attribute. The helper at
39:4CE9 raises classes in the 0x24–0x28 family and class 0x39 by forcing 0x844B
to a higher display row before emitting the selected cell. The per-row height accounting
then folds that raised row into the parent layout. [confirmed]
This means X^2 is stored and walked as ordinary cells in different rows. The row selection
does the work; the glyph for 2 is the ordinary one.
Radicals
The recovered pieces establish where the radical data lives, but not yet the
complete page 39 emission route:
| Finding | Evidence | Confidence |
|---|---|---|
The large-font table contains Lroot code 0x10. | 07:466F | [confirmed] |
Classes 0x2A and 0x31 contain cell 00 10; related cells use low byte E=1F. | Decoded handler recipes | [confirmed] |
39:4F1A does not map 00 10; the ordinary _KeyToString interpretation is All+. | Direct mapper and string table | [confirmed] |
| An upstream or dynamic path must select the final root-mark emitter. | The direct path remains unidentified. | [hypothesis] |
The low-byte E=1F cells use the ordinary token-string path. They are not the
special high-byte D=1F form used by the 39:4E8E IX-backed branch.
[confirmed]
The static 39:5167 path can advance a recursive operand window when selected,
but the demonstrated traces do not connect it to the radical records. The
precise division between fixed root glyphs, radicand placement, and any
vinculum drawing remains open.
Integrals and summations
The visible fnInt( menu cell and the structural integral glyph are separate things.
| Concept | Cell / glyph | Source |
|---|---|---|
fnInt( display name | 00 C8 | Class 0x08/0x30 operator records and page-1 token-name strings. |
| Fixed integral glyph | Lintegral 0x08 | Class 0x0D cells FC3F and 08 42, emitted through 39:4F1A. |
| Summation glyph | 0xC6 family | Fixed glyph data; no direct 00 C6 page 0x39 handler cell has been found. |
The fixed Lintegral glyph is emitted by the ordinary structural-glyph path:
39:4E8E runs the named-token prepass, continues to 39:4F1A, maps the cell
to large-font code 0x08, and emits it. [confirmed]
The static 39:5167 path can compose argument slots around a fixed glyph:
- Place the tall integral glyph on the main axis.
- Walk the lower, upper, integrand, and variable slots in parser order.
- Update
0x844Bby the row step from39:5949. - Emit slot markers through
39:4E0A. - Emit the operand bodies through
39:5B10and39:5B1D.
The parser slot order and the static compositor are identified. The filled and
nested-integral traces use 39:4CA4 instead, so the expression or cursor state
that selects 39:5167 remains open. A headless TilEm trace that inserts the
fnInt( template and walks the cursor across its slots exercises the action
dispatcher once each at 39:51F1 (action 0x03) and 39:52A5 (action
0x04) but takes the non-5167 branches; the window-advance path at
39:50A4 does not execute. The witness state therefore needs either more
arguments than the visible window (count ≥ 8) or a template whose slot walk
crosses a window boundary. Fixed glyph cells use 39:4E8E and
39:4F1A; page 07:4588 copies large-font records. [confirmed]
Archived fixed-token markers
Classes 0x17, 0x18, and 0x19 point to one-row records at 39:62C8,
39:62DF, and 39:62F6. Each record contains ten cells. Page 7 maps them to
fixed-token names 61 00–61 09 (GDB1–GDB0), 60 00–60 09
(Pic1–Pic0), and AA 00–AA 09 (Str1–Str0). [confirmed]
39:6675–66BC looks up each mapped name in the VAT. _FindSym returns the VAT
page byte through the record referenced by HL; five DEC HL instructions at
ram:1785 move from the returned type byte to that page byte. A zero page
returns without output. A nonzero archive page emits display code 2Ah (*)
through ram:3FDB. The original cell then continues through the counted-string
and direct-glyph stages. [confirmed]
Emission paths
Cells reach pixels through a small set of output paths:
| Path | Entry | Use |
|---|---|---|
| Generic cell emitter | 39:4E8E | Dispatches two-byte display cells. |
| Direct large glyph map | 39:4F1A | Maps FC3C–FC40, FE7D–FE81, and xx42 cells to large-font codes. |
| String path | 39:6B66 + page 01:6D10 | Converts ordinary token cells to counted strings. |
| Display-byte remap | page 07:44DE | Remaps FE, FC, and FB prefixed display bytes. |
| Small-font blit | page 01:6293 | _VPutMap; emits small labels and compact limits from 0x86D7. |
| Large-font blit | page 07:4588 | Copies one fixed large-font glyph record. |
| Rule / rectangle helpers | 39:6ABF, 39:6AF5, ram:3555 | Draw fraction UI rectangles, boxes, and fixed chrome lines. |
39:6675 saves a matched fixed-token cell’s E byte in keyExtend and passes
its D byte to 07:44DE. It constructs the VAT lookup name from the remapped
pair. Unmatched cells that map through 39:4F1A instead use 05:4056 to build
a 5C:A matrix name. The prepass emits * when the selected VAT record has a
nonzero page byte. The JavaScript translation preserves the lookup and output
order.
The committed layout artifact contains every table entry addressable from the
main entry. A pinned-byte interpreter compares all 65,536 combinations of
display byte and keyExtend; they reduce to seven paths and 12 branch outcomes.
The separate public entry at 07:44FE is outside this main-entry domain. [confirmed]
The translated 39:4E8E–4F19 outer controller covers all 2,097,152 projected
states formed by D:E, the draw-pass flag and callback result, the
curCol < 15 relation, and effective restriction-byte bits 1 and 2. They
reduce to 39 ordered paths, 22 branch outcomes, and seven minimum
representatives. The installed callback, indexed-string printer, and output
bcalls remain named boundaries rather than simulated return values.
[confirmed]
The marker gate at 39:4F44–4F61 compares D:E with FBC8 and FBC7.
FBC8 selects action 7 and mask 04h; FBC7 selects action 6 and mask
02h. Both actions reach 3D:7DC4 through ram:3891. 3D:7DC4 ANDs the byte
returned by 3D:45D9 with the selected mask. The JavaScript translation covers
all 262,144 combinations of D:E and the two effective restriction bits.
They reduce to five paths, six branch outcomes, and three minimum
representatives. [confirmed]
When the marker gate returns NZ, 39:4F62–4F99 draws a horizontal divider from
$(11, 59 - 8\mathit{curRow})$ through $(94, 59 - 8\mathit{curRow})$, with the
row coordinate reduced modulo 256. The routine copies 00 40 60 5F 5E to the
five-byte display window at 0x8DA2, forces plotFlags.plotDisp during
_DarkLine, and then restores the original plotFlags byte. _DarkLine at
04:4025 preserves AF, so the branch at 39:4F8B consumes the preceding
_CheckSplitFlag result. A horizontal split installs 20 20 60 5F 5E; a
vertical split installs 0C 34 30 2F 2E; otherwise the normal window remains.
The finite model covers all 2,048 row and effective sGrFlags states. They
reduce to three paths, four branch outcomes, and three minimum representatives.
[confirmed]
The 39:6675–66BC translation covers every D:E pair with absent, RAM, and
archived exact-VAT results. These 196,608 projected states reduce to 13 paths,
14 branch outcomes, and six minimum representatives. The existing _FindSym
documentation supplies the exact fixed-token scan contract; the prepass model
accepts a logical VAT snapshot and performs the corresponding three-byte name
match. [confirmed]
_KeyToString at 01:6D10 uses that public entry for FB, FC, FE, and
FF cells, scans 13 high-byte special strings, or selects one of 101 counted
strings through the pointer table at 01:6E05. The JavaScript translation
compares all 65,536 D:E pairs with a pinned-byte interpreter. A second
comparison covers all 1,024 prefix/index states admitted by the _KeyToString
caller at 07:44FE. Together they resolve all 447 unique key-string cells in
the decoded handler records and descriptors. Installed font and token hook
bodies remain explicit external boundaries. [confirmed]
The page-7 large-font service copies fixed glyph rows. It does not measure a radicand or stretch a glyph by itself. [confirmed]
The caller enters 07:4588 with the glyph code in A and its eight-byte
offset in HL. 07:45EB converts that offset to the seven-byte table address
07:45FF + 7 * code. The entry then copies eight consecutive bytes to
ram:845A. The eighth byte is the first row of the next glyph. Code FFh
instead reads the byte CDh at 07:4CFF, immediately after the 256-glyph
table. [confirmed]
The alternate entry at 07:45B6 uses the same address conversion and builds
the nine-byte map record [06h, row0 << 1, ..., row6 << 1, 00h]. The leading
width gives each five-pixel glyph a clear advance column. A pinned byte
interpreter matches the JavaScript translation for all 256 glyph codes at both
entries. [confirmed]
(IY+35h).5 enables the font hook, and (IY+35h).1 enables the localization
hook. A hook that returns Z either completes the copy entry or supplies the
pattern pointer consumed by the shifted entry. The 32 hook predicate states
reduce to 14 complete branch paths. Hook-provided pattern bytes remain external
to the translation. [confirmed]
Evidence anchors
The page is intentionally an architecture summary, not a verifier log. These are the main anchors for readers who want to check the disassembly. [confirmed]
| Address | Meaning |
|---|---|
39:4A74 | Main token/action dispatcher. |
39:4C27 | Class table lookup through 39:5E45. |
39:4DCA | Row-cell pointer computation for handler records. |
39:4DE6 | Row cell stream emitter. |
39:4E8E | Generic two-byte cell emitter. |
39:4F1A | Direct large-glyph classifier. |
39:4F08 | Text-column overflow check before marker handling. |
39:4E0A | Argument-index marker emitter used by the row compositor. |
39:5167 | Multi-argument operand walker and tall-template row compositor. |
39:5949 | Row-step classifier for one-row versus two-row argument advance. |
39:5B10 / 39:5B1D | Saved-E7 wrappers for ascending and descending alphabetic VAT searches. |
39:59E0 / 39:59F9 | _FindAlphaUp and _FindAlphaDn dispatchers. |
39:672E | Template handoff for incoming 0x3D. |
39:683D | Descriptor cell-to-pixel mapper. |
39:68AE | Geometry action handler. |
39:69C8 | Descriptor/fraction geometry selector. |
39:6ABF / 39:6B1C | Fraction focus rectangle and endpoint helper. |
39:6B66 | Generic string selector. |
39:66E9 / 39:66FE | Reverse and forward argument-overflow cues. |
39:6712 | Overflow marker path; resets curCol and emits :. |
07:44DE | Display-byte remapper. |
07:4588 | Large-font fixed glyph blitter. |
01:6293 | _VPutMap small-font pixel output. |
MathPrint pipeline coverage
Coverage here has three layers. The declared control-flow graph defines the branches being counted; finite models exhaust selected routine-level input projections; dynamic traces show which of those branches calculator-created states actually reach. A complete finite projection is not whole-machine coverage, and a branch absent from the corpus is unresolved rather than infeasible unless a separate invariant rules it out.
Scope of the analyzer
tools/ti84re/mathprint/analyze_saturation.py bounds the coverage claim to nine
declared components: settled construction, settled rendering, metrics and
geometry, record allocation, editor layout, small-font/LCD output, point and
line primitives, large-glyph output, and alphabetic VAT selection. It
recursively follows direct ROM edges from named entries, seeds decoded table
destinations, overlays exact next-PC outcomes from 276 retained traces, and
lists direct external targets. Computed dispatch destinations are manually
seeded; bcall and RAM bjump bodies remain outside the direct-edge walk. Of those
traces, 275 reach their state through calculator input. One explicitly
classified synthetic trace inserts an EF36h editor buffer through direct RAM
writes. The report keeps the two provenance classes separate.
tools/oracles/mathprint/mathprint-saturation.json records the resulting branches and trace
hashes. [confirmed]
The analyzer can restore trace identities, provenance, and per-trace summaries from a prior report and the digest-keyed cache. Regeneration therefore scans a new trace once without reopening the other retained TLMT files. [confirmed]
None of the 276 report traces executes 39:5167, 39:523B, the saved-operand
wrappers at 39:5B10–39:5B38, or the dispatchers at 39:59E0/39:59F9.
The 276-digest trace cache also has no hit at those entries. [confirmed]
_FindAlphaUp at 07:50B5 executes once in 112 report traces, but every call
comes from the type-16h cleanup loop at 07:5544. Each observed call returns
carry with OP1 unchanged; no trace supplies a successful alphabetic-search or
MathPrint caller witness. [confirmed]
The report is a symbolic-execution aid rather than a whole-machine proof. It
decodes fixed table rows and partitions selected projected input domains. The
scan-kind dispatcher at 34:5678 partitions all 256 incoming A values into
seven terminal paths. eqdisp_draw_marker_primitive (34:6143) partitions the
$256 \times 2 \times 65{,}536 = 33{,}554{,}432$ projected tuples over incoming
A, (IY+44h).3, and the word at 0x8520. Its predicates reduce those tuples
to 14 branch-path classes and ten terminal actions. This count covers the
projected inputs, not every register and RAM state. The marker-tail callee at
34:759C reduces 16 abstract predicate valuations to five return classes.
Stream length, arbitrary RAM, and unmodeled indirect targets remain outside
these finite models. [confirmed]
The page-7 domains partition all 32 masked type classes, 192 declared type/key
form pairs, all 8,192 type/record-marker pairs, and all 288 abstract candidate
decisions. A fifth domain covers both terminal return states. A sixth
partitions all 33,554,432 combinations of return state, incoming OP extension
bytes, and selected-record continuation byte. The candidate
projection includes direction, type equality, filter result, the FFh
sentinel, source relation, and current-best relation. These domains cover the
translated branch predicates and minimize representatives for their outcomes.
They do not enumerate arbitrary VAT length, every possible eight-byte name, or
every surrounding machine state. [confirmed]
The extended raised-token classifier at 34:580C has a caller-scoped domain of
3,047 packed-token states. 34:5866 handles numeric bytes and B0h first, and
34:5887 skips EF1Eh. The remaining ordinary bytes and 11 two-byte token
families reduce to 12 complete classifier paths. The 5Fh and EBh
designators enter bounded eight- and five-byte name scans. The loop at
34:583D accepts digits 30h–39h and letters 41h–5Bh; a source boundary
or any other byte stops it. The analyzer enumerates every accepted digit/letter
prefix, stop class, and counter exit. These are finite byte-class projections,
not claims that every packed token or name occurs in a calculator-created
expression. [confirmed]
Finite symbolic models
The analyzer generates one deterministic representative for every complete path-equivalence class in 52 finite models. It also computes an exact minimum representative set for the branch outcomes in each model. The checked unit test regenerates the complete corpus from the model functions. Schema 5 of the report stores its aggregate counts rather than repeating the generated representatives. The minimums are per domain: the five- and eight-byte name-loop ABIs share branch addresses, but a representative for one ABI does not cover the other. [confirmed]
| Finite model | Projected inputs | Path classes | Branch outcomes | Minimum representatives |
|---|---|---|---|---|
| Structural scan-kind dispatch | 256 | 7 | 12 | 7 |
| Structural-depth gate | 256 | 2 | 2 | 2 |
| Structural-insertion dispatch | 65,536 | 6 | 10 | 6 |
| Raised extended-token classifier | 3,047 | 12 | 22 | 10 |
| Five-byte raised-name loop | 493,112,577 | 125 | 10 | 4 |
| Eight-byte raised-name loop | 24,977,631,672,321 | 1,021 | 10 | 4 |
| Shared marker draw helper | 33,554,432 | 14 | 26 | 13 |
| Settled render nesting tail | 16,777,216 | 15 | 24 | 11 |
| Point mode and buffer routing | 2,048 | 28 | 22 | 5 |
| Drawing-hook dispatch | 4 | 3 | 4 | 2 |
| Point style dispatch | 512 | 5 | 8 | 5 |
| Point bounds | 33,554,432 | 7 | 14 | 7 |
| Thick-point expansion | 4,294,967,296 | 8 | 14 | 8 |
| Shaded-point expansion | 3,145,728 | 1,850 | 30 | 8 |
| Small-font pointer selection | 65,536 | 16 | 31 | 16 |
| Token-hook dispatch | 1,048,576 | 9 | 10 | 5 |
| Direct cell-to-large-glyph selection | 65,536 | 9 | 16 | 9 |
| Display-byte remapper | 65,536 | 7 | 12 | 7 |
_KeyToString _sOK prefix | 1,024 | 5 | 8 | 5 |
_KeyToString selector | 65,536 | 35 | 40 | 14 |
| Page-39 cell-string selector | 131,072 | 14 | 16 | 8 |
| Page-39 archived-token prepass | 196,608 | 13 | 14 | 6 |
| Page-39 marker restriction gate | 262,144 | 5 | 6 | 3 |
| Page-39 marker row retouch | 2,048 | 3 | 4 | 3 |
| Page-39 cell-emission controller | 2,097,152 | 39 | 22 | 7 |
| Glyph advance and delimiter padding | 131,072 | 6 | 10 | 4 |
_VPutMap byte-boundary gate | 56 | 2 | 2 | 2 |
MathPrint _VPutMap right-edge gate | 3,584 | 4 | 6 | 2 |
MathPrint _VPutMap row state | 112 | 4 | 10 | 2 |
_VPutMap aligned-byte composition | 917,504 | 2 | 2 | 2 |
| Large-glyph hook dispatch | 32 | 14 | 16 | 8 |
| Metric marker-tail gate | 16 | 5 | 8 | 5 |
Editor action 0x03 controller | 131,072 | 11 | 9 | 4 |
Editor action 0x04 controller | 131,072 | 5 | 5 | 3 |
| Reverse argument-overflow cue | 65,536 | 2 | 2 | 2 |
| Editor horizontal viewport | 17,179,869,184 | 8 | 6 | 2 |
| Editor vertical viewport | 17,179,869,184 | 8 | 6 | 2 |
| Editor vertical overflow cues | 4,294,901,760 | 5 | 8 | 3 |
| Editor left-overflow cue | 1,099,494,850,560 | 5 | 8 | 5 |
| Editor right-overflow cue | 281,474,976,710,656 | 5 | 10 | 4 |
| Glyph vertical viewport | 1,099,511,627,776 | 16 | 22 | 6 |
| Glyph viewport gates | 30,064,771,072 | 3 | 4 | 3 |
logBASE counted-string viewport | 8,589,934,592 | 20 | 6 | 1 |
| Embedded-record viewport gate | 4,294,967,296 | 2 | 2 | 2 |
| Record-allocation capacity | 36,893,488,147,419,103,232 | 6 | 6 | 2 |
| Saved-operand wrappers | 16 | 12 | 12 | 8 |
| FindAlpha type normalization | 32 | 31 | 8 | 4 |
| FindAlpha key preparation | 192 | 13 | 14 | 4 |
| FindAlpha record stepping | 8,192 | 8 | 12 | 4 |
| FindAlpha candidate reducer | 288 | 25 | 17 | 10 |
| FindAlpha endpoint | 2 | 2 | 4 | 2 |
| FindAlpha OP scratch transition | 33,554,432 | 2 | 5 | 2 |
The 52 models contain 3,484 path classes and 587 distinct modeled branch outcomes. Their per-domain minimum corpora contain 274 representatives. Each class records its concrete representative, projected-state count, terminal, and complete branch-outcome sequence. These representatives saturate the declared projections. They do not establish calculator reachability or cover state outside those projections. [confirmed]
The table models also distinguish decoded rows from reachable indices.
eqdisp_lookup_render_type (34:5935) scans 16 source-token rows but has 15
first-match classes: row 6
duplicates the 0006h mapping at row 3 and can never win the first-match scan.
The report partitions the other 65,521 packed D:E values into the no-match
class. The render, allocator, and editor-class index models decode all 256
8-bit inputs. Types 0x1F–0x2B select the 13 render and allocator rows;
class bytes 0x00–0x43 select the 68 editor rows. Other inputs read adjacent
ROM bytes. This records local index behavior without asserting that each
overread is reachable through a calculator entry. [confirmed]
The metadata rows use scan kinds 0, 1, 2, 3, 4, and 6. Natural
traces witness six of the seven dispatch classes. Scan kind 2 would take
34:5680 into the fraction scanner at 34:56DF, but no retained invocation
does so. Existing fraction construction instead reaches the same scanner
through another entry route. The outcome remains unresolved because the
metadata value proves local relevance but does not prove caller reachability.
[confirmed]
Dynamic coverage
The report keeps complete path witnesses separate from individual branch
outcome witnesses. A class whose branch outcomes all occur somewhere in the
corpus is not necessarily a class traversed by one invocation. The editor ABI
of eqdisp_draw_marker_primitive has seven complete live path witnesses. The render-table ABI has
one ROM-fixed class because _LdHLind fixes A=0x43. The 34:759C model ends
at the callee return. It records the continuations at 34:755F, 34:6FC9, and
the tail-jump caller at 05:785F separately. A branch outcome unique to each
return class identifies which callee paths have live witnesses. [confirmed]
| Component | Reachable instructions | Natural / all-evidence outcomes | Outcomes in CFG | Natural / all-evidence instruction coverage |
|---|---|---|---|---|
| Settled construction | 991 | 249 / 250 | 408 | 80.93% / 80.93% |
| Settled rendering | 1,898 | 258 / 259 | 302 | 97.52% / 97.52% |
| Metrics and geometry | 470 | 77 / 77 | 80 | 100.00% / 100.00% |
| Record allocator | 64 | 7 / 7 | 8 | 98.44% / 98.44% |
| Alphabetic VAT search | 236 | 17 / 17 | 92 | 34.32% / 34.32% |
| Editor layout | 2,776 | 255 / 255 | 1,098 | 33.03% / 33.03% |
| Small-font and LCD output | 413 | 81 / 81 | 122 | 75.54% / 75.54% |
| Point and line primitives | 508 | 50 / 50 | 134 | 59.65% / 59.65% |
| Large glyphs | 130 | 16 / 16 | 32 | 68.46% / 68.46% |
These counts describe the declared CFG and retained saturation corpus, not all OS entry states. A branch with both outcomes observed is dynamically saturated for that corpus. A branch with one or no outcomes remains open even when its containing routine has been reached. Metrics and geometry and the allocator have no wholly unobserved branch. The other seven components still do. Three of the allocator’s four branches and 37 of the 40 metric branches have both outcomes. [confirmed]
The report classifies all 2,276 enumerated outcomes. Natural calculator input
exercises 1,010. The synthetic EF36h state adds two outcomes, for 1,012 across
all evidence. One allocator outcome is infeasible under its data invariant.
Two metric outcomes are infeasible under the calculator call ABI, and one is
infeasible under the valid Y= editor-entry invariant. Three small-font
pointer outcomes are infeasible under the 01:6702 entry invariant. The full
evidence set leaves 1,257 unresolved; the natural-only set leaves 1,259.
An unobserved outcome never becomes infeasible from absence alone. [confirmed]
The infeasible allocator outcome is the fallthrough at 33:4F4E. The type
0x2B path loads rows and columns from record offsets +0x13 and +0x12, then
_HTimesL (00:1EF6) computes their product. The matrix-creation path at
02:5DCF raises _ErrDimension when either dimension is zero. A valid settled
matrix record therefore reaches 33:4F4E with a nonzero product and takes the
branch. [confirmed]
The calculator metric entries at 34:7377, 34:737A, and 34:7380 all pass
through 34:7386, which loads B=0. The recursive route stores that zero at
0x8512 and reloads it at 34:75F4 before 34:7606. Induction over the
dispatcher recursion therefore fixes B=0. The B!=0 outcomes at 34:73CD
fallthrough and 34:765D return are infeasible under this calculator ABI.
Synthetic direct calls to internal metric handlers do not share the ABI.
[confirmed]
The BBh route through smallfont_glyph_ptr reaches 01:6765 with Z set by
CP BBh; the intervening LD A,L preserves Z. The taken outcome is therefore
infeasible from 01:6702. Both outcomes of 01:6776 are also infeasible
because that comparison’s only predecessor is the dead taken edge at
01:6765. [confirmed]
Minimal diverse trace corpus
The report computes two exact Z3 covers. The first preserves every individual branch outcome observed in the supplied traces. It does not preserve complete invocation paths, register or RAM states, dispatch indices, record cases, or LCD write cases. The all-evidence branch cover selects 20 traces and preserves 1,012 outcomes in 4,424,233,548 bytes. The natural-only cover selects 21 traces and preserves 1,010 outcomes in 4,580,267,958 bytes.
The diversity cover adds complete observed paths, modeled path classes, dispatch indices, record types, and LCD-oracle types. It deliberately excludes individual oracle identities, neighboring editor-state labels, raw register values, and raw token values. Those values belong to regression fixtures rather than the mechanism-diversity objective. The all-evidence universe has 1,106 tags and needs 26 traces. The natural-only universe has 1,104 tags and also needs 26 traces. The retained byte totals are 5,118,199,506 and 5,204,001,186, respectively. Both covers minimize trace count first, retained bytes second, and labels third. [confirmed]
The diversity cover preserves only mechanisms represented by its tags. It does not turn unobserved RAM into an observed state or prove that the traces reach every symbolic valuation. The separate exhaustive models state their preconditions; the dynamic cover states what the retained traces exercise. [confirmed]
The 20-trace all-evidence branch cover retains the nested derivative, complete
root-level structural-navigation, depth-two fraction LEFT, mixed
radical/fraction traversal, integral, and Y=/table runs below. Other selected
traces cover every outcome in the depth-four log-base, log-base marker, and
radical runs, so the exact solver omits them. The macro paths contain no
memwrite command or execution hook. The raw TLMT files remain outside the
repository; their hashes identify the exact inputs used by the report.
[confirmed]
The all-evidence cover omits the token-built matrix traversal because the
synthetic state already covers its otherwise-new 34:6B94 outcome. The
natural-only cover selects it with 15 exclusive outcomes, including the first
natural 34:6B94 taken witness. [confirmed]
| Input | Reproduction macro | Trace SHA-256 | Exclusive outcomes in the full branch cover |
|---|---|---|---|
| Nested derivative with tall body and value | tools/macros/mathprint-nested-tall-nderiv.macro | e11c011b74df79165c55f7f64b699e3aa393bf8087f45ec89a73d616b73cdbb5 | 10 |
| Depth-four log-base and power tree | tools/macros/mathprint-nested-depth4.macro | b8d970906e63db96d36847dfcafed91d97e73fc7699294cc8debd08e7affdd93 | Omitted |
| Log-base marker insertion | tools/macros/mathprint-logbase-boundary-insert.macro | a49e4c13c93358662713da7f5e07862f42863d60a70ce18e141a90987914008b | Omitted |
| Radical marker insertion | tools/macros/mathprint-radical-nonspecial-insert.macro | e7b79e37149f2b9b4a986bdbb114a89b03cd452bbecc6da20490edc972895e98 | Omitted |
| Integral marker insertion | tools/macros/mathprint-integral-boundary-insert.macro | 328b8f52ebe939b35f79e676076984aa85ee59e05c06862647c4fc615069bb3c | 2 |
| Mixed summation traversal | tools/macros/mathprint-editor-summation-left-navigation.macro | 55fee4452906f94c2f3133961879ce4daec8fa0a98a5b69be1c27eae27190d3d | 3 |
| Completed nDeriv and log-base traversal | tools/macros/mathprint-editor-extra-structural-navigation.macro | d77bdeb19c52dd1337db4ea0410c1d5970924a7a3bf6a589742280b508fda776 | 2 |
| Remaining insertable structural traversal | tools/macros/mathprint-editor-remaining-structural-navigation.macro | 6263edce978d46750859f38c964ec4858b2c28fc8f6c914d510a8c332a01d85f | 19 |
| Token-built matrix traversal | tools/macros/mathprint-editor-matrix-navigation.macro | 78639019ccf6b1d01a62b2f88dc5ff619382c08fe81396886aa0c49bcfe962d4 | Omitted |
| Depth-two fraction RIGHT | tools/macros/mathprint-editor-nested-fraction-right-navigation.macro | 15e6bccf136c7212fd36f7bf8ed570fd1ebbe161c8ef58584a439e891237d1ac | Omitted |
| Depth-two fraction LEFT | tools/macros/mathprint-editor-nested-fraction-left-navigation.macro | 6cd38899f36e5a6398a0d1959557f8cb45172b4046db1f39cdfa298250066e6a | 1 |
| Fraction nested in a radical | tools/macros/mathprint-editor-radical-fraction-navigation.macro | 99d813bdbb7102c9bd5ae608c0cc9eb64cd84c0410a06e4f2243e1768d86c574 | 1 |
| Y=/table/power round trip | tools/macros/mathprint-yequ-table-power-insert.macro | ac719f540d2adfca05d2ffa415f065b83eaf407f04fca42f5ae63c440a746b9d | 16 |
| Y= equals-sign selection sweep | tools/macros/mathprint-yequ-state-sweep.macro | 56733273b52ab4281ca2998ec2b89ece3083deb75c01160f97b936f30b73fe2f | Omitted |
The two depth-two fraction traces each contain the same 367 branch outcomes. The LEFT trace is 34,465,218 bytes smaller, so the lexicographic minimum retains it and omits the RIGHT trace. This substitution changes retained bytes without changing the covered-outcome count. [confirmed]
The mixed radical/fraction trace supplies the first natural 34:75B0
fallthrough with A=27h, the radical marker outside the special fraction,
nth-root, and power set. It exercises the last two previously unseen metric
instructions and raises metric/geometry instruction coverage to 100%. The
component has 77 exercised outcomes, two outcomes proven infeasible under the
calculator ABI, and one proven infeasible under the valid Y= editor-entry
invariant. All 80 outcomes are classified. [confirmed]
The retained mathprint_integral_boundary_insert trace reaches 34:6968
taken, 34:6B6D fallthrough, and 34:6B94 fallthrough through calculator
input. It supplies the natural witnesses recorded for all three outcomes.
[confirmed]
Four additional reset-origin traces close ten natural branch outcomes and four
complete editor-helper paths. Their macros use key input only. The screenshots
and A at each discriminator were checked before admission. [confirmed]
| Input | Reproduction macro | Trace SHA-256 | Complete eqdisp_draw_marker_primitive path |
|---|---|---|---|
| Absolute-value marker | tools/macros/mathprint-absolute-boundary-insert.macro | 103f3acc7f1ad13d1bf88af45ecacdc7e34133e66cc9c00fb57587674357cacf | A=0x21 → display code 0x7C |
| $e^x$ marker | tools/macros/mathprint-e-power-boundary-insert.macro | c927963c5db9a1f6f18652213764eabbf7a4fa9f2d2a74b7dae320fe882d7917 | A=0x25 → display code 0xDB |
| $10^x$ marker | tools/macros/mathprint-ten-power-boundary-insert.macro | eb337f479d112e88537f0950fd7d2a917d101cfafda98447fb717a9a35f1e1e4 | A=0x26 → display code 0x1D |
| Summation marker | tools/macros/mathprint-summation-boundary-insert.macro | 980b2d17df5753223881090235fcca4bb4e8457a37c6cb05eef8f7a54314adf8 | A=0x29 → display code 0xC6 |
The synthetic EF36h trace uses
tools/macros/mathprint-ef36-injected-buffer.macro. Its two memwrite
commands place EF 36 31 11 at the editor cursor. It is the sole synthetic
source in the 276-trace report. It supplies the only evidence for
34:5A23 fallthrough and 34:6992 taken. The token-built matrix traversal
supplies the first natural witness for 34:6B94 taken. The full minimum
retains it; the natural minimum excludes it by construction. [confirmed]
MathPrint live editor and settled drawing
TI-84 Plus OS 2.55MP — from the gap buffer to settled pixels.
This page follows one edit through the MathPrint editor: the gap buffer,
the record graph, marker rendering, the page 39 argument layout, and the
settled drawing that reset-origin traces pin byte for byte. It continues
Equation display (MathPrint), which defines the
records, handlers, and cell geometry used here.
Live editor reconstruction
The coverage report in Equation display (MathPrint) says which observations support the recovered logic. This page changes viewpoint: it follows an edit from the gap buffer, through the record graph, and back to pixels. [confirmed]
Gap buffer and record regions
The four editor pointers describe two live byte ranges separated by unused space:
typedef struct {
uint16_t top; /* editTop: first address of the left segment */
uint16_t cursor; /* editCursor: one past the left segment */
uint16_t tail; /* editTail: first address of the right segment */
uint16_t bottom; /* editBtm: one past the right segment */
} MathPrintEditorGapPointers;
/* Logical payload = [top, cursor) followed by [tail, bottom). */
The record-region pointers form a second typed block. Fields that remain unresolved keep address-based names: [confirmed]
#pragma pack(push, 1)
typedef struct {
uint16_t structural_begin; /* 0x8DAF */
uint16_t extended_leaf_end; /* 0x8DB1 */
uint8_t unknown_04[9];
uint16_t leaf_begin; /* 0x8DBC */
uint16_t leaf_end; /* 0x8DBE */
uint16_t unknown_11;
uint16_t active_leaf; /* 0x8DC2 */
} MathPrintArenaState;
#pragma pack(pop)
The in-progress editor is a gap buffer. editTop (0x96F4) and editCursor
(0x96F6) bound the left segment. editTail (0x96F8) and editBtm
(0x96FA) bound the right segment. Moving across a structural object exposes
the six-byte right-segment marker EF type id_lo id_hi EF 2D. An insertion at
that boundary makes the metric walker enter 34:759C with its parsed pointer
at editTail + 6. The comparison at 34:75A1 then returns Z, so 34:75A5
falls through. [confirmed]
The live expression graph spans two record regions.
eqdisp_find_structural_record starts at mathprintArenaState.structural_begin
and stops at mathprintArenaState.leaf_begin. 34:4AF0 advances by the
structural record size. The child words after each 20-byte header remain record
IDs. eqdisp_find_leaf_record starts its leaf-record walk at
mathprintArenaState.leaf_begin. It normally uses
mathprintArenaState.leaf_end as the boundary. Bit 2 of (IY+1) instead
selects mathprintArenaState.extended_leaf_end. [confirmed]
eqdisp_substitute_active_leaf handles the active gap during that leaf walk.
When the gap bit is set and the current record equals
mathprintArenaState.active_leaf,
34:4ABF substitutes
editBtm as the next record pointer. Every other leaf advances by its 19-byte
prefix plus the payload length at +0x11. The active record’s logical payload
is the concatenation of editTop–editCursor and editTail–editBtm.
This explains why a RAM dump can hold structural headers below the entry,
the active leaf in the gap, and later leaf records near the top of RAM.
[confirmed]
Four reset-origin RAM snapshots pin the cursor-to-record mapping. The compact
bytes and expected trees are in tools/oracles/mathprint/mathprint-editor-gap-oracles.json.
Each reproduction uses key input only. [confirmed]
| Editor state | Active leaf | Logical gap payload | Cursor path | Sparse-state SHA-256 | Cursor-off LCD SHA-256 |
|---|---|---|---|---|---|
| Empty fraction numerator | 9 | cursor, EF 1E | numerator | bcabb3961e1f37fe21b4e66c8bbfffb9a3812a162324e85273fdeed0beccc019 | 450e82a31ced68ed319a1c2e8d18d3e2d3813f097de8be9dc2a89d48289cc4c9 |
Integral upper bound after 2 | 10 | 32, cursor | upper bound | 1dab216a05a2604bdb51eaa8a347a881f4dcccb7c8f19965503d73812422f8d5 | 39b937b16e32e4e07f6ffc2d6e60842c0249fd51d3aa661b23af2d2cb8708cea |
| Fraction denominator nested in an integral body | 15 | 32, cursor | body → denominator | 7da6d7fdbb5ea848dda0afb1105237280a06b6dff994879df0a8c0b63e1a5f10 | 1297c2562d7c2fac9612aad6fc2e829ecb8f487606da6a079fb7d49d1c4c64d9 |
| Immediately after a completed integral | 7 | EF 22 08 00 EF 2D, cursor | root sequence after integral | 89dd708b40f3c77f2cb5392783256576be8f0014beea2b411b6ba860dd441ef4 | 80cc504e3a7c6c773906f1e64ca6916e48594fd0c66c946151b3bc9849647f64 |
The empty numerator keeps EF 1E in the right gap segment, while its sibling
denominator leaf resides near the high-memory boundary. The nested case links
integral body leaf 11 to fraction record 13, whose second child is active
leaf 15. The completed-integral case moves the active gap back to entry leaf
7; its cursor follows the complete six-byte type-0x22 marker. These states
show that the graph itself preserves the editable nesting. [confirmed]
decodeMathPrintEditorRam() translates both record walks and the active-leaf
substitution. decodeEditorExpressionGraph() inserts a cursor at
editCursor - editTop, after checking the native token and six-byte marker
boundaries. It recovers the nested cursor path from record IDs and leaf bytes;
the screenshots do not participate in the decode. [confirmed]
constructEditorExpressionProgram() translates the inverse path. It allocates
the entry leaf at ID 7, the transient type-0x1F wrapper at ID 6, and the
same structural and child IDs as the captured arenas. The cursor contributes a
six-pixel cell at render depth zero and a five-pixel cell in a raised row. A
cursor immediately before EF 1E reuses that token’s six-pixel empty-slot cell.
The ordinary structural metric formulas then propagate the active leaf’s
height, width, and baseline through every ancestor. [confirmed]
The structural word at +0x05 identifies the active one-based child along the
cursor path. A completed template outside that path retains its last child.
Containing leaves on the active structural path retain the descendant marker’s
byte offset at +0x0F. The active gap also retains its pre-edit +0x0F and
+0x11 words, so the cursor node carries those two state words explicitly.
They cannot be recovered from the concatenated gap payload alone. [confirmed]
For all four states above, decoding RAM to a cursor-annotated expression and reconstructing it matches every record field by ID. Executing the reconstructed record program in the cursor-off phase also matches the complete 96×64 calculator screenshot bitmap. The hashes in the last column cover all 768 LCD bytes. [confirmed]
Ordinary token insertion
Ordinary token insertion follows 34:4775–47A4 into 34:4BB9–4C0D. The
non-structural branch reaches the page-6 gap writer through 00:3699.
06:4341–4388 checks available space, stores the one- or two-byte packed token
at editCursor, and advances the pointer. In the captured root-leaf transition,
the write at 06:437A stores 32h at 9DE1h; 06:437C then changes
editCursor from 9DE1h to 9DE2h. [confirmed]
tools/oracles/mathprint/mathprint-editor-mutation-oracles.json retains two adjacent pre/post
transitions and one five-write sequence. Appending 2 after root token 1
changes the active payload from 31h to 31h 32h. Inserting 2 into an empty
fraction numerator advances the right gap boundary past EF 1E, so the token
replaces the empty slot. The fraction transition also shows that the
type-0x20 record keeps EFh at +13h instead of recomputing that byte from
the new 32h numerator. The editor AST therefore retains structural +13h
state in addition to the active leaf’s +0Fh and +11h words. [confirmed]
The five-write sequence enters 08 08 31 09 09, the native bytes for [[1]].
Wrapper record 6 continues to point directly to leaf record 7; the live
arena allocates no structural record. After the first and second writes, the
decoder retains one and two unfinished list frames around the cursor. The first
09h closes the inner frame into a one-element list. The second closes the
outer frame into a list whose element is that inner list. The settled
type-0x2B matrix record belongs to the later dimensioned construction path,
not these five live gap writes. [confirmed]
editorInsertPackedToken() consumes the decoded arena, writes the active leaf
payload, and decodes the graph again. All five [[1]] transitions match the
post-key cursor tree, every reconstructed record field, and the complete
cursor-off 96×64 LCD bitmap. Directly appending the byte to the previous
semantic tree would miss four of the five regrouping transitions. Cursor
navigation is tested separately below. [confirmed]
Structural template insertion
Most structural insertions share the same transaction. A small type policy decides which packed token, if any, moves into a child and which child receives the cursor:
\begin{algorithm}
\caption{Insert a structural template}
\begin{algorithmic}
\STATE $rule \gets \operatorname{TemplateRule}(renderType)$
\STATE split the active leaf at the cursor on a packed-token boundary
\STATE consume one token on the right when $rule$ requires replacement
\STATE write placeholder \texttt{EF type 00 00 EF 2D} into the containing leaf
\STATE allocate the structural record and its ordered child leaves
\STATE patch the marker with the allocated record ID
\STATE distribute the left payload according to $rule$
\STATE select $rule.initialChild$ and install its gap payload
\STATE remeasure ancestors while retaining editor-only record fields
\end{algorithmic}
\end{algorithm}
The policy table makes the structural differences explicit. “Initial focus” describes blank insertion; leading, mid-leaf, and leaf-end cases can migrate payload or choose a different child as described below. [confirmed]
| Source token | Type | Ordered children | Initial focus |
|---|---|---|---|
EF2Eh | 0x20 | numerator, denominator | numerator |
00B2h | 0x21 | enclosed expression | enclosed expression |
0024h | 0x22 | lower bound, upper bound, body, variable | lower bound |
0025h | 0x23 | variable, body, evaluation value | variable |
00F1h | 0x24 | index, radicand | radicand, with Ans as the index |
00BFh / 00C1h | 0x25 / 0x26 | exponent | exponent |
00BCh | 0x27 | radicand | radicand |
EF34h | 0x28 | base, argument | base |
EF33h | 0x29 | variable, lower bound, upper bound, body | variable |
00F0h | 0x2A | exponent; base precedes the marker | exponent |
The blank entry line stores a zero-byte active leaf. Its only semantic node is
the cursor at byte offset zero; inactive empty leaves remain invalid. Selecting
the n/d template supplies source token EF 2E. 34:5935 maps that token to
type 0x20. [confirmed]
The insertion follows 34:473A, the depth gate at 35:7B37, and the type
dispatcher at 34:5026. The fraction case at 34:51B8–51D4 calls
34:5467–547E, which writes EF 20 00 00 EF 2D before the allocator patches
the structural record ID. In the reset-origin blank-root capture, the ROM
allocates fraction record 8, numerator record 9, and denominator record
10. It advances the structural depth from zero to one and selects numerator
record 9 as the active leaf. [confirmed]
editorInsertStructuralTemplate() consumes the decoded arena state because the
semantic cursor tree does not contain the next record ID or structural-depth
byte. For this capture, it produces EF 20 08 00 EF 2D, moves the cursor into
the empty numerator, and creates an EF 1E token in both children. The decoded
post-key tree, all five record headers, and the complete cursor-off LCD bitmap
match the calculator. The constructor also returns that decoded arena directly,
so a following translated edit does not need to import RAM again. [confirmed]
Fraction insertion exposes the four cursor classes most clearly:
| Cursor state before insertion | New numerator | Bytes retained after the marker | Selected child |
|---|---|---|---|
| Blank root | EF 1E | none | numerator |
After 1 | 1 | none | denominator |
Between 1 and 2 | 1 | 2 | denominator |
Before 12 | EF 1E | 12 | numerator |
The migrated leaf keeps editor-only header state: the leaf-end case retains
word0F = 0 and word11 = 1. In a nested example, outer records 8–10
remain in place while the allocator appends fraction 11 and children 12
and 13; the left payload migrates to child 12, and structural depth advances
from one to two. Rebuilding the semantic tree from scratch would lose these
record identities. [confirmed]
Natural-input oracles cover all four cursor classes at the root and in both children of an outer fraction. In every case the translated cursor AST, record fields, ancestor metrics, and all 768 LCD bytes match the calculator. Deeper fraction positions remain open. [confirmed]
Integral, nDeriv(, summation, log-base, and the one-child forms join the
shared marker path at 34:5057, then allocate at 34:4862–34:492B.
One-child forms also pass through 34:5473 and 34:58A0. Multi-argument forms
reserve their child IDs in the table order above, initialize every child with
EF 1E, and select the first child. The three captured permutations therefore
distinguish integral (lower, upper, body, variable), derivative
(variable, body, value), and summation (variable, lower, upper, body).
[confirmed]
Across these forms, blank and leaf-end insertion retain the payload to the
left of the cursor; leading and mid-leaf insertion replace one complete packed
token on the right. Root-level natural captures cover all four cursor classes
and match the decoded cursor AST, every record field, and all 768 LCD bytes.
The blank derivative variable is visually distinctive: it adds two pixels
between the derivative fraction and body, repeats after the evaluation bar,
and renders EF 1E as a solid five-pixel focus box. [confirmed]
The nth-root route is separate: 34:504F enters 34:51C0–51D9, then reaches
34:5473, 34:58A0, and the three-record allocator at 34:4862.
Blank-root insertion places Ans (72h) in the index child and EF 1E in the
radicand child. The cursor enters the radicand. Leaf-end and mid-leaf insertion
move the payload left of the cursor into the index. Leading insertion creates
blank index and radicand children and enters the index. Leading and mid-leaf
insertion replace the packed token immediately to the cursor’s right.
tools/oracles/mathprint/mathprint-editor-structural-mutation-oracles.json captures the four
states. The translated AST, every record field, and all 768 LCD bytes match
their calculator states. [confirmed]
Source token F0h maps to postfix-power type 0x2A through
eqdisp_source_type_table. The editor
dispatcher enters 34:50EF–511D, then joins the shared marker and allocation
path at 34:5057. Blank-root insertion supplies Ans (72h) as the base.
Leaf-end and mid-leaf insertion bind the atom immediately left of the cursor.
Leading insertion has no base and replaces the packed token immediately to the
cursor’s right. [confirmed]
The leading state contains EF 2A id_lo id_hi EF 2D without a preceding base.
It is valid while the editor gap is active, and the LCD draws the exponent
cursor above an empty base position. The JavaScript graph uses an editor-only
emptyPowerBase node for this state. Settled graph decoding continues to reject
a postfix-power marker without a base. Four reset-origin captures cover every
root cursor class and match the cursor AST, every record field, and all 768 LCD
bytes. [confirmed]
When the gap precedes an existing structural marker, 34:58A0–58B4 inserts
the new six-byte marker without consuming the old one. Seven natural captures
apply the one-child, nth-root, power, log-base, integral, nDeriv(, and
summation constructors before the same completed fraction. Each post-key root
leaf contains the new marker followed by the original EF 20 id_lo id_hi EF 2D
marker. The existing fraction record also retains its +05h child-selector
byte. The cursor AST carries that byte as editor_child_selector, so later
JavaScript mutations reconstruct the live arena rather than replacing it with
the selector implied by a settled tree. All seven translated states match every
record field and the complete cursor-off LCD bitmap. [confirmed]
The radical template supplies source token 00BCh; 34:5935 maps it to type
0x27. Insertion follows 34:473A, the depth gate at 35:7B37, and
34:4169 into the type dispatcher at 34:5026. The type-0x27 path calls
34:5037, 34:5473–547B, and the marker writer at 34:58A0–58B4 before
34:4862–491D allocates the structural record and its radicand leaf.
[confirmed]
Blank-root insertion allocates radical record 8 and radicand record 9.
The parent leaf receives marker EF 27 08 00 EF 2D; child 9 receives
EF 1E, and the cursor selects that child. Leaf-end insertion retains the left
payload before the marker. With a token to the cursor’s right, the ROM replaces
one packed token with the radical marker. It does not move left payload into
the radicand. The leading 12 capture therefore becomes radical → 2, while
the mid-leaf capture becomes 1 → radical. [confirmed]
A fifth root capture begins with 3 L1 and places the cursor before the two-byte
5D 00 token. Radical insertion removes both bytes and produces 3 → radical.
editorInsertStructuralTemplate() applies the same packed-token boundary rule.
[confirmed]
Insertion into either child of an outer fraction allocates radical record 11
and radicand leaf 12. Numerator insertion replaces payload in leaf 9;
denominator insertion replaces payload in leaf 10. The controller depth moves
from one to two, and the cursor selects leaf 12. Blank, leaf-end, leading, and
mid-leaf captures cover all four cursor classes in both children. [confirmed]
The allocator loads the entry-record pointer from 0x8DBC and invokes unnamed
bcall ID 53ADh at 34:4900–34:4905. Initialization at
34:4908–34:4928 overwrites the new ID, type, parent, selector, and depth
fields. It skips bytes +07h–+10h and does not write +12h or +13h.
Structural insertion therefore retains the byte that occupied +13h in the
old entry record. Root insertion at a nonzero cursor offset retains the entry
leaf’s first payload byte. Insertion at offset zero retains EFh because the
new marker becomes the first payload unit. A nested insertion does not derive
this byte from structural depth or from the active child. [confirmed]
Four additional captures begin with root token 3 and insert a second radical
into the first radical’s radicand. Blank, leaf-end, leading, and mid-leaf cases
cover every cursor class in that child. The new radical record retains 33h
from entry record 7, including the blank case whose active radicand begins
with EFh. A blank fraction inserted at the same position also retains 33h,
which exercises the shared structural-allocation rule. [confirmed]
tools/oracles/mathprint/mathprint-editor-structural-mutation-oracles.json retains the five
macro and trace hashes, both RAM states, sparse arena bytes, screenshots, and
complete LCD hashes. [confirmed]
editorInsertStructuralTemplate() retains the old entry byte before
reconstructing the arena. Across all 17 radical transitions, its cursor AST
matches the decoded post-key tree, reconstruction matches every record field,
and execution matches all 768 LCD bytes. The fraction discriminator has the
same record and LCD parity. Seven additional transitions cover insertion before
an existing fraction marker. Other deeper structural positions and
structural-boundary navigation outside the fraction, integral, summation,
nDeriv(, and log-base cases remain open. [confirmed]
Cursor navigation
Cursor movement is token movement until it reaches a structural boundary. At that point it becomes tree navigation:
\begin{algorithm}
\caption{Move the MathPrint cursor}
\begin{algorithmic}
\IF{a packed token exists in the requested direction}
\STATE move the complete one- or two-byte token across the gap
\ELSIF{the cursor is entering a structural marker}
\STATE select the first child for \textsc{right}, or the last child for \textsc{left}
\ELSIF{a sibling exists in the requested direction}
\STATE commit the current child and select the sibling endpoint
\ELSIF{the cursor is inside a structural record}
\STATE commit the child and return before or after the containing marker
\ELSE
\STATE leave the root state unchanged
\ENDIF
\end{algorithmic}
\end{algorithm}
Ordinary in-leaf navigation uses the page-6 gap movers. LEFT reaches
06:4294–42C7 through 34:42B4 and 00:3B49; RIGHT reaches
06:42C8–4301 through 34:4193 and 00:367B. Both paths call
00:1FE7 so a two-byte native token crosses the gap as one unit. Structural
record markers remain on separate page-34 paths. [confirmed]
tools/oracles/mathprint/mathprint-editor-navigation-oracles.json captures 12 with the cursor
at the end, after LEFT places it between the digits, and after RIGHT returns it
to the end. The middle state splits the logical payload into left byte 31h
and right byte 32h; its cursor offset is one. Its active leaf width is 12
pixels, not 18: before existing payload the cursor overlays the following cell
without adding width. At the leaf end, the cursor allocates a six-pixel cell
and the width returns to 18. All three reconstructed record sets and complete
cursor-off LCD bitmaps match their calculator states. [confirmed]
editorMovePackedTokenCursor() translates both directions and rejects a
structural boundary rather than applying the ordinary token rule there. After
the decoder emits a cursor inside a numeric run, the following digit begins a
new atom before the two sides recombine around the cursor. [confirmed]
A structural marker occupies six bytes:
EF type id_lo id_hi EF 2D. RIGHT immediately before the marker selects the
first child at byte offset zero. LEFT immediately after the marker selects the
last child at the end of its payload. Both paths store the marker’s starting
offset in the containing leaf, set the structural record’s one-based child
selector at +05h, and increment the controller depth. The RIGHT route follows
34:4193–419B, 34:41E6–41F5, and 34:4285–4290. The LEFT route follows
34:42B4–42BC and 34:4311–4338. [confirmed]
The entry routes commit the containing gap leaf before selecting a child. A
leaf can temporarily hold the left-gap byte count at +11h. The commit restores
the complete payload length. A summation followed by X enters its marker from
the right with +11h = 6, then stores +11h = 7 while the cursor is in the
summation. editorMoveCursor() performs the same restoration from the decoded
payload length. [confirmed]
RIGHT at a non-final child endpoint selects the next child at byte offset zero
through 34:4193–41D7. LEFT at a non-first child start selects the preceding
child at its payload end through 34:42B4–42EA. Each route stores the old
child endpoint in that leaf’s +0Fh word and updates the one-based selector.
Ordinary movement within a child changes only the active gap split; its stored
+0Fh word remains unchanged until a structural transition commits the
endpoint. [confirmed]
RIGHT at the final child endpoint returns to the containing leaf immediately
after the marker through 34:41DC–4245. LEFT at the first child start returns
immediately before the marker through 34:42ED–430E. The containing structural
record becomes the controller, and the depth decreases by one. At the root
leaf’s outer endpoints, 34:41AE–41DF and 34:42C5–42CC return without
changing the arena. [confirmed]
tools/oracles/mathprint/mathprint-editor-structural-navigation-oracles.json retains seven
reset-origin traces. Each fraction direction has seven adjacent RAM states.
The integral traces retain 11 RIGHT states and ten LEFT states. A
depth-two fraction trace retains 11 RIGHT states. The summation traces retain
11 RIGHT states and ten LEFT states. Their 60 key transitions cover entry,
ordinary child movement, sibling selection, nested entry and exit, structural
exit, and root endpoint no-ops. editorMoveCursor() reproduces every controller,
active leaf, cursor offset, payload, child list, +05h, +0Fh, and +11h
transition. Its returned decoded arena feeds the next movement directly. All
seven sequences reach every subsequent captured state without replaying a
recorded result. [confirmed]
One additional natural trace walks completed nDeriv(X,X,1) and
logBASE(2,8) templates in both directions. Its four sequences retain 30 RAM
states and 26 adjacent key transitions. The nDeriv( traversal covers its
atomic variable plus ordinary body and evaluation-value children. The
log-base traversal confirms that navigation follows the native base/argument
child order. For these four sequences, editorMoveCursor() also matches every
layout word and the reconstructed LCD bitmap at each state. [confirmed]
A second natural trace adds both directions for absolute value, radical,
$e^x$, $10^x$, nth-root, and postfix power. Its 12 sequences retain 66 RAM
states and 54 adjacent key transitions. Together the two trace files cover 96
states and 80 transitions across 16 sequences. Root-level live navigation is
therefore captured for every insertable structural type 0x20–0x2A.
Type 0x2B matrices are assembled from bracket tokens in the editor rather
than entered as a structural template controller. [confirmed]
One more natural trace walks [[1]] in both directions. Its two sequences
retain 14 RAM states and 12 adjacent key transitions, including the endpoint
no-ops. Moving across the five packed tokens relocates the semantic cursor
outside the outer list, inside either list frame, and on both sides of the
element. editorMoveCursor() decodes each post-move AST rather than replaying
those shapes. [confirmed]
The depth-two fraction’s mirrored LEFT trace adds 11 states and ten transitions. It starts after the outer fraction, enters its atomic denominator, returns to the outer numerator, enters the inner fraction from the right, walks both inner children, and exits both controller levels before checking the root endpoint. [confirmed]
A mixed-controller trace walks a completed fraction inside a radical in both directions. Its 18 states and 16 transitions enter and exit a one-child radical and a two-child fraction at depth two, with geometry-first cursor placement at both marker boundaries. The extra navigation corpus now has 139 states and 118 transitions across 21 sequences. [confirmed]
The reducer decodes each TilEm PNG and compares its black expression pixels with the translated record renderer. Because the blinking cursor may be gray, black, or absent, it masks only the cursor-cell rectangles emitted from the decoded active leaf. All other 96-by-64 pixels must agree. Every state in all 21 sequences passes that independent screenshot comparison as well as the exact arena comparison. [confirmed]
The cursor cell changes live metrics when it moves within a child. Entering the
nDeriv( evaluation value at its end expands that leaf from four to nine
pixels and propagates the five-pixel increase through the structural record and
its ancestors. Log-base applies the analogous propagation and shifts its
argument when the base cell expands. The translated post-move construction
pass reproduces those record updates while leaving the page-6-owned +0Fh and
+11h gap words intact. [confirmed]
A cursor immediately before a fraction, nth-root, or postfix-power marker allocates a large or small cursor cell because those structures begin with geometry rather than a full-size operator cell. Other structural markers let the cursor overlay their leading operator. For postfix power, the base remains before the six-byte marker in the parent leaf. The decoded editor AST therefore keeps a cursor at that boundary inside the power base; placing it after the completed power would reconstruct the cursor six bytes too far right. [confirmed]
The integral variable child uses leaf render type 0x01. Its cursor remains at
byte offset zero. LEFT from the root’s post-marker position enters that child
at zero rather than at the payload end. A second LEFT commits offset zero and
selects the body at its payload end. In the other direction, RIGHT from the
variable’s offset zero commits its full payload length in +0Fh and exits the
integral. The variable therefore has no separate pre-token and post-token
cursor states. [confirmed]
A leaf containing only EF 1E is also atomic. The depth-two fraction trace
enters the outer denominator at offset zero. RIGHT commits the two-byte
payload in +0Fh and exits the outer fraction without exposing a cursor state
after the empty square. This trace supplies the natural witness for
34:75BB fallthrough. [confirmed]
The type-0x29 summation traces combine both atomic forms in one four-child
record. The variable child has type 0x01; the lower-bound child retains
EF 1E; the upper-bound and body children contain ordinary digits. A trailing
root X adds ordinary parent-leaf movement before or after the structural
crossing. Both directions visit all four children. [confirmed]
The summation fill trace retains eight adjacent states from template insertion
through structural exit. The new variable child begins as type 0x01 with an
EF 1E payload. Inserting X reaches the type test at 34:4796–34:479B,
commits the one-byte variable, and calls 34:4181 to select the lower-bound
child automatically. Lower-bound, upper-bound, and body insertion remain in
their current child. The following RIGHT commits that child’s payload length
to +11h and either selects its sibling or exits the summation.
editorInsertPackedToken() and editorMoveCursor() reproduce all seven
transitions as one composable decoded-arena sequence. The sequence starts from
the decoded arena returned by editorInsertStructuralTemplate() for a blank
root rather than from the first recorded summation state. Each reconstructed
state matches the calculator’s record fields and cursor-off LCD bitmap.
[confirmed]
Deletion and structural collapse
Deletion distinguishes ordinary bytes from empty structural children:
\begin{algorithm}
\caption{Delete at the MathPrint cursor}
\begin{algorithmic}
\IF{the target is an ordinary packed token}
\STATE remove the complete native token
\IF{a non-root leaf becomes empty}
\STATE install the empty-slot token \texttt{EF 1E}
\ENDIF
\ELSIF{the target is an empty structural child}
\IF{the record has one child}
\STATE unwrap that child
\ELSIF{the type is a fraction or nth root}
\STATE promote the sibling payload
\ELSE
\STATE retain the blank child
\ENDIF
\ENDIF
\end{algorithmic}
\end{algorithm}
The generic transition tests apply the same decoded-arena rules to types
0x20–0x2B, a six-child matrix, two-byte child tokens, and depth-two nested
markers. The type-0x01 variable rule is also tested in the integral,
nDeriv(, and summation child positions. Live root-level sequence parity now
covers every insertable type 0x20–0x2A; all 16 added directions include
exact layout-word and screenshot parity. One depth-two fraction RIGHT
and one LEFT traversal are also captured. The two token-built matrix
directions and both radical/fraction directions include the same exact parity.
Matrix deletion, row/column edits beyond the captured one-cell stream, and
other deeper structural combinations remain open. [confirmed]
DEL removes the packed token at the right edge of the gap through
34:4570, 00:3687, and 06:4393–43A4. 06:43A5 reads the token and calls
00:1FE7; 06:439C advances editTail once for a one-byte token and twice
when the classifier returns carry. Deleting 2 from the middle of root 12
therefore advances editTail from 0xFC44 to 0xFC45 without changing the
cursor offset. [confirmed]
An empty active leaf takes the additional 34:4549–455B path. It inserts
EF 1E through 34:4BB9–4C0D and the page-6 gap writer, then calls the LEFT
mover so both bytes land in the right gap segment. The cursor remains at byte
offset zero before the restored square. [confirmed]
tools/oracles/mathprint/mathprint-editor-deletion-oracles.json retains adjacent root and
fraction-numerator deletion states. editorDeletePackedToken() produces both
decoded post-key trees exactly, reconstruction matches every record field, and
execution matches both complete cursor-off LCD bitmaps. The finite tests also
delete a two-byte native token as one unit. [confirmed]
DEL on a structural child reaches 34:44F4. 34:47C7 checks that the
active child contains only EF 1E and that the cursor precedes that token.
34:4504–450D then compares the child count at 0x8DBA with one. A one-child
record reaches 34:4537, where 34:47FF removes the six-byte marker and the
record with its direct child leaf. 34:453A–4544 rebuilds the parent layout
and makes the containing leaf active. [confirmed]
Fraction type 0x20 and nth-root type 0x24 take 34:451F–4534 for either
child. 34:452F XORs the one-based child selector with 3, which swaps child
one and child two. The loop copies the sibling’s native payload into the
containing leaf before it removes the wrapper. Deleting an empty numerator can
therefore promote a denominator, and deleting an empty nth-root index can
promote its radicand. The reverse directions promote the numerator or index.
An EF 1E sibling contributes no bytes. [confirmed]
The other multi-argument types fail the 0x20 and 0x24 comparisons at
34:4513 and 34:4517. They fall through 34:451C to ordinary deletion at
34:456C, which leaves the empty-slot token unchanged. Integral 0x22,
nDeriv 0x23, log-base 0x28, and summation 0x29 therefore retain a blank
active child. [confirmed]
Schema 4 of tools/oracles/mathprint/mathprint-editor-structural-deletion-oracles.json retains
nine live transitions. They cover both promotion directions for fractions and
nth-roots, a blank radical, a power that retains its Ans base, and the
protected integral path. editorDeleteStructuralTemplate() mutates the
decoded record graph, then runs the graph decoder at the parent cursor
position. All nine cursor trees, meaningful record fields, and complete
96×64 LCD bitmaps match the calculator. A finite dispatch test applies the
classifier to blank insertion states for every type from 0x20 through
0x2A. [confirmed]
When deletion empties a leaf inside another structural record,
34:454D–455B checks the new controller type and inserts EF 1E. A fraction
deleted from a radical radicand therefore restores a square in the radicand and
leaves the cursor before it. The top-level type-0x1F wrapper returns at
34:4554, so a deleted top-level template can leave a zero-byte root leaf.
[confirmed]
The ROM leaves EFh in physical byte +0x13 when the resulting active payload
is empty. That byte lies outside the logical payload. Other nested deletion
states, matrix type 0x2B, and structural-boundary deletion remain open. [confirmed]
34:759C–75A5 first subtracts six from its record pointer and compares that
source pointer with editTail. Only equality reaches 34:789A. That helper
tests bit 0 of tblFlags; when the bit is clear, it forces NZ, and when the bit
is set, it preserves A while testing cxCurApp against kYequ (49h). A
zero result therefore requires both the bit and the Y= application. Natural
RAM and screenshot captures show the bit set while the inverse-video = field
is selected. [confirmed]
Y= selection state
The Y= editor stores a one-byte selection-field prefix at editTail, then
advances the page-6 record source past it. The first compared source pointer is
therefore editTail + 1; later records advance farther through the bounded edit
buffer. The short X^2 selection trace enters both metric passes with
editTail = 0xFC9A and source pointer 0xFC9B. An overflowing six-power expression
enters 12 times with source deltas 1, 9, 17, 25, 33, and 41 in each of its two
passes. Selecting = on an empty expression makes no metric call. These three
captures are recorded in tools/oracles/mathprint/mathprint-yequ-selection-oracle.json. Thus a
valid state that makes 34:789A return Z has already failed the pointer guard,
and 34:75A9 taken is infeasible under this entry invariant. The JavaScript
translation retains the raw early-return path for byte-level routine parity.
[confirmed]
When the Y= selection guard is false, 34:75AB reads the marker type from
editTail + 1.
34:40F9 groups fraction (0x20), nth-root (0x24), and power (0x2A)
markers; 34:75B0 takes its Z branch for this set. 34:75B8 then reads the
nesting counter at 0x8515, and 34:75BB distinguishes zero from nonzero
depth. tools/macros/mathprint-power-boundary-insert.macro reproduces the
top-level power-marker path. The mixed radical/fraction trace naturally
exercises 34:75B0 fallthrough. Both depth-two fraction directions naturally
exercise 34:75BB fallthrough at nonzero depth. The RIGHT fraction trace
remains the first report witness for the latter. [confirmed]
Record-oracle coverage
The record-oracle corpus contains 114 captured cases and includes every type
from 0x1F through 0x2B. Types 0x20–0x2B have decoded record nodes and
complete accepted-write oracles. The type-0x1F case is the transparent
one-child wrapper described below: it has a captured node, child write stream,
and pixel-exact entry screenshot, but it emits no primitive of its own. This
saturates the 13-type record-node domain, not the internal branches of every
handler. [confirmed]
eqdisp_draw_marker_primitive has two distinct entry ABIs. Render-table row 0
in eqdisp_render_handler_table (34:6119) contains
the bytes 43 61, the pointer 6143h. _LdHLind at 00:0033 executes
the following sequence:
LD A,(HL)
INC HL
LD H,(HL)
LD L,A
RET
Its low-byte load therefore makes a type-0x1F table dispatch enter
eqdisp_draw_marker_primitive with A=0x43. That value follows
the fixed default path to the seven-row bitmap at 34:61BE; (IY+44h).3 and
0x8520 do not affect this ABI. [confirmed]
Shared marker rendering
The editor calls the same helper through a different route. 06:7F29 loads
editTail, 06:7F2D reads the marker type at editTail + 1 into A, and
06:7F2E calls the bjump descriptor at ram:30BD. Its bytes
CD 09 2B 43 61 74 select 34:6143. The radical-marker trace enters with
A=0x27 and (IY+44h).3 set, selecting the bitmap at 34:630C. The integral
trace enters with A=0x22 and emits display code 0x7C. The reproductions in
the coverage table use
tools/macros/mathprint-radical-nonspecial-insert.macro and
tools/macros/mathprint-integral-boundary-insert.macro. Absolute value,
$e^x$, $10^x$, log base, and summation add live paths for A=0x21, 0x25,
0x26, 0x28, and 0x29. The editor marker domain also includes the
exceptional 0x2C marker produced by the EF36h synthetic state. The default
bitmap path handles it. [confirmed]
34:4FD9 allocates type 0x1F as a transient one-child root record.
34:6028 loads A=0x1F, and 34:602B calls 34:7844 to store the current
render type at 0x8DE7. The following jump to eqdisp_render_child1
(34:636C) renders child 1 without using eqdisp_render_handler_table. A natural matrix-entry capture contains wrapper
ID 6 at 0x9DB7 and child leaf ID 7 at 0x9DF9. Executing the captured
graph through the JavaScript walker reproduces its 76-by-10-pixel entry image
with zero differences. The wrapper contributes no pixel operation; all 175
accepted writes come from its child program. [confirmed]
The JavaScript record walker therefore keeps the two ROM-proven ABIs separate.
A one-child type-0x1F node follows the natural eqdisp_render_child1
continuation. A childless node models the independent
eqdisp_render_handler_table entry and its row-0
bitmap. No retained natural trace combines 0x8DE7=0x1F with 34:6105 →
34:6143, so the latter remains a decoded table ABI without a natural record
dispatch. [confirmed]
settledSharedMarkerPrimitive() translates every conditional at
34:6143–61BD. Its finite test enumerates all 256 values of A, both states
of (IY+44h).3, and all 65,536 values of 0x8520 when A=0x2B. Values of
0x8520 are irrelevant for other A values. The resulting 33,554,432-state
projection has 14 path classes and 26 branch outcomes. [confirmed]
For the type-0x2B matrix marker, a nonzero high byte at 0x8521 or a low
byte at or above the active bound emits display code 0x7C and sets
(IY+32h).2. The bound is six when (IY+44h).3 is clear and eight when it is
set. A smaller low byte emits 0xC1 when that flag is set. When it is clear,
the helper emits the five-row bitmap at 34:61C7 and clears (IY-1).0.
Retained natural traces do not yet exercise the four matrix-only conditionals
at 34:6178–34:618E. [confirmed]
The object walker calls the post-render tail at 34:61CE with the current
record type in A, the handler’s one-based child selector in E, and the
structural nesting counter at 0x8515. Types 0x21, 0x27, and 0x2B
always join 34:79C9 and decrement that counter. Type 0x22 decrements for
child 3 or later; types 0x24 and 0x28 decrement for every child except
child 1; type 0x23 decrements only for child 2; and type 0x29 decrements
only for child 4. Every other type/child state preserves the counter. The
decrement is byte-sized, so zero wraps to FFh. [confirmed]
settledRenderNestingTail() translates the returned A, complete conditional
path, and counter transition. A raw interpreter executes the pinned bytes at
34:61CE–34:6209 and 34:79C9 for every type/child byte pair against four
counter values, then checks every counter byte on one decrementing and one
preserving path. The symbolic model partitions all
$256^3=16{,}777{,}216$ type, child, and counter states into 15 paths. Natural
traces cover every conditional outcome except the type-0x2B branch at
34:61E2; its behavior is established by the pinned byte interpreter and
finite transition model rather than claimed as a natural witness. [confirmed]
Page 39 layout control remains incomplete. Its class and handler tables, argument
order, row composition, descriptor mapping, and draw paths are decoded. The
browser-side ROM engine now translates the 39:4A74 token/action dispatch and
its IY+2 exponent-context and IY+9 fraction-context class adjustments
through editorTokenDispatch(). It returns the measured-template handoff at
39:672E separately from normal 39:4C27 handler lookup. [confirmed]
Page 39 argument layout and VAT search
Page 39 maps the current token class to a handler recipe, selects a visible
argument window, and emits its cells. When scrolling needs the neighboring
named operand, saved OP identities feed the alphabetic VAT search rather than
a parser-stream scan. [confirmed]
The editorArgumentClamp(), editorRowFromArg(), and
editorLayoutArgument() translations cover the arithmetic at 39:50CF,
39:5101, and 39:513E: argument-count clamping, six-row window origin,
seven-row mapping, and restoration of the caller’s baseline row. The
cross-page continuation after 39:50CF remains caller state. [confirmed]
The editorSubexpressionWindow() and editorSubexpressionCell() helpers
translate 39:4C5A and 39:4CA4: they compute the visible slot, select the
984A or caller-supplied cell base, and retain styled-argument and empty-menu
cross-page exits as explicit states. [confirmed]
editorAdvanceArgument() and editorRetreatArgument() translate the forward
and reverse slot branches at 39:5167 and 39:523B. They distinguish list
endpoints, one- and two-row movement, subexpression fallback, both styled
scroll directions, and the saved-F2 search’s carry exit. The forward two-row
path compares 0x844B with 6 at 39:5181; the reverse path compares it with 3
at 39:5244. These jumps reach 39:4C5A before the styled-record test. Calls
into scroll helpers remain explicit effects. The saved-operand wrappers derive
their alphabetic outcomes from one shared VAT state. A missing VAT state
stops at the saved-F2 search instead of selecting a scroll branch.
The increment-wrap guard cannot execute: its preceding unsigned predicate
requires a nonzero count and an index at most count - 2. [confirmed]
Alphabetic VAT selection
The saved-operand wrappers at 39:5B10–39:5B44 move nine-byte operand
buffers through OP1 at 0x8478. The E7 wrappers restore from 0x85E7; the F2
wrappers restore from 0x85F2. Each restore uses _Mov9B at 00:1A92.
The ascending wrappers then call 39:59E0; the descending wrappers call
39:59F9. Bit 5 of (IY+11h) gates the entire wrapper. A clear bit preserves
the incoming carry and performs no copy or search. With the bit set, search
carry returns without writeback. Carry clear copies OP1 back to the selected
source: 39:5AD2 writes 0x85E7, and 39:5B08 writes 0x85F2. The page-7
search receives the restored OP1 and the current VAT state. The styled overflow
path applies the F2 writeback before it restores and searches E7. A raw-byte
interpreter covers all 4,096 wrapper, gate, derived-carry, and buffer-source
combinations. It also covers every value in every restored byte and every
value in the seven selected payload bytes written back to a saved operand.
[confirmed]
These page-39 buffers contain the nine identity bytes copied by _Mov9B.
Their restore and writeback operations leave OP1+9 and OP1+10 untouched. The
page-7 entry at 07:50BE copies all 11 bytes from OP1 to OP3 through
00:1A0F. Candidate construction starts by clearing all 11 bytes of OP2 at
07:51ED; 07:522E then copies the byte immediately below the selected VAT
record to OP2+9 at 0x848C, while OP2+10 remains zero. The full-register
copies through 00:1AE7 and 00:1A4E return those values in OP3 and OP1.
Failure instead restores both incoming extension bytes from OP3. The
translation and its raw wrapper oracle model this 11-byte behavior while the
saved E7/F2 slots remain nine bytes. [confirmed]
The local dispatcher below those wrappers is translated separately by
editorAlphaSearch(). 39:59E0 and 39:59F9 first call 39:5A17, which
tests whether 0x85DE is class 0x02. The ascending class-2 path enters
39:59AF, emits 0Dh through RST 28h, and seeds OP1 with 14h at
39:59C6. The descending path enters 39:59B6, scans the eight payload bytes
at 0x85E7+1 through 39:5A2E, emits 0Ch, and conditionally calls
39:1BAF when the emitter leaves carry set before the same 14h seed.
[confirmed]
For other classes the ascending and descending paths execute XOR A, then
cross to 00:3A53 and 00:306F, respectively. The fixed-bank stubs reach
07:50B5 (_FindAlphaUp = 4A44h) and 07:50B8
(_FindAlphaDn = 4A47h). Both bcalls take the current variable name in OP1.
They return the selected variable in OP1 and OP3 and its VAT pointer in HL;
carry reports that no matching entry remains. Carry clear then calls
39:5C2E; only class 0x03 with subclass byte 0x01 enters 39:1942.
A = 06h repeats the alphabetic search, while every other value returns with
carry clear. The JavaScript model derives each result from OP1 and a logical
VAT snapshot. It derives the post-search A from the selected OP1 type, so a
protected-program entry repeats without an injected return sequence. Nested
and multi-argument states can therefore exercise the search without replaying
an LCD stream. The page 39 control flow and bcall identities are [confirmed].
The page 07 inputs, outputs, and flag behavior are [confirmed].
editorFindAlphaVat() translates the selection state over an explicit logical
VAT snapshot. Each snapshot entry contains its nine-byte OP-format identity,
the byte immediately below its record, its VAT type-byte address, and its
data-page byte. 07:50BB loads A = 00h, discarding the caller’s
value; 07:5104–07:511D always compare the normalized type class.
[confirmed] 07:5247 maps protected programs to the program
class, complex lists to the list class, type 0x0B to equation class 0x03,
and types 0x18/0x19 to class zero. [confirmed] The comparator at
07:5199 subtracts eight name bytes from OPx+8 down to OPx+1. Borrow
propagation makes OPx+1 the most-significant alphabetic byte. The scan retains
the nearest name above or below the incoming OP1, independent of physical VAT
entry order. It returns the selected identity plus the two extension bytes in
OP1 and OP3, its VAT pointer in HL, and carry at an alphabetic endpoint.
[confirmed]
The decoder at 07:51BE rejects a first name byte below 41h or equal to
72h. It also handles list prefixes 3Ah and 5Dh 40h through
MenuCurrent, inGroup, and bit 0 of (IY+0). While inGroup is set, an
archived candidate uses its page byte for the final 41h/72h gate.
OP1+2=FFh is the ascending-start sentinel at 07:5151; it admits every
filtered Up candidate and makes Dn return carry. [confirmed]
Before comparison, 07:50C4–07:50F7 chooses the VAT region and clears unused
key bytes. Program-like named types, names beginning with 5Dh, and List/CList
keys beginning with FFh use the named region. Other List/CList forms,
including 72h and 3Ah, use the fixed-token region. The fixed path preserves
three name bytes and clears the remaining five. The named path clears bytes
after the NUL-terminated name length; the one-byte 5Dh prefix has comparison
length two. A failed search restores the complete original, unpadded OP1 from
OP3.
[confirmed]
Success returns A = 00h, Z set, and carry clear. Failure restores all 11
incoming bytes to OP1/OP3 and returns A = FEh, Z clear, and carry set.
[confirmed]
editorDecodeAlphaVatSnapshot() builds the logical snapshot from a 64 KiB RAM
image. The initializer at 07:50BE–07:50F9 chooses one of two regions.
Named/list-name searches start at progPtr and stop at pTemp; fixed-token
searches start at symTable and stop at progPtr. [confirmed]
For a type cursor H, both entry forms store T2 at H-1, version at H-2,
data address low/high at H-3/H-4, and page at H-5. A fixed entry stores
three name bytes at H-6–H-8 and advances to H-9. A named entry stores its
length at H-6, name bytes downward from H-7, and advances by 7+length.
The 72h and 3Ah forms use the fixed three-byte step. Type 09h decodes a
fixed comparison key but uses the variable-length step at 07:512C–07:5149.
[confirmed]
editorForwardOverflowCue() and editorReverseOverflowCue() translate the
closed cue routines at 39:66FE and 39:66E9. The reverse routine subtracts
the selected argument at 0x85E0 from the count at 0x85E2 with byte
arithmetic. A result below 8 returns without drawing. Other results place
display code 0x1F at column 1 and row (winBtm - 1) & 0xFF. The forward
routine places 0x1E at row 1, column 1. Both routines restore the word at
0x844B after their display call; the reverse early return leaves it untouched.
A raw-byte interpreter exhausts all 65,536 count/index pairs and every
winBtm byte. [confirmed]
editorFirstArgumentAction() and editorAdvanceAction() compose those
walkers with actions 0x03 and 0x04. They preserve the zero-count
256-iteration loop, byte-wrapped first-slot arithmetic, the one-call
action-0x04 branch, and the flag-controlled tails. The tests exhaust all
65,536 count/index byte pairs for both values of bit 0 in each outer
controller. The walker tests separately exhaust its layout-class and row
predicates. [confirmed]
The retained corpus observes 255 of 1,098 declared editor branch outcomes. It
does not translate every key-to-graph mutation, cursor action, menu, error, or
row-composition path. The live RAM decoder covers a complete captured graph;
it does not predict the next graph from an arbitrary key action. [confirmed]
Accepted LCD-write parity is a separate result. The translated cases compare every synchronous accepted data write, including writes that leave the byte unchanged. Timer-interrupt run-indicator writes stay outside the MathPrint parity surface. A matching byte stream proves the tested construction and draw path; it does not close an unobserved editor or parser branch. [confirmed]
From live editor state to settled drawing
Trace provenance and scope
Two reset-origin TLMT v2 traces use the pinned ROM SHA-256
7d9a7d96d89fc552ebee6afdbdd011fdc6047be9c16d308245dff07eb1f7bd6d.
The raw traces remain outside the repository because they are 162 MB and 202 MB.
tools/oracles/mathprint/mathprint-trace-report.json records their hashes, emulator provenance,
exact entry counts, state bytes, and replay results. [confirmed]
web/mathprint/draw-order.json preserves the visible pixel mutations after the
final expression key press. The filled-integral trace contains 351 accepted LCD
writes that change 522 pixels; the nested trace contains 391 writes that change
610 pixels. This sequence includes both set and clear transitions, so the
interactive preview can replay the controller’s write order instead of an
inferred glyph order. [confirmed]
| Scenario | Page 0x39 editing hits | Final state | Settled LCD replay |
|---|---|---|---|
int(1,2,X^2,X) | 4CA4 ×1, 4DCA ×2, 4DE6 ×1, 4E8E ×7, 4F1A ×14 | 0x85E1=04, 0x85E8=00 | 43×20; zero pixels differ from the model. |
int(1,2,(1//2)X,X) | The same handler entries, plus 683D ×5, 68AE ×1, and 69C8 ×1 | 0x85E8=10, 0x85EB=06, 0x85EE=02, 0x85EF=02 | 47×23; zero pixels differ from the model. |
Both traces execute eqdisp_emit_subexpr2 at 39:4CA4, not the static
multi-argument entry at 39:5167. The nested case also executes the descriptor
cell mapper and geometry selector, confirming that handler-record emission and
descriptor geometry compose in one rendered expression. Neither trace reaches
the exact entries 39:5167, 39:5949, 39:5B10, 39:5B1D, or 39:6ABF.
[confirmed]
Settled render dispatch
For either a live-editor or settled redraw, page 34 walks the arena
recursively. The live path first substitutes the active gap payload through
eqdisp_substitute_active_leaf. A leaf then executes its token-and-marker
payload; a structural
record delegates placement to the handler for its type:
\begin{algorithm}
\caption{Render an arena record}
\begin{algorithmic}
\STATE $record \gets \operatorname{ResolveRecordId}(id)$
\IF{$record.type < \mathtt{0x1F}$}
\STATE $\operatorname{ExecuteLeafProgram}(record.payload, origin, depth)$
\ELSE
\STATE $handler \gets \operatorname{StructuralHandler}(record.type)$
\STATE $\operatorname{RenderPlacedChildrenAndPrimitives}(handler, record, origin, depth)$
\ENDIF
\end{algorithmic}
\end{algorithm}
The settled walker dispatches object kinds 0–12 through the word table at
34:7012. The handlers are 34:6D0C, 706A, 70B8, 702C, 7133, 70A0,
70E2, 70E2, 7087, 7102, 717E, 70C1, and 71C6, in order. Kinds 6
and 7 share 34:70E2. The nested trace also visits transient records near
0xFBDB and 0xFBEF, so the final RAM dump does not contain every walked
record. [confirmed]
These page 39 entries occur before the final GRAPHVAR key press. The
settled redraw after that key does not re-enter them. It calls 34:6D26 and
34:737A from 34:4347 and 34:434A, then traverses the display objects from
34:6016. [confirmed]
For the nested scenario, tools/ti84re/mathprint/analyze_draw_trace.py attributes all
391 visible-changing writes after the final key. The nearest page 34 frames
are 34:5FE7 → ram:34E9 (158 writes), 34:6CA8 → ram:3CE1 (96),
34:5DA2 → ram:3573 (78), 34:5EA3 → ram:3567 (24), and 34:5DBA →
ram:3579 (10). The remaining 25 writes precede the page 34 object traversal
and come from the large-font path. The fixed-page stubs dispatch to page 04
line and point routines and page 01:6297 small-font output. [confirmed]
Point and line primitives
A structural handler emits local points or axis-aligned lines. The shared drawing backend then applies four stages in order:
- Add the word-sized logical record origin.
- Subtract the horizontal or vertical viewport clip.
- Add the byte-sized physical screen origin and reject out-of-bounds points.
- Route each accepted point to the LCD,
plotSScreen, orappBackUpScreenaccording to the destination flags.
The split between logical and physical origins is explicit in
EqDispViewportState (0x8DFA): [confirmed]
#pragma pack(push, 1)
typedef struct {
uint8_t physical_x;
uint8_t physical_y;
uint8_t right_bound;
uint8_t bottom_bound;
uint16_t logical_x;
uint16_t logical_y;
uint16_t horizontal_clip;
uint16_t vertical_clip;
} EqDispViewportState;
#pragma pack(pop)
The point wrapper at 34:5E85 clips each object coordinate through 34:5DD1
and 34:5DEF. Its closed tail at 34:5E98–34:5EA6 passes B=x,
C=63-y, and D=1 to _PointOn at 04:4155. Dynamic samples include
(x,y)=(3,0) → BC=033Fh and (32,20) → BC=202Bh. [confirmed]
_IPoint first supplies drawing-hook command 0 at 04:4158. An inactive
hook preserves Z from the bit test and takes 04:4161 into the local point
path. An active hook can return Z to request the same continuation; NZ restores
the caller’s AF and returns without a local write. The translated dispatcher
keeps the hook body as an explicit external boundary. [confirmed]
The local preprocessor then tests sGrFlags.g_style_active. Retained MathPrint
calls have both the drawing hook and this style bit clear, so 04:4177 jumps
directly to the coordinate path. That path adds the bytes at
ram:8DA1 and ram:8DA2 to B and C, with byte wraparound, before checking
the display bounds. apiFlg4.fullScrnDraw selects 04:4306, which admits
$x$ bytes below ram:8DA4 and $y$ bytes below 64. Clearing the flag selects
04:42EC, which decrements the width bound and also rejects row zero. Both
flag states occur in retained MathPrint traces. [confirmed]
The same translation covers the shared graph-style path rather than assuming
that style state is always inactive. Style 1 at 04:4196 emits the current
point and one to three neighboring attempts chosen from the unsigned
current/previous coordinate relations. It stores the current B:C at
ram:9315 for the next call. Styles 2 and 3 enter 04:40AD; the phase at
ram:9668, step at ram:966C, style parity, and (IY+1Eh).7 select an aligned
ascending or descending point sweep. 04:6F84–04:6FAC initializes the phase
modulo four, while the graph update path keeps the step in 1–3. Styles
0 and 4–FFh emit only the original point. [confirmed]
settledPage4PointPipeline() processes every accepted expanded point in ROM
order through the existing LCD/plotSScreen/appBackUpScreen byte router.
This ordering preserves repeated visits to one byte instead of evaluating the
expanded points independently. A pinned-byte interpreter agrees with the
JavaScript preprocessor across 169,443 states: every input coordinate under
both bounds helpers, offset and width boundary cases, all thick-point relation
classes, every style-dispatch byte, and valid shaded phase/step samples. The
symbolic report separately partitions all 512 style-dispatch states, all
33,554,432 effective-coordinate bounds states, all 4,294,967,296 thick-point
coordinate relations, and all 3,145,728 graph-initialized shaded states at the
64-row limit. Arbitrary drawing-hook body behavior remains external.
[confirmed]
04:42B5–04:42E3 converts the graph coordinate into the point mask, LCD
commands, and buffer offset. It selects 80h >> (x & 7), uses x >> 3
as the byte column, and computes 3 * ((4 * display_row) & FFh) + byte_column.
The row multiplication keeps its intermediate in one byte. Rows 40h–7Fh
therefore alias rows 00h–3Fh before the column is added. _PointOn fixes
D=1, so 04:424D–04:4254 ORs that mask into the current byte. MathPrint’s
wrapper clips to the 96×64 display before this entry. [confirmed]
The offset addresses plotSScreen at 0x9340 and appBackUpScreen at
0x9872. 04:424C–04:42B4 maps D=0, 1, 2, and 3 to clear, set,
XOR, and test. Test returns the masked bit without writing. The other modes
route the resulting byte according to (IY+3Ch) and plotFlags.1 at
(IY+02h). Bit 3 selects appBackUpScreen and bypasses LCD I/O. Otherwise,
bit 0 selects the same direct-RAM route through plotSScreen. With both bits
clear, the routine reads and writes the LCD. plotFlags.1 chooses the LCD byte
as the source and preserves plotSScreen; clearing it selects and rewrites the
RAM byte. Bit 2 mirrors the result to appBackUpScreen. [confirmed]
Retained MathPrint traces take the LCD route: (IY+3Ch) bits 3, 2, and 0 are
clear, while plotFlags.1 is set. The translated renderer therefore reads the
current LCD byte, applies the point mode, and emits the accepted controller
write without modifying either RAM buffer. A raw interpreter checks all four
modes, every source byte, all eight masks, every (IY+3Ch) byte, and both
values of plotFlags.1. This covers 524,288 routing transitions in addition to
the 8,192 isolated mode/mask transitions. [confirmed]
04:426B contains a controller-dependent workaround. When the hardware check
returns NZ, byte column 5 and rows before command B7h force result bit 0 on
the LCD write. The state transition exposes that condition explicitly. It does
not apply the modified LCD byte to either RAM destination. [confirmed]
The JavaScript translation matches a raw interpreter of the pinned helper
bytes for all 65,536 input-coordinate pairs. A second exhaustive comparison
covers every previous byte for each visible coordinate: 1,572,864 point-on
transitions. The wider diagnostic canvas used for overflow inspection can have
coordinates above FFh; those pixels cannot enter the page-4 byte ABI and are
kept separate from the physical LCD claim. [confirmed]
The line wrappers share eqdispViewport. eqdispViewport.physical_x is the
screen $x$ origin, while eqdispViewport.logical_x is the record $x$ origin.
eqdispViewport.physical_y and eqdispViewport.logical_y are the corresponding
$y$ origins. Keeping those pairs separate matters in
shifted editor modes even though all four values are zero on the normal home
entry line. [confirmed]
34:5D96 passes a clipped vertical segment to 04:431D;
eqdisp_draw_hline_clipped (34:5DA6) swaps the
axes and passes a clipped horizontal segment to 04:4382. The nested trace’s
fraction rule enters 34:5DA6 with object coordinates x=1–5, y=6 and
origins x=16, y=5. Page 04 receives endpoints (17,52) and (21,52).
[confirmed]
The callers supply ordered endpoints. The wrappers treat the two varying
coordinates asymmetrically. An underflow of the first coordinate against the
logical clip clamps to zero. A first coordinate at or beyond the exclusive
screen bound returns without drawing. The second coordinate instead returns
on underflow and clamps at bound-1 on overflow. 04:4379 performs the
exclusive-bound test, while 04:43C0 selects the smaller ending coordinate.
The JavaScript transition keeps the word-sized logical origins and clips
separate from the byte-sized physical origins. [confirmed]
_DarkLine at 04:4025 fixes H=1 before entering _ILine at 04:4029.
04:4042–04:4069 computes the absolute byte deltas, direction bits, major
axis, doubled minor increment, and signed error. 04:4078 calls _IPoint at
04:4157 before each step, so both endpoints produce writes. The major delta
is incremented as a byte; a delta of FFh therefore produces 256 point
visits. [confirmed]
A raw interpreter of the pinned _DarkLine bytes checks all 131,072 ordered
endpoint pairs for horizontal and vertical lines. It also checks all 65,536
nonnegative delta pairs, which covers every major/minor ratio and signed-error
sequence. Separate reverse-direction cases cover the direction branches.
Physical MathPrint lines now compose this stepper with the translated point
state transition. A hook-handled point remains delegated to the external hook
body and produces no inferred local LCD write.
[confirmed]
Word-sized geometry and clipping
The structural handlers retain coordinates and dimensions as 16-bit words.
For example, 34:62B4–34:62C3 reads a radical child’s width word, increments
DE three times, and passes the resulting word endpoint to 34:5DA6.
34:620A–34:622C performs the corresponding word comparison for a fraction.
The translated renderer therefore accepts widths beyond 255 and wraps additions
at 16 bits before viewport clipping. A radical with record width 258 reaches a
clipped vinculum from $x=-167$ through $x=87$ instead of rejecting the record
as byte-sized geometry. [confirmed]
Structural render handlers
The dispatcher is easiest to read as a vocabulary of visual constructs. The paragraphs below retain the coordinate and trace details for each row.
| Type | Construct | Handler | Distinctive ordered output |
|---|---|---|---|
0x20 | Stacked fraction | 34:620A | Numerator, denominator, then a rule sized from the wider child. |
0x21 | Absolute value | 34:6347 | Two vertical bars, then child 1. |
0x22 | Integral | 34:622F | Inclusive stem and four hook points; child placement comes from the record. |
0x23 | nDeriv( | eqdisp_render_handler_table | Derivative fraction, body, variable, evaluation bar, then repeated variable and value. |
0x24 | nth root | 34:6315 | Index, root hook and stem, radicand, then vinculum. |
0x25 / 0x26 | $e^x$ / $10^x$ | 34:6381 | Fixed glyph, then exponent child. |
0x27 | Square root | 34:62A1 | Root hook and stem, radicand, then vinculum. |
0x28 | logBASE( | 34:63B2 | Prefix, base, opening shape, argument, then closing shape. |
0x29 | Summation | 34:6504 | Sigma/equals forms, children 1–3, then delimited child 4. |
0x2A | Postfix power wrapper | 34:6375 | Recursively renders child 1; emits no primitive itself. |
0x2B | Matrix | 34:65AA | Left bracket, row-major children, then right bracket. |
Render-record type 0x22 dispatches through 34:6105 and
eqdisp_render_handler_table to 34:622F. The word at record offset +7 is the integral-sign
height $h$. The handler draws the inclusive stem (2,1)–(2,h-2), then hook
points (3,0), (4,1), (1,h-1), and (0,h-2), in that order. [confirmed]
Render-record type 0x20 dispatches to 34:620A. The handler renders child
records 1 and 2 through 34:636C and 34:6378, then reads each child’s word at
offset +7. It draws an inclusive horizontal line from (1,y) to
(max(w_1,w_2)+1,y), where the parent word at offset +0x0B supplies $y$.
The nested-fraction trace reaches the line wrapper with BC=1, DE=5, and
HL=6, yielding (1,6)–(5,6). [confirmed]
Render-record type 0x2A dispatches to a JP 34:636C at 34:6375.
34:636C selects child record 1 through 34:6CCD before entering the recursive
renderer. The wrapper emits no point, line, or glyph itself. The corrected
record-table trace identifies 0x2A as the X^2 root type. [confirmed]
Render-record type 0x27 dispatches to the radical handler at 34:62A1. It
emits the ten-byte root-hook bitmap through eqdisp_draw_radical_hook
(34:62D0), draws the vertical stem,
selects child 1, and reads that child’s word at offset +7. It then draws the
inclusive vinculum from (2,0) through (w+3,0) and renders child 1 through
eqdisp_render_leaf_program. The cursor-free history redraw for sqrt(X^2+1) has height 8 and a
child +7 width of 0x17. It reaches the wrappers with stem
(2,1)–(2,7) and vinculum (2,0)–(0x1A,0). This produces the 26-pixel
rendered width. The final editable-entry redraw has child width 0x1D and a
vinculum endpoint of 0x20; cursor and edit-state geometry therefore remain
separate from history-echo geometry. [confirmed]
Render-record type 0x21 dispatches to the absolute-value handler at
34:6347. The parent words at +7 and +9 supply height and width. The
handler draws inclusive vertical bars at x=2 and x=w-4, then renders child
1 through 34:636C. The cursor-free abs(X-3) history redraw reaches the line
wrapper with (x,y_1,y_2)=(2,0,6) and (0x1A,0,6). [confirmed]
Render-record type 0x24 dispatches to the nth-root handler at 34:6315. It
renders index child 1, emits the root-hook bitmap at x=w_1-1, draws its short
vertical segment, renders radicand child 2, and draws the vinculum. The
cursor-free nthroot(3,X+1) history redraw reaches the wrappers with vertical
segment (5,3)–(5,4) and vinculum (5,2)–(0x18,2). [confirmed]
The remaining settled render types map to calculator constructs through
eqdisp_source_type_table and post-ENTER traces. Type 0x23 renders
nDeriv(, 0x25 renders $e^x$, 0x26 renders $10^x$, 0x28 renders
logBASE(, 0x29 renders summation, and 0x2B renders a dimensioned matrix.
Type 0x1F is a transient one-child root type. The main draw entry at
34:6016 selects its child directly through 34:636C, so an ordinary history
redraw does not dispatch that root through 34:6105. [confirmed]
Types 0x25 and 0x26 share the body at 34:6381. The handlers position and
conditionally emit fixed display codes 0xDB and 0x1D, respectively. They
then render child 1. The child record supplies its local origin through offsets
+0x0B and +0x0D. [confirmed]
Type 0x28 dispatches to 34:63B2. It emits the three bytes returned by
_KeyToString for 00C1h, renders child 1, emits the opening compound shape
through 34:5D1A, renders child 2, and emits the closing compound shape through
34:5D07. The settled logBASE(2,8) root has child IDs 0x0010 and 0x0011.
[confirmed]
Type 0x29 dispatches to 34:6504. It conditionally emits display code
0xC6, renders children 1–3, and surrounds child 4 with the compound emitters
at 34:5D1A and 34:5D07. It also conditionally emits display code 0x3D
between children 1 and 2. The settled sum(N,1,3,N^2) root contains child IDs
0x0014–0x0017. [confirmed]
Type 0x2B dispatches to 34:65AA. It emits the left vertical bracket and its
two inward points, renders the matrix elements in child-ID order, then emits
the right bracket and its points. 33:4F23 derives the element-loop bound from
the dimensions at record bytes +0x12 and +0x13. The high byte at +0x12
stores the column count. Byte +0x13 stores the row count. A settled
$2\times2$ identity matrix renders four children between the bracket operations.
[confirmed]
Matrix layout
The type-0x2B constructor lays out elements in row-major order. For element
width $w_{r,c}$, height $h_{r,c}$, and baseline $b_{r,c}$, define each column
width, row baseline, descent, and height as [confirmed]
$$ \begin{aligned} C_c &= \max_r w_{r,c}, \\ B_r &= \max_c b_{r,c}, \\ D_r &= \max_c(h_{r,c}-b_{r,c}), \\ R_r &= B_r+D_r. \end{aligned} $$
The first column begins at $x_0=6$, and each later column begins after the previous extent and a six-pixel gap. The first row begins at $y_0=0$, and each later row begins after the previous extent and a two-pixel gap:
$$ \begin{aligned} x_{c+1} &= x_c+C_c+6, \\ y_{r+1} &= y_r+R_r+2. \end{aligned} $$
Each element is centered horizontally and baseline-aligned vertically:
$$ \begin{aligned} X_{r,c} &= x_c+\left\lfloor\frac{C_c-w_{r,c}}{2}\right\rfloor, \\ Y_{r,c} &= y_r+B_r-b_{r,c}. \end{aligned} $$
For $m$ rows and $n$ columns, let $N_e$ be the element count, $H$ the matrix height, $W$ the matrix width, and $y_c$ the vertical center:
$$ \begin{aligned} N_e &= mn, \\ H &= \sum_r R_r+2(m-1), \\ W &= 12+\sum_c C_c+6(n-1), \\ y_c &= \left\lfloor\frac{H}{2}\right\rfloor. \end{aligned} $$
The constructor stores $N_e$, $H$, $W$, and $y_c$ in the words at +5, +7,
+9, and +0x0B, respectively. [confirmed]
Primitive matrix cells all have the same baseline, so they cannot distinguish
baseline alignment from height centering. A mixed-height $1\times2$ trace
does. Its first cell is 2//3^1, with $(h,b)=(16,6)$; the second is
(1+3)*abs(2), with $(h,b)=(7,3)$. The row has $(R,B)=(16,6)$, so the
calculator stores child origins 0 and 3. Height centering would place the
second child at 5, producing 116 wrong pixels while retaining the correct
79-by-16 dimensions. The baseline formula reproduces the captured graph and
all pixels exactly. [confirmed]
The word at +0x11 stores the column count in its high byte and structural
depth in its low byte. The byte at +0x13 stores the row count. When the matrix
contains more than one element, the allocation pass reserves the first child
leaf and then leaves one unused ID before scanning that leaf for nested
records. Primitive captures therefore have reachable child IDs 0x11,
0x13, 0x14, and so on when the matrix record is 0x10. A structural first
cell uses 0x11 for the leaf, leaves 0x12 unused, and assigns its first nested
record ID 0x13. [confirmed]
Five reset-origin traces cover primitive $1\times1$, $1\times2$, $2\times2$,
$2\times3$, and $3\times3$ matrices; a sixth covers the mixed-baseline case
above. The JavaScript constructor matches every captured record field, child
ID, and element position. The matrix result begins
at LCD row 9 and uses $x=95-W$, where $W$ is the outer leaf width at +7.
The generated streams match 32, 46, 92, 134, and 180 synchronous accepted LCD
data writes, respectively. [confirmed]
The $2\times3$ capture also contains eight accepted writes from the standard
timer’s run-indicator handler at 01:6BBA–01:6BFA. That handler reads and
rewrites LCD byte column 11 across rows 0–7 through indicCounter and
indicBusy at 0x8476/0x8477. Removing those asynchronous writes leaves the
134-write MathPrint stream. The generated timeline models the synchronous
settled renderer and labels the interrupt writes separately in its oracle.
[confirmed]
web/mathprint/rom-engine.js implements the complete 0x1F–0x2B structural
dispatch table as an executable record-graph walker. It resolves child IDs
through a node map, adds each child record’s +0x0B and +0x0D origins on
recursive entry, preserves the handler’s depth changes, and returns ordered
primitive and leaf operations. A settled expression enters this layer from a
type-0x00 leaf program at eqdisp_render_leaf_program. The program executor consumes its payload
in order and invokes embedded structural records against the same pen and depth
state. Row 0 uses the bitmap bytes at 34:61BE, as fixed by the table-load ABI;
the captured type-0x1F wrapper instead uses the direct-child ABI above. The
nDeriv( handler renders child 1 again at 34:64B3, then
places display code 0x3D after that child’s +7 width. [confirmed]
Recovering semantic trees from records
A leaf is both text and a small program. Ordinary native tokens expand to display codes; embedded markers invoke structural records by ID without discarding the text on either side:
\begin{algorithm}
\caption{Execute a leaf record program}
\begin{algorithmic}
\WHILE{$pc < payloadEnd$}
\IF{$pc$ names an embedded structural record}
\STATE $\operatorname{RenderRecord}(\operatorname{ResolveRecordId}(marker.id), pen, depth)$
\STATE advance past the marker
\ELSIF{$pc$ is an embedded-object separator}
\STATE advance past the separator
\ELSE
\STATE $(token, pc) \gets \operatorname{DecodeNativeToken}(pc)$
\FOR{each $displayCode$ in $\operatorname{KeyToString}(token)$}
\STATE $\operatorname{EmitGlyph}(displayCode, pen, depth)$
\ENDFOR
\ENDIF
\ENDWHILE
\end{algorithmic}
\end{algorithm}
The record graph is a layout program. The semantic AST is a second view decoded from ordered children, balanced native delimiters, and embedded-record markers. The live-editor path substitutes the active gap payload before this decode. Neither view is inferred from LCD pixels or screenshots. [confirmed]
The trace analyzer recovers leaf records from the resolver path. At 34:6CCD,
DE is the one-based child index and ram:8DF2 points at the parent. At
34:6CD8, DE contains the selected child ID and HL points at its resolved
record. Pairing these observations produces the complete record graph visited
by a settled render. [confirmed]
Leaf payload begins at record offset +0x13; the word at +0x11 gives its
byte count. A one-byte scalar therefore stores its display byte at +0x13.
Compound leaf objects retain the subsequent bytes in the same record. A leaf
may construct and dispatch another structural record while it renders. The
analyzer preserves these secondary dispatches in instruction order. It uses the
first eqdisp_render_leaf_program entry at the shallowest Z80 stack depth
after the final key press
to identify the enclosing leaf program. [confirmed]
The analyzer also decodes the reachable settled graph into a semantic
expression tree. Structural child IDs recover argument order. A type-0x2A
record binds its exponent to the expression immediately before the
embedded-record marker. Balanced standalone 10h and 11h bytes become a
group node, so a postfix power after the close binds to the complete group.
The decoder advances over native two-byte tokens before interpreting grouping
bytes. The pair 5E10h, for example, remains one token rather than opening a
group at its low byte. The decoder preserves EF 1E as an explicit extended
token. The renderer maps the pair to display code 0xF7, so the decoded tree
exposes an unfilled template slot. The tree identifies the expression in a
trace without using LCD pixels or a screenshot. It describes the settled graph
consumed by eqdisp_render_leaf_program. The browser-side ROM engine exposes the same decoder for
its generated AST view as settledAst. The live-editor wrapper applies this
decoder after eqdisp_substitute_active_leaf substitutes the active gap
payload, then inserts the
cursor at the byte boundary selected by editCursor. [confirmed]
A structurally populated live matrix entry demonstrates why leaf decoding
cannot treat every payload as a flat token sequence. Before evaluation, wrapper
ID 6 points to a leaf
whose payload begins 06 06 EF 27 08 00 EF 2D: the outer and first-row
06h containers precede an embedded radical record. The next element contains
an embedded type-0x2A power marker. The decoder balances 06h/07h matrix,
08h/09h list, and function/group delimiters, splits only on depth-zero
2Bh or row-closing 07h, and resolves structural markers inside each cell.
It recovers [[sqrt(2),X^2][3,4]] as a 2-by-2 matrix AST from the captured RAM.
This raw-container representation is distinct from the evaluated type-0x2B
matrix record, although both decode to the same semantic matrix shape.
[confirmed]
Within that program, EF type id_lo id_hi invokes the structural record with
the given little-endian ID. EF 2D terminates or separates the embedded object
without drawing it. Ordinary payload bytes may follow. The settled
sum(N,1,3,N^2) entry invokes type 0x29. Its body child emits N, invokes
type 0x2A, and then closes the exponent object. This byte order matches the
structural dispatch and glyph trace order. [confirmed]
executeSettledRecordProgram() translates this byte stream rather than
replaying captured glyph events. Tests provide record headers, child IDs, and
payload bytes as input. They compare the generated display-code, coordinate,
depth, and order tuples with independently captured 34:6C37 observations for
absolute value, summation with an exponent, and nested nDeriv(. [confirmed]
Glyph selection and hooks
The ordinary-token path resolves payload bytes through smallfont_glyph_ptr
at 01:6702. A zero lead selects the word table at 01:4252. The two-byte
leads 5Ch, 5Dh, 5Eh, 60h–63h, 7Eh, AAh, BBh, and EFh select
tables at 01:4452–01:47E8. The 5Eh second byte selects one of four banks.
The BBh path clamps indices F6h–FFh to F6h. [confirmed]
The raw D:E selector accepts more states than the native token grammar.
Leads 01h–5Ch alias the 5Ch table. Both 5Eh and 5Fh test index bits
4, 5, and 6 in that order, clear the first selected bit, and otherwise clear
bit 7. Leads 64h–7Dh and 7Fh–BAh alias the AAh table. Leads BCh–FFh
alias the EFh table. _IsA2ByteTok prevents these extra aliases during
ordinary token decoding. [confirmed]
01:6765 inherits Z from the preceding CP BBh; LD A,L does not change it.
That JR NZ therefore falls through under the 01:6702 entry ABI. The
CP 13h clamp at 01:6774–01:6778 has no other predecessor and is
unreachable from this entry. A pinned byte interpreter matches the JavaScript
table, normalized index, pointer-word address, and complete branch sequence for
all 65,536 D:E pairs. [confirmed]
01:6788 skips the token hook when (IY+35h).0 is clear. When it is set,
01:7C53 classifies the Catalog2 hook header and version before the token-hook
call. An invalid header returns C and Z, an older version returns C and NZ, the
exact version returns NC and Z, and a newer version returns NC and NZ. Only the
invalid-header class reaches 01:67AC: offsets through 0546h retain BC=0
and the offset in DE, while larger offsets pass the offset in BC and
000Ch in DE. [confirmed]
The page-3B wrapper at 3B:7B8D either enters the installed hook or clears
(IY+35h).0 and returns to the ROM string. The finite model covers both flag
values, four Catalog2 classes, all 65,536 offset words, and both wrapper
outcomes. The installed hook body and any pointer or string that it returns
remain external. [confirmed]
Each selected pointer names one metadata byte followed by a counted
display-code string. Token 72h therefore expands to A, n, s. Token
C2h expands to s, i, n, (. Two-byte token 5D 00 expands to L and
the subscript-1 display code. _GetTokLen = 4591h reads the count, and
_Get_Tok_Strng = 4594h copies the counted bytes. The browser uses the
ROM-extracted tables in web/mathprint/token-strings.json; it preserves native
token boundaries while constructing the settled record. [confirmed]
34:6873 receives each resulting display code. It diverts parentheses 28h
and 29h, and braces 7Bh and 7Dh, to delimiter geometry. This includes
28h embedded in the spelling of sin(. 34:678C dispatches parentheses to
34:5D28 and 34:5D15, and braces to 34:5E0F and 34:5E14. The four paths
emit points and lines instead of a large-font glyph bitmap. [confirmed]
Six reset-origin traces cover Ans+1, Ans^2, sqrt(Ans), X^Ans,
sin(X), and sin(sqrt(X)). Their generated graphs match every record field.
Their complete accepted-write streams contain 49, 40, 63, 32, 56, and 83
writes, respectively. Replaying each generated stream produces the same final
96×64 LCD bitmap as replaying its captured eqdisp_render_leaf_program
interval. X^Ans verifies
the variable-width small-font spelling. sin(sqrt(X)) verifies a counted token
spelling before a structural child and the compound shapes around its taller
metrics. The structural record stores the containing leaf’s accumulated
horizontal anchor at +0x0D. [confirmed]
Five reset-origin traces cover L1, [A], Y1, Str1, and X^L1. Their
generated record graphs match every field after normalizing record IDs. Their
accepted-write streams contain 21, 35, 21, 42, and 22 writes. The generated
stream and captured outer eqdisp_render_leaf_program interval have the same
byte-column, row,
and value for every write. Replaying either stream produces the same 96×64 LCD
bitmap. X^L1 verifies two-byte spelling and width in the small-font exponent
path. [confirmed]
The translated renderer then maps the ordered operations through the ROM font
bitmaps, page-4 point and line behavior, _VPutMap, the page-7 large-glyph
path, and LCD byte packing. Six settled programs reproduce every accepted LCD
data write through the outer eqdisp_render_leaf_program return: absolute value (49 writes), nth
root (69), radical (82), summation (66), nDeriv( (96), and a nested
integral/fraction (114). This comparison includes accepted writes whose value
does not change the displayed byte. [confirmed]
Compositional metrics
When a containing leaf appends a box with metrics $(h,b)$ to its current metrics $(H,B)$, the metric pass unions the extents above and below the baseline: [confirmed]
$$ \begin{aligned} B_{\mathrm{out}} &= \max(B,b), \\ H_{\mathrm{out}} &= B_{\mathrm{out}} + \max(H-B,h-b). \end{aligned} $$
The reset-origin expression
((1*A)/X)//(N-N)//(1*3)*((sum(X,2,1,N)*abs(3))/int(3,1,X,X))
combines an outer fraction with $(h,b)=(21,6)$ and a summation with
$(h,b)=(19,9)$. Its live root record stores
$(H_{\mathrm{out}},B_{\mathrm{out}})=(24,9)$. Maximizing the
height and baseline independently would produce the incorrect height 21.
The small-font table at 03:4CD6 stores seven rows per glyph. _VPutMap emits
the five interior rows. It retains an interior zero row, but it does not emit
the padding row above or below the glyph. A row that crosses an LCD byte
boundary writes the right byte before the left byte at 01:63CE–01:641A.
The large-font path emits all seven rows of its fixed cell. [confirmed]
34:6C37–34:6CAB prepares two page-1 driver states. A root glyph uses the
seven-row record built by 07:45B6. A raised glyph enters 01:6297 with a
one-row source skip and a five-row count, so 01:6354–01:6374 advances past
the first small-font padding row. The selected count also omits the trailing
padding row. Vertical viewport clipping changes the skip and count before the
page-1 call. [confirmed]
34:6C4D loads the width byte from the selected font record. When
fontFlags.2 is clear, 34:6CBC adds three columns for display codes 28h
and 29h, and two for 7Bh and 7Dh. Other codes retain the record width.
The addition at 34:6C5A is byte-sized and can wrap. The translated metric and
draw paths use this result for delimiter cells instead of a separate fixed
width. Root calls set fontFlags.2 and retain the six-column large-font cell.
Raised parentheses begin with width three and raised braces with width four;
the correction expands both families to six columns. A pinned-byte interpreter
covers both flag values and every display-code and width-byte pair. [confirmed]
The two states use different right-edge comparisons. The root state compares
the endpoint with 0x61 at 01:630A, while the raised state compares it with
0x60 at 01:630E. CCF followed by JP C rejects endpoints at or above the
selected limit. A root six-pixel glyph beginning at x=90 therefore draws
through pixel 95. A raised four-pixel glyph beginning at x=92 is rejected as
a unit. Pinned-byte differential tests cover all 3,584 pen-byte, width, and
mode states plus all 112 width, bit-offset, and mode row states. [confirmed]
01:6360–01:6378 computes $8-o-w$, where $o$ is the LCD bit offset and
$w$ is the glyph width. A nonnegative result selects the one-byte path. Its
DJNZ entry arrangement rotates the screen byte by the remaining-space count,
calls 01:6431, and reverses the rotation. A negative result selects the
two-byte path, negates the value to obtain the overflow count, and circularly
rotates the two-byte screen window. [confirmed]
The width-mask table at 01:6446 contains FE FC F8 F0 E0 C0 80. For an
aligned screen byte $s$, mask $m$, and glyph row $g$, 01:6431–01:6445
computes $(s \mathbin{\&} m) \mathbin{\mathtt{xor}} g$ for ordinary text.
When textFlags.3 is set, it computes
$((\mathord{\sim}m) \mathbin{\vert} s) \mathbin{\mathtt{xor}} g$ instead.
The translated composition matches a pinned-byte interpreter for all 917,504
screen-byte, width, glyph-row, and inverse-flag inputs. An independent row test
covers 7,340,032 screen-window, offset, width, and inverse states, including
both sides of every LCD byte boundary. [confirmed]
Absolute values, powers, and roots
The absolute-value constructor translates a closed slice of the earlier record
pass. eqdisp_lookup_render_type maps source token 00B2h through
eqdisp_source_type_table to
render type 0x21. The translated 34:4900, 34:7393, and 34:7609 paths
construct the containing leaf, the absolute-value record, its child leaf, and
their settled metrics. Fresh reset-origin traces for abs(2), abs(X/2), and
abs(X+12) match the generated record fields and every accepted LCD data write.
The trace streams are comparison oracles and are not constructor inputs.
[confirmed]
The compositional constructor translates the type-0x2A power and type-0x27
radical paths. eqdisp_lookup_render_type maps source token 00BCh through
eqdisp_source_type_table to render
type 0x27; the containing leaf embeds the structural ID and constructs the
radicand as child 1. Its settled height, width, and baseline derive from the
child metrics. Raised radicals select the final five rows of the root-hook
bitmap at 34:62D0, while outer radicals use all seven rows. [confirmed]
eqdisp_lookup_render_type maps source token 00F0h through
eqdisp_source_type_table to render type 0x2A.
The containing leaf embeds EF 2A id_lo id_hi EF 2D, and child 1 contains the
raised payload. The metric branches at 34:7393 and 34:7609 distinguish the
first raised row from later raised rows. The JavaScript translation constructs
right-associated record trees and obtains raised-glyph widths from the ROM
small-font table. [confirmed]
Parentheses remain ordinary leaf tokens 0x10 and 0x11 in the settled
record. 34:6873 maps their display codes 0x28 and 0x29 to the compound
emitters at 34:5D1A and 34:5D07. A raised parenthesis keeps a six-pixel
token-cell metric. The shape height and baseline follow the enclosed payload.
The type-0x2A word at +0x0D stores the containing leaf width accumulated
before the power object. It is 0x1E for (X+1)^2, after the five six-pixel
leaf tokens, and 0x0C for the power inside (X^2+1). [confirmed]
Five reset-origin traces cover (X+1), (X^2+1), (X+1)^2, X^(1+2), and
abs(X^2+1). The generated record graphs and complete accepted-write streams
match these traces. The streams contain 49, 60, 59, 32, and 60 writes,
respectively. [confirmed]
List braces remain leaf tokens 0x08 and 0x09; 0x2B separates elements.
Their display codes 0x7B and 0x7D enter the same matching-delimiter scans
at 34:689A and 34:6951 as parentheses. The final renderer uses the brace
paths at 34:5E0F and 34:5E14. Each path emits two top points, an upper
vertical segment, a waist point, a lower vertical segment, and two bottom
points. The waist row equals the enclosed payload’s baseline. It can differ
from the geometric midpoint. [confirmed]
Natural traces for {1,2} and {sqrt(2),1} pin the seven-row symmetric case
and the nine-row case whose baseline is row five. The translated native parser
retains element boundaries in a list node, including nested lists and
structural elements. Both oracle bitmaps match the calculator pixel for pixel.
Three seed-13 list-only differential cases add fractions, roots, variables, and
ordinary operators; all three match their natural calculator screenshots.
[confirmed]
When the immediate base of a power ends in a structural object, 34:70C1–7084
merges that object’s baseline and lower extent into the type-0x2A metrics.
It does not use the containing leaf’s accumulated baseline: an earlier radical
to the left does not raise a later plain-token power. [confirmed]
After the leaf obtains its merged baseline, 34:77AD–77C1 revisits every
directly embedded structural record. It subtracts the record’s baseline at
+0x0B from the leaf baseline in ram:850A, then writes the difference at
+0x0F: [confirmed]
$$ \mathtt{structure.word0F} = \mathtt{leaf.word09} - \mathtt{structure.word0B}. $$
For a trailing structural power base, the leaf baseline equals the power
baseline. The value is therefore 3 for sqrt(X)^2 and abs(X)^2. A grouped
fraction base has baseline 6 and lower extent 7; its outer power has
baseline 12, height 19, and therefore stores 6 at the fraction’s
+0x0F. The fraction’s visible numerator group requires two native
10h…11h pairs: the fraction scanner consumes the outer pair and retains
the inner pair in the numerator leaf. [confirmed]
For a grouped structural base, 34:70C1 saves the base baseline $b$ and lower
extent $d$. After 34:7283 returns the raised child’s height $h_e$ in
ram:8508, the handler stores [confirmed]
$$ \begin{aligned} b_p &= b + h_e - 2, \ h_p &= b_p + d. \end{aligned} $$
The reset-origin trace for (X^(X-2))^(N/X)^(N-3) gives the inner base
$(h,b)=(10,6)$ and the outer power $(h_p,b_p)=(16,12)$. The inner type-0x2A
record stores 6 at +0x0F. The translated record fields and final LCD pixels
match the trace. The same rule at fraction depth gives inner $(h,b)=(8,5)$ and
outer $(h_p,b_p)=(14,11)$. [confirmed]
Reset-origin traces for sqrt(X)^2, abs(X)^2, and
abs(sqrt(X^2+1)) match every generated record field and accepted LCD data
write. Their complete streams contain 35, 33, and 113 writes, respectively.
The nested case verifies the absolute-value bars, radical hook and vinculum,
powered radicand, and leaf glyphs in ROM emission order. [confirmed]
Fresh reset-origin traces for X^2, X^12, 2^X^2, and 2^X^2^3 match the
constructed record fields and every accepted LCD data write. Their streams
contain 17, 22, 22, and 32 writes, respectively. These captures test one, two,
and three raised levels without supplying records or writes to the constructor.
[confirmed]
Fixed-base exponentials and log base
The type-0x25 and type-0x26 constructors map source tokens 00BFh and
00C1h through eqdisp_source_type_table. Both allocate one exponent child. The child begins at
x=6, uses raised small-font metrics, and determines the parent height, width,
and baseline. The handlers at 34:637E and 34:63AD emit fixed large-font
display codes 0xDB and 0x1D before rendering that child. The fixed symbol
stays in the large font when the containing expression is raised. [confirmed]
For exponent metrics $(h,w)$ and render depth $r$, the shared metric path at
34:73DB produces [confirmed]
$$ H=h+4,\qquad W=w+6,\qquad B=\max(h,6[r>0]). $$
The six-row seed affects an exponential nested in a raised child. A
reset-origin nDeriv(logBASE(1,2),X,e^2) trace stores $(H,W,B)=(9,10,6)$ for
the type-0x25 record. Its containing evaluation-value leaf stores height 9
and baseline 6, which places the value at row 4 in the type-0x23 record.
[confirmed]
Reset-origin traces for exp(12), exp(X^2), exp(1//2), and
tenpow(X^2) match every constructed record field and accepted LCD data write.
Each stream contains 22 writes. The JavaScript renderer generates the writes
from the expression tree, constructed records, structural handlers, ROM font
bitmaps, and LCD byte-packing logic. [confirmed]
The type-0x28 constructor maps source token EF34h through
eqdisp_source_type_table and
reserves the base and argument leaves before scanning either payload.
34:76A9–34:76BF decrements the structural-depth byte through 34:79C9.
It places the base one pixel below the argument baseline only when the remaining
depth is zero. The horizontal and height constants also select large-row or
raised-row geometry: [confirmed]
$$ \begin{aligned} x_b &= 11+7[r=0], \\ y_b &= b_a + [d=1], \\ x_a &= w_b+17+7[r=0], \\ y_a &= 0, \end{aligned} $$
where $d$ is the one-based structural depth stored at +0x11, $r$ is the
render depth, and each bracketed comparison contributes one when true and zero
otherwise. The metric pass keeps the large-row base offset only at render depth
zero: [confirmed]
$$ \begin{aligned} H &= \max(h_a, b_a+[r=0]+h_b), \\ B &= b_a, \\ W &= w_b+w_a+23+7[r=0]. \end{aligned} $$
The type-0x28 word at +5 is an active-child selector, not a depth or metric
field. 34:4900–34:491D initializes a new structural record to 1.
34:41BB–34:41D7 compares the selector with the type’s child count and
increments it before entering the next child. Native-source construction follows
the 2,1 child order in its eqdisp_child_scan_table row and leaves this field at 1.
Interactive template entry can leave the same completed two-child record at
2. Neither value changes the type-0x28 LCD handler. [confirmed]
Reset-origin traces for logbase(12,345), logbase(X,X^2),
logbase(3,1//2), and logbase(1//2,3) match every constructed record field.
Their complete accepted-write streams contain 99, 79, 91, and 71 writes. These
cases cover multi-token children, a powered argument, and a stacked fraction in
each child position. [confirmed]
Reset-origin traces for abs(logbase(A,3)),
abs(exp(2))-abs(logbase(A,3)), and abs(abs(logbase(A,3))) cover structural
depths two and three plus mixed baselines in one leaf. The middle case stores
2 at the second absolute-value record’s +0x0F. The nested log-base records
place their base leaf at y=3 while retaining height 9; interactive template
entry leaves their active-child selector at 2. After separating that editor
state from native-source construction, the stable graph fields and their 79-,
130-, and 95-write LCD streams match the traces. [confirmed]
Eight additional reset-origin traces cover radicals, sequences inside radicals,
nested radicals, powers inside radicals, and radicals inside powers. The deepest
cases are sqrt(2^X^2), sqrt(sqrt(2)), X^sqrt(2), and
sqrt(X^sqrt(2)). Their generated graphs and complete accepted-write streams
match the traces. The root bitmap comparison includes accepted writes whose
value does not change the LCD byte. [confirmed]
Nth roots and fractions
The type-0x24 constructor maps source token 00F1h through
eqdisp_source_type_table, then
allocates the containing leaf, structural record, index child, and radicand
child. The index uses the raised small-font metrics. The radicand begins four
pixels after the index width and four pixels below the parent origin. Its
height, width, and baseline determine the structural record metrics. At
34:62D0, an outer nth root selects all seven root-hook rows and a raised nth
root selects the final five. [confirmed]
Fresh traces for nthroot(2,2), nthroot(12,X+12),
nthroot(3,X^2), and X^nthroot(3,2) match every generated record field and
accepted LCD data write. These cases cover a multi-token index, a structural
radicand, and a raised nth root. [confirmed]
The type-0x20 constructor maps the stacked-fraction source token EF2Eh
through eqdisp_source_type_table. It renders both children one depth below the containing leaf.
For numerator height $h_n$, denominator height $h_d$, and child widths $w_n$
and $w_d$, the settled metrics are [confirmed]
$$ \begin{aligned} w &= \max(w_n,w_d), \\ x_n &= 2 + \left\lfloor\frac{w-w_n}{2}\right\rfloor, \\ x_d &= 2 + \left\lfloor\frac{w-w_d}{2}\right\rfloor. \end{aligned} $$
The vertical positions and parent metrics are:
$$ \begin{aligned} y_d &= h_n + 3, \\ H &= h_n + h_d + 3, \\ W &= w + 4, \\ B &= h_n + 1. \end{aligned} $$
The numerator begins at $y=0$. The metric pass clears the numerator leaf’s
word at +0x0F. The renderer consumes its payload through the word at +0x11.
[confirmed]
eqdisp_allocate_record allocates structural records in a fraction numerator before it
allocates the enclosing type-0x20 record. It allocates the numerator leaf
afterward. For (X^2)//3, IDs 0x11 and 0x12 identify the power record and
its exponent leaf. ID 0x13 identifies the fraction, 0x14 its numerator
leaf, and 0x15 its denominator leaf. The graph points from the numerator leaf
back to the earlier power record. A structural denominator follows the ordinary
recursive allocation order. A fraction nested in the numerator recursively
applies the same hoisting rule. [confirmed]
Thirteen reset-origin traces cover leaf, sequence, power, radical, nth-root,
and nested-fraction operands. The generated graphs match every captured record
field and ID. Their accepted LCD data-write streams also match through the outer
eqdisp_render_leaf_program return. [confirmed]
Integral, summation, and nDeriv( records in a fraction numerator follow the
same hoisting rule. eqdisp_allocate_record allocates the multi-argument record and reserves
its child leaves before it allocates the enclosing type-0x20 fraction. The
nested structural record stores 0x10 at +0x13. Raised integral layout uses
a 10-pixel body-to-variable gap; the outer layout uses 12 pixels. In a raised
nDeriv(X^2,X,...) numerator, the body leaf stores 0x58 before the
type-0x2A marker, and the power record stores 4 at +0x0D. [confirmed]
Six reset-origin traces cover integral, summation, and nDeriv( numerators,
each with an ordinary body and a powered body. Before parity is accepted, the
trace analyzer must decode each settled graph to the asserted expression. The
JavaScript constructor then matches every record field and allocation ID, plus
every accepted LCD data write through the outer eqdisp_render_leaf_program
return. [confirmed]
Integrals, summations, and derivatives
The type-0x22 constructor maps integral source token 0024h through
eqdisp_source_type_table. eqdisp_allocate_record allocates the integral
record, then reserves all four child
leaf IDs before it scans any child payload. The children hold the lower bound,
upper bound, body, and differential variable in that order. A structural child
allocates its records after all four reservations. A nested integral repeats the
same reservation rule recursively. [confirmed]
The bounds render one depth below the containing leaf. The body and variable render at the containing depth. For lower-bound metrics $(h_l,w_l)$, upper-bound metrics $(h_u,w_u)$, body metrics $(h_b,w_b,b_b)$, and variable metrics $(w_v,b_v)$, the integral positions and parent metrics are [confirmed]
$$ \begin{aligned} y_b &= \max(5,h_u), \\ s_l &= \max(5,h_l), \\ H &= y_b+h_b+s_l, \\ B &= y_b+b_b. \end{aligned} $$
The horizontal positions and width are:
$$ \begin{aligned} x_b &= \max(w_l,w_u)+12, \\ x_v &= x_b+w_b+12, \\ W &= x_v+w_v+2, \\ y_v &= B-b_v. \end{aligned} $$
The lower bound begins at $(6,H-h_l)$, the upper bound at $(6,0)$, and the body
at $(x_b,y_b)$. The type-0x22 record stores $H$, $W$, and $B$ in the words at
+7, +9, and +0x0B. The variable child uses render type 1. [confirmed]
Twelve reset-origin traces cover unequal token-bound widths, a multi-token body,
a different variable, power, radical, fraction, and nth-root bodies, structural
bounds, and a nested integral. The JavaScript constructor matches every record
field and ID. It also reproduces all accepted LCD data writes through the outer
34:660A return. The traces supply comparison oracles, not constructor input.
[confirmed]
The type-0x29 constructor maps summation source token EF33h through
eqdisp_source_type_table. eqdisp_allocate_record allocates the summation
record, then reserves child leaves
for the variable, lower bound, upper bound, and body in that order. It fills
their payloads after all four reservations. Structural arguments therefore
allocate their records after the reserved leaves. A nested summation applies
the same rule recursively. [confirmed]
The variable, lower bound, and upper bound render one depth below the containing
leaf. The body renders at the containing depth. The variable leaf uses render
type 1. For child height, width, and baseline metrics $(h,w,b)$, define
[confirmed]
$$ \begin{aligned} L &= w_v+4+w_l, \\ O &= \max(w_u,L,12), \\ S_u &= \max(5,h_u), \\ S_l &= \max(h_v,h_l). \end{aligned} $$
Let $B_0=S_u+4$. The parent and body metrics are:
$$ \begin{aligned} B &= \max(B_0,b_b), \\ y_u &= B-B_0, \\ y_l &= B+5, \\ y_b &= B-b_b, \\ H &= \max(y_l+S_l,y_b+h_b), \\ x_b &= O+6, \\ W &= x_b+w_b+5. \end{aligned} $$
The variable begins at $(0,y_l)$ and the lower bound begins at
$(w_v+4,y_l)$. Placing both on the common lower row keeps structural lower
bounds aligned with the variable. The upper bound begins at
$(\lfloor(O-w_u)/2\rfloor,y_u)$. The body begins at $(x_b,y_b)$. The
type-0x29 record stores 3, $H$, $W$, and $B$ at +5, +7, +9, and
+0x0B, respectively. [confirmed]
For ordinary five-row limits and a body whose baseline does not exceed $B_0$,
these equations reduce to $H=S_u+9+S_l$ and $B=S_u+4$. A reset-origin record
capture for
sum(A,1,1,sqrt(int(1,3,N,A))//sqrt(A)^1^X) exercises the taller-body branch.
The body stores $(h_b,b_b)=(33,18)$; the summation stores $(H,B)=(33,18)$,
places the upper limit at $y=9$, and places the lower row at $y=23$.
[confirmed]
Eleven reset-origin traces cover the representative sum(N,1,3,N^2) case,
unequal-width token limits, multi-token limits, power limits, radical,
nth-root, fraction, and power bodies, a different variable, and a nested
summation. The JavaScript constructor matches every record field and ID. It
also reproduces every accepted LCD data write through the outer 34:660A
return. The traces supply comparison oracles, not constructor input.
[confirmed]
The settled lower-bound leaf for sum(N,1,3,N^2) contains 0x31. The byte
pair EF 1E instead emits display code 0xF7, the empty template square. It
appears in captures whose template navigation leaves a slot unfilled, including
discarded summation and nDeriv( captures. [confirmed]
The type-0x23 constructor maps source token 0025h through
eqdisp_source_type_table. eqdisp_allocate_record allocates the nDeriv(
record, then reserves child leaves for the
variable, body, and evaluation value in that order. It fills those leaves
before it allocates structural descendants of the body or value. A nested
nDeriv( applies the same reservation rule recursively. [confirmed]
For body metrics $(h_b,w_b,b_b)$, variable metrics $(h_v,w_v)$, and
evaluation-value metrics $(h_e,w_e,b_e)$, the metric branches at 34:7485 and
the positioning branches at 34:76C2–34:76EF produce [confirmed]
$$ \begin{aligned} B &= \max(6,b_b,b_e-4), \\ x_v &= 5, \\ y_v &= B+2, \\ x_b &= 16, \\ y_b &= B-b_b, \\ x_e &= w_b+w_v+29, \\ y_e &= B+4-b_e. \end{aligned} $$
The record height is the union of all three positioned children, and the total width ends after the evaluation value:
$$ \begin{aligned} H &= \max(y_v+h_v,y_b+h_b,y_e+h_e), \\ W &= x_e+w_e. \end{aligned} $$
The type-0x23 record stores 3, $H$, $W$, and $B$ at +5, +7, +9,
and +0x0B. The variable leaf uses render type 1. The valid settled scalar
case nDeriv(X,X,1) stores 0x58 in both the variable and body leaves. The
same body token precedes the type-0x2A marker in valid powered-body captures.
[confirmed]
Twelve reset-origin traces cover ordinary and unequal-width arguments, power,
radical, fraction, nth-root, and integral bodies, plus nested nDeriv(. The
JavaScript constructor matches every record field and ID. It also reproduces
every accepted LCD data write through the outer 34:660A return. The traces
supply comparison oracles, not constructor input. [confirmed]
A further reset-origin trace covers a tall logBASE( body and a summation
evaluation value whose body contains a raised logBASE(. It proves the
small-row type-0x28 constants and the $B+4-b_e$ evaluation-value placement.
The translated graph matches all 18 records and the final normalized
88-by-19-pixel entry bitmap. The synchronous renderer contributes 195 accepted
LCD writes. Their byte-column and row sequence matches the trace after removing
an eight-write timer interrupt. [confirmed]
34:6C6B adds the four-pixel glyph advance to logical pen positions 87,
91, and 95. 34:6C76 derives the one-past-right coordinate 96.
Final clipping and the run indicator
The compare at 34:6C7C draws the first two glyphs because their endpoints are
91 and 95; it skips the third glyph because its endpoint is 99.
The JavaScript applies the same whole-glyph gate. [confirmed]
The interrupt reaches run_indicator_tick at ram:027B with
indicCounter=1 and indicBusy=0x78. 01:6BBA reloads the counter to 0x14,
rotates the busy byte to 0x3C, and rewrites pixel 95 across rows 0–7 as
0,0,1,1,1,1,0,0. Inserting this translated operation after the captured 28th
renderer operation reproduces all 203 accepted writes, including subsequent
read-modify-write bytes, with SHA-256
1cd0a761fab7b948a1bd55cf47d627cdcab0c24620a2da0d3fe8204d1c3691a1.
The insertion point is timer phase, not expression-tree state. Generated
expression timelines therefore keep this operation labeled as asynchronous UI
state. [confirmed]
Flat absolute-value bodies and expressions composed from ordinary token runs,
the native Ans, sin(, cos(, tan(, ln(, and log( tokens,
right-associated powers, $e^x$, $10^x$, logBASE(, radicals, nth roots,
stacked fractions, and numeric matrices now run from tokens through record
construction, layout, drawing operations, and LCD byte writes. Integrals,
summations, and nDeriv( compose with the same translated forms in their
arguments and in a stacked-fraction numerator. The remaining
arbitrary-expression branches are still untranslated.
[confirmed]
Each dispatch also captures eqdispViewport.logical_x and
eqdispViewport.logical_y.
Nested fraction 1/2 reaches 34:5DA6 with the local rule (1,6)–(5,6)
and origin (16,5). Page 4 therefore receives the translated endpoints
(17,52) and (21,52). [confirmed]
Record header recap
The fixed 20-byte record header contains a two-byte ID at +0, a type byte at
+2, eight unaligned little-endian words at +3, +5, +7, +9, +0x0B,
+0x0D, +0x0F, and +0x11, and a byte at +0x13. The analyzer names words
by offset until each render type establishes its meaning. Words following the
root header are child IDs. 34:6CCD passes an ID through 34:4B05 and
34:4A83 to resolve the child record; these words are not RAM pointers.
[confirmed]
Exact point counts matter here. The resolver’s --funcs mode groups an
instruction under the nearest preceding symbol. It places 69 instructions in
the 39:5167 bucket for each scenario even though the entry itself has zero
hits. tools/tests/trace/test_hardware_trace.py covers this distinction. [confirmed]
Page 39 cell encoding
eqdisp_emit_glyph (39:4E8E) interprets each packed D:E cell as one of
four output classes:
| Cell class | Meaning | Result |
|---|---|---|
D = 1Fh | Cursor marker | Update cursor state without drawing a glyph. |
D = 82h | Indexed string or title | Emit the selected string. |
| Counted-token case | 39:6B66 to _KeyToString (45CAh, implemented at 01:6D10) | Emit each display code from the counted string. |
| Direct-glyph case | Mapper at 39:4F1A | Map the packed cell to one large-font code. |
The direct mapper recognizes three ranges: FC3C–FC40 becomes
E - 3Ch + 5, FE7D–FE81 becomes E - 7Dh, and cells with E = 42h and
D < 0Ah become glyph D. 00C8 therefore draws the literal name fnInt(,
not one glyph. The full decode is in
tools/notes/cell-glyph-spec.md and
tools/notes/token-name-spec.md; the placement geometry
(683D, 6B1C, 5167/5949, pen conversion) is in
tools/notes/geometry-spec.md. [confirmed]
The JavaScript translation of 39:4F1A preserves the returned accumulator,
carry flag, and every conditional outcome through 39:4F43. The handler-cell
classifier consumes that translation rather than a separate mapping table. A
pinned-byte interpreter compares all 65,536 D:E inputs. They reduce to nine
complete paths and 16 branch outcomes. [confirmed]
MathPrint validation and browser model
TI-84 Plus OS 2.55MP — how the standalone renderer is checked against the ROM.
The standalone renderer in web/mathprint is checked against the ROM at
several boundaries: pinned-byte interpreters, captured LCD streams, settled
traces, and browser-level behavior. This page lists that verification stack
and states where the browser model stops claiming ROM parity. The mechanisms
being verified are described in Equation display
(MathPrint) and MathPrint live editor and settled
drawing.
Validation and renderer checks
No single comparison establishes parity. The verification stack deliberately checks increasingly large boundaries:
| Check | Compares | What it establishes |
|---|---|---|
| Pinned-byte differential | JavaScript transition against the corresponding ROM helper bytes | Closed helper branches, flags, and wrap behavior. |
| Decoded-graph oracle | Requested AST against the calculator’s RAM record graph | The calculator accepted the intended expression structure. |
| Accepted-write oracle | Ordered LCD (column, row, value) tuples | Construction and draw order, including accepted writes that do not change a byte. |
| Final-bitmap differential | Generated 96×64 pixels against TilEm | Visible parity, but not operation order by itself. |
| Fuzz run | Native tokens through calculator RAM and screen against the translated graph and frame | Composition across supported constructors. |
From LCD writes to pixels
The renderer writes through the LCD ports rather than a RAM framebuffer.
tools/ti84re/trace/lcd.py replays reset-origin TilEm TLMT v2 LCD I/O through the
pinned TilEm T6A04 state model, including mirrored ports, data reads, busy-write
rejection, and the controller’s 128×64 backing RAM. This reconstructs the
emulator bitmap for a complete compatible trace; it is not a physical-controller
claim. The controller behavior is [standard] for the pinned TilEm source model;
synthetic tests confirm the replay implementation.
tools/ti84re/mathprint/parity.py selects that replay when tracing is enabled.
The local ignored tools/rom.bin enables pinned-ROM reproduction when present.
For screenshot differentials, the tool also decodes the final RAM graph through
the 34:4ACE and 34:4A83 walks in Equation display
(MathPrint). It compares that calculator-decoded
semantic expression with the JavaScript graph before comparing pixels. A
dropped key or incomplete template exit therefore rejects the run instead of
appearing as a renderer mismatch. [confirmed]
RAM and AST differential oracles
The RAM oracle avoids an instruction trace for each fuzz case. TilEm still runs
the calculator to accept the key sequence and produce the screen, but the
ordinary case retains only a RAM dump and screenshot. A mismatched graph gets
retries at two slower key cadences. The final retry uses a 0.24-second key
delay and a 0.12-second inter-key wait. The depth-four seed-505 corpus has 20
calculator graphs matching their requested ASTs and 20 exact pixel matches. In
case 14, the first calculator entry omits native multiply token 82h; the graph
check rejects that entry. A slower accepted graph contains the token and
matches the translated framebuffer. [confirmed]
The differential generator computes structural-record depth independently
from its syntactic generation depth. Calculator comparisons retain expressions
at depth four or below. Deeper generated ASTs remain valid inputs to the
JavaScript renderer, but the home-screen editor cannot construct their fifth
structural record through the path below. The optional
--validate-entry-depth run checks accepted depth-four and rejected depth-five
forms for every translated structural constructor. Seed 606 at generation
depth five rejects one over-limit candidate, replaces it, and produces 15 exact
decoded-graph and pixel matches with no inconclusive case. One long case needs
the final key-cadence retry. [confirmed]
A matrix-only seed-815 corpus constructs numeric $1\times1$, $1\times2$, and
$2\times2$ literals, decodes the live RAM graph before evaluation, and compares
the translated type-0x2B render with the post-ENTER history block. Fourteen of
15 cases have exact decoded ASTs and pixels. The remaining 89-by-30 render is
inconclusive because the 96-by-64 history display exposes only 28 of its rows;
the harness rejects the clipped block instead of comparing it. This corpus
found the baseline-alignment error. Its reduced mixed-baseline case now
matches every captured record field and all 1,264 framebuffer pixels.
[confirmed]
The browser’s generated path encodes native calculator bytes, scans their
one- and two-byte token boundaries through translations of 34:58F9 and
34:5911, and splits nested arguments through the page-34 parse-ahead state
machine at 34:5A99–34:5CAC. The translation includes the public
_AHEADEQUAL = 4B49h, _PARSAHEADS = 4B4Ch, and _PARSAHEAD = 4B4Fh
entries plus the internal entries at 34:5AA3, 34:5AA7, and 34:5AA9.
It constructs settled records and emits accepted LCD data bytes. Each
write replaces one eight-pixel span in a 96×64 framebuffer. Six changed and
deeply nested expressions pin every intermediate write and the packed final
framebuffer without loading a captured write stream. These deterministic cases
exercise summation, integral, nDeriv(, matrix, and a three-level raised
fraction. [confirmed]
Horizontal viewport
The editable input
int(1,3,(1//2)X,X)+int(1,3,(1//2)X,X) has a 106-pixel expression endpoint.
The root record stores 112 at +7: the expression plus a six-pixel cursor
cell. Its child origins remain local at $x=0$, $16$, $56$, and $72$.
[confirmed]
The editor scrolls this record horizontally. 34:5DBE adds
eqdispViewport.logical_x to each local $x$ coordinate. 34:5DC2 then
subtracts eqdispViewport.horizontal_clip, and the admitted path at 34:5DE3
adds eqdispViewport.physical_x: [confirmed]
$$ x_{\mathrm{LCD}} = x_{\mathrm{screen}} {}+ x_{\mathrm{local}} {}+ x_{\mathrm{record}} {}- x_{\mathrm{clip}}. $$
34:5F5D updates the clip for the cursor at the expression endpoint. The
traced editor state has a previous clip of $12$, a cursor width of $6$, and a
right bound of $95$. 34:5F87 stores the resulting clip $17$: [confirmed]
$$ x_{\mathrm{clip}} = \max\left(x_{\mathrm{clip,old}},;106+6-95\right) = 17. $$
The general path follows 16-bit instruction order. 34:5F61 first subtracts
the previous clip. A borrow clears ram:8E02 and restores the unshifted
endpoint. Bit 3 of (IY+44h) selects a six-pixel cursor when set and a
five-pixel cursor when clear. The two callers add either zero or three more
pixels through DE. Both additions wrap as Z80 words before 34:5F7F
compares the result with the low-byte right bound. Carry returns without a
store. Carry clear adds the remaining distance to the current clip and writes
it at 34:5F87. An endpoint left of the previous clip can therefore clear the
clip, and an endpoint near 0xFFFF can wrap before the bound comparison.
[confirmed]
The clip is editor state, not a function of the current width alone. If an edit
shrinks the 162-pixel three-integral record to the 106-pixel two-integral
record while ram:8E02 is 73, 34:5F81 returns and retains 73. Shrinking the
record below 73 takes the borrow path at 34:5F64 and clears the clip.
[confirmed]
The web renderer carries this word across input events and applies the same transition to both its full model metadata and its 96-pixel LCD writer. An eight-integral boundary regression reaches a 442-pixel record and clip 353 without truncating its 127 native token bytes. This case is a deterministic translation regression.
Vertical viewport
The vertical editor viewport is a separate word transition at
34:5F8B–34:5FC0. The routine reads the logical cursor top from ram:8518
and subtracts the previous clip at ram:8E04. A borrow at 34:5F96 clears the
old clip. Bit 3 of (IY+44h) selects a seven-row cursor when set and a five-row
cursor when clear. The live MathPrint redraw calls the routine first with
DE=0, then with DE=4. Both calls compare their wrapped 16-bit coordinate
with the low-byte bottom bound at ram:8DFD; carry returns without a store,
while carry clear advances ram:8E04. [confirmed]
A natural depth-four balanced fraction has record height 125, baseline 62,
cursor top 59, and bottom bound 62. The first pass changes the clip from 0 to
4. The second pass changes it from 4 to 8. The settled expression is therefore
translated upward by eight rows before the LCD writer applies the visible
window. 34:67C8–34:6872 rejects complete glyph cells above or below that
window and admits crossing cells for row clipping. [confirmed]
The glyph gate continues to the lower-edge comparison after an accepted
upper-edge crossing. 34:6807 stores the number of rows above the window in
0x9D01. Raised glyphs add their leading padding-row skip at 34:6848–34:684C.
An endpoint below the lower edge stores the explicit row count in 0x9B72 at
34:683A–34:683F. Bit 0 of (IY-1) marks an active source-row skip, bit 1
marks a rejected glyph, and bit 7 of (IY+32h) marks an active row-count byte.
A viewport shorter than the glyph can therefore clip both edges in one call.
[confirmed]
The finite model partitions every logical-top word, vertical-clip word, and
render-depth byte at the MathPrint bottom bound 0x3E. Its 16 path classes
cover 1,099,511,627,776 projected states. Pinned-byte differential tests also
exercise 229,456 boundary states across nine byte-sized bounds, including word
wrap and dual-edge clipping. [confirmed]
The translated LCD crop is 17×61 pixels and matches the calculator pixel for
pixel. Its SHA-256 is
7516b14104afaa3259d45b4b1577d0a9ae96df4a9bbc65bb52b147e3cb59910d.
tools/oracles/mathprint/mathprint-vertical-viewport-oracle.json records the ROM, trace, RAM,
LCD-write, and crop hashes;
tools/macros/mathprint-nested-fraction-vertical.macro reproduces the natural
entry. [confirmed]
34:6000–34:6015 appends the vertical editor chrome after the settled
expression. A nonzero ram:8E04 clip calls bcall 53DAh; its body at
35:7116 draws the upper cue from the four rows at 35:717D. 34:60A0 loads
the root height, subtracts one and the clip, and applies the same bottom-bound
comparison as 34:5DF8. A remaining endpoint at or beyond the bound calls
bcall 53D7h; 35:715B draws the lower cue from 35:7182. [confirmed]
The normal home editor centers both seven-pixel cells from the horizontal
bound at ram:8DFC, giving $x=44$. Their top rows are 0–3 and 58–61. The final
16 accepted writes in the natural trace exactly match the translated byte
columns, rows, values, and order. Appending them produces the complete 96×64
calculator LCD with zero pixel differences; its flat-byte SHA-256 is
5e34c3710b0dbe45c5f8a8152fbc9db81ac098faa5698df429f5793ec6876d99.
The 17×61 crop above deliberately excludes this separate chrome stream.
[confirmed]
The visible expression therefore begins at effective $x=-17$, while the cursor
cell begins at $x=89$. When ram:8E02 is nonzero, 34:5FF2 calls 34:6031.
That routine draws the seven-row left-overflow bitmap at 34:60B8 through
34:61B2 after the expression. The translated expression plus this cue emits
198 accepted LCD writes. Their byte-column, row, and value triples match the
natural calculator redraw after removing the eight asynchronous right-cue
writes. The compact oracle is
tools/oracles/mathprint/mathprint-editor-overflow-oracle.json; the reproduction input is
tools/macros/mathprint-double-integral.macro. [confirmed]
The centering path does not use an unbounded record height. In normal editor
mode, 34:753F loads the root’s +07h height word. 34:6043 substitutes the
one-byte bottom bound when the height has a nonzero high byte, and 34:604A
does the same when its low byte exceeds the bound. Editor mode 49h bypasses
the load and uses the bound directly. The natural combined-overflow case has
height 125, horizontal clip 15, vertical clip 8, and bottom bound 62, so the
cue occupies rows 28–34 instead of being centered off-screen. The translated
expression and all three overflow cues reproduce all 6,144 screenshot pixels.
tools/oracles/mathprint/mathprint-combined-viewport-oracle.json pins the accepted graph, native
tokens, RAM state, write-stream hash, and full-LCD hash. [confirmed]
Glyph clipping precedes the font blitter. 34:6C5F compares a glyph’s left
edge with ram:8E02; the carry path at 34:6C69 reaches 34:6C81 and skips
the whole glyph while still advancing the logical pen. It does not draw the
suffix of a glyph that begins left of the viewport. The reset-origin expression
(sqrt(X)*1^3)+(N^2+(X*A)) reaches this branch with the radical’s X beginning
three pixels left of the visible edge. Applying the whole-glyph skip, followed
by the seven-row left-overflow cue, reproduces all 870 pixels of the cropped
87×10 calculator frame. [confirmed]
Root-hook bitmaps use the same display-unit gate. 34:630C enters 34:6C37,
whose bitmap header supplies a five-pixel advance before 34:6C5F performs the
left-edge comparison. The reset-origin expression
(sqrt(nDeriv(1,A,1)+11111)) reaches 34:6C69 with logical pen 6 and clip 7.
Nested clipping
The subtraction produces FFFFh with carry, so the ROM omits the complete
five-pixel hook. The vertical stem and vinculum continue through 34:5D96 and
34:5DA6. Translating that unit skip reproduces all pixels in the cropped
87×15 calculator frame. tools/oracles/mathprint/mathprint-radical-viewport-oracles.json pins
the input, trace, viewport state, branch witness, LCD writes, and final bitmap.
[confirmed]
Embedded records have an earlier whole-subtree gate. 34:6641–34:6655 adds
the embedded record’s +09h width to the current logical pen and record origin,
then subtracts ram:8E02. Carry at 34:6659 skips the embedded renderer;
equality draws it. The reset-origin depth-four reproduction reaches the carry
path with logical endpoint 56 and clip 63, producing translated word FFF9h.
It omits the off-left nested power subtree while retaining its logical advance.
The translated record program removes the same two high-level operations and
still matches the calculator’s 87×25 bitmap. Its flat-byte SHA-256 is
b4a60c6f5b1bc78d5a59f6b6fb0f379c999e70dc09c409131677142c0c2b1b09.
tools/macros/mathprint-nested-depth4.macro reproduces trace
b8d970906e63db96d36847dfcafed91d97e73fc7699294cc8debd08e7affdd93.
[confirmed]
The right-edge gate uses the same logical glyph advance. 34:6C6B–34:6C71
adds the advance to the pen. 34:6C73–34:6C7A derives the one-past-right
viewport coordinate, and 34:6C7C skips the glyph when its endpoint is larger.
Equality draws the glyph, so an endpoint of 96 may occupy pixel 95. The
translated viewport applies both whole-glyph gates before rasterization.
[confirmed]
The logBASE prefix is a counted string, not one viewport unit. 34:6C26
loads one display code, 34:6C2A calls the ordinary glyph path, and the
DJNZ at 34:6C2F repeats for the remaining codes. The word pen advances
after a skipped code, so l may be left of the clip while o and g draw;
the last code may likewise be rejected at the right edge without partially
drawing it. The translated editor expands the 6Ch 6Fh 67h string into three
ordered glyph calls before applying either viewport gate. An exact finite
model partitions every initial pen word and clip word for both the root widths
6,6,6 and the raised widths 3,4,4, including pen wrap. [confirmed]
The eight writes inserted at instruction index 56 come from
page_34:6CA8 → ram:3CE1. That call stack does not pass through 34:608F, so
the stream is separate from the right-side bitmap path and remains outside the
settled expression timeline. [confirmed]
The actual 34:608F path is observed elsewhere in the natural trace. 34:607A
loads the wrapper record’s +09h width, subtracts one, adds the logical origin,
and subtracts ram:8E02. Carry returns Z. A zero translated endpoint skips the
second decrement; otherwise 34:6089 decrements once more before 34:5DDB
compares the word with the right bound. A value at or beyond the bound returns
NZ, so 34:5FFD calls 34:608F. The retained witness compares HL=98 with
DE=95 and takes that call. [confirmed]
34:608F places the four-pixel cue at physical screen origin plus right bound
minus four. Its writes update byte column 11 at rows 8–14 with 00, 08,
0C, 0E, 0C, 08, and 00. A fresh-clip home-editor redraw suppresses
this path for every 16-bit expression endpoint at physical origin zero;
shifted or retained-clip viewports still follow the complete selector. Cursor
blink separately writes 0x7C
to byte column 11 on the same rows. The browser models the cue selector and
keeps the unrelated 34:6CA8 stream outside settled expression timelines.
[confirmed]
Text overflow and structural depth
The text-cell path has a separate overflow boundary. 39:4F08 compares
curCol (0x844C) with 0x0F before marker handling and calls the fixed-bank
_EraseEOL jump at 00:3CB7. 39:6712 then sets curCol to 1, emits the
: marker through 00:3FDB, and gates subsequent display modes with 0x85E5.
These page-39 bytes do not control the page-34 horizontal pixel clip above.
[confirmed]
The retained sum(N,1,3,N) trace exposes four calls through 34:5AA3 with
C=1. The scanner stops on the three depth-zero comma bytes. At the closing
0x11 byte, it returns A=0x11, DE=0xFF00, and sets Z and C. The JavaScript
translation matches these registers, flags, and scratch bytes. Static byte
decoding covers the other mode bits and token classes; they do not yet have
independent dynamic coverage for every exit branch. [confirmed]
34:5A05 classifies function openers from packed D:E tokens. Ordinary
one-byte tokens pass through 34:5A52, BB tokens through 34:5A28, and EF
tokens through 34:5A14. The JavaScript scanner applies those comparisons and
ROM tables directly. Generic function runs can therefore contain translated
structural children without depending on a list of preview function names.
[confirmed]
Structural insertion through 34:473A calls the bjump descriptor at
ram:2E41, whose body is 35:7B37. The body reads the structural-depth byte
at 0x8DB6, increments it modulo 256, and compares it with 0x05. Input values
0x00–0x03 preserve A and return with carry clear. Values 0x04–0xFE
return A=0x03 with carry set. Input 0xFF wraps to zero and takes the
carry-clear path. The caller sends carry set through 34:473F to 34:54D2,
which sets (IY+45h).6 and writes 0x05 to 0x9D20. [confirmed]
A normal POWER insertion reaches the gate with A=0x2A. The depth-four trace
enters with depth byte 0x03, increments it to 0x04, and returns through
34:4744 to insert the record. The depth-five trace enters with 0x04, returns
A=0x03 with carry set, and reaches 34:54D2. The reproduction macros are
tools/macros/mathprint-depth4-power-accept.macro and
tools/macros/mathprint-depth5-power-reject.macro; their trace SHA-256 values
are 66c3c43dc306cbf43ba9579171824f180faff7377a23f33b584e07bf5dba78d5
and b8355cb4a58eb2f0a97dd17238e900705c20c5d99ab80f60ee16aa1f876e1f3a.
tools/oracles/mathprint/mathprint-depth-limit-oracle.json records the branch states. [confirmed]
Paired calculator runs place power, fraction, radical, nth-root, absolute,
integral, summation, nDeriv(, $e^x$, $10^x$, and log-base at the same
boundary. All 11 depth-four forms produce the requested decoded graph and
pixel-exact JavaScript frame. All 11 depth-five forms reject the requested
record. This is a calculator-entry limit; it does not limit programmatically
constructed JavaScript ASTs or decoded record graphs. [confirmed]
EF36h also takes this gate. 34:5935 maps it to type 0x2C, and 34:4690
branches through 34:473A instead of using eqdisp_child_scan_table. The accepted
gate path preserves A=0x2C; the rejected path returns A=0x03 as above.
[confirmed]
Below the cap, 34:58A0 inserts EF 2C 00 00 EF 2D. 34:4862 allocates the
type-0x2C record and patches its ID into the marker. The allocator at
33:4F42 supports types 0x1F–0x2B; type 0x2C indexes the adjacent bytes
at 33:4FA9. Those bytes produce E=0x42, BC=0x0002, and HL=0x0018.
In the first observed context, the allocator creates record ID 8 with parent
ID 7 and this 20-byte header: [confirmed]
08 00 2C 07 00 01 00 06 00 03 00 00 00 00 00 06 00 01 00 EF
The parent leaf marker changes from EF 2C 00 00 EF 2D to
EF 2C 08 00 EF 2D. Construction returns normally through 34:547E.
The terminal failure occurs during geometry calculation. 34:7609 indexes the
13-word table at 34:7611 with type 0x2C and reads the code bytes at
34:762B as the word 3BCDh. The dispatcher calls ram:3BCD, whose bjump
reaches 03:467F. That routine returns through the dispatcher’s extra stack
word to ram:0002, entering the reset path through ram:028C and 3F:412C.
The JavaScript translation reports this reset boundary and does not define
type-0x2C metadata, geometry, or rendering support. [confirmed]
Source grammar boundaries
The English external token table names EF37h MATHPRINT and EF38h
CLASSIC; it has no EF36h entry. These names come from the
TI-Toolkit token sheet, not from the
ROM-local control-flow trace. [hypothesis]
The first byte of each eqdisp_child_scan_table row selects a scan policy at
34:5678.
Scan kind 3 enters 34:56E3 with B=2 and selects one unary child. Scan kind
4 enters 34:56EC with C=1 for each source argument. The remaining
nonzero metadata bytes map source arguments to child-record indices. They are
[3,4,1,2] for integral, [2,1,3] for nDeriv(, [2,1] for logBASE(,
and [4,1,2,3] for summation. The JavaScript scanner returns each half-open
source-byte range with its child index and verifies the terminating comma or
0x11 byte. The retained summation trace reaches 34:56EC four times and
matches those ranges. [confirmed]
Scan kind 1 enters 34:5699 for F0h power and F1h nth-root operators.
It saves the source cursor, returns an operand endpoint in BC, and restores
the source cursor at 34:56AC–34:56B3. X^12 returns after both digit
bytes. The 2^(X^(2³)) editor buffer contains explicit 10h–11h raised
slots; separate calls return the outer and inner closing-slot endpoints. The
JavaScript scanner translates these numeric and delimited-slot branches and
requires native construction to stop at the same half-open byte boundary.
The X^Ans buffer stores 58 F0 10 72 11; the X^L1 buffer stores
58 F0 10 5D 00 11. Both traces take 34:56BF and return the byte after the
closing 11h. Raw native bytes for these one- and two-byte named operands now
reproduce the captured record graphs and complete accepted LCD writes.
34:580C also admits direct letters, Ans, list, matrix, and string names,
π, BB31h, and bounded 5Fh/EBh names. The JavaScript translation applies
the classifier and bounded name loop directly. It groups a name designator and
its accepted bytes as one expression atom, so a raised name ends at the same
half-open byte boundary as the ROM scan. [confirmed]
Scan kind 2 enters 34:56DF → 34:5795 for the EF2Eh and EF2Fh
stacked-fraction operators. It rewinds to the numerator, calls 34:5AA7 with
B=14h, and returns the operator’s EFh byte in BC. A second scan selects
the denominator range. The wrapper at 34:57A1–34:57C1 distinguishes
nesting depth in D, unwound boundary count in E, and the saved depth byte
at ram:9D05. The JavaScript scanner retains these results and verifies both
operand ranges before constructing a type-0x20 record. Leaf, powered,
nested-denominator, and raised-fraction cases cover the translated branches.
[confirmed]
Scan kind 6 enters 34:568A for each matrix element. Native matrix values
use 06h and 07h square-bracket tokens for the outer container and each row.
34:57C2 reads the current element token, rewinds ram:965D by one byte, and
then 34:5AA7 scans with B=20h. The returned BC points to a depth-zero
2Bh comma or the row-closing 07h. The 0x9D05 result is 0 for a comma
and FFh for the row close. The JavaScript scanner retains these results and
derives row-major element ranges. Retained $1\times1$, $1\times2$,
$2\times3$, and $3\times3$ value traces pin primitive numeric cells. A
$2\times2$ trace with sqrt(2) and $X^2$ pins the structural-cell path. At
34:5BA7, B=20h makes a function opener increment D. Its closing 11h
sets bit 6 in B, resumes the scan, and reaches the matrix delimiter. The
JavaScript parse-ahead translation applies that branch to ordinary, BB, and
EF opener classes. [confirmed]
Browser model and fuzz domains
When a fraction appears inside a matrix element or another delimited argument,
the direct 34:5795 scan can pass the enclosing 07h or 11h delimiter. The
translated parser intersects that scan endpoint with the active kind-6 or
structural-argument boundary before constructing the child record. [confirmed]
The browser expands every accepted byte into eight ordered pixel results. A timeline row records the previous byte, replacement byte, all eight destination bits, and which bits changed. Accepted writes with equal previous and replacement bytes therefore remain visible in the trace. [confirmed]
The text field uses a preview-specific semantic grammar for ordinary input. It
does not drive the TI-OS editor state machine. The ROM engine separately
decodes a captured live editor arena, active gap leaf, and cursor into a semantic
tree and translates ordinary packed-token insertion, in-leaf navigation, and
packed-token deletion on that state. The browser
does not yet expose the mutation API as an interactive calculator editor. An
input prefixed with hex: bypasses the preview grammar and passes the
listed native bytes to the translated constructor. Malformed streams and
untranslated structural types produce an error; this path does not select the
model compositor. Each accepted LCD byte remains available as eight ordered
pixel results in the live timeline. [confirmed]
The 5,019-case Node test remains a deterministic parser/layout smoke test. Six
settled record programs provide exact final-pixel and complete accepted-write
parity for their expressions. Three fresh absolute-value cases, four power
cases, eight power/radical composition cases, four nth-root cases, thirteen
fraction cases, twelve integral cases, and eleven summation cases verify
token-to-record construction and complete accepted-write streams. Four
exponential cases and four logBASE( cases verify their child metrics, nested
structures, and accepted-write streams. Twelve nDeriv( cases verify its three
arguments, structural bodies, and recursive nesting. Six raised multi-argument
numerator cases also require the settled record graph to decode to the asserted
semantic expression. Three nested-baseline cases verify depth-sensitive
logBASE( placement and the per-structure +0x0F adjustment. Five grouping
cases cover flat and structural groups, grouped power operands, and a structural
absolute-value child. The deepest power oracle has three raised levels. Six named-token cases verify counted
spellings, raised small-font widths, compound parentheses, structural children,
and complete accepted-write streams. Five two-byte-token cases verify list,
matrix-name, equation-variable, and string-variable tables in large and raised
contexts. Two native-list cases verify 08h/09h parsing, brace geometry,
baseline-sensitive stretching, and semantic graph decoding. Two longer trace
scenarios cover the editor and display activity
around the final key press.
[confirmed]
tools/ti84re/mathprint/fuzz_diff.py builds a semantic expression tree, encodes its
native calculator bytes, constructs the translated record graph, and compares
the resulting pixels with a reset-origin TilEm screenshot produced from the
corresponding key sequence. It does not compare the calculator with the preview
compositor. Each run uses a new emulator state file. Adjacent equal keys receive
an explicit scan delay. Integral and summation templates receive rebuild delays
after menu selection and each slot transition. A pixel mismatch triggers an
instruction trace for branch and LCD-write diagnosis; exact cases do not pay
the trace cost.
Trace-limit cases leave the screenshot mismatch intact and report only the
trace diagnosis as inconclusive. [confirmed]
The opt-in generic-function domain wraps arbitrary admitted trees in the
single-byte sin(, cos(, tan(, ln(, and log( tokens. Their arguments
include nested functions and every structural constructor accepted by the
depth-four entry gate. Seed 917 at depth four produces 20 calculator inputs;
all 20 translated LCD bitmaps match their reset-origin screenshots exactly.
The corpus includes the left-clipped radical case
log(sqrt(int(3,1,nDeriv(1,A,3),N))). [confirmed]
The opt-in list domain wraps two arbitrary admitted trees in native 08h and
09h tokens. It types the braces with [2nd] ( and [2nd] ), then
compares the decoded element tree and pixels. List containers do not allocate a
structural record, but structural elements still contribute to the depth-four
entry gate. [confirmed]
34:62D0 selects seven root-hook rows when ram:8515 is zero and five rows
when it is nonzero. The routine subtracts that row count from the radical
height and returns the difference in DE. 34:62A7 decrements DE before
34:62AE passes the vertical stem to 34:5D96. The stem endpoint is therefore
$h-8$ for the seven-row hook and $h-6$ for the five-row hook. In the
tall-summation input above, the final trace reaches 34:62D0 with
ram:8515=2, radical height 17, and stem endpoint 11. Translating the returned
word removes the final two-pixel difference from the reset-origin screenshot.
[confirmed]
Browser model and current boundary
Closed supported expressions use the translated record graph. The browser does not replay a record fixture or captured LCD stream for this path. Partial or unsupported editor text remains a separate preview mode and is not presented as ROM parity. [confirmed]
| Browser path | Input | Output | Boundary |
|---|---|---|---|
| Translated ROM path | Supported complete native expression | Record graph, primitive stream, ordered LCD writes, and pixels | Untranslated source or constructor branches fail explicitly. |
| Live-arena decoder | Captured arena, active gap leaf, and cursor | Cursor-annotated semantic AST | Does not predict every next key mutation. |
hex: path | Explicit native bytes | Translated construction and render result | Malformed or unsupported forms report an error. |
| Fallback compositor | Partial or unsupported preview text | Approximate editable boxes | Not a ROM-parity claim. |
The class table, decoded handler records, selected descriptors, and page-7
display-byte tables are extracted to web/mathprint/layout.json by
tools/ti84re/mathprint/export_layout.py;
the fonts to web/mathprint/font.json by tools/ti84re/mathprint/export_font.py; and the token,
_KeyToString, and inline cell strings to web/mathprint/token-strings.json
by tools/ti84re/mathprint/export_token_strings.py. The font
data appears on the interactive
renderer’s font-table tab. tools/js/interp-cells.js and the browser share the executable
translations in web/mathprint/rom-engine.js. The translated routines consume
layout.json for handler lookup, row-cell iteration, direct glyph selection,
archived fixed-token lookup, display-byte remapping, descriptor iteration, fraction
endpoints, and class-6 row stepping. web/mathprint/record-programs.json
contains six retained record
snapshots for offline comparison. The browser does not fetch them. Closed
expressions accepted by the native constructor use the translated record graph
and primitive stream for both the generated LCD view and the model view.
Partial, unsupported, and over-wide text keeps the separate trace-fitted box
compositor so the editor can continue to display incomplete input. It constructs
supported named-token, absolute-value, power,
radical, nth-root, stacked-fraction, integral, summation, and nDeriv(
expressions from native token bytes, including nesting among the structural
forms. The translated renderer exposes every generated LCD byte and the
resulting pixel frame as a live timeline. This mode does not load a record
fixture or captured LCD event stream. Multi-argument and generic-function
boundaries pass through the translated 34:5AA3 state machine. Numeric and
delimited raised operands also pass through the translated 34:5699 scan, and
stacked-fraction operands pass through the translated 34:5795 scan. The
remaining source grammar and record-construction branches are listed in
MathPrint pipeline coverage.
[confirmed]
The standalone nth-root encoder supplies an inferred template boundary because the retained trace does not expose the final source buffer. [hypothesis]
Keyboard and link port
The keypad scanner and link-port drivers provide the calculator’s local input and wired data-transfer paths. The keyboard path turns matrix scans into cooked key codes, while the link path sends bytes through the legacy two-wire port or the hardware-assisted interface.
Deep dives: Keypad and ON-key hardware covers the electrical matrix, scanner timing, debounce, repeat, ON interrupts, and wake. Two-wire link port hardware covers port
0x00, electrical encoding, raw byte handshakes, timeouts, and background detection. Link / data transfer covers silent-link packets and variable send/receive.
Keyboard
The matrix keypad is read through port 0x01: software writes an active-low group mask and reads active-low key lines. Standard hardware timer 1 scans it through ram:03B4. The separate ON circuit reports its level and interrupt state through ports 0x03 and 0x04. [confirmed]
_GetCSC = 4018, bodyram:04B2, atomically reads and clears the one-bytekbdScanCodemailbox. It returns raw scan events and does not block. [confirmed]_GetKey = 4972, body06:491E, blocks, processes hooks and APD state, applies 2nd and ALPHA, and returns a cookedTIKeyCode. [confirmed]_KeyToStringat01:6D10maps a cooked key code to an editor token or string. [confirmed]
Scan codes such as skEnter identify a matrix position. Cooked key codes such as kEnter = 5 incorporate OS modifier and context policy. _GetCSC returns the former; _GetKey returns the latter. The complete matrix, scan-code formula, diagonal-arrow exception, five-sample release filter, repeat timing, modifier state, and 46.7 ms ON debounce are reconstructed in Keypad and ON-key hardware.
Key → token translation [confirmed]
_KeyToString (01:6D10) turns a key code into a TI-BASIC token for the editor. It’s not a single flat table — it combines:
- range arithmetic: contiguous key ranges map to token ranges by a fixed offset (e.g. key
0x1F→'P'-based,0x59→'a'for lowercase) — letters/digits; - per-mode lookup tables on another page, reached via
cross_page_jump(the 2nd/ALPHA-mode and function-key token tables); - special key codes
0xFB/0xFC/0xFE/0xFFare not tokens — they’re the menu / context-switch return codes the main event loop branches on (see Boot, contexts & errors), so_KeyToStringroutes them out viacross_page_jumprather than translating.
So the input path is: keypad → ISR → kbdScanCode → _GetKey (cooked kXxx + modifiers) → _KeyToString → token → parser (Tokenizer & TI-BASIC).
Link port
The 2.5 mm I/O link uses two open-collector lines. Port 0x00 drives and samples them directly; ports 0x08–0x0D provide the TI-84 Plus hardware-assisted byte path. [standard] for the electrical interface; [confirmed] for the ROM port use.
_SendAByte = 4EE5, body 3C:420D, sends legacy bits least-significant first. It writes 1 for bit 0 and 2 for bit 1, waits for a both-low acknowledgement, releases its line, and waits for idle. _RecAByteIO = 4F03, body 3C:443F, performs the inverse handshake. Both paths use bounded waits that enter the link-error machinery on timeout. [confirmed]
Installed error callback 3C:6136 reaches 3C:618D for applicable transfer states. Its raw branch marks the link busy, selects nominal 6 MHz, drives both lines low for a 7,077,785-base-T-state loop, releases them, and clears busy; its USB branch skips port 0x00. The documented Flash opcode wait raises the loop to 8,191,881 T-states. This is the OS’s transport-specific abort cleanup. [confirmed] for the ROM role and base count; [standard] for the wait-state-adjusted count.
Two-wire link port hardware reconstructs the port read/write inversion, four transitions, receiver rotation, errors, and timer-driven activity check. USB ASIC and link assist covers the assist FIFO selected by the same byte routines.
Variable-transfer packet framing
A TI link packet is a 4-byte header (machine-ID, command-ID, length-lo, length-hi) optionally followed by data[len] and a 16-bit LE checksum; commands include 0x06 VAR, 0x09 CTS, 0x15 DATA, 0x56 ACK, 0x5A NAK, 0x92 EOT. Link transfer covers the framing, silent-link send/receive engine (link_xfer_op, _SendVarCmd), checksum and acknowledgement handling, and 16-byte Flash-batched receive path. [confirmed]
Keypad and ON-key hardware
TI-84 Plus OS 2.55MP — Matrix scanning, debounce, repeat, ON interrupts, and wake behavior.
The TI-84 Plus reads most keys through an active-low 8×8 matrix on port 0x01. The ON key uses a separate level and interrupt circuit. This page follows both paths from the electrical interface through the OS scanner, _GetCSC, _GetKey, shutdown, and wake.
Evidence layers
The matrix’s electrical behavior, the ROM’s filtering policy, and emulator input models are separate evidence layers.
| Layer | Main evidence | What it establishes |
|---|---|---|
| TI-OS kernel | ram:015B, ram:03B4–ram:04BE, and ram:0964–ram:0A5D | matrix transactions, scan-code construction, release filtering, repeat, ON debounce, and low-power control [confirmed] |
| TI-OS banked code | _GetKey = 4972, body 06:491E | blocking input, hooks, APD interaction, modifiers, and cooked key codes [confirmed] |
| TI-OS dynamic execution | tools/macros/power-cycle.macro and /tmp/tilem-power-cycle.trace | a complete [2nd]+ON shutdown/wake cycle and a live [2nd] matrix scan [confirmed] |
| Public hardware notes | WikiTI ports 0x01, 0x03, and 0x04 | matrix wiring, capacitance, ghosting, bounce, and interrupt-port semantics [standard] |
| Emulator models | TilEm commit f56ad63, Wabbitemu commit 48c2dc0, and MAME 0.287 | three different matrix algorithms and ON-edge policies [standard] |
| Native emulator execution | guarded TilEm, Wabbitemu, and MAME keypad/interrupt runs | matrix reads, injected-key state, reset state, and ON status [standard] |
Two input circuits
The keypad has two paths into the ASIC. Matrix keys are polled by software. ON has its own active-low level and can request an interrupt while the CPU is halted. [standard]
flowchart LR
K["matrix key"] --> M["diode-less 8×8 matrix"]
M --> P1["port 01<br/>group select and columns"]
P1 --> SCAN["kbd_tick_debounce_repeat<br/>ram:03B4"]
SCAN --> CSC["kbdScanCode<br/>_GetCSC"]
CSC --> GK["_GetKey<br/>modifiers and cooked code"]
ON["ON key"] --> P4["port 04 bit 3<br/>active-low level"]
ON --> IRQ["port 04 bit 0<br/>pending interrupt"]
IRQ --> ISR["on_irq · ram:015B"]
ISR --> DB["on_key_debounce_power<br/>ram:0964"]
DB --> RUN["break, power-off, or wake flow"]
The conventional scan-code number 0x29 occupies the unused matrix position at group 5, bit 0, but ON is not electrically present at that position. TilEm also uses 0x29 as its injected-key identifier. Port 0x04, not port 0x01, reports its physical state. [standard]
Matrix wiring and scan codes
Writing port 0x01 selects groups with zero bits. Reading the same port returns one bit per key line, where zero means closed. 0xFF releases every group, and 0x00 selects all groups. [standard]
Worked matrix scan. The active-low electrical contract is [standard]. The group masks and 8g + b + 1 scan-code construction at ram:0410–0453 are [confirmed].
The table gives the complete TI-84 Plus matrix. Each parenthesized byte is the scan code that kbd_scan_matrix at ram:0406 constructs for a single key. A dash is an unwired position. [confirmed] for the ROM formula; [standard] for the physical map.
| Group mask | Bit 0 | Bit 1 | Bit 2 | Bit 3 | Bit 4 | Bit 5 | Bit 6 | Bit 7 |
|---|---|---|---|---|---|---|---|---|
0xFE | ↓ (0x01) | ← (0x02) | → (0x03) | ↑ (0x04) | — | — | — | — |
0xFD | ENTER (0x09) | + (0x0A) | − (0x0B) | × (0x0C) | ÷ (0x0D) | ^ (0x0E) | CLEAR (0x0F) | — |
0xFB | (−) (0x11) | 3 (0x12) | 6 (0x13) | 9 (0x14) | ) (0x15) | TAN (0x16) | VARS (0x17) | — |
0xF7 | . (0x19) | 2 (0x1A) | 5 (0x1B) | 8 (0x1C) | ( (0x1D) | COS (0x1E) | PRGM (0x1F) | STAT (0x20) |
0xEF | 0 (0x21) | 1 (0x22) | 4 (0x23) | 7 (0x24) | , (0x25) | SIN (0x26) | APPS (0x27) | X,T,θ,n (0x28) |
0xDF | — | STO→ (0x2A) | LN (0x2B) | LOG (0x2C) | x² (0x2D) | x⁻¹ (0x2E) | MATH (0x2F) | ALPHA (0x30) |
0xBF | GRAPH (0x31) | TRACE (0x32) | ZOOM (0x33) | WINDOW (0x34) | Y= (0x35) | 2nd (0x36) | MODE (0x37) | DEL (0x38) |
0x7F | — | — | — | — | — | — | — | — |
For group number $g$ from 0 through 7 and bit number $b$ from 0 through 7, the ordinary scan code is
$$ \operatorname{scanCode} = 8g + b + 1. $$
At ram:0410, the scanner starts with mask 0xFE and group counter 1. RLC C at ram:044C advances through 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, and 0x7F. The eight-iteration loop at ram:0435 counts low bits and records the one-based bit position. ram:0453 subtracts one from the group counter, shifts it three times, and adds the bit position. [confirmed]
The scanner rejects an ordinary sample containing more than one closed key. Two low bits within a group, or a second nonempty group after the first, return with carry set at ram:0459. [confirmed]
Diagonal-arrow exception
When mouseFlag1 bit 0 at IY+0x2C is set, group 0 has four accepted two-key values. The routine returns the raw active-low byte before the ordinary single-key reduction. [confirmed]
| Raw value | Low bits | Keys |
|---|---|---|
0xF5 | 1 and 3 | ←+↑ |
0xF3 | 2 and 3 | →+↑ |
0xFA | 0 and 2 | ↓+→ |
0xFC | 0 and 1 | ↓+← |
These bytes are active-low group samples, not ordinary values from the
scan-code formula. The repeat filter treats values 0xF3 and above as
repeatable. The public App mouse bcall family owns the enabling flag and
interprets all four values as two-axis cursor movement. [confirmed]
App mouse flag lifecycle
The App mouse API is a software cursor interface for applications. It uses the
ordinary keypad scanner rather than a separate pointing device. The main bcall
table maps its IDs to page 3B; the public equates supply the names below.
[confirmed] for mappings and bodies; [standard] for names.
| Bcall | Body | Role |
|---|---|---|
_AppStartMouse = 4D47 | 3B:78F9 | initialize the workspace, display the cursor, and wait for a supported key |
_AppStartMouseNoSetup = 4D4A | 3B:78FC | display and wait without reinitializing the workspace |
_AppMouseGetKey = 4D4D | 3B:78FF | enable diagonal scans, halt until _GetCSC returns an event, and classify it |
_AppDispMouse = 4D50 | 3B:77D9 | select display rather than erase, then enter the shared cursor renderer |
_AppEraseMouse = 4D53 | 3B:77CF | select erase rather than display, then enter the shared cursor renderer |
_AppSetupMouseMem = 4D56 | 3B:75B0 | set center coordinates and copy a 26-byte cursor workspace template to 0x8100 |
_AppUpdateMouse = 4D65 | 3B:7A56 | redraw and commit the pending coordinates, then wait for another key |
_AppDispPrevMouse = 4D68 | 3B:76BD | restore or redraw the cursor around a pending movement |
_AppUpdateMouseCoords = 4DA4 | 3B:7721 | apply the row delta before committing the coordinate word |
_AppUpdateMouseXY = 4DCE | 3B:7724 | copy pending coordinates to the committed word and clear both mouse flag bytes |
_AppMouseForceKey = 4E55 | 3B:7913 | classify a supplied scan value without waiting for _GetCSC |
_AppSetupMouseMemCoords = 4E58 | 3B:78B7 | initialize the workspace with caller-supplied coordinates |
_AppMoveMouse = 4E5B | 3B:78E6 | force one key, mark the redraw state, and update the cursor |
A raw scan of all 64 physical pages finds four explicit bit-0 operations and
one immediate write that replaces the complete byte at IY+0x2C. The
surrounding instructions confirm all five as code. [confirmed]
| Address | Instruction | Effect |
|---|---|---|
ram:0415 | BIT 0,(IY+0x2C) | admit the four group-0 diagonal samples when set |
3B:773B | LD (IY+0x2C),0x00 | clear every mouseFlag1 bit after committing pending coordinates |
3B:7907 | SET 0,(IY+0x2C) | enable diagonal recognition immediately before EI, HALT, and _GetCSC |
3B:791A | RES 0,(IY+0x2C) | disable the mode after the first nonzero event and before key classification |
3B:7A8B | RES 0,(IY+0x2C) | ensure that _ExecuteApp = 4C51 enters a new app with the mode disabled |
_AppMouseGetKey re-enables the flag on every wait. _AppUpdateMouse commits
the previous movement and jumps back to that wait. The scanner can therefore
publish held diagonal repeats while the app continues the update/get-key loop,
but unrelated input code sees ordinary multi-key rejection. [confirmed]
flowchart LR
START["_AppStartMouse<br/>4D47 · 3B:78F9"] --> SETUP["setup 0x8100 workspace<br/>row 31 · column 48"]
SETUP --> DISP["display cursor"]
DISP --> WAIT["_AppMouseGetKey<br/>set mouseFlag1 bit 0"]
WAIT --> SCAN["timer scanner<br/>ram:0406"]
SCAN --> CSC["_GetCSC<br/>ram:04B2"]
CSC --> FORCE["_AppMouseForceKey<br/>clear bit 0 · classify"]
FORCE --> APP["return pending movement to app"]
APP --> UPDATE["_AppUpdateMouse<br/>redraw · commit"]
UPDATE --> WAIT
App mouse coordinate and key contract
_AppSetupMouseMem writes 0x301F to 0x986D. The little-endian bytes are
row 31 and column 48, the center of the 64×96 display. _AppMouseForceKey
copies this committed coordinate word to 0x8122, adjusts the pending copy,
and normally returns the pending word in HL. _AppUpdateMouseXY at
3B:7724 copies 0x8122 back to 0x986D. This distinguishes the displayed
cursor position from the next requested position. [confirmed]
The row range is 0–63; the column range is 0–95. A diagonal at one
edge still moves along its unblocked axis. A cardinal direction at its edge, or
a diagonal blocked on both axes, returns to the internal wait loop instead of
returning a no-movement result. [confirmed]
| Scan value | Input | Pending-coordinate change |
|---|---|---|
0x01 | ↓ | row + 1 |
0x02 | ← | column − 1 |
0x03 | → | column + 1 |
0x04 | ↑ | row − 1 |
0x09 | ENTER | no movement; return A=0x0C |
0xF3 | →+↑ | row − 1, column + 1 |
0xF5 | ←+↑ | row − 1, column − 1 |
0xFA | ↓+→ | row + 1, column + 1 |
0xFC | ↓+← | row + 1, column − 1 |
A normal movement returns A=0x0A and the pending coordinates in HL. If
shift2nd is already set, the same movement returns A=0x08 without loading
the coordinate word into HL. ENTER clears shift2nd and returns
A=0x0C. Scan code 0x36 reaches an XOR 0x00 no-op at 3B:792D and waits
again; every other unsupported value also waits. [confirmed]
One port transaction
kbd_read_group at ram:0480 takes an active-low group mask in A, reads port 0x01, releases all groups with 0xFF, and returns the sampled byte. [confirmed]
ram:0480 push af
ram:0481 in a,(0x02)
ram:0483 and 0x80
ram:0485 jr nz,ram:0497 ; TI-84 Plus timing path
ram:0487 pop af
ram:0488 out (0x01),a
ram:048A nop ; four-NOP path
ram:048B nop
ram:048C nop
ram:048D nop
ram:048E in a,(0x01)
ram:0490 ld b,a
ram:0491 ld a,0xFF
ram:0493 out (0x01),a ; release all groups
ram:0495 ld a,b
ram:0496 ret
ram:0497 in a,(0x20) ; CPU-speed selector
ram:0499 and 0x01
ram:049B jr z,ram:0487 ; nominal 6 MHz: four NOPs
ram:049D pop af
ram:049E out (0x01),a
ram:04A0 nop ; nominal 15 MHz: add three NOPs
ram:04A1 nop
ram:04A2 nop
ram:04A3 jr ram:048A ; then the four-NOP tail
Port 0x02 bit 7 selects the newer-hardware path. On a TI-84 Plus, port 0x20 bit 0 chooses between four settling NOPs at nominal 6 MHz and three NOPs plus a taken backward branch plus the four-NOP tail at nominal 15 MHz. The port-0x20 read is a CPU-speed test, not a link-port gate. [confirmed]
Every call ends with OUT (0x01),0xFF. WikiTI attributes the reset requirement and variable delay to capacitance in the keypad lines: a line from the previous group can remain charged after that group is unselected. Published measurements report that a single key often settles within eight 6 MHz cycles, while some multi-key arrangements take more than 50 cycles. These measurements vary by keypad. [standard]
An interrupt can overwrite the caller’s group selection between a manual OUT and IN. Code that scans port 0x01 outside the OS must mask interrupts or provide an ISR that leaves the transaction undisturbed. [standard]
Ghosting and physical bounce
The matrix has no isolation diode at each key. Three closed switches can connect an unselected group to a selected group and create a fourth apparent closure. WikiTI’s example selects group 0xF7: pressing 2, 3, and 6 also couples group 0xFB, making 5 appear closed. Unwired positions can ghost too. [standard]
TilEm reproduces this topology in tilem_keypad_read_keys. It starts with all keys in selected groups, then repeatedly unions any group sharing a set bit until the set stops growing. The returned byte is the complement of that transitive closure. [standard]
closed = union(keysDown[group] for each selected group)
repeat
previous = closed
for each group:
if closed intersects keysDown[group]:
closed = closed union keysDown[group]
until closed == previous
return bitwise_not(closed)
The physical switches also bounce during release. WikiTI reports release bounce but little observable press bounce at ordinary scan rates. TilEm changes matrix bits immediately on an injected event; it models neither switch bounce nor capacitive settling. [standard]
Timer scanner, release filter, and repeat
Standard hardware timer 1 enters standard_timer1_irq at ram:0167. Its keyboard call at ram:0198 reaches kbd_tick_debounce_repeat at ram:03B4. With the OS’s port-0x04 setting, the nominal tick period is $304/32768$ seconds, or 9.27734375 ms. [confirmed] for the call path and register write; [standard] for the quartz-derived time.
The scanner uses the following contiguous RAM state. [confirmed]
| Address | Name | Role |
|---|---|---|
0x843F | kbdScanCode | event mailbox consumed by _GetCSC |
0x8440 | kbdLGSC | last scan code accepted by the filter |
0x8441 | kbdPSC | previous raw scan result |
0x8442 | kbdWUR | wait-until-repeat countdown |
0x8443 | kbdDebncCnt | stable-release countdown |
0x8444 | kbdKey | cooked-key workspace used by _GetKey |
0x8445 | kbdGetKy | most recent nonzero published scan code |
0x8446 | keyExtend | extended key-processing state |
kbd_tick_debounce_repeat applies asymmetric filtering: [confirmed]
kbd_scan_matrixfirst writes0x00to port0x01as an all-groups probe. It returns immediately when every read bit is one.- A multi-key rejection sets
kbdPSC = 0xFFand reloadskbdDebncCnt = 5; it publishes no event. - A changed raw result is copied to
kbdPSC, andkbdDebncCntis reloaded to 5. - A changed nonzero result proceeds immediately. The ROM does not require five equal pressed samples.
- A zero result must appear on five consecutive scanner calls. The first zero reloads and immediately decrements the counter; the fifth reaches zero and accepts release.
Five samples span four complete tick intervals between the first and accepted zero, about 37.109 ms. Phase relative to the physical release gives an overall detection latency of about 37.109–46.387 ms under the documented timer rate. This digital filter complements the slower periodic sampling; it does not model matrix capacitance directly. [confirmed] for the sample count; [standard] for wall time.
Repeat policy
A newly accepted nonzero key is published immediately and loads kbdWUR = 0x32. Only these held values repeat: [confirmed]
- arrow scan codes
0x01–0x04; - DEL, scan code
0x38; - the diagonal-arrow raw values
0xF3–0xFCaccepted by the special path.
Other matrix keys produce one event until release. A repeatable key waits 50 timer ticks before the first repeat and reloads 10 ticks after each repeat. Those intervals are about 463.867 ms and 92.773 ms. [confirmed] for the counters; [standard] for wall time.
kbd_publish_scan_code at ram:04A5 always stores A in kbdScanCode and sets kbdSCR, bit 3 of IY+0. A nonzero value also updates kbdGetKy; zero leaves kbdGetKy unchanged. The caller sets kbdKeyPress, bit 4 of IY+0, for a newly accepted nonzero press. [confirmed]
_GetCSC and _GetKey
_GetCSC = 4018, body ram:04B2, is the nonblocking raw-event interface. It disables interrupts, reads kbdScanCode, clears the byte, clears kbdSCR, re-enables interrupts, and returns the event in A. It returns zero when no event is pending. Repeat events generated by the timer scanner are visible through this same mailbox. [confirmed]
ram:04B2 ld hl,0x843F
ram:04B5 di
ram:04B6 ld a,(hl)
ram:04B7 ld (hl),0
ram:04B9 res 3,(iy+0) ; kbdSCR
ram:04BD ei
ram:04BE ret
The mailbox is one byte deep. If the scanner publishes another event before _GetCSC consumes the previous one, the newer value replaces it. The interrupt-masked read prevents a torn read-and-clear operation, but it does not queue multiple keys. _GetCSC also executes EI unconditionally rather than restoring the caller’s prior interrupt-enable state. [confirmed]
_GetKey = 4972, body 06:491E, is the blocking cooked-key interface. Its loop calls _GetCSC at 06:4973, services input hooks and link/USB conditions, participates in cursor and APD handling, and waits until a cooked key appears in kbdKey. It turns matrix scan codes into kXxx values and applies 2nd and ALPHA state. [confirmed]
The two APIs therefore have different contracts:
| API | Waits | Value | Modifiers and hooks | Repeat source |
|---|---|---|---|---|
_GetCSC | no | raw scan event, or zero | no cooking | timer scanner |
_GetKey | yes | cooked TIKeyCode | 2nd, ALPHA, hooks, context policy | events consumed from the scanner |
_GetK compatibility entry [confirmed]
_GetK = 0x4744, body 37:746D, combines two otherwise separate results.
It reads kbdGetKy at ram:8445. A zero byte selects integer zero; a nonzero
byte is consumed, used as an index into the byte table at 37:7487, and
converted to a real value in OP2 through the inline cross-page call at
37:7482. The tail jump at 37:7484 enters _GetCSC, so the returned A is
the current raw scan mailbox result, not the table result placed in OP2.
In a controlled trace, kbdGetKy = 0x01 selects table byte 0x22 and leaves
OP2 as real 34 (00 81 34 00 00 00 00 00 00 00 00). The fixture separately
clears and restores kbdScanCode; this avoids attributing an old launch key to
the table conversion. The reduced trace is in
tools/data/community-bcall-semantics.csv. [confirmed] under TilEm.
Modifier state
_GetKey stores modifier state in shiftFlags at IY+0x12. [confirmed]
| Bit | Equate | Meaning |
|---|---|---|
| 3 | shift2nd | 2nd pending |
| 4 | shiftAlpha | alpha mode active |
| 5 | shiftLwrAlph | lowercase rather than uppercase |
| 6 | shiftALock | alpha lock |
| 7 | shiftKeepAlph | prevent automatic alpha clearing |
From idle, scan code 0x36 sets shift2nd at 06:4AD5 and loops without returning. A second 2nd cancels it at 06:4B8E; another key clears the flag at 06:4B87 before translation. Scan code 0x30 sets uppercase alpha at 06:4AE8. [2nd] then ALPHA sets both shiftALock and shiftAlpha at 06:4B96–06:4B9A. In a lowercase-capable context, another ALPHA sets shiftLwrAlph at 06:4C0D; the next cycle cancels alpha. [confirmed]
key_clear_alpha_if_unlocked at ram:04BF preserves alpha when shiftALock or
shiftKeepAlph is set and otherwise clears shiftAlpha. legacy_link_irq at
ram:01E0 can also clear a pending 2nd, preventing an abandoned modifier
from persisting indefinitely. [confirmed]
_KeyToString at 01:6D10 performs the next layer: cooked key code to editor token or string. The complete input path is matrix → scanner → _GetCSC → _GetKey → _KeyToString → tokenizer. See Tokenizer & TI-BASIC. [confirmed]
ON interrupt and level
The ON circuit uses two ports. [standard]
| Register | Bit | Meaning |
|---|---|---|
port 0x03 write | 0 | one enables ON interrupts; zero disables and acknowledges the pending request |
port 0x03 read | 0 | ON interrupt enable state |
port 0x04 read | 0 | ON interrupt pending |
port 0x04 read | 3 | live ON level, active low |
The IM1 dispatcher reads port 0x04 and enters on_irq at ram:015B when bit
0 is set. That branch calls on_key_debounce_power, clears a link/interrupt
sub-flag, and acknowledges through the common port-0x03 path. [confirmed]
Port 0x04 bit 0 says that the source is pending; bit 3 says whether the button is currently held. Software must not substitute one for the other. The handler uses bit 3 to decide whether the stable state is press or release. [confirmed]
The port-0x03 clear-on-zero sequence, source priority, and the differing TilEm and Wabbitemu ON-edge policies are detailed in Interrupts (IM1).
ON debounce
on_key_debounce_power normalizes its timing before polling the level. On TI-84 Plus hardware, it saves port 0x20 in E and writes zero to select nominal 6 MHz. It then requires port-0x04 bit 3 to remain unchanged for 0x1016, or 4,118, loop iterations. Any change reloads the counter. [confirmed]
ram:096A in a,(0x20)
ram:096C ld e,a
ram:096D xor a
ram:096E out (0x20),a ; nominal 6 MHz
ram:0970 ld b,0
ram:0972 ld hl,0x1016 ; reload after a level change
ram:0975 in a,(0x04)
ram:0977 and 0x08
ram:0979 cp b
ram:097A ld b,a
ram:097B jr nz,ram:0972
ram:097D dec hl
ram:097E ld a,l
ram:097F or h
ram:0980 jr nz,ram:0975
In the power-cycle trace, successive reads at ram:0975 are 68 trace clock units apart. The stable sequence runs from clock 95,685,465 through 95,965,421, then restores port 0x20 at clock 95,965,535. Counting 4,118 iterations gives 280,024 nominal 6 MHz cycles, or about 46.671 ms. [confirmed]
The routine restores the saved CPU-speed selector at ram:09B3 and writes 0x06 to port 0x04 at ram:09B7. A stable low level takes the power-on/pressed branch at ram:09AC; a stable high level follows the release and running-state checks from ram:0985. [confirmed]
This debounce is independent of the matrix’s five-sample release filter. It polls ON rapidly at forced 6 MHz instead of waiting for timer-1 scans. [confirmed]
2nd+ON, APD, and wake
_GetKey recognizes the ON request as an internal 0xFF event at 06:4A93. It clears shift2nd. If appRetKeyOff is set, it returns the context key 0x3F; otherwise it jumps to _PowerOff, body ram:09E6. [confirmed]
_GetKeyRetOff = 0x500B, body 06:491A, consists of SET 7,(IY + 0x28) and
then falls directly into _GetKey at 06:491E. A controlled interactive trace
drains one explicit ENTER, enters _GetKeyRetOff, then injects
2nd+ON. It reaches the 0xFF comparison at 06:4A93, loads
A = 0x3F at 06:4A9B, observes the set flag at 06:4A9D, takes the return
branch at 06:4AA1, and records A = 0x3F in the caller. The trace does not
enter _PowerOff. Its reduced result is in
tools/data/community-getkey-ret-off.csv. [confirmed] under TilEm; physical
ON timing remains covered only by the separate hardware probes below.
Explicit power-off and Auto Power Down (APD) perform different cleanup, then
join poweroff_shared_tail at ram:0A24. The final hardware operations are:
[confirmed]
| Address | Operation | Effect |
|---|---|---|
ram:0A29 | OUT (0x03),0x08 | acknowledge and temporarily disable interrupt sources while cleanup continues |
ram:0A4B | OUT (0x04),0x06 | select map mode 0 and the slow standard-timer rate |
ram:0A4F | OUT (0x03),0x11 | enable ON and link wake, disable standard timers, and select low power on HALT |
ram:0A51 | clear shift2nd | discard the power-off modifier |
ram:0A55 | clear onRunning | mark the OS powered down |
ram:0A5B | EI | accept a selected wake interrupt |
poweroff_halt_loop at ram:0A5C | HALTJR ram:0A5C | remain in the low-power loop |
Port-0x03 bit 3 being clear selects low power only when the Z80 executes HALT. The 0x11 write alone does not complete shutdown. ON and link activity remain wake sources. [standard]
The trace confirms the complete sequence. The shutdown side restores CPU
speed, writes 0x06 to port 0x04, acknowledges with 0x08, disables the LCD
with command 0x02, writes 0x11, and reaches poweroff_halt_loop. A later
ON event enters the same 4,118-iteration debounce. The wake side then writes
normal interrupt mask 0x0B at ram:0C9E and sends LCD commands
0x40,0x05,0x01,0x03,0x17,0x0B,0xEF through 06:4D38. [confirmed]
Dynamic reproduction
The resolver can print injected key events and restrict both key and I/O output to an inclusive trace-clock window.
nix develop -c python3 -m ti84re.trace.resolve \
/tmp/tilem-power-cycle.trace \
--initial-mapping ti84p-reset --names tools/symbols/names.txt \
--key-events
nix develop -c python3 -m ti84re.trace.resolve \
/tmp/tilem-power-cycle.trace \
--initial-mapping ti84p-reset --names tools/symbols/names.txt \
--io-ports 01 --event-clock 93285080-93450000
nix develop -c python3 -m ti84re.trace.resolve \
/tmp/tilem-power-cycle.trace \
--initial-mapping ti84p-reset --names tools/symbols/names.txt \
--io-ports 03,04,10,20 --event-clock 95965000-95967000
The [2nd] scan contains this transaction: [confirmed]
clk=93375052 ram:049e OUT (0x01) <- 0x00
clk=93375112 ram:048e IN (0x01) -> 0xdf
clk=93375137 ram:0493 OUT (0x01) <- 0xff
...
clk=93378814 ram:049e OUT (0x01) <- 0xbf
clk=93378874 ram:048e IN (0x01) -> 0xdf
clk=93378899 ram:0493 OUT (0x01) <- 0xff
The all-groups probe finds bit 5 low. The group walk later selects 0xBF; bit 5 remains low, producing scan code 6 × 8 + 5 + 1 = 0x36, 2nd. Every sample releases the matrix afterward. [confirmed]
Emulator comparison
All four pinned implementations omit electrical settling and mechanical bounce. Their keypad handlers return the current modeled matrix state without a delay, although MAME’s host input fields latch forced changes on a video-frame update. Their digital matrix algorithms do not all agree. [standard]
| Area | TilEm f56ad63 | Wabbitemu 48c2dc0 | MAME 0.287 | jsTIfied 20170706a |
|---|---|---|---|---|
| Selected rows | active-low write, all eight bits | complements the write, then considers seven rows | active-low write, seven rows | active-low write and row scan |
| Ordinary combination | OR of selected rows | OR of selected row results | XOR of each selected pressed position | selected key-state rows are combined into an active-low result |
| Ghosting | iterated transitive closure | one pairwise-overlap pass | none | no electrical settling model |
| Same-column keys in two selected rows | remain low | remain low | XOR twice and cancel to high | remain low |
| ON level | separate active-low port-0x04 bit 3 | separate active-low port-0x04 bit 3 | separate active-low port-0x04 bit 3 | separate standard-interrupt state |
| ON request edge | press and release | press only | press only | press only while not already latched |
| ON detection | injected-state event | standard-interrupt device evaluation | fixed 256 Hz timer-1 callback | key-event handler |
The guarded TilEm direct-core interrupt probe begins with ON masked. Press,
enable while held, release, acknowledge, press, acknowledge while held,
release, disable, and press while disabled produce port-0x04 values 00,
00, 09, 08, 01, 00, 09, 08, and 00. This confirms both-edge
latching in TilEm without executing the ROM. It does not establish the
physical ASIC edge policy. [standard]
TilEm begins with the union of selected rows, then repeatedly adds every row intersecting the current closed-bit set. It therefore propagates through an arbitrarily long chain of row intersections. It stores eight row bytes, including the physically unwired eighth row, and uses the OS-compatible matrix-position numbers for ordinary keys. Its injected identifier 0x29 represents the separate ON key rather than a port-0x01 position. [standard]
Native TilEm confirmation. The guarded direct-core probe reads 0xFE for
one key and for two same-column keys in selected rows 0 and 1. The three-key
rectangle reads 0xFC, and the five-key transitive chain reads 0xF8.
Column 7 and row 7 both participate. Group bytes 0x00, 0x7F, 0x80,
0xFE, and 0xFF remain stored exactly. [standard]
The same run verifies immediate row-major scancodes 1–64, idempotent duplicate
events, ignored scancodes 0 and 65, and keypad reset. Selecting group 5 while
injecting TILEM_KEY_ON leaves the matrix at 0xFF. ON press and release
produce status 0x01 and 0x09, respectively, when enabled. Two isolated
runs produce identical canonical native JSON with SHA-256
1f75a4010773a7c8a108d62239cb937e02aa029affa55263906688eb73ba536c.
The native binary SHA-256 is
9553bdafadf042dd9af634221b52b8795b572d0c047f839e119dabc957063323.
[standard]
Wabbitemu first constructs a result for each row by unioning that row with every row that directly intersects it. It does not iterate the result, so a three-row chain can stop after the second row where TilEm reaches the third. It considers rows 0–6 and ignores row 7. ON press detection compares the current state with a saved state when the standard-interrupt model runs; release updates the saved state without latching a request. [standard]
Native Wabbitemu confirmation. The guarded initialized-core probe reads 0xFE for one key, 0xFE for two same-column keys in selected rows 0 and 1, and 0xFC for the three-key rectangle. The five-key transitive chain also reads 0xFC, while the iterated TilEm model predicts 0xF8. Selecting row 7 with one injected key reads 0xFF. [standard]
The same run observes port 0x04 change from 0x00 to 0x01 only after the standard-interrupt device evaluates a new ON press. Acknowledging while ON remains held leaves status 0x00, including after another evaluation. Release changes the live level to 0x08; evaluating that release does not set pending bit 0. The next press changes 0x00 to 0x01 when evaluated. The run advances zero T-states, so it establishes callback-state transitions rather than polling frequency or latency. [standard]
MAME does not compute a union. Starting from 0xFF, it XORs the column bit for every pressed key in every selected row. Two selected pressed positions in one column therefore toggle the bit twice and disappear from the read. Its ON press is sampled by the fixed 256 Hz standard-timer callback; a held press does not create another request until a callback has observed a release. The TI-84 Plus driver remains marked MACHINE_NOT_WORKING. [standard]
Native MAME confirmation. The guarded live-input probe injects exact group and column positions through MAME’s :BIT0–:BIT7 fields. It waits for each forced value to cross a video-frame input update, then writes and reads port 0x01 through the main CPU I/O space. A single selected key reads 0xFE; the same key in an unselected group reads 0xFF. Two same-column keys in selected groups 0 and 1 cancel to 0xFF, and the three-key rectangle reads 0xFE. A key in column 7 reads 0x7F. Selecting all groups with two positions in column 0 and one in column 1 reads 0xFD. Writes 0xFF and 0x7F both leave every group unselected. Two isolated runs produce byte-identical native reports with SHA-256 f684472b1f139b649245f54d140190bd5f91bf2508aa9e4764ddc0ce88079477. [standard]
A separate guarded interrupt run drives MAME’s :ON input while the Z80 waits
in DI RAM. A masked press and enabling ON while it remains held both leave
status zero. Release produces live level 0x08; the next enabled press produces
0x01, and release retains pending status 0x09. Clearing port-0x03 bit 0
returns 0x08. The adapter waits through the host-input update and timer-1
sample, so the sequence verifies the press-only latch and release rearming in
the running driver. [standard]
These discrepancies are emulator behavior, not competing physical measurements. TilEm’s closure is topologically plausible for a diode-less matrix, but the physical result still depends on resistance, capacitance, switch state, and the delay between the group write and read. [hypothesis]
Reusable keypad tools
tools/ti84re/hardware/keypad.py exposes the three source-pinned matrix algorithms,
ON-edge policies, and byte-confirmed App mouse movement model.
tools/ti84re/hardware/describe_keypad.py accepts numeric GROUP,BIT positions,
which keeps ghost and unwired-position experiments independent of UI key names.
tools/ti84re/emulators/tilem/keypad.py derives an ordered native case report from that model.
Its builder validates the pinned TilEm commit and tree before compilation.
tools/ti84re/emulators/tilem/run_keypad_probe.py guards the exact binary and writes the
observations, source-model comparison, input identities, and evidence scope.
tools/ti84re/emulators/wabbitemu/keypad_probe.py provides the independent case oracle.
tools/ti84re/emulators/wabbitemu/run_keypad_edge_probe.py guards the native report with the
exact OS 2.55MP ROM hash and writes a JSON manifest containing both binary
hashes and evidence scope. tools/ti84re/emulators/mame/keypad.py parses and checks the MAME
matrix against the reusable source model. tools/ti84re/emulators/mame/run_keypad_probe.py
guards the exact MAME executable, ROM, Lua adapter, and isolated runtime.
tools/ti84re/emulators/mame/interrupt.py adds the independent timer-sampled ON-edge sequence.
The physical keypad settling probe
uses the same numeric group order but does not apply an emulator matrix model.
It records every raw byte so held-key metadata and ASIC revision can be compared
without assuming one of the three source algorithms.
# Three-key rectangle: TilEm/Wabbitemu read 0xFC; MAME reads 0xFE.
nix develop -c python3 -m ti84re.hardware.describe_keypad matrix \
--mask 0xFE --key 0,0 --key 1,0 --key 1,1
# Transitive chain: TilEm reaches bit 2; Wabbitemu stops at bit 1.
nix develop -c python3 -m ti84re.hardware.describe_keypad matrix \
--mask 0xFE --key 0,0 --key 1,0 --key 1,1 --key 2,1 --key 2,2
nix develop -c python3 -m ti84re.hardware.describe_keypad on press release
nix develop -c python3 -m ti84re.hardware.describe_keypad mouse 0xF5 \
--row 0x1F --column 0x30
nix develop -c python3 -m ti84re.hardware.describe_keypad --json profiles
tilem_keypad_tmp=$(mktemp -d /tmp/ti84-tilem-keypad.XXXXXX)
git clone https://github.com/debrouxl/tilem.git "$tilem_keypad_tmp/tilem"
git -C "$tilem_keypad_tmp/tilem" checkout \
f56ad637d0524ee841dd381be6ecbaf5b8975600
nix shell \
github:NixOS/nixpkgs/f13ff45afd1bb73e640eaa08a7066dbed07e3238#gcc \
--command python3 -m ti84re.emulators.tilem.build_probe --probe keypad \
--source "$tilem_keypad_tmp/tilem" \
--output "$tilem_keypad_tmp/tilem-keypad-probe" --json
tilem_keypad_parent=$(mktemp -d /tmp/ti84-tilem-keypad-report.XXXXXX)
python3 -m ti84re.emulators.tilem.run_keypad_probe \
--binary "$tilem_keypad_tmp/tilem-keypad-probe" \
--expected-binary-sha256 \
9553bdafadf042dd9af634221b52b8795b572d0c047f839e119dabc957063323 \
--output-dir "$tilem_keypad_parent/run" --json
keypad_probe_parent=$(mktemp -d /tmp/ti84-keypad-probe.XXXXXX)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_keypad_edge_probe \
--rom tools/rom.bin \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$keypad_probe_parent/run" --json
mame_keypad_parent=$(mktemp -d /tmp/ti84-mame-keypad.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_keypad_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_keypad_parent/run" --json
tools/ti84re/rom/indexed_flags.py provides the page-aware raw signature scan used for
the flag-lifecycle audit. Its CLI accepts a ROM hash guard and emits JSON:
nix develop -c python3 -m ti84re.rom.analyze_flags \
--offset 0x2C --bit 0 --index iy \
--expect-sha256 \
7d9a7d96d89fc552ebee6afdbdd011fdc6047be9c16d308245dff07eb1f7bd6d
The five results are raw byte-sequence candidates. The address-level claims above additionally require disassembly of their surrounding routines.
Resolved findings and open hardware tests
- [confirmed] The fast TI-84 Plus scan path tests CPU speed and executes three NOPs, a taken branch, and the shared four-NOP tail before reading port
0x01. - [confirmed] Ordinary multi-key samples are rejected, while four diagonal-arrow active-low bytes bypass the ordinary scan-code formula under
mouseFlag1bit 0. - [confirmed] The App mouse bcall family owns
mouseFlag1bit 0. It enables the mode only while waiting for_GetCSCand maps the four raw bytes to two-axis cursor movement. - [confirmed]
_AppMouseForceKeystages coordinates at0x8122;_AppUpdateMouseXYcommits them to0x986Dwithin row0–63and column0–95. - [confirmed] A new press is accepted immediately; release requires five consecutive zero samples.
- [confirmed] Initial and subsequent repeat delays are 50 and 10 timer-1 ticks, and only arrows, diagonals, and DEL repeat at this layer.
- [confirmed]
_GetCSCis a destructive one-byte mailbox read, so it can lose overwritten events. - [confirmed] ON debounce forces nominal 6 MHz and requires 4,118 stable reads, about 46.7 ms in the trace.
- [confirmed] Explicit power-off and APD share
poweroff_shared_tail; ON wake restores the interrupt mask and reinitializes the LCD. - [standard] TilEm iterates matrix closure, Wabbitemu performs only pairwise closure, and MAME XORs selected positions.
- [standard] TilEm requests ON interrupts on press and release; Wabbitemu and MAME request only on press.
- [standard] A guarded direct-core TilEm run reproduces its transitive closure, eight stored rows, exact group byte, scancode bounds, separate ON path, both-edge latch, and reset state.
- [standard] A guarded initialized-core Wabbitemu run reproduces the pairwise matrix reads, ignored row 7, press-only ON latch, held-key suppression, and release rearming described by the pinned source.
- [standard] A guarded live-input MAME run reproduces its seven-group, eight-column scan, ignored write bit 7, XOR cancellation, lack of matrix closure, and all-groups result.
- [standard] A guarded MAME interrupt run reproduces its press-only ON latch, held-press suppression, release rearming, live-level bit, and bit-0-clear acknowledgement.
- [confirmed] The prepared
HWKEYSprobe encodes all eight group writes, four instruction gaps, and 16 trials per point, then unselects every group; no physical AppVar has been recorded. - [hypothesis] The exact capacitance and minimum safe settle time should be measured across TA2 and TA3 calculators, including worst-case chords.
- [hypothesis] A logic-analyzer test should establish which physical ON transitions request interrupts on each ASIC revision rather than selecting an emulator policy by majority.
Sources
| Source | Used for |
|---|---|
WikiTI port 0x01 | matrix map, active-low protocol, capacitance, settling, ghosting, bounce, and interrupt interference |
WikiTI port 0x03 | interrupt enables, acknowledgement, and low-power-on-HALT behavior |
WikiTI port 0x04 | ON pending and active-low level bits |
TilEm keypad.c | matrix closure, instant key state, and ON edge policy |
TilEm x4_io.c | port 0x01, port 0x03, and port 0x04 model |
TilEm scancodes.h | injected key identifiers |
Wabbitemu keys.c and 83psehw.c | pairwise matrix algorithm and press-edge ON latch |
MAME 0.287 ti85.cpp and ti85_m.cpp | keypad map, XOR scan, and timer-polled ON edge |
jsTIfied deployed 20170706a artifact and readable mirror | fourth active-low matrix implementation and press-edge ON policy |
WikiTI _AppStartMouse, _AppEraseMouse, and _AppUpdateMouse | literature names and published API synopsis; ROM bytes determine flag ownership and coordinate staging here |
Local headless TilEm trace.c at commit 8da5457 | key-event trace record format |
Two-wire link port hardware
TI-84 Plus OS 2.55MP — port 0x00, raw bytes, and line handshakes.
The 2.5 mm link port carries each byte over two bidirectional lines. This page reconstructs the port encoding, the four-transition bit handshake, timeout behavior, and background link detection.
The packet layer above these byte routines is covered in Link / data transfer. The hardware-assisted byte path is covered in USB ASIC and link assist.
Evidence boundaries
The sources answer different questions:
| Source | What it establishes | Confidence |
|---|---|---|
| OS 2.55MP bytes | Values written to port 0x00, values accepted on reads, bit order, acknowledgements, and error branches | [confirmed] |
| TilEm and Wabbitemu | Two independent digital models of port reads, local output latches, connected endpoints, and link assist | [standard] where both match the public port contract |
| MAME 0.287 | A third raw-port implementation, optional link-bus devices, advertised assist state, and interrupt omissions | [standard] |
| Guarded TilEm link edge probe | Direct-core raw truth table, assist port map, byte transfers, status, interrupts, and reset retention | [standard] |
| Guarded Wabbitemu link edge probe | Initialized-core raw truth table, assist port map, byte transfers, status, and interrupts | [standard] |
| Guarded MAME raw-link probe | CPU-visible PCR readback, connector-facing output fields, peer inputs, and advertised-but-inert assist ports | [confirmed] for the pinned emulator run |
| TI Link Protocol Guide and WikiTI | Open-collector electrical description and red/tip versus white/ring names | [standard] |
| Physical measurements | Rise time, pull-up resistance, voltage thresholds, and ASIC-specific edge timing | [hypothesis] until measured on this hardware |
The code below therefore uses line 0 and line 1 for ROM-derived behavior. Connector names appear only where an external hardware source supplies them.
Port 0x00 line encoding
The low two write bits are active-high drive controls. Setting a bit pulls that line low. Clearing it releases the line so the pull-up can make it high. The low two read bits have the opposite sense: a set bit reports a physically high line. [standard]
| Low write bits | Local action | Unopposed low read bits |
|---|---|---|
0 | release both lines | 3 |
1 | pull line 0 low | 2 |
2 | pull line 1 low | 1 |
3 | pull both lines low | 0 |
Open-collector model. The electrical contract is [standard]. OS 2.55MP’s port values and bit order are [confirmed]; pull-up resistance, thresholds, and rise time remain [hypothesis] until measured.
Let L be the local two-bit pull-low mask and P the peer mask. TilEm computes the physical high-line mask as:
$$ H = \mathord{\sim}(L \mathbin{|} P) \mathbin{\&} 3 $$
Wabbitemu uses the equivalent expression ((L | P) & 3) ^ 3. Both models put the local output latch in read bits 4–5, giving (L << 4) | H. WikiTI documents the same latch behavior. [standard]
The ROM masks reads with AND 0x03, so the raw byte routines do not depend on
bits 2–7. The reusable model in tools/ti84re/link/port.py keeps the physical contract
separate from implementation profiles. Its TilEm and Wabbitemu profiles use the
two line bits and bits 4–5 latch directly; its MAME profile also preserves the
internal PCR byte needed to reproduce that driver’s expressions.
Connector-contact names
The archived TI Link Protocol Guide calls line 0 red/tip and line 1 white/ring. WikiTI gives the same bit-to-contact mapping. It also describes both lines as open-collector outputs with pull-ups. [standard]
OS 2.55MP sends a zero bit with write 1 and a one bit with write 2. That agrees with the guide’s rule that zero pulls red/tip first and one pulls white/ring first. [confirmed] for the write values; [standard] for the physical contact names.
Community terminology traps
The archived TI-83 Plus link tutorial uses the familiar 0xD0–0xD3 writes,
masks reads with AND 3, and implements the same two-line acknowledgement
sequence. Its prose labels write 0xD0 as “both lines low” and 0xD3 as
“both lines high,” however, while its read table uses the opposite numeric
sense. Those electrical labels invert the open-collector action in the table
above: low write bits clear release the lines, and set bits pull them low. The
code is historical corroboration for port usage, not a reliable electrical
description. [confirmed] for the source instructions; [standard] for the
open-collector correction.
The archive’s two shipped calculator files are not TI-83 Plus link files:
example.8xp and EXAMPLE.83P both begin with the TI-83 container signature
2A 2A 54 49 38 33 2A 2A 1A 0A 00. Their SHA-256 values are
fdc5d25fd21abd1d6f06a4e4e3bfb7d562b2c5964e34c6cf0a7d5c1d6b7c3e2b
and 396e9c499cafaccb30b39bf6d44bd726e07064f961d7e9926a85e2d8f97096a5.
A headless TI-84 Plus run transferred those files, invoked a valid
Asm(prgmEXAMPLE) wrapper, reached the E_Invalid shim at ram:2729 once,
and never reached the compiled-program handoff at 07:57B4 or ram:9D95.
The result is therefore a release-packaging inconsistency, not a runtime test
of the source’s TI83P branch. [confirmed]
A separate 40-byte TI-83+ fixture transcribes the source’s initial one-sided
wait loop. On TilEm x4 with no peer, it entered at ram:9D95, first read
0x03 from port 0x00, wrote 0xD1 once to port 0x00, selected keypad row
0xBF 214,766 times, observed injected MODE value 0xBF, and returned through
07:57D1. This confirms the source-level port sequence in that bounded
emulator scenario. It does not test a peer exchange, the rest of the tutorial,
physical voltage, or electrical polarity. Compact results and exact trace
identities are in tools/data/community-link-wait.csv and
tools/data/community-linktutorial-release.csv. [confirmed]
The tutorial archive source/linktutorial83plus.zip has SHA-256
7a0917379bd1b46b45e802b44c9bdc129ac5db42f94c979fae01c29c6b5ca8fe.
Members link tutorial.txt and example.z80 have SHA-256
1dcbf9ae6fc658546ab18138f68cce6f7187cf1e7b1339b60cfcce6a24abbb13
and 108f13b42dfbc61ff51adca1ea3ea8557d8a79a9cd06602a2b5bb6d82aac5f9c,
respectively. [confirmed]
Stopwatch’s readme separately calls the link port nominally +5VDC. It gives
no identified unit, load, instrument, or measurement procedure. The archive
programs/stopwatch.zip has SHA-256
232728ba0d3ce38f07fe78f249a7da412e79861195347c7eb93053128bf22f04,
and member stopwatch.txt has SHA-256
eb944f2bdc93dfb76ca244a39218d52424c9edf1ed0c6fe2587732b2b301a710.
That claim does not replace the still-open voltage, pull-up, threshold, or load
measurements. The identified readme wording is [confirmed]; the physical values
remain [hypothesis].
Differential audio output
Software can use the two output controls as a three-level differential source. If the peer releases both lines and $V_0$ and $V_1$ denote the resulting logical high/low levels of line 0 and line 1, the idealized differential signal is $V_0 - V_1$: [standard]
Port-0x00 write | Line 0 | Line 1 | Idealized differential state |
|---|---|---|---|
0 | released/high if unopposed | released/high if unopposed | zero |
1 | driven low | released/high if unopposed | negative |
2 | released/high if unopposed | driven low | positive |
3 | driven low | driven low | zero |
Differential state map. The three-level output follows the [standard] open-collector model. It shows logical polarity rather than analog voltage or safe loading; those physical properties remain [hypothesis].
An interrupt routine can therefore write 1 and 2 for opposite polarities,
or use either equal-line state for the midpoint. This is the same
open-collector digital contract used by link transfers; it is not a separate
audio peripheral. The repository’s tools/badapple/README.md describes one
software example and preserves the upstream program’s oscillator and tracker
encoding. [standard]
The table does not specify voltage, output impedance, safe load, loudness, or analog bandwidth. Those depend on the unmeasured pull-ups, ASIC drive behavior, connector load, and edge timing. A physical calculator and load must be measured before treating the idealized levels as an electrical schematic. [hypothesis]
Sending one byte at 3C:420D
_SendAByte = 4EE5, body 3C:420D, copies the byte from A to C. The model probe at 3C:420E selects the link-assist path when available. The legacy path sends eight bits least-significant first. [confirmed]
3C:4214 LD B,8
3C:4216 LD DE,0xFFFF
3C:4219 RR C
3C:421B JR NC,send_zero
3C:421D LD A,2
3C:421F JP drive_bit
send_zero:
3C:4222 LD A,1
drive_bit:
3C:4224 OUT (0),A
RR C places the next low bit in carry. Carry clear chooses write 1; carry set chooses write 2. Repeating the rotation eight times consumes the original byte from bit 0 through bit 7. [confirmed]
After driving the selected line, the sender polls until both lines read low:
3C:4226 IN A,(0)
3C:4228 AND 3
3C:422A JP Z,acknowledged
3C:422D IN A,(0)
3C:422F AND 3
3C:4231 JP Z,acknowledged
3C:4234 DEC DE
3C:4237 JP NZ,0x4226
3C:423A JP 0x2799
The receiver acknowledges by pulling the other line low. The combined read value becomes 0. The sender then writes 0 to release its own line and waits for read value 3, which means the receiver also released its acknowledgement. [confirmed]
acknowledged:
3C:423D LD A,0
3C:423F OUT (0),A
3C:4241 LD DE,0xFFFF
3C:4244 DEC DE
3C:4249 IN A,(0)
3C:424B AND 3
3C:424D CP 3
3C:424F JP NZ,0x4244
3C:4252 DJNZ 0x4216
3C:4254 RET
The four-transition handshake
Each bit uses the same four transitions. The sender chooses which line moves first; the receiver pulls the other line low; then each endpoint releases its own line. [confirmed]
| Phase | Sender drive | Receiver drive | Read value for bit 0 | Read value for bit 1 |
|---|---|---|---|---|
| Sender asserts | 1 for bit 0; 2 for bit 1 | 0 | 2 | 1 |
| Receiver acknowledges | unchanged | the other line | 0 | 0 |
| Sender releases | 0 | unchanged | 1 | 2 |
| Receiver releases | 0 | 0 | 3 | 3 |
This is a level handshake, not a clocked UART waveform. Either endpoint can pause a transfer by delaying its next transition, up to the software or hardware timeout. Byte boundaries are supplied by the calling protocol rather than a separate wire symbol. [standard]
Receiving one byte at 3C:447E
_RecAByteIO = 4F03, body 3C:443F, reaches the legacy receiver at 3C:447E when the model probe does not select link assist. The receiver waits for a single-low state and rejects both-low. [confirmed]
3C:447E LD B,8
3C:4486 LD DE,0xFFFF
3C:448B IN A,(0)
3C:448D AND 3
3C:448F JR Z,link_error
3C:4491 CP 3
3C:4493 JP NZ,decode_bit
3C:4496 IN A,(0)
3C:4498 AND 3
3C:449A JR Z,link_error
3C:449C CP 3
3C:449E JP NZ,decode_bit
Only read values 1 and 2 reach decode_bit:
| Initial read | Sender drove | Received bit | Receiver acknowledgement |
|---|---|---|---|
2 | line 0 with write 1 | 0 | write 2 |
1 | line 1 with write 2 | 1 | write 1 |
The comparison at 3C:44AA also prepares carry for RR C. Read 2 makes carry clear, inserting a zero at bit 7. Read 1 makes carry set, inserting a one. After eight rotations, C contains the byte in its original order even though the wire bits arrived least-significant first. [confirmed]
decode_bit:
3C:44AA CP 2
3C:44AC JR Z,received_zero
3C:44AE LD A,1
3C:44B0 OUT (0),A
3C:44B2 RR C
; wait until read 2: sender released line 1
received_zero:
3C:44DE LD A,2
3C:44E0 OUT (0),A
3C:44E2 RR C
; wait until read 1: sender released line 0
Once the sender releases, the receiver writes 0. It samples briefly for idle and uses DJNZ to begin the next bit. _Rec1stByte at 3C:439C adds APD and first-activity handling before entering the same decoder. [confirmed]
The TI-Keyboard error delimiter
Both-low has three context-dependent roles. It is the normal acknowledgement
midpoint after a receiver has recognized a single-low data bit. It is a link
error if the raw receiver sees both lines low before selecting a bit. The
TI-Keyboard decoder deliberately uses that otherwise exceptional condition as
a frame delimiter after prefix byte 0xE0. [confirmed] for the ROM branches.
_KeyboardGetKey = 50E9 resolves through the main bcall table to 3C:6D5E.
After accepting 0xE0, it calls 3C:6CC1. The assist branch treats port-0x09
bit 6 as the expected delimiter; the legacy branch waits for a non-idle raw
state and accepts only the both-low value. A timeout or ordinary single-low
state returns status 0x02. The decoder then calls 3C:6D17 for two bytes.
That helper compares the first with 0x01, saves the comparison flags, receives
the second, and restores the flags before returning. The second byte is
therefore consumed as a scan code or modifier mask, but the public routine
replaces it with status 0x01. [confirmed]
The complete accepted sequence is:
0xE0
deliberate DBUS error / both lines low
0x01
scan code or modifier mask
The ROM establishes the receiver grammar and status control flow. WikiTI’s
historical _KeyboardGetKey page independently says the TI-Keyboard transmits
the same sequence, but no physical capture was made for this reconstruction.
[standard] for that peripheral claim.
The explicit OS 2.55MP return tails are:
| Status | Tail | ROM condition |
|---|---|---|
0x00 | 3C:6DA0 | No accepted raw or assist activity; the early no-assist return at 3C:6D6A also leaves A=0. |
0x01 | 3C:6DDB | Prefix and delimiter accepted; first following byte is 0x01. |
0x02 | 3C:6DE2 | Ordinary receive did not produce 0xE0, or the required delimiter condition failed. |
0xF9 | 3C:6D95 | Entry assist status has bit 6, but neither masked buffered-data/activity bit. |
0xFA | 3C:6D8E | Entry assist error has buffered data other than 0xE0. |
0xFB | 3C:6D87 | Entry assist error has buffered 0xE0; cleanup and two additional reads follow. |
0xFC | 3C:6DE9 | The first post-prefix byte is not 0x01. |
0xFD | 3C:6DF0 | The legacy prefix receive returned nonzero low-level status. |
0xFE | 3C:6DF7 | The assist prefix receive returned nonzero status with C != 0xE0. |
0xFF | 3C:6DFE | The installed error handler caught a lower-level error. |
These descriptions follow the ROM branches rather than the historical status
list, which does not fully characterize the 0xFC and 0xFD paths.
Timeouts and malformed states
The raw routines use loop counters, not a wall-clock register. CPU speed and I/O timing therefore affect the elapsed timeout. The fixed count alone does not justify a duration claim. [confirmed]
| Condition | ROM response | Evidence |
|---|---|---|
| Sender never sees both-low acknowledgement | exhaust DE = 0xFFFF, then jump to _JErrorNo | 3C:4216–423A [confirmed] |
| Peer never releases after acknowledgement | exhaust DE = 0xFFFF, then share the same _JErrorNo edge | 3C:4241–424F [confirmed] |
| Receiver waits too long for a non-idle state | jump to _ErrLinkXmit | 3C:4486–44A7 [confirmed] |
| Receiver sees both lines low before acknowledging | jump to _ErrLinkXmit | 3C:448B–449A, 3C:44F9 [confirmed] |
| Sender fails to release its selected line | exhaust DE = 0xFFFF, then jump to _JErrorNo | 3C:44B4–44C6, 3C:44E4–44F6 [confirmed] |
_ErrLinkXmit = 44D4, body 00:278D, loads error 0x9F before the common error path. The include file names 0x9F E_LnkErr. _JErrorNo at 00:2799 raises the error already stored by the surrounding link operation. [confirmed]
The send acknowledgement loop reads port 0x00 twice before decrementing DE. A cycle estimate that treats it as one read per iteration is incorrect. [confirmed]
Header setup and idle recovery
The packet-header sender at 3C:41C3 prepares the transport before sending its four header bytes. On the raw path it writes 0, requires low bits 3 for idle, and invokes the receive-status path when a peer already holds a line low. It loads the first header byte at 3C:41F8 and calls the byte sender at 3C:41FB. [confirmed]
The receiver entry at 3C:43B4 repeatedly samples port 0x00 until the low bits differ from 3. It then initializes the eight-bit decoder at 3C:43C5. This separates the unbounded wait for the first activity from the bounded waits inside a byte. [confirmed]
Background link detection and interrupts
The standard-timer handler contains a raw-line activity check. After its surrounding link-service gates pass, ram:01B1 calls the hardware-model probe. The legacy branch reads port 0x00 & 3; a value other than 3 calls the common link-activity bjump at ram:3FD5. The assist branch instead checks port 0x09 & 0x18, pulses port 0x08, and calls the same bjump. [confirmed]
This explains why OS input and timer activity can react to a peer that pulls one raw line low. It does not mean every port transition immediately vectors the Z80. Port 0x03 controls the legacy link-interrupt enable, while this specific silent-link check occurs inside standard_timer1_irq at ram:0167. [confirmed]
Port-0x03 bit 4 also keeps link activity available as a wake source in the standard hardware interrupt block. The power-off path writes 0x11 before HALT, enabling ON and link wake while disabling the standard timers. [confirmed] for the ROM write; [standard] for the port-bit role.
Port-0x04 bit 4 has lower OS dispatch priority than the three programmable
timers and standard timer 2, but higher priority than ON and standard timer 1.
The branch enters legacy_link_irq at ram:01E0. See
Interrupts (IM1) for
acknowledgement and simultaneous-source behavior. [confirmed]
Error cleanup and the both-low abort pulse
Callback provenance
3C:618D has one genuine direct caller, at 3C:614E. Raw searches also find six CALL 0x618D instructions on page 05, but those resolve to unrelated page-05 code because both the call sites and destination occupy the same banked window. They are not callers of 3C:618D. ti84re.rom.analyze_calls reports this inferred physical destination as resolved_target, which keeps same-address routines on different pages distinct. [confirmed]
The real entry chain starts at page-0 bjump stub 00:2D51, whose inline descriptor resolves to 3C:6136. Six higher-level paths load HL=0x2D51 and call the error-callback installer at 00:27DA: 36:4BAD, 36:5B7D, 3D:6D77, 3D:6EE3, 3D:6F10, and 3D:6F40. The callback examines sndRecState at 0x8672. State 0x0A immediately rethrows the pending error. State 0x15 follows an ioFlag bit-1 branch without reaching the pulse. Other states fall through 3C:614C, call 3C:618D, and then invoke page-0 stub 00:2F31, which resolves to 07:7AC3 and stores 1 in ioErrState. This makes the routine part of installed link-error cleanup rather than an arbitrary command delay. [confirmed]
Transport-specific cleanup
3C:618D first tests link-mode bit 5 at (IY+0x1B). The bit is set by USB initialization elsewhere in the ROM. When it is set, the routine skips all raw port activity and calls lnk_clr_busy_b at 3C:4F32. The raw branch performs this sequence: [confirmed]
3C:6193 call 6971h ; lnk_set_busy
3C:6196 ld a,3
3C:6198 out (0),a ; pull both raw lines low
3C:619A call 0DBDh ; save port 20; select speed mode 0
... delay ...
3C:61AE call 0CF8h ; restore saved port-20 bit 0
3C:61B1 ld a,0
3C:61B3 out (0),a ; release both lines
3C:61B5 call 4F32h ; lnk_clr_busy_b
3C:61B8 ret
The public DBus guide defines simultaneous assertion of both lines as the electrical error/abort condition. Error-callback provenance, explicit busy-state bracketing, the both-low/release waveform, and the USB bypass together identify 3C:618D as the OS’s transport abort cleanup; the pulse is specific to the raw two-wire path. [confirmed] for the ROM role; [standard] for the public waveform name.
Exact software delay
The loop at 3C:619D–61AE loads HL=0xFFFF. For every outer iteration it loads A=4, executes four padding NOPs, runs a four-iteration DEC A/JR NZ loop, decrements HL, and repeats until zero. Base Z80 timing is 7,077,785 T-states across 1,114,096 opcode fetches. Under the documented wait-state semantics, the OS’s mode-0 configuration (port 0x29=0x17, port 0x2E=0x45) adds one T-state to each Flash opcode fetch, making the delay loop 8,191,881 T-states. At the nominal 6 MHz selected by 00:0DBD, that is 1.3653135 seconds. 00:0CF8 later restores only bit 0 of the saved port-0x20 byte, which is sufficient for the OS’s normal modes 0 and 1 but would collapse modes 2 and 3. The count excludes the surrounding calls and I/O instructions. [confirmed] for the base instruction and fetch counts; [standard] for the configured wait-state and nominal clock conversion.
The archived protocol guide describes an abort assertion of approximately 250 µs and a two-second maximum bit time. This OS deliberately holds the condition far longer than that example while remaining below the nominal two-second timeout. Physical CPU frequency, wait-state behavior, and line rise/fall time still need measurement before assigning an exact oscilloscope duration. [standard] for the guide; [hypothesis] for the physical waveform.
Emulator comparison
The pinned sources implement materially different levels of the link stack. These are executable software behaviors, not measurements of the ASIC. [standard]
| Detail | TilEm f56ad63 | Wabbitemu 48c2dc0 | MAME 0.287 | jsTIfied 20170706a |
|---|---|---|---|---|
Raw write 1/2 reaches connector | yes | yes | no; both external lines are released | yes, through the browser link endpoint |
Disconnected read after 1/2 | 0x12/0x21 | 0x12/0x21 | 0x12/0x21 | modeled raw-line latch and peer state |
| Peer pull-low affects reads | yes | yes | yes | yes |
| Read bits 4–5 | local low-two-bit latch | local low-two-bit latch | low write bits copied into PCR bits 4–5 | local output state |
| Link-assist advertisement | yes | yes | yes, through port 0x02 = 0xC3 | yes |
| Assist ports present | 0x08–0x0D | 0x08, 0x09, 0x0A, 0x0D | only port 0x09, fixed read zero | 0x08–0x0D state machine |
| Assist byte transfer | implemented | implemented | absent | implemented |
| Raw-line activity interrupt | transition model present | no transition assertion in the raw port handler | absent from mask, status, and port handlers | modeled through link state changes |
| Driver status | usable link model | usable link model | MACHINE_NOT_WORKING | browser emulator source model |
TilEm and Wabbitemu digital agreement
TilEm and Wabbitemu agree on the raw digital contract:
- each endpoint stores a two-bit pull-low mask;
- connected masks combine with OR because either endpoint can pull a line low;
- reads invert the combined mask so set bits mean physically high lines;
- read bits 4–5 report the local output latch.
TilEm’s external-line setter detects a peer transition not already hidden by a
local low output and can assert the link-activity interrupt when port 0x03
bit 4 enables it. Its link core also implements the four-phase assist state
machine and two-second emulator timeout policy. [standard]
Wabbitemu’s port-0x00 handler implements the same raw truth table, while its
virtual-cable sender and receiver implement the LSB-first handshake at a higher
level. The raw handler itself only updates and reads line masks; it does not
assert a link-activity interrupt when the client mask changes. Its assist
engine can assert CPU interrupts for read-ready, idle, and error conditions.
[standard]
Wabbitemu represents a disconnected calculator by making client point back
to its own host latch. OR-ing the latch with itself gives the expected idle
and self-drive reads. The separate link_disconnect function instead assigns
client = NULL, while the port handler unconditionally evaluates
client[0]. A subsequent port read can therefore dereference a null pointer.
This is an emulator lifecycle defect, not link-port behavior. [standard]
Native TilEm raw and assist edges
A guarded direct-core run exercises TilEm’s registered port handlers and link
state machine at commit f56ad637d0524ee841dd381be6ecbaf5b8975600.
The raw port produces the same 16-value local-major truth table shown for
Wabbitemu below. Writing 0xA6 gives 0x21. Pulling peer line 0 low while the
local latch is zero gives 0x02 and asserts the link-activity interrupt when
port-0x03 bit 4 enables it. [standard]
TilEm maps all six assist ports from 0x08 through 0x0D. A fresh disabled
engine reads status 0x20. Writes 0x91, 0xA2, 0xB3, and 0xC4 to
ports 0x09–0x0C remain in internal auxiliary registers, while the read
sides continue to return computed status or zero. [standard]
Enabling idle-ready produces status 0x22 and asserts the CPU interrupt.
Reading port 0x0D leaves both conditions unchanged. Sending 0xA5 drives
2,1,2,1,1,2,1,2, least-significant bit first, and returns to 0x22 after
eight controlled acknowledgements. Receiving the same byte completes at
0x31; reading port 0x0A returns 0xA5 and changes status to 0x20.
[standard]
An illegal both-low receive state produces status 0x64 and asserts the CPU
interrupt. The first port-0x09 read clears the interrupt request but retains
the error flag, so the next status is 0x60. Full reset restores port
0x08 = 0x80 and clears active assist state. It retains the four auxiliary
registers and the externally supplied peer-line state. Direct port calls add
zero modeled CPU clocks. These are initialized-core TilEm behaviors, not
TI-OS execution, electrical measurements, or physical reset guarantees.
[standard]
Native Wabbitemu raw and assist edges
A guarded initialized-core run exercises the registered Wabbitemu handlers. The raw port produces this local-major matrix, with peer masks across each row: [standard]
| Local drive | Peer 0 | Peer 1 | Peer 2 | Peer 3 |
|---|---|---|---|---|
0 | 0x03 | 0x02 | 0x01 | 0x00 |
1 | 0x12 | 0x12 | 0x10 | 0x10 |
2 | 0x21 | 0x20 | 0x21 | 0x20 |
3 | 0x30 | 0x30 | 0x30 | 0x30 |
Writing 0xA6 gives 0x21, so bits 2–7 do not alter the two-bit drive
latch. Pulling peer line 0 low while the local latch is zero gives 0x02.
That peer-state change leaves the CPU interrupt line clear even when
port-0x03 bit 4 is enabled, matching the absence of transition logic in the
raw handler. The probe assigns a controlled peer mask directly; it does not
exercise Wabbitemu’s connection or disconnection lifecycle. [standard]
The initialized assist block maps ports 0x08, 0x09, 0x0A, and 0x0D.
Ports 0x0B and 0x0C are absent and reject reads. Port 0x08 resets to
0x80, while status and both data latches reset to zero. Enabling idle-ready
interrupts produces status 0x22 and asserts the CPU line. Reading port
0x0D clears ready and returns status to zero. [standard]
Writing byte 0xA5 to port 0x0D drives masks
2,1,2,1,1,2,1,2, least-significant bit first. Eight controlled peer
acknowledgements complete the transfer with status 0x22; reading port
0x0D returns 0xA5 and clears ready. The receive direction reconstructs
0xA5, reports status 0x11, asserts the CPU line, and clears read-ready when
port 0x0A is read. These transitions use Wabbitemu’s device evaluator, not
TI-OS or a physical cable. [standard]
The pinned source contains no assignment that raises the assist error field.
The probe seeds that internal field to test its observable contract. With
error interrupts enabled and a single-low peer state being received, status
is 0x4C and the CPU line asserts. The first status read clears error; the
second reads 0x08, retaining only the receiving flag. This verifies the
read-to-clear handler but does not establish a naturally reachable error path.
[standard]
MAME’s readback-versus-connector split
MAME’s TI-Plus write handler copies write bits 0–1 into internal PCR bits 4–5. Its read handler masks the peer inputs with the inverse of those PCR bits. Consequently, ordinary disconnected reads reproduce the public contract: [standard]
| Write | MAME PCR after reset | Local latch | Disconnected read |
|---|---|---|---|
0x00 | 0x00 | 0 | 0x03 |
0x01 | 0x10 | 1 | 0x12 |
0x02 | 0x20 | 2 | 0x21 |
0x03 | 0x30 | 3 | 0x30 |
A guarded MAME 0.287 run reproduces these reads through the main CPU I/O
space: 03, 12, 21, and 30. It then writes zero to the PCR and injects
the four peer pull-low masks through the link-port device’s saved input fields.
The resulting reads are 03, 02, 01, and 00. This confirms that the
live read handler observes both the PCR latch and peer input state. [confirmed]
The connector callbacks use different bits. MAME drives tip low only when
write bits 2 and 4 are both set, and ring low only when bits 3 and 5 are both
set. Normal OS writes 1 and 2 satisfy neither pair and therefore release
both external lines. Values 0x14 and 0x28 drive tip and ring respectively,
but those are not the TI-84 Plus raw protocol values. A local MAME program can
thus read an apparently correct self-latch while a connected link-bus device
sees no asserted bit. [standard]
The guarded run reads MAME’s connector-facing m_tip_out and m_ring_out
save items after every write. Values 0x00–0x03 leave both at 1, meaning
released. Write 0x14 changes the pair to 0,1; 0x28 changes it to 1,0;
and 0x3C changes it to 0,0. These are internal MAME device levels. The run
does not attach an optional link device or observe a physical connector.
[confirmed]
MAME’s reusable link-bus layer has a four-phase bit/byte implementation, one-second timeout placeholders, collision handling, and optional bit-socket, Graph Link, tee, and speaker devices. The TI-84 Plus host handler’s mismatched control bits prevent normal raw writes from reaching those devices. The presence of the generic bus layer does not repair the calculator-side wiring. [standard]
MAME also returns port 0x02 = 0xC3, advertising link assist through bit 6,
while its TI-84 Plus I/O map omits ports 0x08, 0x0A, 0x0B, 0x0C, and
0x0D. Port 0x09 alone returns zero. OS 2.55MP therefore selects an assist
path that this driver cannot execute. The driver also ignores port-0x03 bit
4 and never reports port-0x04 link activity. [standard]
The same native run reads port 0x02 = C3. Ports 0x08–0x0D all return
zero before and after distinct writes A8–AD. The runtime result proves
that no writable assist state is visible through those six ports. It cannot
distinguish the fixed-zero handler at port 0x09 from the five unmapped
ports; the pinned I/O map establishes that distinction. [confirmed] for the
runtime values; [standard] for handler coverage.
The emulator agreement corroborates the raw state transitions only where the implementations actually agree. None establishes analog voltage thresholds, pull-up values, connector wear behavior, or edge timing on a physical TI-84 Plus. Those analog details remain hypotheses until measured. [hypothesis]
Reusable debugging tools
tools/ti84re/link/port.py provides the wired-AND model, port-read decoder, byte
encoding, receive assembly, four-phase trace, and pinned implementation
profiles. tools/ti84re/link/describe_port.py exposes those operations as a CLI:
nix develop -c python3 -m ti84re.link.describe_port profiles
nix develop -c python3 -m ti84re.link.describe_port drive 0x02
nix develop -c python3 -m ti84re.link.describe_port wire --local 1 --peer 2
nix develop -c python3 -m ti84re.link.describe_port byte 0xA5
nix develop -c python3 -m ti84re.link.describe_port receive 1 2 1 2 2 1 2 1
nix develop -c python3 -m ti84re.link.describe_port compare 1 2 0
nix develop -c python3 -m ti84re.link.describe_port emulator mame 0x14 0x28
nix develop -c python3 -m ti84re.link.describe_port abort-pulse
nix develop -c python3 -m ti84re.link.describe_port keyboard \
--prefix 0xE0 --delimiter-error --command 0x01 --data 0x42
nix develop -c python3 -m ti84re.link.describe_port keyboard-path \
--assist-status 0x50 --buffered 0xE0
nix develop -c python3 -m ti84re.link.describe_port keyboard-rom
Add --json before the subcommand for machine-readable output. The model uses neutral line numbers so a trace remains valid even when the physical contact mapping is under review.
The keyboard-rom command verifies the 0x50E9 bcall entry and hashes the
three OS 2.55MP byte regions that support the decoder; it rejects a different
control-flow body instead of applying the fixed status model silently.
Prepared physical readback matrix
The raw two-wire link probe writes
all four low-bit drive states and records the complete port-0x00 byte after 0,
1, 4, and 16 NOP instructions. It repeats each point 16 times, compares the
exported result with port_read_value from the reusable model, and releases
both lines during cleanup. The decoder reports low-line, local-latch,
exact-byte, stability, and idle-cleanup results separately. [confirmed] for the
probe bytes and decoder; [hypothesis] for pending physical samples.
The matrix must run with an empty connector because it preconditions both lines low before every target write. It can test the public disconnected truth table and bound a CPU-visible settling change. It cannot measure analog rise time or voltage without external instrumentation.
Run the guarded TilEm matrix with the pinned clean source tree:
tilem_link_tmp=$(mktemp -d /tmp/ti84-tilem-link.XXXXXX)
git clone https://github.com/debrouxl/tilem.git "$tilem_link_tmp/tilem"
git -C "$tilem_link_tmp/tilem" checkout \
f56ad637d0524ee841dd381be6ecbaf5b8975600
nix shell \
github:NixOS/nixpkgs/f13ff45afd1bb73e640eaa08a7066dbed07e3238#gcc \
--command python3 -m ti84re.emulators.tilem.build_probe --probe link \
--source "$tilem_link_tmp/tilem" \
--output "$tilem_link_tmp/tilem-link-probe" --json
tilem_link_parent=$(mktemp -d /tmp/ti84-tilem-link-report.XXXXXX)
python3 -m ti84re.emulators.tilem.run_link_probe \
--binary "$tilem_link_tmp/tilem-link-probe" \
--expected-binary-sha256 \
b878d9be860a92da72c5712e82a4c2974fb3cad125e078e61f8444172b887896 \
--output-dir "$tilem_link_parent/run" --json
tools/ti84re/emulators/tilem/link.py derives the expected raw matrix, byte order, assist
status, acknowledgement, and reset boundary from tools/ti84re/link/port.py. The
guarded runner records the source tree, exact binary, native report, and
evidence scope.
Run the guarded Wabbitemu matrix with:
wabbit_link_parent=$(mktemp -d /tmp/ti84-wabbit-link.XXXXXX)
python3 -m ti84re.emulators.wabbitemu.run_link_edge_probe \
--rom tools/rom.bin \
--binary "$wabbit_tmp/wabbitemu-headless" \
--output-dir "$wabbit_link_parent/run" --json
tools/ti84re/emulators/wabbitemu/link_probe.py derives the raw matrix, byte order, mapped
ports, and assist status from the reusable model. The guarded CLI requires the
exact ROM and records the ROM and binary hashes.
Run the guarded MAME raw-link matrix with:
mame_link_parent=$(mktemp -d /tmp/ti84-mame-link.XXXXXX)
nix shell nixpkgs#mame --command python3 -m ti84re.emulators.mame.run_link_probe \
--expected-mame-sha256 \
fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
--output-dir "$mame_link_parent/run" --json
tools/ti84re/emulators/mame/link.py derives every expected read and connector output from
tools/ti84re/link/port.py. The guarded CLI retains the exact MAME, ROM, Lua script,
native report, and parsed oracle identities. It does not execute a TI-OS
transfer or attach a virtual cable.
Resolved findings and open hardware tests
- [confirmed]
_SendABytewrites1for bit 0 and2for bit 1, least-significant bit first. - [confirmed] The receiver maps initial read
2to bit 0 and read1to bit 1, then acknowledges on the other line. - [confirmed] Both-low is the acknowledgement midpoint during a valid transfer, but it is an error when a receiver sees it before choosing a bit.
- [confirmed]
_KeyboardGetKeydeliberately accepts that error condition after prefix0xE0, then consumes command0x01and one data byte while returning status0x01. - [confirmed] The installed error callback reaches
3C:618Dfor applicable transfer states; its raw branch brackets a both-low pulse with link-busy state and its USB branch skips the raw lines. - [confirmed] The raw delay loop is 7,077,785 base T-states and 8,191,881 T-states with the OS’s mode-0 Flash opcode wait.
- [confirmed] The standard-timer path detects non-idle raw lines and routes them to the OS link-activity handler.
- [standard] Port reads use active-high physical levels, writes use active-high pull-low controls, and bits 4–5 reflect the local output latch.
- [standard] Public hardware references map bit 0 to red/tip and bit 1 to white/ring.
- [standard] TilEm and Wabbitemu reproduce the raw open-collector truth table.
- [confirmed] The prepared
HWLINKprobe encodes the four-state, four-delay, 16-trial matrix and releases both lines during cleanup; no physical AppVar has been recorded. - [standard] The guarded TilEm run verifies the raw matrix, activity interrupt, all six assist handlers, LSB-first
0xA5transfers, status and data acknowledgement, sticky error flag, auxiliary-register retention, and external-line retention across reset. - [confirmed] The guarded MAME run reproduces its local readback and peer-input matrix while normal writes
1and2leave both modeled connector outputs released. - [standard] The guarded Wabbitemu run verifies the complete raw matrix, absent assist ports
0x0B/0x0C, idle-ready and read-ready interrupts, LSB-first0xA5send and receive, data-register acknowledgement, and seeded-error read-to-clear behavior. - [confirmed] MAME reports port
0x02 = C3, while ports0x08–0x0Dremain zero before and after patterned writes. - [standard] MAME’s source map gives port
0x09a fixed-zero handler and omits the other five assist ports. - [hypothesis] Physical tests must measure pull-up resistance, high/low thresholds, line rise time, timeout duration at both CPU speeds, and the actual duration and voltage waveform of the
3C:618Dabort pulse.
External references
- WikiTI port
0x00— public bit meanings and output-latch behavior; treated as a secondary source. - TI Link Protocol Guide — archived open-collector, contact-name, four-transition, abort-condition, and timeout description.
- WikiTI
_KeyboardGetKeyrevision 5510 — historical peripheral sequence; treated as secondary literature and checked against the ROM decoder. - TilEm link core at
f56ad63andx4_io.c— raw lines, activity interrupt, link assist, and timeout policy. - Wabbitemu
83psehw.cat48c2dc0andlink.c— raw port, assist engine, virtual-cable handshake, and disconnect lifecycle. - MAME 0.287
ti85.cpp,ti85_m.cpp, andti8x.cpp— I/O coverage, PCR expressions, connector callbacks, and generic link-bus state machine. - jsTIfied deployed
20170706aartifact and readable mirror — fourth raw-line, browser endpoint, and link-assist implementation.
Link and data transfer
The data-transfer subsystem sends variables and system objects through the
packet layer over either the 2.5 mm link or the TI-84 Plus USB/link-assist path.
It builds on _SendAByte (3C:420D) and _RecAByteIO (3C:443F), described in
Two-wire link port hardware. USB ASIC and link
assist covers the ASIC-facing ports.
Raw disassembly preserves the register-passed arguments and
SET/RES/BIT b,(IY+d) state operations that the decompiler can mis-render.
The silent-link engine shares Flash page 3C with archive command code.
Transfer layers
flowchart TB
SRC(["user 'Send…' / TI-Connect"])
subgraph VAR["Variable layer · page 3C"]
LX["link_xfer_op 3C:4DD2<br/>silent-link variable send"]
SV["_SendVarCmd 3C:4A14→4EDD<br/>DI / cleanup-wraps a send"]
end
subgraph PKT["PACKET layer"]
direction LR
SH["send header 41C3"]
RH["receive header 4338"]
SD["send DATA 40DA"]
RD["receive DATA 4292"]
AK["send ACK 42FB · cmd 0x56"]
CK["checksum 4167 / 6356"]
end
subgraph BYTE["BYTE layer · keyboard & link"]
direction LR
SB["_SendAByte 420D"]
RB["_RecAByteIO 443F"]
HW["bit-bang port 0 + HW-assist FIFO ports 8/9/0D"]
end
SRC --> VAR --> PKT --> BYTE
RAM state block [confirmed]
All labels below are confirmed from ti83plus.inc. This contiguous block at 0x8670 is the
silent-link control/scratch area:
| Addr | Label (.inc) | Meaning |
|---|---|---|
8670 | ioFlag | I/O state flags (bit4 tested on receive completion) |
8672 | sndRecState | transfer type / phase: 0x08 selects backup-send framing, 0x0A appears in backup receive/orchestration, 0x15 is variable DATA, and 0x0B is request/directory |
8673 | ioErrState | link error sub-state |
8674 | header | packet header byte 0 = machine-ID |
8675 | header+1 | packet header byte 1 = command-ID |
8676 | header+2 | packet length, word (LE) — also the running payload byte budget |
8678 | (running) | running 16-bit checksum accumulator (sum of payload byte values) |
867D | ioData | scratch: built var-header length / data ptr setup |
867F | — | the variable header (type+name) copied from OP1 via _MovFrOP1 |
8688/8689 | ioNewData | “new var arrived” status (bit7 of 8689) |
868B | bakHeader | saved 9-byte header for echo/ACK comparison (_Mov9B to/from 8674) |
84DB | iMathPtr5 | active data pointer during a streaming transfer |
848E–8492 | — | three backup-section lengths parsed from or written to the backup header |
8494 | — | saved user-memory boundary used after backup restore |
9834 | pagedCount | bytes buffered in the 16-byte staging block (Flash-write batching) |
9836 | pagedGetPtr | write cursor into pagedBuf |
983A | pagedBuf | 16-byte staging block for received Flash-window data |
9C86 | — | HW-assist TX timeout reload (0xFA) |
9CAC | — | HW-assist TX/RX timeout down-counter (seeded from CPU speed, port 0x20) |
85D9 | varClass | variable class (backup sub-type check, =0x0A) |
IY-relative flag bytes used by the link code (IY = flags base, 0x89F0): IY+0x1B is the
link-mode/peer-type byte (which machine-ID to advertise, USB-vs-DBUS, single-byte mode), IY+0x12
bit2 “command in progress”, IY+0x24 bit1/2 transfer-active, IY+0xC bit2 APD-disable
save, IY+0x3E bit0 / IY+0x3D bit5 USB-presence.
Byte layer [confirmed]
Two-wire link port hardware covers the complete raw port-0x00 send and receive handshakes. This section records how the byte entries select and report the hardware-assist path.
Hardware-assist send [confirmed]
_SendAByte (3C:420D) starts:
CALL probe_hw_model_keep_a
JP Z,0x6BB2
If the model probe sets Z, the 84+ link-assist hardware is present, and the
routine jumps to 3C:6BB2:
6BB2: setup line / 2× short delay (6BD2 seeds 9CAC from port 0x20 = CPU speed)
6BBB: LD A,0xFA
LD (0x9C86),A ; reload inner timeout
IN A,(0x09)
BIT 5,A ; port 0x09 bit 5 = TX buffer empty/ready
JR Z,6BCA ; not ready → spin
LD A,C
OUT (0x0D),A ; write the byte to the assist FIFO
RET
6BCA: CALL 6BE4 ; decrement 0x9CAC
JR Z,6BBB ; retry
JP 4434 ; timeout
So the assist path is: poll port 0x09 bit 5, then OUT (0x0D),byte, with a CPU-speed-scaled timeout. The legacy fall-through writes port 0x00 directly; see Two-wire link port hardware for its two-read polling loop and four-transition handshake.
Receive path and decoder [confirmed]
443F: DI
CALL 447E ; arm/clock the line
CALL 444A ; get status
RET C/NZ ; loop if Z
444A: CP 1 ; status 1 selects the error-status path
LD A,C ; A = candidate byte/status marker
JR NZ,4456 ; other status: normal byte or marker
CP 0xE0
JP NZ,_ErrLinkXmit
JR 4470
4456: CP 0xE0
RET NZ ; return an ordinary byte from C
IN A,(0x02)
AND 0x80 ; port 0x02 bit 7 set = non-83+-Basic
JR Z,4469 ; legacy path: 6CC1 polls the bit-bang lines
IN A,(0x09)
BIT 6,A
JR NZ,4470 ; transmission error → abort
AND 0x19
JR NZ,4475 ; link error/active flags
4470: CALL 6D17
XOR A
RET ; error/no byte → return 0
Key port semantics (84+ assist): port 0x09 bit 5 = TX ready, bit 6 = transmission error,
bit 4 = byte received, bits 0x19 = error/active; port 0x0D = data FIFO; port 0x02
bit 7 = non-83+-Basic (used here as the assist-present gate; WikiTI’s dedicated “link-assist
available” flag is port 0x02 bit 6).
The assist receiver at 3C:6C20 returns a normal port-0x0A byte in C with
A=0; the port-0x09 bit-6 path returns A=1. lnk_rec_status compares the
returned C value with 0xE0; _RecAByteIO does not preserve a caller-supplied
A value. The exceptional byte is the TI-Keyboard frame prefix. The public
_KeyboardGetKey = 50E9 table entry resolves to 3C:6D5E, whose decoder
requires 0xE0, a deliberate DBUS error delimiter, command byte 0x01, and a
final scan-code or modifier byte. 3C:6D17 preserves the comparison of the
command byte with 0x01 while receiving the final byte. The public routine
then discards that byte and returns status 0x01. [confirmed]
The ROM proves what the calculator accepts, not what a physical keyboard emits.
The historical WikiTI _KeyboardGetKey revision 5510
independently describes the peripheral sending the same four-part sequence;
that transmitter behavior remains [standard] until captured from hardware.
The linked external disassembly is no longer available and was not used as
evidence. See Two-wire link port hardware
for the status tails and executable decoder model.
_Rec1stByte (3C:439C) / _Rec1stByteNC (3C:43A3, “no-clear”) are the same logic
wrapped with APD/_ApdSetup and the bit-bang start-bit detect, used to wait for the first byte
of an incoming packet (peer may be idle for a long time).
TI link packet framing [confirmed]
A TI link packet is a 4-byte header optionally followed by data + 2-byte checksum:
+--------+--------+--------+--------+ +============+----------+
| mach-ID| cmd | len-lo| len-hi| | data[len] | chk16 LE |
+--------+--------+--------+--------+ +============+----------+
8674 8675 8676 8677 streamed 8678 acc
As a C struct:
typedef struct {
uint8_t machine_id; /* +0: peer/local device class */
uint8_t command_id; /* +1: command byte */
uint16_t data_length; /* +2: little-endian length */
} LinkPacketHeader; /* 4 bytes at header = 0x8674 */
The typed RAM view therefore exposes header.machine_id, header.command_id,
and header.data_length; the disassembly below retains the concrete addresses
that establish those fields. [confirmed]
Sending a header [confirmed]
41C3: 6D4B (drive line)
short delay
CALL probe_hw_model_keep_a (model probe)
… (HW handshake on 84+, or bit-bang line-idle wait, with failure reaching _ErrLinkXmit) …
41F2: (8678)=0 ; reset checksum accumulator
LD A,(8674)
CALL _SendAByte ; machine-ID
LD A,(8675)
CALL _SendAByte ; command-ID
LD A,(8676)
CALL _SendAByte ; length lo
LD A,(8677)
CALL _SendAByte ; length hi
419B is the generic “send a 0-length control packet”: it sets the local machine-ID (620A),
stores the command from H, and calls 41C3. Convenience entries: 4195 H=0x92 (EOT),
4199 H=0x09 (CTS), 41BC ID=0x73/cmd=0x68 (RTS).
Receiving a header [confirmed]
4338: CALL _RecAByteIO
(8674)=A ; machine-ID, validated against the known set:
0x95 0x73 0x23 0x74 0x82 0x02 0x12 0x83 0x03 0x13 0x08 (else fall to 2nd-byte machine list)
4370: CALL _RecAByteIO
(8675)=A ; command-ID, validated: 0x68 0x47 0x74 0x2D … else _JErrorNo
438F: CALL _RecAByteIO
(8675)=A ; command ID on the validated path
4392: CALL _RecAByteIO
(8676)=A ; length lo
4395: CALL _RecAByteIO
(8677)=A ; length hi
RET
An unrecognised machine-ID or command-ID byte aborts via _JErrorNo (→ E_LnkErr 0x9F).
Machine-ID selector [confirmed]
The local machine-ID advertised in outgoing packets depends on the peer-type bits in IY+0x1B:
620A: LD L,0x82 ; default / TI-84+ silent
BIT 2,(IY+0x1B)
RET NZ
LD L,0x95 ; computer / TI-Connect USB host
BIT 1,(IY+0x1B)
RET NZ
LD L,0x83
BIT 3,(IY+0x1B)
RET NZ
LD L,0x03 ; TI-83
BIT 4,(IY+0x1B)
RET NZ
LD L,0x73 ; TI-73 / fallback
RET
Command-ID byte reference [hypothesis]
Confirmed in the code; semantics are the standard TI link protocol:
| cmd | name | seen at | meaning |
|---|---|---|---|
0x06 | VAR | link_xfer_op reply check 4E86 CP 6 | variable header packet (type+name+size) |
0x09 | CTS | 4199 (H=0x09) | clear-to-send (receiver ready for DATA) |
0x15 | DATA | 40DA/407C send, 426D CP 0x15 recv | the variable’s data bytes |
0x2D | DEL | header-validate 4382 CP 0x2D | delete / directory variants |
0x36 | SKIP/EXIT | link_xfer_op 4E7C CP 0x36 | peer refused this var → abort transfer |
0x56 | ACK | built by 42FB (LD H,0x56); checked 418F CP 0x56 | acknowledge |
0x5A | ERR/NAK | built by 6356/6385 (LD H,0x5A) | checksum/length error reply |
0x68 | RTS | 41BC (LD H,0x68) | request-to-send |
0x92 | EOT | 4195 (H=0x92) | end of transmission |
0xA2/0xB7 | request | link_xfer_op 4E2B/4E2F | request var (A2=DATA-type, B7=other) |
_SendPacket [confirmed]
_SendPacket = 0x4ED6, body 3C:4139, consumes the shared
LinkPacketHeader at ram:8674, takes the payload pointer from iMathPtr5 at
ram:84DB, and uses header.data_length as the payload byte count. It clears
pagedPN at ram:9835, sends the four-byte header through 3C:41C3, clears
the 16-bit checksum at ram:8678, and streams the payload. A zero pagedPN
reads bytes directly from HL; a nonzero value uses the paged read helper at
ram:17BB. Each byte is added to the checksum before _SendAByte is called.
After the last byte, the routine sends checksum low then high, receives a reply
header, and returns only when its command byte is 0x56 ACK. A line failure,
bad reply, or checksum-side failure takes the link error machinery rather than
returning a status code.
A one-sided controlled trace supplies a one-byte DATA packet and an installed
calculator error handler. With no peer, the run reaches _SendPacket once,
the header helper once, and _SendAByte twice before _ErrLinkXmit and
_JErrorNo transfer control to the fixture’s handler. It does not reach the
payload read at 3C:4160. This dynamically establishes the no-peer boundary;
the payload, checksum, and ACK success path above remains ROM control-flow
evidence until a paired trace is captured. The reduced result is in
tools/data/community-send-packet.csv. [confirmed] under TilEm for the
one-sided path.
Checksum and acknowledgement tail [confirmed]
After the data payload, the sender appends the 16-bit sum and waits for the ACK:
4167: LD HL,(8678)
LD A,L
CALL _SendAByte ; checksum lo
LD A,H
CALL _SendAByte ; checksum hi
4178: CALL 4318 (save hdr→bakHeader)
CALL 4338 (recv reply header)
417E: LD A,(8675)
… CALL 430F (compare/store)
CP 0x56
RET Z
JP _JErrorNo
On the receive side the matching check is 6356: after streaming the payload it compares the
accumulated checksum 8678 against the received 16-bit checksum; on mismatch it sends a 0x5A ERR
packet:
6385: LD H,0x5A
CALL 419B
It then raises _JErrorNo. The ACK-builder 42FB saves the
caller’s header to 868B bakHeader, then builds an ACK with a fresh local machine-ID (CALL 620A),
command = 0x56, length = 0, sends it, and _Mov9B restores the saved header.
DATA payload receive path [confirmed]
3C:4261 stores the destination in iMathPtr5 at 0x84DB, validates a DATA
header, and enters 3C:4292. The payload loop loads the destination once at
3C:42AB. Bit 7 of H then selects one of two storage paths: [confirmed]
- A RAM destination (
HL >= 0x8000) is written directly at3C:42D4, and the loop incrementsHLafter each byte. - A Flash-window destination (
HL < 0x8000) is buffered at0x983A.3C:42CFflushes each full 16-byte block through3C:6AB1, and3C:42ECflushes a nonzero remainder.
4292: BC=(8676) len
(8678)=0
if BC==0 → checksum tail
pagedGetPtr=983A
pagedCount=0
HL=(84DB) dest
loop: 1FD6 (break check)
_RecAByteIO → A
if BIT 7,H: (HL)=A
INC HL
else: store A via pagedGetPtr
INC pagedCount
when pagedCount==0x10 → CALL 6AB1
(8678) += received_byte
DEC BC
loop while BC
if pagedCount!=0 → CALL 6AB1
42EF: _RecAByteIO ×2 → received checksum
CALL 6356 (verify len/sum, NAK 0x5A on mismatch)
42FB: send ACK (cmd 0x56)
Flash-window staging flush — 3C:6AB1 [confirmed]
flush_paged_flash_block at 3C:6AB1 clears pagedCount, resets
pagedGetPtr to 0x983A, and loads the write state below. It preserves caller
BC, DE, and HL. [confirmed]
_WriteFlash input | Source at 3C:6AB1 |
|---|---|
A destination page | arcInfo.page at 0x83EE |
DE destination address | iMathPtr5 at 0x84DB |
BC length | B=0, C=pagedCount from 0x9834 |
HL RAM source | pagedBuf at 0x983A |
The protected sequence at 3C:6AD9–3C:6AE5 opens the port-0x14 command
gate. The routine classifies the page through 3C:6B79, calls _WriteFlash
(80C9h) at 3C:6AF5, and relocks through 3C:66D5. The bytes are
EF C9 80; this is the guarded _WriteFlash entry, not _WriteFlashUnsafe
(8087h). [confirmed]
3C:6B79 preserves the incoming A around the model probes at 00:1837 and
00:182F, then applies one range: [confirmed]
| Model branch | Page mask | Upper bound, exclusive | Pages accepted by 3C:6AB1 |
|---|---|---|---|
| TI-84 Plus | 0x3F | 0x2A | 0x08–0x29 |
| legacy | 0x1F | 0x16 | 0x08–0x15 |
| expanded | 0x7F | 0x6A | 0x08–0x69 |
The TI-84 Plus branch requires port 0x02 bit 7 set and port 0x21 bits
0–1 clear. A page below 0x08 or at or above the selected upper bound skips
the bcall. [confirmed]
After the bcall or page rejection, 3C:6B06 saves the resulting DE in
iMathPtr5 (0x84DB). The comparison at 3C:6B0A increments arcInfo.page
at 0x83EE
when the starting DE is greater than or equal to the final DE. Normal
receive callers pass 1–16 bytes on an eligible page. A dispatcher call with
zero count or an invalid page leaves DE unchanged, so the equality case
still increments the stored page. [confirmed]
The direct callers are the full-block and remainder sites at 3C:42CF and
3C:42EC. Dispatcher mode 3 at 3C:6F57 also jumps here. The page-0 bjump
stub at 00:2D45 targets that dispatcher; its only adjacent mode-3 caller is
36:415C. [confirmed]
That caller belongs to the USB receive-to-memory loop at 36:40E7. The Flash
branch at 36:413A caps a chunk at 16 bytes, points HL at 0x983A, and calls
the page-0 bjump stub at 00:2E17. The stub targets the endpoint helper at
35:4FA1, whose byte loop reads port 0xA1 at 35:500E. The page-36 loop
then stores the count at 0x9834 and invokes dispatcher mode 3. Its RAM
branch at 36:416C uses the same endpoint helper with chunks of at most 64
bytes and does not call the Flash flush. [confirmed]
tools/ti84re/link/analyze_flash_staging.py checks the ROM signatures and complete
caller sets. Its importable model also reports page classification, RAM-direct
versus Flash-buffered routing, block counts, destination crossing, and the
equality quirk.
The header-classifier 6994 shows the receive-and-store sequence a var-receive runs:
6994: 4255 (reset chk)
6298 (machine-ID re-validate)
RST4 on (867F) (classify var header)
6D4B/4338 recv header
expect (8675)==0x09 (VAR/CTS) else _JErrorNo
4338 recv DATA header
expect (8675)==0x15 (DATA) else _JErrorNo
BC=(8676) len
RST5 → store the variable into the VAT (creates RAM/Flash entry)
i.e. the receiver reproduces the VAT-create / _InsertMem path from sub-vat-archive.md.
Silent-link variable send [confirmed]
This is the path a “Send” hits (TI-Connect pulls a var, or a calc-to-calc send). OP1 = the variable name. It negotiates, sends the VAR header, waits for CTS, then streams the DATA.
link_xfer_op (3C:4DD2):
CALL probe_hw_model_keep_a ; model/HW probe, spin on port 0x20 if assist busy
SET 1,(IY+0x24) ; mark "transfer active"
RES 3,(IY+0x1B)
save IY+0xC (APD)
install cleanup handler 4F3E via 27DA
CALL _OP1ToOP6 ; preserve the var name
(build the var header into 867F) :
LD DE,0x867F
CALL _MovFrOP1 ; header = var type byte + name token(s)
decide request command:
LD A,(8672) sndRecState
CP 0x15
A = 0xA2 (DATA-type) else 0xB7
CALL 6971 (set "cmd in progress")
USB negotiation (when IY+0x1B bit0 & bit5/6 set): poll port 0x4D bits 5/6, cross_page 2E0B
CALL 4055 (send the VAR/request header via 40DA→41C3)
CALL 6184 → _Rec1stByteNC (wait for peer reply)
CP 0x36 (SKIP/EXIT) → 427E
_JErrorNo ; peer refused
CP 0x06 (VAR/CTS ok) → continue, else 4D45 _JErrorNo
CALL 4255
CALL 687A (check transfer state 8688==0x07)
if sndRecState==0x15 (DATA):
CALL 4763 (resolve var data: type/size/ptr, archive-aware)
CALL ... send DATA
else: send the symbol-table/listing payload (4261)
RES 1,(IY+0x24)
FUN_ram_2800 (restore)
JP 4F3E (cleanup)
Resolving the variable for sending [confirmed]
lnk_resolve_var (3C:4763) reads the var-header type byte at 0x867F and
branches by class. For graph/equation types (0x0F–0x14) it uses a
cross-page helper. Otherwise 3C:47AB calls _CkOP1Real, checks the size,
then calls _ChkFindSym (ram:0E60)
to locate the VAT entry. An archived variable routes through the Flash path,
where _Chk_Batt_Low saves arcInfo.size at 0x83F7. _SetupPagedPtr
supplies the data pointer, page, and length inside the DATA sender.
Sending the DATA payload [confirmed]
40DA: CALL _SetupPagedPtr (17AC) ; initialize the paged source from HL, DE, and B
(84DB)=ptr ; iMathPtr5
(8676)=len ; packet length
6971
620A (machine-ID)
(8674)=ID
if sndRecState == 0x08 and varClass == 0x0A and len > 0x037D:
(8676)=0x037D
send header
checksum=0
send 0x63,0x00
DE=0x037B
HL=data ptr+2
413D: CALL 41C3 (send DATA header, cmd already 0x15 from 4055)
HL=(84DB) ptr
DE=(8676) len
(8678)=0
loop 4150: 1FD6 (clock)
_PagedGet (17BB) the next byte (handles Flash page-cross)
41AB → _SendAByte
accumulate (8678)
DEC DE
loop
4167: send 2-byte checksum (8678 lo,hi)
recv reply header
CP 0x56 (ACK)
else _JErrorNo
The comparison at 3C:410A computes 0x037D - len. An equal length takes the
ordinary path; only a larger source enters the backup branch. The resulting
wire payload is byte-pinned as follows. [confirmed]
| Source length | DATA header length | DATA payload |
|---|---|---|
len <= 0x037D | len | source[0:len] |
len > 0x037D with sndRecState = 0x08, varClass = 0x0A | 0x037D | 63 00 followed by source[2:0x037D] |
The exceptional source is the first section of a three-part calculator backup.
At 3C:4B52, the backup reply passes HL=0x89F0 and DE=0x13A5 to the DATA
sender. This source spans flags through 0x9D94. The setup at 3C:4CCD
caps the advertised VAR length to 0x037D; 3C:410F applies the same cap to
the DATA packet. The transmitted section therefore covers 0x89F0–0x8D6C.
[confirmed]
The bytes 63 00 are the normalized image of RAM 0x89F0–0x89F1, not an
embedded section length. The restore path at 3C:46FC loads the first section
length from 0x848E, sets DE=0x89F0, and calls the DATA receiver at
3C:4261. The receiver writes the packet bytes to that destination. The first
restored system-flags byte is thus 0x63 (bits 0, 1, 5, and 6 set), and the
second is zero. The sender fixes these bytes instead of copying their live
values. [confirmed]
The fixed word selects a mixture of persistent mode, input, display, and
unnamed bits. The symbol column below comes from the bundled public
ti83plus.inc; the instruction counts come from an independent raw scan of
the retail ROM. Each count covers an exact memory-only BIT, RES, or SET
instruction using IY = 0x89F0. [standard] for the public names; [confirmed]
for the fixed values and byte-pattern counts.
| RAM bit | Sent | Public symbol | Direct ROM bit operations |
|---|---|---|---|
0x89F0.0 | 1 | inDelete | 10 BIT, 4 RES, 1 SET |
0x89F0.1 | 1 | — | 5 BIT, 4 RES, 2 SET |
0x89F0.2 | 0 | trigDeg | 13 BIT, 3 RES, 2 SET |
0x89F0.3 | 0 | kbdSCR | 2 BIT, 2 RES, 2 SET |
0x89F0.4 | 0 | kbdKeyPress | 1 BIT, 1 RES, 2 SET |
0x89F0.5 | 1 | donePrgm | 1 BIT, 0 RES, 4 SET |
0x89F0.6 | 1 | — | none |
0x89F0.7 | 0 | — | 4 BIT, 1 RES, 2 SET |
0x89F1.0 | 0 | — | none |
0x89F1.1 | 0 | — | none |
0x89F1.2 | 0 | editOpen | 39 BIT, 2 RES, 2 SET |
0x89F1.3 | 0 | AnsScroll | 6 BIT, 5 RES, 3 SET |
0x89F1.4 | 0 | monAbandon | 13 BIT, 12 RES, 8 SET |
0x89F1.5 | 0 | — | 1 BIT, 1 RES, 1 SET |
0x89F1.6–7 | 0 | — | none |
This rules out a live-state snapshot. The fixed word clears the degree-mode
bit, both pending-keyboard bits, the editor-open bit, answer scrolling, the
monitor-abandon bit, and the unnamed active bit at 0x89F1.5. It sets the
public donePrgm bit. Those choices are consistent with a canonical
post-restore state. Bits 0x89F0.0, .1, and .6 keep the stronger conclusion
open: .0 and .1 have active consumers, while .6 has no direct indexed
bit operation anywhere in this ROM. No TI source or older-ROM comparison has
yet been found that establishes whether those three values instead encode
model or OS-version compatibility. [hypothesis]
The audit is reproducible without subsystem-specific parsing:
python3 -m ti84re.link.describe_backup legacy-flags
External format evidence. [standard] tilibs commit
791d2535813fa7ffef8f9feadf110998d4ae57fb provides an independent format
check. calc_73.cc::send_backup passes data_part1 unchanged to SEND_XDP.
files8x.cc::ti8x_file_write_backup writes data_length1 before
data_part1, outside the section bytes. The file and wire implementations
therefore agree that 0x0063 belongs to the RAM image. The reason the ROM
chooses this particular system-flags mask remains [hypothesis].
Calls to 3C:41AB add 0x63, 0x00, and the remaining 0x037B bytes to the
same 16-bit checksum at 0x8678. The checksum covers all 893 transmitted bytes
modulo 0x10000. [confirmed]
_PagedGet makes the streamer transparent to RAM-vs-archived data: an archived program is read
straight out of the Flash window, advancing the bank-A page (port 0x06) at the 0x8000 boundary,
exactly like _FlashToRam.
_SendVarCmd [confirmed]
The bcall most code/TI-BASIC reaches for to silent-send. It is a thin DI-wrapped front for the same machinery:
4EDD: DI
save IY+0xC (APD)
RES 2,(IY+0xC)
install cleanup 4F3E via 27DA
LD A,0x0B
LD (8672),A ; sndRecState = request/directory
LD A,0xC9
CALL 6971 ; command setup
CALL 62B0 ; clear link sub-state in 8A0B
SET 2,(IY+0x1B)
CALL 58ED ; sets IY+0x24 bit 1 and calls _ChkFindSym
JR 4EAD ; shared tail with link_xfer_op
4EAD: RES 1,(IY+0x24)
2800 (restore)
JP 4F3E
Note 4EDD physically overlaps / shares the tail (4EAD) with link_xfer_op; they are two
entry points into one routine body. _SendVarCmd is the “send by name from the running context”
door; link_xfer_op is the “OP1 already set up, do the silent transfer” door.
APD, cleanup, and idle-line wait [confirmed]
-
27DA(FUN_ram_27da) installs an error callback.link_xfer_opand_SendVarCmdinstall3C:4F3E, which restores link state, the APD timer, andIY+0xCbit 2 after_JError:4F3E: POP AF BIT 2,A restore IY+0xC bit 2 continue at 4F31 4F31: RES 2,(IY+0x12) re-enable timers EI -
Six other transfer paths install page-0 stub
2D51, which bjumps to3C:6136. That callback dispatches onsndRecState; for the applicable non-DATA states it calls the raw/USB-aware abort cleanup at3C:618D, then recordsioErrState=1through stub2F31→07:7AC3. The raw branch drives both port-0x00lines low for an exact software delay before releasing them. See Two-wire link port hardware. -
_ApdSetup(00:03AE) is called before any long blocking receive (6177,6184) so the calc doesn’t auto-power-down mid-transfer. -
62B0/62BBclear the link error sub-state byte (8A0B, the low bits ofIY+0x1B-area flags).
Flash-object dispatch and error handling
| Trigger | Address | Error |
|---|---|---|
| send/receive line timeout, bad echo, unexpected reply cmd | _JErrorNo 00:2799 | E_LnkErr 0x9F “ERR:LINK” |
lnk_rec_status returned A=1 with C != 0xE0; header-send line never went idle | _ErrLinkXmit 00:278D → _JError(0x9F) | E_LnkErr 0x9F |
| received checksum/length mismatch | 6356→ sends 0x5A NAK → 2799 | E_LnkErr 0x9F |
| peer sent SKIP/EXIT (0x36) | link_xfer_op 4E80/4E83 | E_LnkErr 0x9F |
incoming variable-header type at 0x867F equals 0x22 | 3C:463D → _JError 00:2793 | raw error 0x22, displayed as ERR:LINK |
The ordinary timeout, checksum, and unexpected-command paths collapse to
E_LnkErr (0x9F). The error display masks bit 7, so this becomes table code
0x1F; pointer entry 07:6B08 selects 07:6C55, the string LINK.
_JError(0x22) uses pointer entry 07:6B0E, which selects the same string.
The two raw codes therefore produce the same visible ERR:LINK message.
tools/ti84re/rom/error_table.py decodes this ROM table, and python3 -m ti84re.rom.describe_error 0x22 0x9F
reproduces both lookups. [confirmed]
The include file labels 0x22–0x25 as E_LinkIOChkSum,
E_LinkIOTimeOut, E_LinkIOBusy, and E_LinkIOVer, but the same block marks
all four numbers obsolete. Those names do not describe the dispatcher at
3C:45D7: it reloads the variable-header type from 0x867F, not the packet
command at 0x8675. [confirmed]
The independent tilibs type tables name 0x23 OS/AMS, 0x24 Flash
application, and 0x25 certificate; they define no Z80 Flash-object type at
0x22. The ROM control flow agrees with those three names. [standard] for the
host-library names; [confirmed] for the ROM branches.
| Header type | ROM behavior |
|---|---|
0x22 | 3C:463D jumps to _JError with A=0x22, producing ERR:LINK. |
0x23 — OS/AMS | 3C:45EA enters negotiation at 3C:45EE; its 3C:5735 branch checks the battery, initializes MD5 through _MD5Init = 808Dh, and calls _ReceiveOS = 8072h. |
0x24 — Flash application | 3C:45DA requires sender machine ID 0x73, then jumps to the application-specific path at 3C:512C, whose first operation is _Chk_Batt_Low. A separate receive path at 3C:550D also selects type 0x24 and requires PC sender ID 0x23. |
0x25 — certificate | 3C:462D requires sender machine ID 0x73, then jumps through 3C:5114 to the certificate path at 3C:566B; that path calls _FindFirstCertField = 8027h and uses 0x00E8-byte blocks at 3C:5659. |
A linear scan of all 64 physical pages finds 32 direct references to
_JError at 00:2793 and no rst 28h call with bcall ID 44D7h.
Reviewing those direct sites finds the 0x22 path above, but no site that
loads 0x23, 0x24, or 0x25 as a fixed _JError argument. The ROM bytes
do not support the claim that a separate assembly-callable transfer API emits
all four obsolete values. The 0x22 collision is the only one of these four
header branches that passes its value directly to _JError. [confirmed]
The external cross-check uses tilibs commit
791d253
for the type table and its
calc_73.cc
DBus implementation for the OS, application, and certificate transfer shapes.
End-to-end program transfer [standard]
- Host (TI-Connect, machine-ID 0x95) opens the USB/DBUS link; calc detects it (
IY+0x1Bbit1). - Host requests the directory or a specific var; calc’s receiver (
4338) parses the request header,6994/6298classify it. - To send a var:
link_xfer_op/_SendVarCmdbuilds the VAR header (type byte + name from OP1, size) at867F, sends it (41C3, cmd path), waits forCTS(0x09). 40DAstreams theDATA(0x15) payload via_PagedGet→_SendAByte(Flash-transparent), appends the 16-bit checksum, waits forACK(0x56)._GetSysInfo(07:7345, id0x50DD)-style metadata and anEOT(0x92) close the session.- Receive direction is the mirror: header in → CTS out → DATA in (RAM direct,
or Flash staged in 16-byte blocks through
3C:6AB1) → checksum verify (3C:6356, NAK0x5Aon error) → ACK out → VAT store.
Routine index
| space:addr | name | what |
|---|---|---|
3C:420D | _SendAByte | send one byte: HW-assist (port 0x09/0x0D) or bit-bang (port 0) |
3C:6BB2 | lnk_send_byte_hw | HW-assist send: poll port 0x09 bit5, OUT (0x0D) |
3C:443F | _RecAByteIO | receive one byte (blocking) |
3C:444A | lnk_rec_status | decode low-level status and returned C; C=0xE0 is the TI-Keyboard prefix and re-arms or joins its exceptional delimiter path |
3C:6D5E | _KeyboardGetKey | decode the 0xE0, deliberate-error, 0x01, data sequence and return a status byte |
3C:439C | _Rec1stByte | wait for first byte of a packet (APD + start-bit) |
3C:43A3 | _Rec1stByteNC | as above, no line-clear |
3C:41C3 | lnk_send_header | send 4-byte header (ID, cmd, len-lo, len-hi) |
3C:419B | lnk_send_ctrl_pkt | send a 0-length control packet (cmd in H) |
3C:4195 | lnk_send_eot | send EOT (cmd 0x92) |
3C:4199 | lnk_send_cts | send CTS (cmd 0x09) |
3C:4338 | lnk_recv_header | receive + validate 4-byte header |
3C:620A | lnk_local_machine_id | pick local machine-ID from IY+0x1B mode |
3C:42FB | lnk_send_ack | build+send ACK (cmd 0x56, fresh local machine-ID), restoring the saved header |
3C:4292 | lnk_recv_data | receive DATA payload, 16-byte Flash batching, checksum |
3C:6356 | lnk_verify_cksum | verify count vs len; NAK 0x5A on mismatch |
3C:6AB1 | flush_paged_flash_block | program one 1–16-byte staged Flash block through _WriteFlash and port 0x14 |
3C:4DD2 | link_xfer_op | silent-link variable send orchestrator (OP1=name) |
3C:4EDD | _SendVarCmd | bcall _SendVarCmd (4A14) body; DI-wrapped send-by-name |
3C:4763 | lnk_resolve_var | resolve var class/size/ptr for sending (archive-aware) |
3C:40DA | lnk_send_data | send DATA payload (_PagedGet→_SendAByte) + checksum + ACK wait |
3C:4167 | lnk_send_cksum_tail | append 16-bit checksum, recv reply, expect ACK 0x56 |
3C:4F3E | lnk_cleanup | error/abort cleanup (restore APD/timers/flags) |
3C:6136 | lnk_error_cleanup | installed state-aware error callback; reaches raw/USB abort cleanup where applicable |
3C:618D | lnk_abort_transport | clear USB busy state or issue the raw both-low abort pulse |
3C:62B0 | lnk_clear_substate | clear link error sub-state (8A0B) |
3C:6994 | lnk_recv_store | receive var + VAT store sequence (expects 0x09 then 0x15) |
00:278D | _ErrLinkXmit | _JError(0x9F) E_LnkErr |
00:2799 | _JErrorNo | raise current pending error (link → 0x9F) |
07:7345 | _GetSysInfo (id 0x50DD) | system info reply (used in link sessions) |
00:4A14 | _SendVarCmd (bcall id) | → 3C:4EDD |
Ports: 0x00 = raw two-wire link; 0x08–0x0D = HW link-assist control/status/data
FIFO (port 0x09 bit5 TX-ready, bit6 transmission-error, bit4 byte-received, bits 0x19 error);
0x02 bit7 = non-83+-Basic (assist-present gate on 84+; WikiTI’s “link-assist available” is bit6);
0x20 = CPU speed (timeout scaling); 0x4D bits5/6 = USB negotiation; 0x14 = Flash
write/erase (received-to-archive path). See sub-usb-asic.md for the assist port
state machine. RAM block: ioFlag 8670 … bakHeader 868B, staging
pagedBuf 983A for Flash-window receive staging.
Command IDs: 0x06 VAR · 0x09 CTS · 0x15 DATA · 0x2D DEL · 0x36 SKIP/EXIT · 0x56 ACK · 0x5A ERR/NAK · 0x68 RTS · 0x92 EOT · 0xA2/0xB7 request. Machine IDs: 0x82/0x73 calc(84+/73), 0x95 computer (TI-Connect), 0x03 TI-83, plus the 0x02/0x12/0x23/0x74/0x83/0x13/0x08 set accepted.
Open items
- Determine why the legacy backup normalizer chooses system-flags word
0x0063. Its RAM destination, replacement behavior, section bounds, and checksum coverage are confirmed. A complete direct indexed-bit audit shows that the word clears degree-mode, keyboard, editor, answer-scroll, and monitor state while settingdonePrgm; the remaining gap is why active unnamed bits 0 and 1 and unreferenced bit 6 of0x89F0are set. - The prior USB target gap is now mapped in sub-usb-asic.md:
link_xfer_opcallsram:2E0B, across_page_jumpthunk to35:4280, after sampling port0x4D.
USB ASIC and link assist
The USB/link-assist interface exposes control, status, interrupt, endpoint, and FIFO registers through Z80 I/O ports. This page traces those ports and the transport selection that chooses USB or the 2.5 mm link. Link / data transfer covers the packet protocol and variable-transfer state machine.
The full USB controller is broader than the variable-transfer path, but OS 2.55MP does expose enough of it to map the public USB entry points, the link-assist byte path, and the interrupt/event path. This page is ROM-grounded: the confirmed claims below come from OS 2.55MP disassembly/decompilation and cite the address ranges that show them. External WikiTI names are used only as orientation where noted, not as proof.
ROM-grounded surface
The ROM shows four transport-facing surfaces:
| Layer | Port range | Role |
|---|---|---|
| Legacy link | 0x00 | 2.5 mm raw bit-banged byte path; see Two-wire link port hardware. [confirmed] |
| Link-assist FIFO | 0x08–0x0D | Hardware byte send/receive assist used below _SendAByte and _RecAByteIO. [confirmed] |
| USB line / interrupt gates | 0x4D, 0x55, 0x56 | Line-state and event/status gates used before and during link handling. [confirmed] |
| USB controller / endpoints | 0x4A–0x5B, 0x80–0xA2 | Page-35 USB host/device stack, including setup, endpoint FIFOs, callbacks, and data transfer. [confirmed] |
In the variable-transfer code, the OS mostly treats USB as a transport selector around the existing
TI link protocol. The packet layer still sends machine IDs, command bytes, checksums, ACK/NAK, and
EOT exactly as described in sub-link-transfer.md. The hardware difference is
below that packet layer: bytes go through the assist FIFO when the ASIC path is enabled, and through
port 0x00 bit-banging otherwise. [confirmed]
Observed port map [confirmed]
| Port | Observed use in OS 2.55MP | Evidence |
|---|---|---|
0x02 | Hardware/model gate before using assist paths. The link code tests bit 7 before touching ports 0x08–0x0D. | 3C:6C82, 3C:6CB8, 3C:6D15 |
0x08 | Link-assist control/idle latch. The OS writes 0x80 when clearing an inactive/error-free assist state, and 0x00 when marking the assist state active. | OUT (0x08) at 3C:6C4D/6C50, 3C:6D48, 3C:6D5B |
0x09 | Link-assist status on reads. Bit 5 is TX-ready; bit 6 is a transmission/error condition; bit 4 marks a received byte. Masks 0x19, 0x58, and 0x99 are used as error/activity predicates. On writes, the OS setup value 0x97 matches WikiTI’s CPU-speed-0 signaling-rate register. | 3C:6BB6–6BC5, 3C:444A, 3C:6BFA, 3C:6CCE, 3C:6D33; WikiTI port 09 |
0x0A | Assist receive/data register on reads; the confirmed receive path reads the byte here. On writes, the OS setup value 0xB4 matches WikiTI’s CPU-speed-1 signaling-rate register. TilEm models reads as “last received byte” and stores writes as opaque assist state. | 3C:6C20, 3C:6C2B, 3C:6C39; WikiTI port 0A; TilEm x4_io.c |
0x0B, 0x0C | Assist signaling-rate configuration for CPU speed modes 2 and 3, initialized with 0xB4. The ROM byte-transfer path writes them during setup but does not read them back. TilEm stores the writes without emulating timing from the values. | 3C:6C3D, 3C:6C3F; WikiTI ports 0B/0C; TilEm x4_io.c |
0x0D | Assist TX FIFO/data register. _SendAByte writes the outgoing byte here after port 0x09 bit 5 becomes set. | 3C:6BBC–6BBF |
0x20 | CPU speed bit used to select assist/link wait-loop reloads. The send timeout uses 0xFFFF when bit 0 is set and 0x6800 when clear. | 3C:6BCC, 3C:6C8B, 3C:6CC1 |
0x4B | Controller-side setup control. Reset paths write 0x00, then conditionally write 0x20; another setup path writes 0x20 before waiting for port 0x4C = 0x5A. WikiTI calls this USB power control, but describes its bit meanings as mostly speculative. | 35:4C69, 35:4C76–4C80, 35:59AB; duplicated at 2F:59B6, 2F:59C3–59CD; WikiTI port 4B |
0x4C | USB controller handshake/status byte. The page-35 stack compares it with 0x5A/0x1A and 0x12/0x52, and clears or primes it with 0x00/0x08 during setup. TilEm returns 0x22 to make the calc see no attached USB peer. | 35:42B7, 35:42F6, 35:403C, 35:40E6; TilEm x4_io.c |
0x4D | USB line-state gate. link_xfer_op samples bits 5 and 6 before the page-0 bjump at ram:2E0B, which targets 35:4280. Page-35 handlers also branch on bits 0, 1, 4, 5, 6, and 7. TilEm returns 0xA5 to emulate “USB disconnected.” | 3C:4E4A–4E6F, 35:42BF, 35:4B6A–4B9F; TilEm x4_io.c |
0x4F, 0x50 | Unnamed setup controls used beside USB GPIO and line-state accesses. The ROM writes 0x27 to 0x50. It then updates 0x4F to `(old & 0xBF) | 0x88, waits, and updates it to old & 0x37`. The electrical and PHY effects are unknown. |
0x55 | USB interrupt status, active-low in the low five bits. The IM1 dispatcher tests (in(0x55) ^ 0xFF) & 0x1F first. | 00:006F–0075 |
0x56 | USB line-event bitmap used by the IM1 dispatcher after port 0x55 reports USB activity. Bits 4, 5, 6, 7, and 1 dispatch to page-35 handlers through page-0 bjumps. | 00:0085–00AE, 00:0113–0127 |
0x57, 0x5B, 0x4A, 0x54 | USB controller control/ack registers used by page-35 setup and event handlers. The ROM confirms values such as 0x10, 0x20, 0x22, 0x50, 0x80, 0x90, 0x93 on 0x57, 0x00/0x01 on 0x5B, 0x20 on 0x4A, and 0x02/0x44/0xC4 on 0x54. | 35:4038–4060, 35:42C5–42EA, 35:4B6A–4C14 |
0x5A | Presentation-link setup writes bit 0 and reads the port back. The next instruction replaces the read value, so this routine does not test the result. It then configures indexed endpoint 2. | 35:58B2–58DE |
0x80–0xA2 | Endpoint/status/FIFO region used by the public USB API. Examples: _SendUSBData writes 64-byte chunks to 0xA2; _RequestUSBData reads 8-byte records from 0xA1; setup/config paths write descriptor bytes through 0xA0 and use selector/status ports 0x8E, 0x8F, 0x91, 0x94, and 0x98. | 35:4DD3, 35:470B, 35:48BA, 35:48F8 |
The endpoint receive helper at 35:4FA1 accepts a count in B, caps it at 64
bytes, and reads port 0xA1 in the loop at 35:500E. The USB
receive-to-memory body at 36:40E7 reaches it through the page-0 bjump stub at
00:2E17. For a Flash-window destination, 36:413A caps the chunk at 16
bytes, receives it into 0x983A, and calls the page-3C Flash-staging
dispatcher in mode 3 at 36:415C. RAM destinations use chunks of up to 64
bytes and skip that Flash flush. [confirmed]
The project-local tools/symbols/ports.txt names the observed assist, USB-control, and
USB-interrupt ports so future Ghidra rebuilds show the same surface in the
database. Neutral labels retain Unknown for ports 0x4F and 0x50 because
the ROM does not identify their signals. The file also applies the FDRC-family
names below to ports 0x80–0xA2. Those names identify the register layout;
they do not prove the exact ASIC implementation or its electrical behavior.
Transceiver and enable-timer ports without confirmed ROM control flow
WikiTI assigns low-level USB meanings to three ports that OS 2.55MP does not use through a confirmed I/O instruction:
| Port | WikiTI description | Evidence limit |
|---|---|---|
0x49 | Raw USB-transceiver status, including proposed D+ and D− bits 1 and 2 | The page calls several other bits only “something” or “possible.” No ROM read, emulator handler, datasheet, or physical sample confirms the bit map. [hypothesis] |
0x51 | Delay between starting a separate 48 MHz crystal and enabling USB, counted in two-tick units from 32.768 kHz | No ROM write or cited primary source establishes the clock, unit, or enable effect. [hypothesis] |
0x52 | Charge-pump enable timer with timing like port 0x51 | The public description says only that it has “something to do” with charge-pump timing. [hypothesis] |
The complete immediate-port and conservative literal-C scan initially
reports six candidates for these ports. Raw descriptor decoding accounts for
four of them. tools/ti84re/rom/analyze_io.py attaches this classification to each
report and --exclude-descriptors removes the four structural overlaps:
[confirmed]
| Linear candidate | Actual bytes and role |
|---|---|
00:3EDC — apparent OUT (0x51),A | Inline target 37:51D3, page byte 0x77, after CALL 2B09h; this is a cross-page call descriptor. |
37:46D7 — apparent IN A,(0x52) | Low and high bytes DB 52 of bcall ID 52DB, _ResetGraphSettings. |
39:583F — apparent OUT (0x51),A | Low and high bytes D3 51 of unnamed bcall ID 51D3. |
3B:620A — apparent IN A,(0x49) | Low and high bytes DB 49 of bcall ID 49DB, which resolves to 36:7DA9. |
The remaining apparent port-0x49 instruction at 01:4304 and
port-0x51 instruction at 3B:4F45 lie in table-shaped byte regions. Neither
has a direct page-local control-flow reference, and neither has trace evidence.
The complete ROM I/O candidate audit
also finds no containing Ghidra function or xref for either location. They are
reviewed data decodes rather than I/O evidence. The scan confirms no OS 2.55MP
transaction for ports 0x49, 0x51, or 0x52.
The separate raw ED-opcode census covers register and block I/O without
assuming that C can be propagated through control flow. Its only two aligned
instructions access RTC ports 0x48 and 0x44, so it adds no hidden USB-port
transaction. [confirmed]
TilEm, Wabbitemu, MAME, and jsTIfied omit all three ports from their TI-84 Plus USB handlers. Their omission cannot establish physical absence or reset values. The read-only USB control snapshot records these ports together with the adjacent low-USB controls on a physical calculator. No exported result has been recorded. [standard] for emulator coverage; [confirmed] for the assembled probe; [hypothesis] for pending physical values.
Presentation-link mirroring setup
The helper at 35:58B2 sets IY+0x41 bit 0, writes 0x01 to port
0x5A, and reads the port back. LD A,0x02 immediately replaces the read
value, so the helper does not branch on or retain it. The remaining writes
select indexed endpoint 2 and initialize its transmit registers: [confirmed]
; 35:58B2
SET 0,(IY+41h)
LD A,01h
OUT (5Ah),A
IN A,(5Ah) ; discarded by the following LD
LD A,02h
OUT (8Eh),A
LD A,22h
OUT (98h),A
LD A,48h
OUT (91h),A
LD A,02h
OUT (90h),A
XOR A
OUT (87h),A
OUT (89h),A
OUT (8Bh),A
OUT (5Bh),A
LD A,10h
OUT (92h),A
IN A,(92h)
NOP
NOP
NOP
RET
The port-0x8E index and port-0x98 endpoint-type interpretation follow the
FDRC-family match below. The exact writes and selected index are [confirmed];
the imported register names remain [hypothesis].
A static call scan finds one direct caller at 35:4481. It calls this helper,
then 35:3EF1, then 35:58DF. The last routine performs 64 iterations over
LCD ports 0x10 and 0x11: it writes commands, reads 12 data bytes, and
writes those bytes back. This control flow ties the port-0x5A setup to LCD
traffic and presentation mode, but the ROM alone does not expose what appears
on the USB wire. [confirmed]
WikiTI calls port 0x5A the presentation-link mirroring enable. It reports
that bit 0 mirrors writes to LCD ports 0x10 and 0x11 as two-byte packets on
outgoing bulk endpoint 2, and that the feature works only in host mode. The
page also labels part of its packet interpretation untested. No cited vendor
datasheet or physical capture establishes the packet format or host-mode
restriction, so those details remain [hypothesis].
TilEm, Wabbitemu, MAME, and jsTIfied do not implement port 0x5A or a connected
endpoint-2 transfer for this calculator. Emulator execution therefore cannot
validate the mirroring behavior. The port accesses above were regenerated from
the retail OS 2.55MP bytes with tools/ti84re/rom/analyze_io.py; the surrounding
instructions come from the same page through tools/ti84re/rom/z80_disassembly.py.
Physical validation requires a presentation-link adapter or a controlled USB
capture. [standard] for the emulator implementations; [confirmed] for the ROM
sequence.
Mentor FDRC register-family match [hypothesis]
The ROM-visible accesses in the controller region at 0x80–0x9B align with the Mentor Graphics
MUSBFDRC register file. A Mentor-authored 2004 mu_fdrdf.h header assigns offsets 0x00–0x1F
in the same order and places the non-AHB FIFO window at offset 0x20. The preserved header labels
itself proprietary; it is primary-origin source code in a third-party SDK tree, not a publicly
released TI ASIC specification. The independent VSF FDRC implementation reproduces the compact
ordering. Adding the candidate TI base port 0x80 produces the map below. [standard] for the two
external layouts; [hypothesis] for applying that identity to the TI ASIC.
The ROM does not contain a silicon identifier. Board-level identification remains open, so the family identification remains [hypothesis].
| TI ports | FDRC names | ROM cross-check |
|---|---|---|
0x80 | FADDR | The control-transfer path defers a device-address write until the status stage at 35:4630. [confirmed] |
0x81 | POWER | Initialization polls bit 6 and later writes or modifies bits 0–3. The FDRC masks call these VBUSVAL, ENSUSPEND, SUSPENDM, RESUME, and RESET. [confirmed] for the operations; [hypothesis] for the imported bit names |
0x82–0x85 | INTRTX1/2, INTRRX1/2 | The protocol handler reads transmit and receive endpoint-event bytes at 35:4D03 and 35:4D57. [confirmed] |
0x86 | INTRUSB | Host setup waits for bit 4; the peripheral handler branches on bit 2 at 35:40A2 and 35:4CFE. Those masks match FDRC CONNECT and RESET. [confirmed] for the branches; [hypothesis] for the event names |
0x87–0x8A | INTRTX1E/2E, INTRRX1E/2E | Setup enables transmit events with 0xFF at 35:407B and receive endpoint events with 0x0E at 35:4084. [confirmed] |
0x8B | INTRUSBE | The ROM uses masks including 0x05, 0x21, 0xA1, and 0xF7. FDRC defines the bits as suspend, resume, reset/babble, SOF, connect, disconnect, session request, and VBUS error. [confirmed] for the masks; [hypothesis] for the imported names |
0x8C–0x8D | FRAME1/2 | Initialization waits for the low frame byte to become nonzero at 35:411B and 35:418D. [confirmed] |
0x8E | INDEX | Endpoint setup and transfer routines select a pipe before using the shared endpoint registers. [confirmed] |
0x8F | DEVCTL | The ROM tests bit 7 for B-device state and bit 2 for host mode, then writes bit 0 to start a session. These are the FDRC BDEVICE, HM, and SESSION masks. [confirmed] for the operations; [hypothesis] for the imported names |
0x90–0x92 | TXMAXP, CSR0/TXCSR1, CSR02/TXCSR2 | Endpoint 0 uses CSR0; nonzero indexed endpoints use the transmit CSR pair. The ROM writes bit 1 to launch endpoint-0 packets and bit 0 to launch nonzero-endpoint packets. [confirmed] |
0x93–0x97 | RXMAXP, RXCSR1/2, COUNT0/RXCOUNT1/2 | Receive paths select an endpoint, test RXCSR1 bit 0, read the count, drain the matching FIFO, and clear the ready condition. [confirmed] |
0x98–0x9B | TXTYPE, TXINTERVAL/NAKLIMIT0, RXTYPE, RXINTERVAL | Host setup writes endpoint type/address and interval values before starting transfers. [confirmed] |
0x9C–0x9F | TXFIFO1/2, RXFIFO1/2; FIFOSIZE/CONFIGDATA aliases at 0x9F | These offsets complete the Mentor FDRC register file. A static page-2F/35 scan found no resolved immediate or literal-C access, so the TI use of these registers remains [hypothesis]. |
0xA0–0xAF | endpoint FIFOs 0–15 | Mentor’s non-AHB macro maps endpoint $n$ to offset 0x20 + n. The ROM confirms FIFO 0 at 0xA0, FIFO 1 at 0xA1, and FIFO 2 at 0xA2; higher endpoints remain [hypothesis]. |
The FDRC ordering matters because the common HDRC/MUSB byte layout in Linux’s Mentor/TI-copyrighted driver header places several interrupt registers at different offsets. These offsets distinguish the candidates:
| TI port | Relative offset | FDRC candidate | Common HDRC candidate | ROM cross-check |
|---|---|---|---|---|
0x86 | 0x06 | INTRUSB | low byte of INTRTXE | The ROM waits on bit 4 and branches on bit 2. FDRC names these global connect and reset/babble events. [confirmed] for the operations; [hypothesis] for the names |
0x87 | 0x07 | INTRTX1E | high byte of INTRTXE | Setup writes 0xFF. Both candidates make this byte an enable register, although they assign it to different endpoint ranges. This access alone does not distinguish the layouts. [confirmed] |
0x89 | 0x09 | INTRRX1E | high byte of INTRRXE | Setup writes 0x0E, matching receive endpoints 1–3 in the FDRC low-byte register. [confirmed] for the value; [hypothesis] for the imported endpoint names |
0x8B | 0x0B | INTRUSBE | INTRUSBE | Both layouts agree at this offset; the write masks do not distinguish them. [confirmed] |
0x8F | 0x0F | DEVCTL | TESTMODE | The ROM tests bits 7 and 2 and sets bit 0. FDRC names them B-device, host mode, and session. [confirmed] for the operations; [hypothesis] for the names |
The combination at 0x86, 0x89, and 0x8F favors the compact FDRC ordering over the common
HDRC map. It does not identify the surrounding TI ASIC or its PHY. Linky commit 89586b0
independently calls this block MUSBFDRC and performs the same initialization sequence. Linky is
calculator software evidence, not a vendor specification. [hypothesis]
Sending one byte through the assist FIFO [confirmed]
The hardware send entry is lnk_send_byte_hw at 3C:6BB2 (the preceding byte at 3C:6BB1 is a
RET from the prior helper). It is the assist branch behind _SendAByte (3C:420D).
Mechanically, it does four things:
- Seed the inner retry counter at RAM
0x9C86with0xFA. - Read port
0x09. - If bit 5 is set, copy the outgoing byte from
Cto port0x0Dand return. - If bit 5 is clear, call the timeout decrementer (
3C:6BE4/lnk_timeout_dec) and retry until the outer counter at0x9CACexpires, then fall into the link error path at3C:4434.
The ROM disassembles to:
; 3C:6BB2, assist send path
6BB2: CALL 6D4Fh ; clear/prepare assist I/O latch
6BB5: CALL 6BD2h ; seed 9CAC from CPU speed
6BB8: CALL 6BD2h
6BBB: LD A,0FAh
6BBD: LD (9C86h),A ; inner retry reload
6BC0: IN A,(09h)
6BC2: BIT 5,A
6BC4: JR Z,6BCAh ; TX not ready
6BC6: LD A,C
6BC7: OUT (0Dh),A ; write byte to assist FIFO
6BC9: RET
6BCA: CALL 6BE4h ; decrement 9CAC, Z means keep polling
6BCD: JR Z,6BBBh
6BCF: JP 4434h ; link timeout/error path
lnk_set_timeout (3C:6BD2) seeds 0x9CAC from CPU speed. When port 0x20 bit 0 is clear it uses
0x6800; when the bit is set it leaves the larger 0xFFFF seed. The ROM confirms the two reload
values, while the wall-clock timeout they target is not measured here. [confirmed]
Receiving and status handling [confirmed]
The receive path is split between _RecAByteIO (3C:443F), lnk_rec_status (3C:444A), and the
assist helpers around 3C:6BF4–6D40.
The hardware-facing receive loop waits until port 0x09 & 0x58 becomes nonzero. In the confirmed
path:
0x40(bit 6) is treated as a transmission/error condition.0x10(bit 4) is the “byte received” condition.0x08is an assist read-busy/activity bit: it wakes the wait loop, but the byte is not accepted until bit 4 or an error/status bit is also present. TilEm names the corresponding stateTILEM_LINK_ASSIST_READ_BUSY.- When the receive condition is accepted, the byte is read from port
0x0AintoC. - The status masks
0x19and0x99select error/activity cases before the code resets or re-arms the assist latch through port0x08.
The assist receiver returns a normal byte in C with A=0; the port-0x09
bit-6 path returns A=1. lnk_rec_status compares the returned C with
0xE0, and raises E_LnkErr when A=1 and C != 0xE0. _RecAByteIO
preserves no caller-supplied A. The exceptional 0xE0 is the TI-Keyboard
prefix, not an assist-register sentinel: _KeyboardGetKey = 50E9, body
3C:6D5E, expects it before a deliberate DBUS error delimiter, command 0x01,
and a final data byte. The ROM-confirmed decoder and the independently sourced
peripheral description are separated in
Two-wire link port hardware.
[confirmed] for the receive path and decoder; [standard] for the reported
physical-keyboard transmitter sequence.
The assist reset/enable sequence at 3C:6C3B writes:
OUT (0x00),0x00
OUT (0x09),0x97
OUT (0x0A),0xB4
OUT (0x0B),0xB4
OUT (0x0C),0xB4
OUT (0x08),0x80
OUT (0x08),0x00
IN A,(0x09)
SET 0,(IY+0x3E)
The sequence proves the ports touched and the RAM flag used by the OS. WikiTI names these writes as
link-assist signaling-rate setup values for CPU speed modes 0-3: ports 0x09, 0x0A, 0x0B, and
0x0C correspond to speed modes 0, 1, 2, and 3 respectively. Its field description says bits 5-7
select the link-assist clock divisor as 2^n, with 111b halting the assist, and bits 0-4 select
the inter-bit wait. Under that decoding, the ROM constants are:
| Port | CPU speed mode | Value | Divisor field | Wait field |
|---|---|---|---|---|
0x09 | 0, 6 MHz | 0x97 (10010111b) | 100b → divide by 16 | 0x17 |
0x0A | 1 | 0xB4 (10110100b) | 101b → divide by 32 | 0x14 |
0x0B | 2, 15 MHz duplicate 1 | 0xB4 (10110100b) | 101b → divide by 32 | 0x14 |
0x0C | 3, 15 MHz duplicate 2 | 0xB4 (10110100b) | 101b → divide by 32 | 0x14 |
Direct ROM scans found the page-3C byte-transfer path writing those constants during setup, then
using the read side of 0x09 for status and 0x0A for received bytes. TilEm agrees on the runtime
status/data behavior and stores ports 0x09–0x0C, but its x4/xn/xs/xz models label the
write-side settings as unknown or timeout-like and do not derive link timing from 0x97/0xB4. [confirmed]
USB selection in link_xfer_op [confirmed]
link_xfer_op (3C:4DD2, bcall ID 0x50FB) is the OS entry that sends a silent link request and
prefers the USB path when its mode flags ask for it. ti83plus.inc names bcall 0x50FB
_GetVarCmdUSB, the USB variant of _GetVarCmd (0x4A11) / _SendVarCmd (0x4A14); that public
name matches the USB-first variable-command behavior decoded here, while link_xfer_op is the
inferred name for the page-3C body. The ROM-confirmed setup is:
OP1holds the variable type/name.sndRecState(0x8672) is0x15for DATA-style receive.IY+0x1Bbit 0 selects USB-first behavior; reset means use the link port path.
The OS confirms that contract in the 4E35–4E73 gate:
- If
IY+0x1Bbit 0 is clear, it skips USB probing and sends through the ordinary link path. - If bit 0 is set and either
IY+0x1Bbit 5 or bit 6 asks for USB handling, it reads port0x4D. - If port
0x4Dbit 5 is clear, or bit 5 is set and bit 6 is clear, the OS setsIY+0x1Bbit 5 and calls the page-0 bjump atram:2E0B. ram:2E0Bdispatches via inline descriptor80 42 75, which is target35:4280after the normal page mask. That routine calls the public_InitUSBDevicebody at35:42B0, then accepts only TI vendor0x0451with product IDs0xE003,0xE008, or0xE00F; success returns carry clear, while mismatch or init failure returns carry set.- On carry set,
link_xfer_opclearsIY+0x1Bbit 5 and continues intolnk_send_data_867d(3C:4055), which sends the same TI link request/VAR/DATA packets described in the link-transfer page. - On carry clear, the USB path remains selected and the OS calls the bjump reached through
ram:3FC3withA=0x0A.
This makes link_xfer_op a USB-first wrapper around the existing link transfer engine. It does not
replace the packet format. The transport choice happens before _SendAByte writes each byte through
the assist FIFO or falls back to port 0x00. [confirmed]
Interrupt integration [confirmed]
The IM1 dispatcher (ram:006F) tests the USB interrupt status before the separate legacy controller:
IN A,(0x55)
XOR 0xFF
AND 0x1F
If no low-five-bit USB source is active, the handler falls through to the other interrupt work. If a
USB source is active, it reads port 0x56 and branches on event bits. In the visible dispatcher,
bits 4, 5, 6, 7, and 1 are routed to subhandlers; the surrounding code also checks 84+ hardware mode
through (IY+0x09) bit 3 and port 0x07 == 0x81 before using the USB/timer event path. The page-0
bjumps resolve as:
port 0x56 bit | Page-0 dispatch | Page-35 target | Observed role |
|---|---|---|---|
| 4 | 00:0122 → ram:3FA5 | 35:4B6A | line/event settle path; waits on 0x4D bits 7 and 0, writes 0x57 = 0x22. |
| 5 | 00:0127 → ram:3FAB | 35:4B9F | event clear/re-arm path; may clear 0x4C, reset USBFlag2 bit 6, and write 0x57 = 0x50/0x93. |
| 6 | 00:0113 → ram:3F93 | 35:40B2 | USB setup path; sets IY+0x1B bit 5, initializes controller state, and waits for 0x4C = 0x1A/0x5A. |
| 7 | 00:0118 → ram:3F99 | 35:4C14 | cleanup/reset path; clears 0x5B, resets USBFlag2 bit 0, and jumps through the common controller reset. |
| 1 | 00:011D → ram:3F9F | 35:4031 | alternate setup path; waits for 0x4C = 0x12/0x52 and uses endpoint/status ports 0x87/0x89/0x8B. |
Both paths are [confirmed].
The timer/idle side of the same handler also bridges to the assist path. At ram:01B1 it calls
ram:1837:
IN A,(0x2)
AND 0x80
XOR 0x80
This is the same hardware-model gate used elsewhere before assist-port access. On the legacy path it checks port 0x00 & 0x03; on the assist
path it checks port 0x09 & 0x18. If either assist bit is set, it reloads 0x9C86 = 0xFA, pulses
port 0x08 with 0x80 then 0x00, sets IY+0x3E bit 0, and calls the common link activity hook
at ram:3FD5. [confirmed]
The raw-line encoding, the corresponding port-0x00 receiver, and the distinction between this periodic check and a direct line interrupt are detailed in Two-wire link port hardware.
For application code, this means a custom interrupt handler that does not chain to the OS handler
must account for port 0x55/0x56 activity itself and then either reproduce the relevant page-35
event handling or deliberately leave USB disabled. The OS still acknowledges the legacy interrupt
mask through port 0x03 on exit, but the USB event work is selected by 0x55/0x56 and page-35
controller ports, not by a writeable 0x56 mask. Port 0x55 is not a summary of ON, standard-timer,
or legacy link requests. See Interrupts (IM1) for the
two-stage dispatch and legacy acknowledgement. [confirmed]
Public USB API bodies [confirmed]
The public USB names in ti83plus.inc are backed by the main page-3B bcall table for the 0x50xx,
0x52xx, and 0x53xx IDs. The table entries are addr_lo, addr_hi, page; page bytes like 0x75
mask to physical page 0x35.
| Bcall ID | Public name | Body | ROM-grounded behavior |
|---|---|---|---|
50F2 | _SendUSBData | 35:4DD3 | Sends from HL with byte count in DE; stores progress at 0x9C7E/0x9C81 and writes 64-byte chunks to port 0xA2. |
50F5 | _AppGetCBLUSB | 3B:54C7 | Sets IY+0x1B bit 1, clears bit 2, then reaches _GetVarCmdUSB. |
50F8 | _AppGetCalcUSB | 3B:54F0 | At 3B:54DE clears IY+0x16 bit 0 and sets sndRecState=0x15, then bcall 0x50FB (shared get-var path). |
50FB | _GetVarCmdUSB / link_xfer_op | 3C:4DD2 | USB-first variable command wrapper described above. |
5254 | _InitUSBDeviceCallback | 35:4696 | Initializes device mode, stores callback page/address at 0x9C13/0x9C14, and returns 0xFC–0xFF style error bytes with carry set on failure. |
5257 / 5311 | _KillUSBDevice / _RecycleUSB | 35:46FC / 35:5B9B | Clears callback state and recycles through the same cleanup path. |
525A | _SetUSBConfiguration | 35:470B | Builds an 8-byte request block at 0x9C29 and writes it through port 0xA0. |
525D / 5260 | _RequestUSBData / _StopReceivingUSBData | 35:48BA / 35:48D1 | Stores or clears the receive-buffer descriptor at 0x9C1E; receive records are read from port 0xA1. |
528A / 528D | _EnableUSBHook / _DisableUSBHook | 3B:7DC6 / 3B:7DD1 | Stores USBActivityHookPtr/page at 0x9BD4/0x9BD6 and toggles (IY+0x3A) bit 0. |
5290 | _InitUSBDevice | 35:42B0 | Main controller/device initialization path; uses 0x4C/0x4D line handshakes and endpoint ports 0x80–0x9B. |
5293 | _KillUSBPeripheral | 35:59CF | Peripheral teardown; sets controller state 0x9C28 = 5 and manipulates ports 0x54/0x81. |
530B | _ToggleUSBSmartPadInput | 35:5B84 | Sets or clears bit 3 in 0x9C75 according to A == 1. |
530E | _IsUSBDeviceConnected | 35:5B92 | Preserves A; returns flags from IN (0x81) & 0x40 (bit 6). (The .inc comment guesses bit 4,(81h), but the body actually masks bit 6.) |
Boot-page OS receive API
The retail boot table on page 3F also exposes a USB stack whose bodies run on page 2F. This stack receives an operating-system image. It is separate from the page-35 application-facing API above. The table bytes and entry prologues can be reproduced with tools/ti84re/rom/inspect_bcall.py. [confirmed]
| Bcall | ID | Table bytes | Body | Observed role |
|---|---|---|---|---|
_AttemptUSBOSReceive | 80E4 | 45 41 2F | 2F:4145 | Wait for or dispatch a USB line event, initialize the controller, then enter the OS-receive pipeline. [confirmed] |
_ReceiveOS_USB | 80F6 | CA 48 2F | 2F:48CA | Negotiate transfer records and write the received OS image through the Flash-control path. [confirmed] |
_USBErrorCleanup | 8105 | 58 59 2F | 2F:5958 | Clear port 0x5B, restore controller line state, and re-arm according to port 0x4D. [confirmed] |
_InitUSB | 8108 | A4 52 2F | 2F:52A4 | Initialize peripheral mode and return carry set after timeout cleanup. [confirmed] |
| unnamed entry | 810B | C5 62 2F | 2F:62C5 | Set port 0x81 mask 0x01, then wait through the timer-3 delay helper. [confirmed] |
_KillUSB | 810E | 61 59 2F | 2F:5961 | Run the error-cleanup sequence with an additional OUT (0x4C),0. [confirmed] |
Inspect a named entry and the unnamed slot directly:
nix develop -c python3 -m ti84re.rom.inspect_bcall 0x8108 --bytes 24
nix develop -c python3 -m ti84re.rom.inspect_bcall 0x810B --bytes 24
_AttemptUSBOSReceive input and dispatch
The first instruction at 2F:4145 is JR NZ,2F:414A. The input Z flag therefore controls whether the routine waits for a new event. With Z set, usb_wait_line_event at 2F:514C checks the cancel/timeout helper, then samples port 0x4D bit 6. If that bit is clear, it returns port 0x56 & 0xF2 instead. With Z clear, dispatch begins with the caller’s A unchanged. [confirmed]
The dispatcher tests event bits in this order: 5, 4, 6, then 7. Bits 5 or 4 call the line-state cleanup helper and resume waiting. Bit 6 calls _InitUSB. Bit 7 jumps to the common error exit at 2F:4FFD. When none of those bits is set, the routine reads port 0x4D; bit 5 selects _InitUSB, while the other branch calls the controller setup path at 2F:5220. Both successful branches continue at 2F:4170 into the receive protocol. [confirmed]
The ti83plus.inc comment says Z means “wait” and NZ means “dispatch the supplied port value.” The entry bytes verify that contract and establish the bit priority. [confirmed]
_InitUSB transaction and return
_InitUSB sets IY+0x1B bit 5 and writes controller state 2 to 0x9C28. It then performs this prefix: [confirmed]
; 2F:52AD
LD A,80h
OUT (57h),A
XOR A
OUT (4Ch),A
IN A,(4Ch)
LD A,02h
OUT (54h),A
LD A,20h
OUT (4Ah),A
CALL 59C3h
LD A,08h
OUT (4Ch),A
The reset helper at 2F:59C3 drives port 0x4B, pulses port 0x54, and uses programmable timer 3 through ports 0x36–0x38 for a delay. _InitUSB then waits for port 0x4C to equal 0x1A or 0x5A. Each poll decrements a 16-bit DE timeout through 2F:5313. [confirmed]
After the handshake, the routine writes 0xFF to port 0x87, zero to 0x92, reads 0x87, writes 0x0E to 0x89, clears 0x9C26 and 0x9C27, and writes 0x21 to 0x8B. The tail at 2F:52F6 gives port 0x8C five timeout windows to become nonzero. Success clears carry with:
OR A
RET
Failure calls _USBErrorCleanup through 2F:5B87, sets carry, and returns. [confirmed]
The unnamed bcall 810B reads port 0x81, ORs mask 0x01, writes the result back, and jumps to the timer-3 delay at 2F:5A06. The ti83plus.inc comment calls this bit 1, while mask 0x01 sets bit 0. No controller-state poll occurs in this entry itself. [confirmed]
Receive and cleanup boundaries
_ReceiveOS_USB disables interrupts, enters the record-transfer helpers, and feeds the values 0, 8, 3, 0, 0x0104, 0, and 0 through 2F:42AA. It then sets port 0x20 to 1, clears receive state at 0x8271, 0x822F, and 0x83A4, and uses 0x86EC as a 0x0104-byte record workspace. Later branches subtract a four-byte framing size, validate record fields, and program Flash through port 0x14. [confirmed]
This body is an OS installer, not a general USB receive primitive. It changes CPU speed, validates memory and page state, and writes Flash. Error branches converge on 2F:4FFD, which calls _USBErrorCleanup. Application code should use the page-35 API instead. [confirmed]
_USBErrorCleanup and _KillUSB share almost all their code: [confirmed]
; _USBErrorCleanup = 2F:5958
XOR A
OUT (5Bh),A
CALL 591Bh
JP 58D0h
; _KillUSB = 2F:5961
XOR A
OUT (5Bh),A
CALL 591Bh
XOR A
OUT (4Ch),A
JP 58D0h
The helper at 2F:591B chooses the port-0x4C value from port 0x4D bits 5 and 6, writes 0x02 to port 0x54, and clears low control bits on port 0x39. The tail at 2F:58D0 re-arms port 0x57 according to the current line state. _KillUSB differs only by forcing port 0x4C to zero between those helpers. [confirmed]
The setup paths also update GPIO data at port 0x3A and GPIO configuration at
port 0x39. Their low-bit read-modify-write sequences are decoded in
ASIC status, identity, protection, and GPIO.
The ROM ties those bits to USB setup but does not expose their electrical signal
names. [confirmed] for the operations; [hypothesis] for signal assignments.
Emulator comparison
The four pinned emulators implement disconnected or partial USB behavior. None implements the page-35 endpoint transactions needed for a connected transfer. [standard]
| Area | TilEm f56ad63 | Wabbitemu 48c2dc0 | MAME 0.287 | jsTIfied 20170706a |
|---|---|---|---|---|
| Controller ports | fixed reads at 0x4C, 0x4D, 0x55–0x57 | handlers at 0x4A, 0x4C, 0x4D, 0x55–0x57, 0x5B, and 0x80 | fixed reads at 0x55 and 0x56 only | fixed reads at 0x4C, 0x4D, 0x55–0x57 |
Initial/disconnected 0x4C, 0x4D | 0x22, 0xA5 | 0x22, 0xA5 | unmapped | 0x22, 0xA5 |
Initial 0x55, 0x56, 0x57 | 0x1F, 0x00, 0x50 | 0x1F, 0x50, 0x00 | 0x1F, 0x00, unmapped | 0x1F, 0x00, 0x50 |
| Line/event state | fixed | paired-state latch and event byte | none | fixed |
| FDRC block | unmapped | only device address at 0x80 | unmapped | unmapped |
| Connected transfer | unavailable | unavailable | unavailable | unavailable |
| Driver status | disconnected traces run | source calls the block Fake USB | TI-84 Plus driver is MACHINE_NOT_WORKING | fixed disconnected browser-emulator values |
TilEm’s fixed port 0x4C = 0x22 cannot satisfy _InitUSB’s 0x1A/0x5A handshake. Its x4_io.c has no controller or endpoint write cases. A TilEm trace can therefore exercise timeout and disconnected cleanup, but not connected setup or receive. [standard] for emulator behavior; [confirmed] for the ROM comparison.
Wabbitemu assigns paired states to port 0x4D: D+ low/high in bits 0/1, D- low/high in bits 2/3, ID low/high in bits 4/5, and VBUS high/low in bits 6/7. Reset value 0xA5 therefore selects D+ low, D- low, ID high, and VBUS low under its own labels. Port 0x56 starts at 0x50, port 0x57 stores an event mask, and port 0x55 reports line and protocol requests as active-low bits 2 and 4. [standard]
The partial model has five source-visible defects: [standard]
- Device initialization registers port
0x55twice. The first handler was written for port0x54, so the port-0x54PHY control model is unreachable. GenerateUSBEventdoes not consult the mask stored at port0x57; it raises the CPU interrupt unconditionally.- From reset state, writing
0x08to port0x4Asets VBUS-high bit 6 without clearing VBUS-low bit 7. The line byte becomes0xE5, in which both Wabbitemu VBUS state bits are set. - The same write records a D-minus-high event by changing the event byte from
0x50to0x58, but it does not set D-minus-high in the line byte. Repeated writes can therefore regenerate the event. - Port
0x4Dtries to select one D+ state withBIT(1) & ~BIT(0)or its inverse. Each expression evaluates to one set bit. The handler ORs that bit into the line-state byte without clearing the paired bit.
These inconsistencies prevent Wabbitemu from serving as a connected PHY reference. Its paired-state representation and active-low summary still provide an independent comparison with the ROM’s bit tests. The electrical labels remain emulator evidence because the ROM does not name the signals. [standard] for source behavior; [hypothesis] for physical signal assignments.
MAME maps ports 0x55 and 0x56 to constant disconnected values 0x1F and
zero. Ports 0x4A–0x5B outside that pair and the FDRC region at
0x80–0xA2 are absent from the TI-84 Plus map. A guarded native sweep reads
zeros across 0x4A–0x5B except for 0x55 = 0x1F; patterned writes leave the
complete block unchanged. A soft reset produces the same pair. [standard]
Native Wabbitemu USB edges
A guarded initialized-core run invokes the registered handlers without
executing TI-OS. Ports 0x4A, 0x4C, 0x4D, 0x55–0x57, 0x5B, and
0x80 accept reads. Port 0x54 is inactive and returns the unhandled-port
fallback 0xFF. Reset reads are 0x04, 0x22, 0xA5, 0x1F, 0x50,
0x00, 0x00, and 0x00 in mapped-port order. The run also checks the
internal reset fields: line state 0xA5, events 0x50, mask zero, both
interrupt fields clear, and all three stored control bytes zero. [standard]
Port 0x57 stores both 0xFF and zero. With the mask set to zero, writing
0x08 to port 0x4A asserts Wabbitemu’s CPU interrupt and line-interrupt
fields. The line state becomes 0xE5, the event byte becomes 0x58, port
0x55 reads 0x1B, and port 0x56 reads 0x58. Clearing only the CPU
interrupt field and repeating the same port write asserts it again. This
confirms the mask omission and repeat-event path in the initialized core.
[standard]
Directly seeded handler-contract cases produce the complete port-0x55
matrix 0x1F, 0x1B, 0x0F, and 0x0B for neither, line, protocol, and
both requests. Port 0x5B masks 0xFF to bit 0, and port 0x80 masks it to
0x7F. The two port-0x4D cases return 0xA7 and 0xE7, retaining both D+
bits after the handler adds the selected bit. These directly seeded states
test handler arithmetic; the run does not claim that registered ports can
reach them naturally. [standard]
Controlled boot-ROM paths
A separate Wabbitemu mode boots the retail OS 2.55MP ROM, installs controlled
digital handlers for ports 0x4A–0x5B and 0x80–0xA2, and then calls the
untouched page-2F boot routines from RAM. The two injected programs are:
; Call _InitUSB = 8108h, then stop in RAM.
RST 28h
.dw 8108h
HALT
; Dispatch event 40h through _AttemptUSBOSReceive = 80E4h.
LD A,40h
OR A ; NZ selects dispatch of the supplied event.
RST 28h
.dw 80E4h
HALT
The pinned SPASM-independent byte images are EF 08 81 76 and
3E 40 B7 EF E4 80 76. The native runner executes those bytes and checks the
page resolved by each bcall. It stops the second case at 2F:4170, before the
endpoint payload and Flash-programming pipeline. [confirmed] for these ROM
dispatches in the controlled run.
The harness returns 0x5A or 0x02 from port 0x4C to select handshake
success or timeout. It returns nonzero or zero from port 0x8C to select frame
readiness or timeout. Port 0x4D starts at 0xA5; all other controlled bytes
start at zero except the unused status defaults 0x55 = 0x1F and
0x56 = 0x50. This contract supplies digital branch inputs. It does not model
a USB device, packet timing, or a PHY. [confirmed] for the harness contract.
| Case | Controlled result | Instructions / T-states | Polls and boundary | Return |
|---|---|---|---|---|
| initialization success | 0x4C = 0x5A, 0x8C != 0 | 5,923 / 62,196 | 2 timeout ticks; 2 port-0x4C reads; 1 port-0x8C read | carry clear, A = 0x01 |
| handshake timeout | 0x4C = 0x02 | 783,929 / 7,739,783 | 65,535 timeout ticks and port-0x4C reads | carry set, A = 0x50 |
| frame timeout | 0x4C = 0x5A, 0x8C = 0 | 3,012,144 / 28,842,346 | 327,676 timeout ticks; 327,670 port-0x8C reads | carry set, A = 0x50 |
event 0x40 dispatch | success inputs | 5,935 / 62,310 | reaches 2F:4170 once | stopped at receive boundary |
Every case visits _InitUSB at 2F:52A4 and the reset helper at 2F:59C3
once. Success writes the byte-derived sequence
57:80 4C:00 54:02 4A:20 4B:00 54:00 54:C4 4C:08 87:FF 92:00 89:0E 8B:21.
The handshake-timeout path appends
5B:00 4C:00 54:02 57:50 after the eight-write initialization prefix. The
frame-timeout path appends 4C:00 54:02 57:50 after endpoint setup and does
not write port 0x5B. The bytes at 2F:5958 contain the port-0x5B write;
the frame cleanup at 2F:58C8 starts with CALL 2F:591B and bypasses it.
[confirmed]
The runner compares the complete 1 MiB Flash array before and after each case.
All four comparisons report zero changed bytes and no execution-protection
reset. This establishes that initialization, both timeouts, and dispatch up to
2F:4170 do not mutate Flash under these inputs. It does not exercise
_ReceiveOS_USB, endpoint payload transfer, command-busy behavior, electrical
USB signaling, or a calculator. [confirmed] for the controlled Wabbitemu ROM
execution; [hypothesis] for corresponding physical behavior.
Controlled installer-record rejection
A second constant-memory mode continues from _InitUSB into
_ReceiveOS_USB at 2F:48CA. Direct entry requires the session state normally
created by the preceding negotiation: IY = 0x89F0, frame size 0x0104,
staged offset zero, timeout 0x0014, and bit 0 of IY+0x42 set. The harness
scripts three endpoint FIFO packets: [confirmed]
0000000205
E000
0000000C0400000000000500003E000000
The first two packets form the five-byte type-0x05 transport header and its
E0 00 acknowledgement payload. The final type-0x04 frame selects service
0x0005 and supplies an installer record whose page byte is 0x3E. The ROM
transmits the exact request and acknowledgement below: [confirmed]
0000000E040000000800030000010400000000
0000000205E000
Execution reaches stream receive at 2F:4610, installer dispatch at
2F:495B, _DisplayOSProgress, page validation at 2F:5079, the invalid-page
branch at 2F:49A2, and _USBErrorCleanup at 2F:5958. It stops at
2F:5000, before the error UI. _DisplayOSProgress precedes page validation,
so the isolated validator case explicitly seeds 0x82A3 = 0x3E immediately
before that call. This makes the progress helper a no-op for the already
displayed page and prevents its unrelated persistent progress-byte update from
obscuring the rejection path. The complete Flash comparison then reports zero
changed bytes. [confirmed] for this controlled Wabbitemu-core execution.
This intervention is part of the result, not a claim about a natural complete OS-install session. The run validates transport framing, the retail calling context, record dispatch, page rejection, cleanup, and unchanged Flash across the isolated rejection. It does not model a USB device, endpoint timing, a PHY, natural progress persistence, or a physical calculator. [standard]
Reusable USB tools
tools/ti84re/hardware/usb.py contains the FDRC offset map, the common HDRC comparison map, pinned source
provenance, imported global bit names, link-assist rate fields, page-35 and boot-event decoders,
paired line-state decoder, emulator profiles, and pure functions for Wabbitemu’s USB read handlers.
tools/ti84re/hardware/describe_usb.py exposes the general models as text or JSON.
tools/ti84re/emulators/wabbitemu/usb_receive.py decodes the transport frames and enforces the
exact receive packets, ROM transmissions, execution boundaries, calling
context, intervention, and whole-Flash result. The guarded
tools/ti84re/emulators/wabbitemu/run_usb_receive_probe.py CLI checks both ROM and adapter
hashes and writes a JSON manifest. The native runner stores packet payloads and
fixed counters only; it does not emit an instruction-by-instruction trace.
tools/ti84re/emulators/wabbitemu/usb_probe.py validates native reports against the reusable handler model, while
tools/ti84re/emulators/wabbitemu/run_usb_edge_probe.py provides the exact-ROM guard and writes a hashed JSON
manifest. tools/ti84re/emulators/wabbitemu/usb_rom.py contains the byte-derived boot-ROM
oracle, and tools/ti84re/emulators/wabbitemu/run_usb_rom_probe.py exposes its four controlled
cases as a hash-guarded JSON CLI. The link-assist state model remains in tools/ti84re/link/port.py;
tools/ti84re/emulators/tilem/link.py and tools/ti84re/emulators/tilem/run_link_probe.py add the guarded TilEm
native report and manifest. tools/ti84re/rom/port_definitions.py parses the project port
labels with duplicate checks. tools/ti84re/rom/analyze_io.py uses that library to
attach labels to static I/O reports and can restrict output to ports absent
from the label file. tools/ti84re/rom/io_coverage.py pins and reconciles the complete
ROM-wide set of aligned non-descriptor candidates absent from that file.
# Map global, indexed, dynamic-sizing, and FIFO registers.
nix develop -c python3 -m ti84re.hardware.describe_usb \
register 0x80 0x91 0x9F 0xA2
# Compare the FDRC hypothesis with the common HDRC byte layout.
nix develop -c python3 -m ti84re.hardware.describe_usb layouts
nix develop -c python3 -m ti84re.hardware.describe_usb --json layouts
# Keep active-low port-0x55 and port-0x56 interpretations separate.
nix develop -c python3 -m ti84re.hardware.describe_usb events 0x1F 0x50
nix develop -c python3 -m ti84re.hardware.describe_usb assist 0x97 0xB4 0xE0
nix develop -c python3 -m ti84re.hardware.describe_usb line 0xA5 0xE5
nix develop -c python3 -m ti84re.hardware.describe_usb reads 0x4C 0x4D 0x55 0x56 0x57 0x80
nix develop -c python3 -m ti84re.hardware.describe_usb wabbit-port4a 0x08
# Audit direct page-35 accesses whose ports lack project-local labels.
nix develop -c python3 -m ti84re.rom.analyze_io \
--page 0x35 --direct-only --unlisted --summary 0x40-0x7F
# Retain only the two table-shaped candidates for unobserved USB ports.
nix develop -c python3 -m ti84re.rom.analyze_io \
--direct-only --exclude-descriptors 0x49 0x51 0x52
# Verify every candidate for every port absent from tools/symbols/ports.txt.
nix develop -c python3 -m ti84re.rom.describe_io_coverage --json
usb_rom_parent=$(mktemp -d /tmp/ti84-usb-rom.XXXXXX)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_usb_rom_probe \
--rom tools/rom.bin \
--binary /path/to/wabbitemu-headless \
--expected-binary-sha256 \
3acb6a18280f9c42d6fe324188eab73f87280ee70b973e1251fcfa50f54fb14e \
--output-dir "$usb_rom_parent/run" --json
usb_receive_parent=$(mktemp -d /tmp/ti84-usb-receive.XXXXXX)
nix develop -c python3 -m ti84re.emulators.wabbitemu.run_usb_receive_probe \
--rom tools/rom.bin \
--binary /path/to/wabbitemu-headless \
--expected-binary-sha256 \
3acb6a18280f9c42d6fe324188eab73f87280ee70b973e1251fcfa50f54fb14e \
--output-dir "$usb_receive_parent/run" --json
The FDRC names and bit labels remain a controller-family hypothesis. The CLI identifies that evidence boundary in its register records; it does not promote imported Mentor names to TI silicon confirmation.
How to use it in code [confirmed]
Prefer the OS entry points unless the program is deliberately writing a USB driver:
| Need | OS surface | ROM support |
|---|---|---|
| Send or request a variable over USB/link | _GetVarCmdUSB/link_xfer_op (50FB → 3C:4DD2) or _SendVarCmd (4A14 → 3C:4EDD) | Packet engine and USB-selection gate confirmed on page 3C. 0x50FB is _GetVarCmdUSB in ti83plus.inc. |
| Send one byte on the active link transport | _SendAByte (4EE5 → 3C:420D) | Assist branch writes C to port 0x0D after port 0x09 bit 5. |
| Receive one byte on the active link transport | _RecAByteIO (4F03 → 3C:443F) | Status path checks port 0x09 and reads port 0x0A on the assist path. |
| Use the raw assist FIFO | Poll port 0x09 bit 5, then write the byte to port 0x0D; for receive, observe port 0x09 bit 4/error bits and read port 0x0A. | Confirmed as an OS pattern, but not a complete public API. |
The raw FIFO sequence is only the byte layer. A working transfer still needs the packet layer: machine ID, command, length, payload checksum, ACK/NAK, and EOT. That framing is documented in Link transfer.
Native TilEm link-assist edges
The guarded TilEm direct-core probe maps all handlers from 0x08 through
0x0D. A fresh disabled engine reports 0x20. The read sides of
ports 0x09–0x0C remain computed status or zero after the write sides store
0x91, 0xA2, 0xB3, and 0xC4. This covers the complete four-register
setup surface that OS 2.55MP initializes. [standard]
Idle-ready reports 0x22 and asserts the CPU interrupt. Reading port 0x0D
does not acknowledge that condition. A completed 0xA5 receive reports
0x31; reading port 0x0A returns the byte and changes status to 0x20.
An illegal both-low input reports 0x64; the first status read clears only the
interrupt request, leaving error status 0x60. [standard]
Full reset restores port 0x08 = 0x80 and clears the active transfer fields,
but retains the four auxiliary write registers and external peer-line state.
Direct handler calls consume zero modeled CPU clocks. These results describe
the pinned TilEm implementation, not physical assist timing or reset
retention. See Two-wire link port hardware
for the raw matrix, LSB-first transfer sequence, and guarded command.
[standard]
Native Wabbitemu link-assist edges
The guarded Wabbitemu initialized-core probe maps ports 0x08, 0x09,
0x0A, and 0x0D; ports 0x0B and 0x0C are absent. This means its assist
engine cannot represent the OS’s complete four-register signaling-rate setup
for CPU speed modes 0–3. [standard]
The same run sends and receives 0xA5 through controlled peer handshakes.
Transmit completion reports 0x22; receive completion reports 0x11.
Reading port 0x0D clears ready, and reading port 0x0A clears read-ready.
Both enabled conditions assert Wabbitemu’s CPU interrupt line. A separately
seeded error produces 0x4C and clears on the first port-0x09 read, but the
pinned source contains no path that sets the error field. These are emulator
state-machine results, not physical assist timing or TI-OS execution.
[standard]
See Two-wire link port hardware for the full raw matrix, transfer sequence, and guarded command.
Practical rules:
- Set up
IY+0x1Bconsistently before callinglink_xfer_op. Bit 0 is the USB-first selector. - Do not write ports
0x08–0x0Dwhile the OS link engine is active; the OS keeps state inIY+0x3Ebit 0,0x9C86, and0x9CAC. - If a custom interrupt handler is installed, either chain to the OS handler or service the same
source gates. The OS itself expects to handle port
0x55/0x56events. - Use the public USB bcalls for endpoint/controller work. The raw page-35 endpoint ports are mapped well enough to identify the FIFOs and state variables, but their bit-level protocol is not a stable public API.
Limits
- The ROM calls
ram:2E0B, across_page_jumpthunk to35:4280. Its carry-clear/carry-set result is decoded above. - The public
0x50xx/0x52xx/0x53xxUSB APIs and the boot-page0x8xxxUSB entries are mapped above. The controlled harness executes_InitUSB, both timeout paths,_AttemptUSBOSReceivethrough2F:4170, and a scripted_ReceiveOS_USBinstaller record through invalid-page cleanup. A natural connected transfer and valid page-programming session remain dynamically untested. - The FDRC layout names the endpoint register block, but physical tests have not confirmed every
imported bit meaning or the TI-specific PHY at ports
0x4A–0x5B. TilEm does not model physical timing from the assist setup values. ROM-confirmed claims remain limited to written constants, comparisons, branch bits, RAM state, FIFO direction, and the transfer sequences cited above. - The ROM confirms the port-
0x4Bwrites and the port-0x4F/0x50read-modify-write sequence. It does not identify their electrical effects. Port-0x5Abit 0, endpoint-2 setup, and subsequent LCD traffic are ROM-confirmed; mirroring on the wire, its packet format, and the reported host-mode restriction remain physically unverified. - The guarded TilEm link probe verifies handler-visible assist behavior, but it does not establish physical signaling-rate divisors, wait states, electrical levels, or reset retention.
- TilEm, Wabbitemu, MAME, and jsTIfied do not implement a connected page-35 transfer. The initialized-core Wabbitemu run confirms its port-registration, event-mask, contradictory-line-state, repeat-event, and paired-bit handler defects. The controlled port harness now drives exact endpoint FIFO packets through invalid-page cleanup, but a natural complete transfer still requires physical hardware or a device-level model.
Sources
| Source | Use |
|---|---|
| Retail OS 2.55MP and boot 1.03 ROM bytes | Main and boot bcall tables, page-2F/35 bodies, ports, branches, and RAM state |
tools/symbols/ti83plus.inc | Historical public names and comments, checked against table entries and bodies |
TilEm x4_io.c at f56ad63 | Link-assist implementation and fixed disconnected USB reads |
Mentor mu_fdrdf.h revision 1.7 as preserved in lightcube | Mentor-authored 2004 FDRC register offsets and bit masks. The header labels itself proprietary; the mirror is controller-family evidence, not TI silicon identification. |
VSF FDRC register structure at 4327394 | Independent implementation that corroborates the compact FDRC byte ordering; not TI-84 Plus evidence |
Linux musb_regs.h at db2ddb8 | Mentor/TI-copyrighted common HDRC/MUSB map used as the comparison candidate; not TI-84 Plus silicon documentation |
Linky at 89586b0 | Independent calculator software that names MUSBFDRC and exercises the same ports |
Wabbitemu 83psehw.c at 48c2dc0 | Partial line-state and interrupt model, with the implementation limits described above |
MAME 0.287 ti85.cpp and ti85_m.cpp | Fixed USB interrupt reads and absent controller/endpoint ports |
jsTIfied project 42 and deployed 20170706a artifact | fixed disconnected values matching TilEm and absence of an endpoint/FDRC model; artifact SHA-256 c7325a38f976f64eaa34182da17d838fe4831eece4650b92d5db710cf7a8fc5b |
WikiTI port 0x09 | Historical link-assist timing-field interpretation, kept separate from ROM observations |
WikiTI port 0x4B | Historical USB-power orientation. The page calls its own bit descriptions mostly speculative. |
WikiTI port 0x49 | Historical raw-transceiver bit claims; no primary hardware source or ROM use found |
WikiTI port 0x51 | Historical USB enable-timer claim; physical clock and units remain unverified |
WikiTI port 0x52 | Historical charge-pump timer claim; physical behavior remains unverified |
WikiTI port 0x5A | Historical presentation-mirroring description, treated as an unverified physical claim beyond the ROM-visible setup sequence |
WikiTI _KeyboardGetKey revision 5510 | Historical TI-Keyboard transmitter sequence, checked against but not substituted for ROM control flow |
bcall index
The main table below lists the live-confirmed 0x4xxx bcall system calls. Each has an ID (the 2-byte value after rst 28h) and a body at page:addr. Use your browser’s find, or the wiki search box. See The bcall Mechanism for how dispatch works. The 0x8xxx boot bcall names at the end are official SDK equates resolved from the retail boot table on page 3F; USB boot entries point into page 2F.
| bcall | ID | Body (page:addr) |
|---|---|---|
app_5de7 | 5326 | 3D:5DE7 |
arc_5936 | 51AC | 07:5936 |
arc_59f1 | 4A68 | 07:59F1 |
cplx_op_arrange | 4648 | 02:494F |
c_log_prep | 50FE | 02:6F1B |
disp_paged_str | 51CA | 01:7C4D |
draw_zero_op1 | 4873 | 04:620B |
drw_5df1 | 53F5 | 04:5DF1 |
drw_5df4 | 4825 | 04:5DF4 |
drw_638e | 487C | 04:638E |
dsp_6240 | 4C42 | 01:6240 |
dsp_65ea | 4D6B | 01:65EA |
edt_5d6f | 5464 | 03:5D6F |
edt_69f8 | 5461 | 03:69F8 |
edt_6bd1 | 5458 | 03:6BD1 |
fps_push_real | 4A83 | 07:6365 |
fpx_4a7b | 5185 | 02:4A7B |
fpx_5d70 | 4669 | 02:5D70 |
fpx_5dbb | 466C | 02:5DBB |
fpx_7069 | 5101 | 02:7069 |
fpx_7d9d | 533B | 02:7D9D |
fpx_7dfe | 5338 | 02:7DFE |
get_pos_list_elem | 4666 | 02:5BBB |
_GetVarVersion | 510A | 33:5023 |
_GET_INDEX_LST | 47C8 | 33:707A |
_HEAP_SORT | 47CB | 33:7097 |
_PUT_INDEX_LST | 47C5 | 33:7066 |
grc_454b | 5263 | 37:454B |
grc_4556 | 5266 | 37:4556 |
grc_4575 | 5269 | 37:4575 |
_DispAppRestrictions | 52FF | 37:4611 |
grc_51c2 | 517F | 37:51C2 |
grc_5223 | 51A0 | 37:5223 |
grc_5d44 | 51D6 | 37:5D44 |
grc_5f42 | 5200 | 37:5F42 |
grc_60cb | 51FA | 37:60CB |
grf_435f | 5140 | 33:435F |
grf_5e06 | 5476 | 33:5E06 |
lcd_blit_region | 4D26 | 07:5431 |
link_xfer_op | 50FB | 3C:4DD2 |
list_idx_times9 | 53D1 | 35:79E9 |
lnk_62b0 | 5182 | 3C:62B0 |
mde_7da9 | 49DB | 36:7DA9 |
mnu_6ddb | 5467 | 39:6DDB |
op1_int_part_neg | 489A | 04:74E8 |
push_zero_op1 | 4651 | 02:49C0 |
rcl_c_list_elem | 464B | 02:49A7 |
rcl_c_list_elem_b | 464E | 02:49B5 |
rcl_list_elem_b | 463C | 02:47FE |
rcl_list_elem_to_op1 | 4639 | 02:47FB |
rcl_var_push | 4B9A | 3A:5D07 |
screen_split | 5227 | 05:7712 |
scr_4056 | 51F1 | 05:4056 |
scr_4619 | 51E5 | 05:4619 |
sta_5d3c | 5203 | 35:5D3C |
sta_5eef | 4B9D | 3A:5EEF |
sta_760f | 4BA9 | 3A:760F |
vert_split_draw | 48DC | 05:5D88 |
_AbsO1O2Cp | 410E | 00:1987 |
_AbsO1PAbsO2 | 405A | 00:225B |
_ACos | 40DE | 02:76DF |
_ACosH | 40F0 | 02:7964 |
_ACosRad | 40D2 | 02:76C9 |
_AdrLEle | 462D | 02:47C5 |
_AdrMEle | 4609 | 02:4002 |
_AdrMRow | 4606 | 02:4000 |
_AHEADEQUAL | 4B49 | 34:5A99 |
_AllEq | 4876 | 04:6218 |
_AllocFPS | 43A5 | 00:1534 |
_AllocFPS1 | 43A8 | 00:1537 |
_Angle | 4102 | 02:6A38 |
_AnsName | 4B52 | 38:74B7 |
_ApdSetup | 4C93 | 00:03AE |
_AppGetCalc | 4C78 | 3B:54EC |
_AppGetCbl | 4C75 | 3B:54C3 |
_AppInit | 404B | 00:0936 |
_Arc_Unarc | 4FD8 | 07:6248 |
_ArcChk | 5014 | 3D:61AF |
_ASin | 40E4 | 02:76F1 |
_ASinH | 40ED | 02:7956 |
_ASinRad | 40DB | 02:76DA |
_ATan | 40E1 | 02:76E9 |
_ATan2 | 40E7 | 02:7749 |
_ATan2Rad | 40D8 | 02:76D4 |
_ATanH | 40EA | 02:7909 |
_ATanRad | 40D5 | 02:76CF |
_BinOPExec | 4663 | 02:53DD |
_Bit_VertSplit | 4FA8 | 00:215D |
_BufClear | 4936 | 00:222E |
_BufClr | 5074 | 04:6074 |
_BufCpy | 5071 | 04:60A6 |
_bufInsert | 4909 | 06:42E5 |
_CAbs | 4E97 | 02:6C47 |
_CAdd | 4E88 | 02:6BA5 |
_CanAlphIns | 4C69 | 00:04C6 |
_CDiv | 4E94 | 02:6BF3 |
_CDivByReal | 4EBB | 02:6DAC |
_CEtoX | 4EA9 | 02:6D1D |
_CFrac | 4EC1 | 02:6DCF |
_CheckSplitFlag | 49F0 | 00:2060 |
_CheckTimer | 527E | 33:5F16 |
_CheckTimerRestart | 5281 | 33:5F27 |
_ChkFindSym | 42F1 | 00:0E60 |
_chkTimer0 | 5176 | 37:557E |
_chkTmr | 5143 | 37:54C1 |
_Chk_Batt_Level | 5221 | 33:4E9B |
_Chk_Batt_Low | 50B3 | 00:0D07 |
_CIntgr | 4EC4 | 02:6DDD |
_CircCmd | 47D4 | 33:74CE |
_CkInt | 4234 | 00:1E06 |
_CkOdd | 4237 | 00:1E0A |
_CkOP1C0 | 4225 | 00:1DE4 |
_CkOP1Cplx | 40FC | 00:193A |
_CkOP1FP0 | 4228 | 00:1DE9 |
_CkOP1Pos | 4258 | 00:1E5D |
_CkOP1Real | 40FF | 00:1942 |
_CkOP2FP0 | 422B | 00:1DEE |
_CkOP2Pos | 4255 | 00:1E58 |
_CkOP2Real | 42DF | 00:214E |
_CkPosInt | 4231 | 00:1DFD |
_CkValidNum | 4270 | 00:1E9B |
_CleanAll | 4A50 | 07:52CF |
_ClearParserHook | 5029 | 3B:7C3B |
_ClearRect | 4D5C | 3B:6935 |
_ClearRow | 4CED | 01:6934 |
_CLine | 4798 | 33:6028 |
_CLineS | 479B | 33:6034 |
_CLN | 4EA0 | 02:6CCA |
_CLog | 4EA3 | 02:6CE7 |
_CloseEditBuf | 48D3 | 05:5675 |
_CloseEditBufNoR | 476E | 03:4743 |
_CloseEditEqu | 496C | 06:4771 |
_CloseProg | 4A35 | 07:4FB4 |
_ClrCursorHook | 4F69 | 3B:7AEA |
_ClrGraphRef | 4A38 | 07:4FD8 |
_ClrLCD | 4543 | 01:60F5 |
_ClrLCDFull | 4540 | 01:60E4 |
_ClrLp | 41D1 | 00:1BC4 |
_ClrOP1S | 425E | 00:1E68 |
_ClrOP2S | 425B | 00:1E63 |
_ClrRawKeyHook | 4F6F | 3B:7B88 |
_ClrScrn | 4549 | 01:6167 |
_ClrScrnFull | 4546 | 01:6162 |
_ClrTxtShd | 454C | 01:616F |
_CMltByReal | 4EB8 | 02:6D94 |
_CmpSyms | 4A4A | 07:519E |
_CMult | 4E8E | 02:6BB7 |
_Conj | 4EB5 | 02:6D8F |
_ConvDim | 4B43 | 38:741F |
_ConvDim00 | 4B46 | 38:7422 |
_ConvKeyToTok | 4A02 | 07:44DE |
_ConvLcToLr | 4A23 | 07:4CFF |
_ConvLrToLc | 4A56 | 07:5368 |
_ConvOP1 | 4AEF | 38:7433 |
_COP1Set0 | 4105 | 00:195F |
_Cos | 40C0 | 02:7346 |
_CosH | 40CC | 02:762E |
_CpHLDE | 400C | 00:21BB |
_CPoint | 4DC8 | 04:43D8 |
_CPointS | 47F5 | 04:43DD |
_CpOP1OP2 | 4111 | 00:198D |
_CpOP4OP3 | 4108 | 00:197A |
_CpyO1ToFPS1 | 445C | 00:16D4 |
_CpyO1ToFPS2 | 446B | 00:16ED |
_CpyO1ToFPS3 | 4477 | 00:1701 |
_CpyO1ToFPS4 | 4489 | 00:172B |
_CpyO1ToFPS5 | 4483 | 00:171C |
_CpyO1ToFPS6 | 447D | 00:170B |
_CpyO1ToFPS7 | 4480 | 00:1712 |
_CpyO1ToFPST | 444A | 00:16B5 |
_CpyO2ToFPS1 | 4459 | 00:16CF |
_CpyO2ToFPS2 | 4462 | 00:16DE |
_CpyO2ToFPS3 | 4474 | 00:16FC |
_CpyO2ToFPS4 | 4486 | 00:1726 |
_CpyO2ToFPST | 4444 | 00:16AB |
_CpyO3ToFPS1 | 4453 | 00:16C5 |
_CpyO3ToFPS2 | 4465 | 00:16E3 |
_CpyO3ToFPST | 4441 | 00:16A6 |
_CpyO5ToFPS1 | 4456 | 00:16CA |
_CpyO5ToFPS3 | 4471 | 00:16F7 |
_CpyO6ToFPS2 | 4468 | 00:16E8 |
_CpyO6ToFPST | 4447 | 00:16B0 |
_CpyStack | 4429 | 00:167C |
_CpyTo1FPS1 | 4432 | 00:168D |
_CpyTo1FPS10 | 43F3 | 00:1617 |
_CpyTo1FPS11 | 43D8 | 00:15CF |
_CpyTo1FPS2 | 443B | 00:169C |
_CpyTo1FPS3 | 4408 | 00:1647 |
_CpyTo1FPS4 | 440E | 00:1651 |
_CpyTo1FPS5 | 43DE | 00:15DF |
_CpyTo1FPS6 | 43E4 | 00:15EF |
_CpyTo1FPS7 | 43EA | 00:15FE |
_CpyTo1FPS8 | 43ED | 00:1608 |
_CpyTo1FPS9 | 43F6 | 00:1621 |
_CpyTo1FPST | 4423 | 00:1674 |
_CpyTo2FPS1 | 442F | 00:1688 |
_CpyTo2FPS2 | 4438 | 00:1697 |
_CpyTo2FPS3 | 4402 | 00:163F |
_CpyTo2FPS4 | 43F9 | 00:162B |
_CpyTo2FPS5 | 43DB | 00:15DA |
_CpyTo2FPS6 | 43E1 | 00:15EA |
_CpyTo2FPS7 | 43E7 | 00:15F9 |
_CpyTo2FPS8 | 43F0 | 00:160D |
_CpyTo2FPST | 4420 | 00:166F |
_CpyTo3FPS1 | 442C | 00:1683 |
_CpyTo3FPS2 | 4411 | 00:1656 |
_CpyTo3FPST | 441D | 00:166A |
_CpyTo4FPST | 441A | 00:1665 |
_CpyTo5FPST | 4414 | 00:165B |
_CpyTo6FPS2 | 43FF | 00:163A |
_CpyTo6FPS3 | 43FC | 00:1635 |
_CpyTo6FPST | 4417 | 00:1660 |
_CpyToFPS1 | 445F | 00:16D7 |
_CpyToFPS2 | 446E | 00:16F0 |
_CpyToFPS3 | 447A | 00:1704 |
_CpyToFPST | 444D | 00:16B8 |
_CpyToStack | 4450 | 00:16BD |
_Create0Equ | 432A | 00:1131 |
_CreateAppVar | 4E6A | 00:114B |
_CreateCList | 431B | 00:1109 |
_CreateCplx | 430C | 00:10B0 |
_CreateEqu | 4330 | 00:113C |
_CreatePair | 4B0D | 38:6785 |
_CreatePict | 4333 | 00:1140 |
_CreateProg | 4339 | 00:1153 |
_CreateProtProg | 4E6D | 00:114F |
_CreateReal | 430F | 00:10B8 |
_CreateRList | 4315 | 00:10C4 |
_CreateRMat | 4321 | 00:1115 |
_CreateStrng | 4327 | 00:1123 |
_CRecip | 4E91 | 02:6BE6 |
_CSqRoot | 4E9D | 02:6C84 |
_CSquare | 4E8B | 02:6BB4 |
_CSub | 4E85 | 02:6BA2 |
_CTenX | 4EA6 | 02:6D08 |
_CTrunc | 4EBE | 02:6DBD |
_Cube | 407B | 00:237D |
_CursorOff | 45BE | 06:7C5F |
_CursorOn | 45C4 | 06:7D34 |
_CXrootY | 4EAC | 02:6D3B |
_CYtoX | 4EB2 | 02:6D5C |
_DarkLine | 47DD | 04:4025 |
_DarkPnt | 47F2 | 04:43D6 |
_DataSize | 436C | 00:1485 |
_DataSizeA | 4369 | 00:1466 |
_DeallocFPS | 439F | 00:1526 |
_DeallocFPS1 | 43A2 | 00:152A |
_DecO1Exp | 4267 | 00:1E6F |
_DelListEl | 4A2F | 07:4F43 |
_DelMem | 4357 | 00:1368 |
_DelRes | 4A20 | 07:72F5 |
_DelVar | 4351 | 00:1308 |
_DelVarArc | 4FC6 | 00:12D9 |
_DelVarNoArc | 4FC9 | 00:130E |
_DisableApd | 4C84 | 3B:7AA8 |
_Disp | 4F45 | 37:51D3 |
_DispDone | 45B5 | 01:69B0 |
_DispEOL | 45A6 | 01:689F |
_DispHL | 4507 | 01:5BF6 |
_DisplayImage | 4D9B | 3B:6A72 |
_DispMenuTitle | 5065 | 39:4D21 |
_DispOP1A | 4BF7 | 04:7844 |
_DivHLBy10 | 400F | 00:0269 |
_DivHLByA | 4012 | 00:026B |
_DrawCirc2 | 4C66 | 3B:7171 |
_DrawCmd | 48C1 | 04:7B8B |
_DrawRectBorder | 4D7D | 3B:68F5 |
_DrawRectBorderClear | 4D8C | 3B:692A |
_DToR | 4075 | 00:236B |
_EditProg | 4A32 | 07:4F6B |
_EnableApd | 4C87 | 3B:7AAD |
_EnoughMem | 42FD | 00:0FA6 |
_EOP1NotReal | 4279 | 00:1F06 |
_Equ_or_NewEqu | 42C4 | 00:20FD |
_EraseEOL | 4552 | 01:61C5 |
_EraseRectBorder | 4D86 | 3B:68F1 |
_ErrArgument | 44AD | 00:2711 |
_ErrBadGuess | 44CB | 00:2751 |
_ErrBreak | 44BF | 00:273D |
_ErrCustom1 | 4D41 | 00:2771 |
_ErrDataType | 44AA | 00:2708 |
_ErrDimension | 44B3 | 00:2719 |
_ErrDimMismatch | 44B0 | 00:2715 |
_ErrDivBy0 | 4498 | 00:26EC |
_ErrDomain | 449E | 00:26F4 |
_ErrD_OP1NotPos | 42C7 | 00:2119 |
_ErrD_OP1NotPosInt | 42CD | 00:2125 |
_ErrD_OP1Not_R | 42CA | 00:2120 |
_ErrD_OP1_0 | 42D3 | 00:212D |
_ErrD_OP1_LE_0 | 42D0 | 00:212A |
_ErrIncrement | 44A1 | 00:26F8 |
_ErrInvalid | 44BC | 00:2729 |
_ErrIterations | 44C8 | 00:274D |
_ErrLinkXmit | 44D4 | 00:278D |
_ErrMemory | 44B9 | 00:2721 |
_ErrNonReal | 4A8C | 38:42E1 |
_ErrNon_Real | 44A4 | 00:26FC |
_ErrNotEnoughMem | 448C | 00:1735 |
_ErrOverflow | 4495 | 00:26E8 |
_ErrSignChange | 44C5 | 00:2749 |
_ErrSingularMat | 449B | 00:26F0 |
_ErrStat | 44C2 | 00:2741 |
_ErrStatPlot | 44D1 | 00:2759 |
_ErrSyntax | 44A7 | 00:2700 |
_ErrTolTooSmall | 44CE | 00:2755 |
_ErrUndefined | 44B6 | 00:271D |
_EToX | 40B4 | 02:705C |
_Exch9 | 43D5 | 00:15CA |
_ExLp | 4222 | 00:1DDA |
_ExpToHex | 424F | 00:1E4E |
_Factorial | 4B85 | 35:7995 |
_FillBasePageTable | 5011 | 00:2692 |
_FillRect | 4D62 | 3B:6939 |
_FillRectPattern | 4D89 | 3B:6814 |
_FindAlphaDn | 4A47 | 07:50B8 |
_FindAlphaUp | 4A44 | 07:50B5 |
_FindApp | 4C4E | 3D:5EE3 |
_FindAppDn | 4C4B | 3D:5DE6 |
_FindAppNumPages | 509B | 3D:4AA3 |
_FindAppUp | 4C48 | 3D:5DDA |
_FindSym | 42F4 | 00:0E65 |
_Find_Parse_Formula | 4AF2 | 38:758A |
_FiveExec | 467E | 02:69BC |
_FixTempCnt | 4A3B | 07:4FEC |
_FlashToRam | 5017 | 3D:6745 |
_FlashWriteDisable | 4F3C | 3C:66D5 |
_ForceFullScreen | 508F | 39:66D2 |
_FormBase | 50AA | 06:57C0 |
_FormDCplx | 4996 | 06:59D3 |
_FormEReal | 4990 | 06:5799 |
_FormReal | 4999 | 06:5ACF |
_FourExec | 467B | 02:6889 |
_FPAdd | 4072 | 00:229E |
_FPDiv | 4099 | 00:2541 |
_FPMult | 4084 | 00:238B |
_FPRecip | 4096 | 00:253D |
_FPSquare | 4081 | 00:238A |
_FPSub | 406F | 00:2297 |
_Frac | 4093 | 00:24E3 |
_GetBaseVer | 4C6F | 00:0284 |
_GetCSC | 4018 | 00:04B2 |
_getDate | 514F | 37:550B |
_GetDateString | 5152 | 37:55E8 |
_getDtFmt | 5155 | 37:5581 |
_getDtStr | 5158 | 37:55A9 |
_GetK | 4744 | 37:746D |
_GetKey | 4972 | 06:491E |
_GetKeyRetOff | 500B | 06:491A |
_GetLToOP1 | 4636 | 02:47EA |
_GetMToOP1 | 4615 | 02:4044 |
_GetStringInput2 | 4E61 | 37:5194 |
_GetSysInfo | 50DD | 07:7345 |
_getTime | 515B | 37:5551 |
_GetTimeString | 515E | 37:567E |
_getTmFmt | 5161 | 37:5593 |
_getTmStr | 5164 | 37:55CF |
_GetTokLen | 4591 | 01:66E5 |
_Get_Tok_Strng | 4594 | 01:66EA |
_GrBufClr | 4BD0 | 04:6071 |
_GrBufCpy | 486A | 04:60A3 |
_GrphCirc | 47D7 | 33:758D |
_HLTimes9 | 40F9 | 00:1930 |
_homeup | 4558 | 01:6216 |
_HorizCmd | 48A6 | 04:793E |
_HTimesL | 4276 | 00:1EF6 |
_IBounds | 4C60 | 04:42EC |
_IBoundsFull | 4D98 | 04:4306 |
_ILine | 47E0 | 04:4029 |
_IncLstSize | 4A29 | 07:4EF4 |
_InitTimer | 526C | 33:5E38 |
_InsertList | 4A2C | 07:4F07 |
_InsertMem | 42F7 | 00:0F81 |
_Int | 40A5 | 00:2621 |
_Intgr | 405D | 00:2263 |
_InvCmd | 48C7 | 04:7D6A |
_InvertRect | 4D5F | 3B:693D |
_InvOP1S | 408D | 00:24BD |
_InvOP1SC | 408A | 00:24BA |
_InvOP2S | 4090 | 00:24CD |
_InvSub | 4063 | 00:227D |
_IOffset | 4C63 | 04:42B5 |
_IPoint | 47E3 | 04:4157 |
_IsA2ByteTok | 42A3 | 00:1FE8 |
_IsEditEmpty | 492D | 00:21A7 |
_IsOneTwoThree | 516D | 37:5438 |
_IsOP112or24 | 5173 | 37:5413 |
_JError | 44D7 | 00:2793 |
_JErrorNo | 4000 | 00:2799 |
_JForceCmd | 402A | 00:0747 |
_JForceCmdNoChar | 4027 | 00:0746 |
_JForceGraphKey | 5005 | 01:6BFD |
_JForceGraphNoKey | 5002 | 01:6BFB |
_KeyToString | 45CA | 01:6D10 |
_KillTimer | 526F | 33:5E4E |
_LCD_DRIVERON | 4978 | 06:4D02 |
_LdHLind | 4009 | 00:0033 |
_LineCmd | 48AC | 04:796A |
_LnX | 40AB | 02:6EFD |
_LoadCIndPaged | 501D | 00:029F |
_LoadDEIndPaged | 501A | 3C:6B36 |
_LoadPattern | 4CB1 | 01:6267 |
_Load_SFont | 4783 | 03:4A8F |
_LogX | 40AE | 02:6F16 |
_Max | 4057 | 00:224D |
_MemChk | 42E5 | 00:0E20 |
_MemClear | 4C30 | 3B:7138 |
_MemSet | 4C33 | 3B:7139 |
_Min | 4054 | 00:2244 |
_Minus1 | 406C | 00:2294 |
_Mov10B | 415C | 00:1A90 |
_Mov18B | 47DA | 00:192B |
_Mov7B | 4168 | 00:1A96 |
_Mov8B | 4165 | 00:1A94 |
_Mov9B | 415F | 00:1A92 |
_Mov9OP1OP2 | 417D | 00:1B06 |
_Mov9OP2Cp | 410B | 00:1982 |
_Mov9ToOP1 | 417A | 00:1B01 |
_Mov9ToOP2 | 4180 | 00:1B07 |
_MovFrOP1 | 4183 | 00:1B0C |
_NewLine | 452E | 01:5F4A |
_NZIf83Plus | 50E0 | 00:1837 |
_newContext | 4030 | 00:077E |
_OneVar | 4BA3 | 3A:6420 |
_OP1ExOP2 | 421F | 00:1DD2 |
_OP1ExOP3 | 4219 | 00:1DB7 |
_OP1ExOP4 | 421C | 00:1DBC |
_OP1ExOP5 | 420D | 00:1DA0 |
_OP1ExOP6 | 4210 | 00:1DA5 |
_OP1ExpToDec | 4252 | 00:1E77 |
_OP1Set0 | 41BF | 00:1BA4 |
_OP1Set1 | 419B | 00:1B38 |
_OP1Set2 | 41A7 | 00:1B50 |
_OP1Set3 | 41A1 | 00:1B44 |
_OP1Set4 | 419E | 00:1B3D |
_OP1ToOP2 | 412F | 00:1A2F |
_OP1ToOP3 | 4123 | 00:1A0F |
_OP1ToOP4 | 4117 | 00:19EC |
_OP1ToOP5 | 4153 | 00:1A80 |
_OP1ToOP6 | 4150 | 00:1A78 |
_OP2ExOP4 | 4213 | 00:1DAA |
_OP2ExOP5 | 4216 | 00:1DAF |
_OP2ExOP6 | 4207 | 00:1D93 |
_OP2Set0 | 41BC | 00:1B96 |
_OP2Set1 | 41AD | 00:1B60 |
_OP2Set2 | 41AA | 00:1B55 |
_OP2Set3 | 4198 | 00:1B30 |
_OP2Set4 | 4195 | 00:1B29 |
_OP2Set5 | 418F | 00:1B22 |
_OP2Set60 | 4AB0 | 38:5DDC |
_OP2Set8 | 418C | 00:1B1B |
_OP2SetA | 4192 | 00:1B24 |
_OP2ToOP1 | 4156 | 00:1A88 |
_OP2ToOP3 | 416E | 00:1AE7 |
_OP2ToOP4 | 411A | 00:19F5 |
_OP2ToOP5 | 414A | 00:1A68 |
_OP2ToOP6 | 414D | 00:1A70 |
_OP3Set0 | 41B9 | 00:1B8A |
_OP3Set1 | 4189 | 00:1B16 |
_OP3Set2 | 41A4 | 00:1B4B |
_OP3ToOP1 | 413E | 00:1A4E |
_OP3ToOP2 | 4120 | 00:1A07 |
_OP3ToOP4 | 4114 | 00:19E3 |
_OP3ToOP5 | 4147 | 00:1A60 |
_OP4Set0 | 41B6 | 00:1B85 |
_OP4Set1 | 4186 | 00:1B11 |
_OP4ToOP1 | 4138 | 00:1A44 |
_OP4ToOP2 | 411D | 00:19FE |
_OP4ToOP3 | 4171 | 00:1AEF |
_OP4ToOP5 | 4144 | 00:1A58 |
_OP4ToOP6 | 4177 | 00:1AF9 |
_OP5ExOP6 | 420A | 00:1D98 |
_OP5Set0 | 41B3 | 00:1B80 |
_OP5ToOP1 | 413B | 00:1A49 |
_OP5ToOP2 | 4126 | 00:1A17 |
_OP5ToOP3 | 4174 | 00:1AF4 |
_OP5ToOP4 | 412C | 00:1A27 |
_OP5ToOP6 | 4129 | 00:1A1F |
_OP6ToOP1 | 4135 | 00:1A3F |
_OP6ToOP2 | 4132 | 00:1A37 |
_OP6ToOP5 | 4141 | 00:1A53 |
_OutputExpr | 4BB2 | 03:4AF2 |
_PagedGet | 5023 | 00:17BB |
_PARSAHEAD | 4B4F | 34:5AA1 |
_PARSAHEADS | 4B4C | 34:5A9D |
_ParseInp | 4A9B | 38:5987 |
_PDspGrph | 48A3 | 04:7904 |
_PixelTest | 48B5 | 04:79E7 |
_Plus1 | 4069 | 00:2285 |
_PointCmd | 48B2 | 04:79B2 |
_PointOn | 4C39 | 04:4155 |
_PopMCplxO1 | 436F | 00:14BC |
_PopOP1 | 437E | 00:14EA |
_PopOP3 | 437B | 00:14DA |
_PopOP5 | 4378 | 00:14CA |
_PopReal | 4393 | 00:1512 |
_PopRealO1 | 4390 | 00:150F |
_PopRealO2 | 438D | 00:150A |
_PopRealO3 | 438A | 00:1505 |
_PopRealO4 | 4387 | 00:1500 |
_PopRealO5 | 4384 | 00:14FB |
_PopRealO6 | 4381 | 00:14F6 |
_PosNo0Int | 422E | 00:1DF7 |
_PowerOff | 5008 | 00:09E6 |
_PToR | 40F3 | 02:50BD |
_PushMCplxO1 | 43CF | 00:15A6 |
_PushMCplxO3 | 43C6 | 00:1594 |
_PushOP1 | 43C9 | 00:1599 |
_PushOP3 | 43C3 | 00:1581 |
_PushOP5 | 43C0 | 00:1573 |
_PushReal | 43BD | 00:155F |
_PushRealO1 | 43BA | 00:155C |
_PushRealO2 | 43B7 | 00:1554 |
_PushRealO3 | 43B4 | 00:154F |
_PushRealO4 | 43B1 | 00:154A |
_PushRealO5 | 43AE | 00:1545 |
_PushRealO6 | 43AB | 00:1540 |
_PutAway | 4039 | 00:08AF |
_PutC | 4504 | 01:5B4C |
_PutMap | 4501 | 01:5A98 |
_PutPS | 4510 | 01:5C73 |
_PutPSB | 450D | 01:5C52 |
_PutS | 450A | 01:5C39 |
_PutTokString | 4960 | 06:46FD |
_PutToL | 4645 | 02:4829 |
_PutToMat | 461E | 02:406C |
_RandInit | 4B7F | 36:7E8A |
_Random | 4B79 | 36:7DC9 |
_RclAns | 4AD7 | 38:679F |
_RclGDB2 | 47D1 | 33:72D9 |
_RclN | 4ADD | 38:67A9 |
_RclSysTok | 4AE6 | 38:683E |
_RclVarSym | 4AE3 | 38:67B1 |
_RclX | 4AE0 | 38:67AE |
_RclY | 4ADA | 38:67A4 |
_Rcl_StatVar | 42DC | 00:2149 |
_Rec1stByte | 4EFA | 3C:439C |
_Rec1stByteNC | 4EFD | 3C:43A3 |
_RecAByteIO | 4F03 | 3C:443F |
_RedimMat | 4A26 | 07:4D3B |
_Regraph | 488E | 04:6764 |
_ReleaseBuffer | 4771 | 03:47AC |
_ReloadAppEntryVecs | 4C36 | 3B:73E4 |
_RestartTimer | 5275 | 33:5E9D |
_RestoreDisp | 4870 | 04:6176 |
_RName | 427F | 00:1F4C |
_RndGuard | 409F | 02:6A57 |
_RnFx | 40A2 | 02:6A71 |
_Round | 40A8 | 00:2623 |
_RToD | 4078 | 00:2374 |
_RToP | 40F6 | 02:50DB |
_RunIndicOff | 4570 | 01:6531 |
_RunIndicOn | 456D | 01:6518 |
_SaveDisp | 4C7B | 39:5DD8 |
_SendAByte | 4EE5 | 3C:420D |
_SendPacket | 4ED6 | 3C:4139 |
_SendVarCmd | 4A14 | 3C:4EDD |
_SetAllPlots | 4FCC | 38:49C7 |
_setDate | 516A | 37:536E |
_SetExSpeed | 50BF | 00:0DCA |
_SetFuncM | 4840 | 36:7D11 |
_SetGetKeyHook | 4F66 | 3B:7D00 |
_SetNorm_Vals | 49FC | 00:220F |
_SetParM | 4849 | 36:7D39 |
_SetParserHook | 5026 | 3B:7D6E |
_SetPolM | 4846 | 36:7D2C |
_SetSeqM | 4843 | 36:7D1F |
_SetSilentLinkHook | 50CE | 3B:7DBB |
_SetTblGraphDraw | 4C00 | 00:00F5 |
_SetTokenHook | 4F99 | 3B:7D0B |
_setTime | 5170 | 37:540D |
_SetupPagedPtr | 5020 | 00:17AC |
_SetXXOP1 | 478C | 33:5F7E |
_SetXXOP2 | 478F | 33:5F83 |
_SetXXXXOP2 | 4792 | 33:5F9E |
_SetZeroOne | 5167 | 37:5359 |
_SFont_Len | 4786 | 03:4ABD |
_ShRAcc | 41D4 | 00:1BCB |
_Sin | 40BD | 02:7342 |
_SinCosRad | 40BA | 02:733E |
_SinH | 40CF | 02:7632 |
_SinHCosH | 40C6 | 02:7626 |
_SqRoot | 409C | 02:6E38 |
_SrchVLstDn | 4F12 | 07:71D7 |
_SrchVLstUp | 4F0F | 07:707F |
_SStringLength | 4CB4 | 3B:61A6 |
_StartTimer | 5272 | 33:5E58 |
_StMatEl | 4AE9 | 38:6C8F |
_StoAns | 4ABF | 38:6251 |
_StoGDB2 | 47CE | 33:71AC |
_StoN | 4ACB | 38:6274 |
_StoOther | 4AD4 | 38:62A9 |
_StopTimer | 5278 | 33:5F42 |
_StoR | 4AC5 | 38:6264 |
_StoRand | 4B7C | 36:7E06 |
_StoSysTok | 4ABC | 38:623B |
_StoT | 4ACE | 38:629B |
_StoTheta | 4AC2 | 38:625C |
_StoX | 4AD1 | 38:62A3 |
_StoY | 4AC8 | 38:626C |
_StrCopy | 44E3 | 00:2810 |
_StrLength | 4C3F | 36:7F91 |
_Tan | 40C3 | 02:734A |
_TanH | 40C9 | 02:762A |
_TanLnF | 48BB | 04:7A43 |
_TenX | 40B7 | 02:7066 |
_ThetaName | 427C | 00:1F48 |
_ThreeExec | 4675 | 02:64ED |
_timeCnv | 5179 | 37:56C4 |
_Times2 | 4066 | 00:2282 |
_TimesPt5 | 407E | 00:2382 |
_TName | 428E | 00:1F69 |
_ToFrac | 4657 | 02:4BBE |
_Trunc | 4060 | 00:2279 |
_UCLineS | 4795 | 33:6010 |
_UngroupVar | 50C8 | 39:764A |
_UnLineCmd | 48AF | 04:797C |
_UnOPExec | 4672 | 02:5E14 |
_VertCmd | 48A9 | 04:7955 |
_VPutMap | 455E | 01:6293 |
_VPutS | 4561 | 01:646D |
_VPutSN | 4564 | 01:644D |
_VtoWHLDE | 47FB | 04:4410 |
_WaitTimer | 527B | 33:5EA4 |
_XftoI | 4804 | 37:41EB |
_Xitof | 47FE | 04:441E |
_XName | 4288 | 00:1F61 |
_XRootY | 479E | 33:632E |
_YftoI | 4801 | 37:41DF |
_YName | 428B | 00:1F65 |
_YToX | 47A1 | 33:6340 |
_Zero16D | 41B0 | 00:1B6F |
_ZeroOP | 41CE | 00:1BBC |
_ZeroOP1 | 41C5 | 00:1BAF |
_ZeroOP2 | 41C8 | 00:1BB4 |
_ZeroOP3 | 41CB | 00:1BB9 |
_ZmDecml | 484F | 36:7BA4 |
_ZmFit | 485B | 36:7A57 |
_ZmInt | 484C | 04:5F85 |
_ZmPrev | 4852 | 04:5FFE |
_ZmSquare | 485E | 36:7ABE |
_ZmStats | 47A4 | 33:65DC |
_ZmTrig | 4861 | 36:7B36 |
_ZmUsr | 4855 | 04:601D |
_ZooDefault | 4867 | 36:7BF9 |
_lcd_busy | 4051 | 00:0CC3 |
Corpus coverage and map provenance
tools/ti84re/community/audit_bcalls.py scans the extracted community corpus for
numeric bcall macros, raw rst 28h plus word sequences, and complete
EF low high instruction bytes. The pinned snapshot contains 112 such uses of
56 IDs. Three uses encode _CursorOn, _KeyToString, and _CursorOff as raw
bytes; the other 109 use macro or rst 28h forms. Exact archive and source
hashes are in tools/data/community-bcall-uses.csv. [confirmed] for the
checked scan output.
The scan exposed 24 aligned main-table IDs that the old generated map omitted.
All 24 were already literal entries in the OS 2.55MP table on page 0x3B, and
all 24 names were already present in the bundled TI include. Adding them changed
the main map from 621 to 645 rows and the include-backed subset from 599 to 623;
the 22 project-inferred names did not change. They are therefore new to this
repository’s resolver input, not new ROM routines or newly invented names.
[confirmed]
The main table above is the single address catalog. ABI and side-effect notes
live with their subsystems: contexts and errors, display, floating point,
keypad, tokenizer, link transfer, execution protection, resident hooks,
archive/group handling, and ASIC status. Reduced trace results are kept in the
tools/data/community-*.csv files. This separation matters because an aligned
table entry proves only an ID-to-body mapping; neither an include name nor a
community comment proves the body’s complete behavior.
Malformed active call
The scan also finds one active malformed call. graphics/cool.zip:cool.asm
defines _copygbuf = 4B9Ch and emits EF 9C 4B; the packaged cool.8xp
contains the same bytes. 4B9Ch is not aligned to a three-byte main-table
entry. Page 0x3B supplies overlapping bytes 7A EF 5E, which requests
logical address 0xEF7A instead of a valid body. The TI-83 Plus _GrBufCpy
ID is 486Ah, with body 04:60A3.
A complete trace of the packaged cool.8xp enters at ram:9D95, executes the
malformed call at ram:9DAF, and reaches logical 0xEF7A once. It never
reaches _GrBufCpy at 04:60A3, _ExecutePrgm cleanup at 07:57D1, or
E_Invalid at ram:2729; it later reaches the OS handoff vector at
ram:0053. The final display is blank. This confirms a nonreturning malformed
dispatch under TilEm, not the physical-calculator failure mode. The trace and
artifact identities are in tools/data/community-invalid-bcall-traces.csv.
[confirmed] under TilEm; physical behavior remains [hypothesis].
The Cool archive SHA-256 is
1593afbeb85b831e910793d293963a9af52377bdf8af481300b13d1fa3b346f6.
Member cool.asm has SHA-256
6d05c43420f4514357803fc09221a49252d0147e172a12ebdd12b74aef001998;
member cool.8xp has SHA-256
ef2304ef5a731c1e4eee9f3602c1bd7c9ddb7c85427a037fbc7cecd18af0cd1c.
Preprocessor triage excludes four target-platform artifacts from the TI-84
Plus map. Math Pack’s 4166h _cpop1op2 equate is inside the non-TI83P
branch; its TI-83 Plus build obtains _CpOP1OP2 = 4111h. Eliza’s 4004h,
477Dh, and 4014h values come from source/ti83asm.INC, which only its
TI83I branch includes. Lock’s 5371h _getcsc is a direct call address in
the TI86 branch. None is an active TI-84 Plus bcall. [confirmed] for source
preprocessor structure.
tools/data/community-symbolic-bcall-triage.csv preserves every active row and
excluded platform case with archive/member hashes, line numbers, and the TI-84
Plus replacement ID where applicable.
Retail boot (0x8xxx) bcalls
The retail table has 87 populated entries across IDs 0x8018–0x80D2 and
0x80E4–0x8129. The full 2007 ti83plus.inc defines 83 of them; the four
snake-case rows below are project-inferred names for otherwise unnamed slots.
tools/ti84re/rom/resolve_bcalls.py emits the public-name target file only when page 3F
has the retail boot prefix, not when it sees BootFree. See
Retail boot page for the intervening dispatch
stub and evidence behind the inferred rows. [confirmed]
| bcall | ID | Body (page:addr) |
|---|---|---|
_MD5Final | 8018 | 3F:6964 |
_RSAValidate | 801B | 3F:6CB4 |
_cmpStr | 801E | 3F:7195 |
_WriteAByte | 8021 | 3F:4C9F |
_EraseFlash | 8024 | 3F:4C2A |
_FindFirstCertField | 8027 | 3F:4D62 |
_ZeroToCertificate | 802A | 3F:4DAF |
_GetCertificateEnd | 802D | 3F:4D53 |
_FindGroupedField | 8030 | 3F:4E8C |
_ret_1 | 8033 | 3F:4867 |
_ret_2 | 8036 | 3F:4867 |
_ret_3 | 8039 | 3F:4867 |
_ret_4 | 803C | 3F:4867 |
_ret_5 | 803F | 3F:4867 |
_Mult8By8 | 8042 | 3F:7059 |
_Mult16By8 | 8045 | 3F:705B |
_Div16By8 | 8048 | 3F:7146 |
_Div16By16 | 804B | 3F:7148 |
certificate_reconcile_id_fields | 804E | 3F:4924 |
_LoadAIndPaged | 8051 | 3F:486E |
_FlashToRam2 | 8054 | 3F:4888 |
_GetCertificateStart | 8057 | 3F:4D46 |
_GetFieldSize | 805A | 3F:4DB8 |
_FindSubField | 805D | 3F:4DFB |
_EraseCertificateSector | 8060 | 3F:4E3F |
_CheckHeaderKey | 8063 | 3F:4B4A |
certificate_find_matching_field_data | 8066 | 3F:4F91 |
certificate_count_matching_fields | 8069 | 3F:4EFF |
_Load_LFontV2 | 806C | 3F:7C8A |
_Load_LFontV | 806F | 3F:7C8A |
_ReceiveOS | 8072 | 3F:5DCE |
_FindOSHeaderSubField | 8075 | 3F:5018 |
_FindNextCertField | 8078 | 3F:4D5C |
_GetByteOrBoot | 807B | 3F:5C64 |
_getSerial | 807E | 3F:442F |
_ReceiveCalcID | 8081 | 3F:5EDC |
_EraseFlashPage | 8084 | 3F:4C1E |
_WriteFlashUnsafe | 8087 | 3F:4CA6 |
_dispBootVer | 808A | 3F:44F1 |
_MD5Init | 808D | 3F:68ED |
_MD5Update | 8090 | 3F:6907 |
_MarkOSInvalid | 8093 | 3F:5209 |
_FindProgramLicense | 8096 | 3F:4B1A |
_MarkOSValid | 8099 | 3F:51F5 |
_CheckOSValidated | 809C | 3F:52C6 |
_SetupAppPubKey | 809F | 3F:53CA |
_SigModR | 80A2 | 3F:7225 |
_TransformHash | 80A5 | 3F:723F |
_IsAppFreeware | 80A8 | 3F:52E1 |
_FindAppHeaderSubField | 80AB | 3F:500A |
_WriteValidationNumber | 80AE | 3F:540B |
_Div32By16 | 80B1 | 3F:706E |
_FindGroup | 80B4 | 3F:4E61 |
_getBootVer | 80B7 | 3F:477C |
_getHardwareVersion | 80BA | 3F:4781 |
_xorA | 80BD | 3F:5C6D |
_bignumpowermod17 | 80C0 | 3F:6CBD |
_ProdNrPart1 | 80C3 | 3F:6209 |
_WriteAByteSafe | 80C6 | 3F:4C9A |
_WriteFlash | 80C9 | 3F:4C8F |
_SetupDateStampPubKey | 80CC | 3F:548C |
_SetFlashLowerBound | 80CF | 3F:4784 |
_LowBatteryBoot | 80D2 | 3F:5834 |
_AttemptUSBOSReceive | 80E4 | 2F:4145 |
_DisplayBootMessage | 80E7 | 3F:6127 |
_NewLine2 | 80EA | 3F:73DD |
_DisplayBootError10 | 80ED | 3F:5789 |
_Chk_Batt_Low_B | 80F0 | 3F:6171 |
_Chk_Batt_Low_B2 | 80F3 | 3F:6163 |
_ReceiveOS_USB | 80F6 | 2F:48CA |
_DisplayOSProgress | 80F9 | 3F:62D0 |
_ResetCalc | 80FC | 3F:5ED3 |
_SetupOSPubKey | 80FF | 3F:5387 |
_CheckHeaderKeyHL | 8102 | 3F:4B4D |
_USBErrorCleanup | 8105 | 2F:5958 |
_InitUSB | 8108 | 2F:52A4 |
usb_set_port81_bit0_delay | 810B | 2F:62C5 |
_KillUSB | 810E | 2F:5961 |
_DisplayBootError1 | 8111 | 3F:63DB |
_DisplayBootError2 | 8114 | 3F:5789 |
_DisplayBootError3 | 8117 | 3F:5789 |
_DisplayBootError4 | 811A | 3F:5789 |
_DisplayBootError5 | 811D | 3F:5789 |
_DisplayBootError6 | 8120 | 3F:5789 |
_DisplayBootError7 | 8123 | 3F:5789 |
_DisplayBootError8 | 8126 | 3F:5789 |
_DisplayBootError9 | 8129 | 3F:5789 |
2-byte token tables
The second-byte tables for every 2-byte token on the TI-84 Plus (OS 2.55MP). The tokenizer detects a 2-byte lead via _IsA2ByteTok (00:1FE8) — see Tokenizer & TI-BASIC — then the second byte indexes the group below.
Names are sourced from the TI-Toolkit/tokens sheet (8X.xml) and filtered to tokens that exist on the monochrome 84+ (≤ 2.55MP): the TI-84+CSE/TI-84+CE color/Python-era 2-byte tokens are excluded. The Since column is the model that introduced each token. Token text is the English display form (the source also carries an ASCII-accessible spelling, e.g. >DMS for ►DMS).
492 two-byte tokens across 11 lead bytes. (Single-byte tokens are modeled by the TIToken enum — see Tokenizer & TI-BASIC.)
This file is generated by tools/ti84re/wiki/gen_token_tables.py; edit that, not this.
5C — Matrix names ([A]–[J])
10 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | [A] | TI-82 |
01 | [B] | TI-82 |
02 | [C] | TI-82 |
03 | [D] | TI-82 |
04 | [E] | TI-82 |
05 | [F] | TI-83 |
06 | [G] | TI-83 |
07 | [H] | TI-83 |
08 | [I] | TI-83 |
09 | [J] | TI-83 |
5D — List names (built-in L₁–L₆)
6 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | L₁ | TI-82 |
01 | L₂ | TI-82 |
02 | L₃ | TI-82 |
03 | L₄ | TI-82 |
04 | L₅ | TI-82 |
05 | L₆ | TI-82 |
5E — Equation variables (Y= functions, parametric, polar, sequence)
31 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
10 | Y₁ | TI-82 |
11 | Y₂ | TI-82 |
12 | Y₃ | TI-82 |
13 | Y₄ | TI-82 |
14 | Y₅ | TI-82 |
15 | Y₆ | TI-82 |
16 | Y₇ | TI-82 |
17 | Y₈ | TI-82 |
18 | Y₉ | TI-82 |
19 | Y₀ | TI-82 |
20 | X₁ᴛ | TI-82 |
21 | Y₁ᴛ | TI-82 |
22 | X₂ᴛ | TI-82 |
23 | Y₂ᴛ | TI-82 |
24 | X₃ᴛ | TI-82 |
25 | Y₃ᴛ | TI-82 |
26 | X₄ᴛ | TI-82 |
27 | Y₄ᴛ | TI-82 |
28 | X₅ᴛ | TI-82 |
29 | Y₅ᴛ | TI-82 |
2A | X₆ᴛ | TI-82 |
2B | Y₆ᴛ | TI-82 |
40 | r₁ | TI-82 |
41 | r₂ | TI-82 |
42 | r₃ | TI-82 |
43 | r₄ | TI-82 |
44 | r₅ | TI-82 |
45 | r₆ | TI-82 |
80 | u | TI-82 |
81 | v | TI-82 |
82 | w | TI-82 |
60 — Pictures (Pic1–Pic0)
10 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | Pic1 | TI-82 |
01 | Pic2 | TI-82 |
02 | Pic3 | TI-82 |
03 | Pic4 | TI-82 |
04 | Pic5 | TI-82 |
05 | Pic6 | TI-82 |
06 | Pic7 | TI-83 |
07 | Pic8 | TI-83 |
08 | Pic9 | TI-83 |
09 | Pic0 | TI-83 |
61 — Graph databases (GDB1–GDB0)
10 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | GDB1 | TI-82 |
01 | GDB2 | TI-82 |
02 | GDB3 | TI-82 |
03 | GDB4 | TI-82 |
04 | GDB5 | TI-82 |
05 | GDB6 | TI-82 |
06 | GDB7 | TI-83 |
07 | GDB8 | TI-83 |
08 | GDB9 | TI-83 |
09 | GDB0 | TI-83 |
62 — Statistics, regression, and output variables
60 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
01 | RegEQ | TI-82 |
02 | n | TI-82 |
03 | x̄ | TI-82 |
04 | Σx | TI-82 |
05 | Σx² | TI-82 |
06 | Sx | TI-82 |
07 | σx | TI-82 |
08 | minX | TI-82 |
09 | maxX | TI-82 |
0A | minY | TI-82 |
0B | maxY | TI-82 |
0C | ȳ | TI-82 |
0D | Σy | TI-82 |
0E | Σy² | TI-82 |
0F | Sy | TI-82 |
10 | σy | TI-82 |
11 | Σxy | TI-82 |
12 | r | TI-82 |
13 | Med | TI-82 |
14 | Q₁ | TI-82 |
15 | Q₃ | TI-82 |
16 | a | TI-82 |
17 | b | TI-82 |
18 | c | TI-82 |
19 | d | TI-82 |
1A | e | TI-82 |
1B | x₁ | TI-82 |
1C | x₂ | TI-82 |
1D | x₃ | TI-82 |
1E | y₁ | TI-82 |
1F | y₂ | TI-82 |
20 | y₃ | TI-82 |
21 | 𝑛 | TI-82 |
22 | p | TI-82 |
23 | z | TI-82 |
24 | t | TI-82 |
25 | χ² | TI-82 |
26 | 𝙵 | TI-82 |
27 | df | TI-82 |
28 | p̂ | TI-82 |
29 | p̂₁ | TI-82 |
2A | p̂₂ | TI-82 |
2B | x̄₁ | TI-82 |
2C | Sx₁ | TI-82 |
2D | n₁ | TI-82 |
2E | x̄₂ | TI-82 |
2F | Sx₂ | TI-82 |
30 | n₂ | TI-82 |
31 | Sxp | TI-82 |
32 | lower | TI-82 |
33 | upper | TI-82 |
34 | s | TI-82 |
35 | r² | TI-82 |
36 | R² | TI-82 |
37 | df | TI-82 |
38 | SS | TI-82 |
39 | MS | TI-82 |
3A | df | TI-82 |
3B | SS | TI-82 |
3C | MS | TI-82 |
63 — Window and system variables
56 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | ZXscl | TI-82 |
01 | ZYscl | TI-82 |
02 | Xscl | TI-82 |
03 | Yscl | TI-82 |
04 | UnStart | TI-82 |
05 | VnStart | TI-82 |
06 | U𝑛-₁ | TI-82 |
07 | V𝑛-₁ | TI-82 |
08 | ZUnStart | TI-82 |
09 | ZVnStart | TI-82 |
0A | Xmin | TI-82 |
0B | Xmax | TI-82 |
0C | Ymin | TI-82 |
0D | Ymax | TI-82 |
0E | Tmin | TI-82 |
0F | Tmax | TI-82 |
10 | θmin | TI-82 |
11 | θmax | TI-82 |
12 | ZXmin | TI-82 |
13 | ZXmax | TI-82 |
14 | ZYmin | TI-82 |
15 | ZYmax | TI-82 |
16 | Zθmin | TI-82 |
17 | Zθmax | TI-82 |
18 | ZTmin | TI-82 |
19 | ZTmax | TI-82 |
1A | TblStart | TI-82 |
1B | 𝑛Min | TI-82 |
1C | ZPlotStart | TI-82 |
1D | 𝑛Max | TI-82 |
1E | Z𝑛Max | TI-82 |
1F | 𝑛Start | TI-82 |
20 | Z𝑛Min | TI-82 |
21 | ΔTbl | TI-82 |
22 | Tstep | TI-82 |
23 | θstep | TI-82 |
24 | ZTstep | TI-82 |
25 | Zθstep | TI-82 |
26 | ΔX | TI-82 |
27 | ΔY | TI-82 |
28 | XFact | TI-82 |
29 | YFact | TI-82 |
2A | TblInput | TI-82 |
2B | 𝗡 | TI-83 |
2C | I% | TI-83 |
2D | PV | TI-83 |
2E | PMT | TI-83 |
2F | FV | TI-83 |
30 | P/Y | TI-83 |
31 | C/Y | TI-83 |
32 | w(𝑛Min) | TI-83 |
33 | Zw(𝑛Min) | TI-83 |
34 | PlotStep | TI-83 |
35 | ZPlotStep | TI-83 |
36 | Xres | TI-83 |
37 | ZXres | TI-83 |
7E — Graph-format and mode tokens
19 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | Sequential | TI-82 |
01 | Simul | TI-82 |
02 | PolarGC | TI-82 |
03 | RectGC | TI-82 |
04 | CoordOn | TI-82 |
05 | CoordOff | TI-82 |
06 | Connected | TI-82 |
07 | Dot | TI-82 |
08 | AxesOn | TI-82 |
09 | AxesOff | TI-82 |
0A | GridOn | TI-82 |
0B | GridOff | TI-82 |
0C | LabelOn | TI-82 |
0D | LabelOff | TI-82 |
0E | Web | TI-82 |
0F | Time | TI-82 |
10 | uvAxes | TI-82 |
11 | vwAxes | TI-82 |
12 | uwAxes | TI-82 |
AA — String variables (Str1–Str0)
10 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | Str1 | TI-83 |
01 | Str2 | TI-83 |
02 | Str3 | TI-83 |
03 | Str4 | TI-83 |
04 | Str5 | TI-83 |
05 | Str6 | TI-83 |
06 | Str7 | TI-83 |
07 | Str8 | TI-83 |
08 | Str9 | TI-83 |
09 | Str0 | TI-83 |
BB — Extended command page (2-byte commands)
232 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | npv( | TI-83 |
01 | irr( | TI-83 |
02 | bal( | TI-83 |
03 | ΣPrn( | TI-83 |
04 | ΣInt( | TI-83 |
05 | ►Nom( | TI-83 |
06 | ►Eff( | TI-83 |
07 | dbd( | TI-83 |
08 | lcm( | TI-83 |
09 | gcd( | TI-83 |
0A | randInt( | TI-83 |
0B | randBin( | TI-83 |
0C | sub( | TI-83 |
0D | stdDev( | TI-83 |
0E | variance( | TI-83 |
0F | inString( | TI-83 |
10 | normalcdf( | TI-83 |
11 | invNorm( | TI-83 |
12 | tcdf( | TI-83 |
13 | χ²cdf( | TI-83 |
14 | 𝙵cdf( | TI-83 |
15 | binompdf( | TI-83 |
16 | binomcdf( | TI-83 |
17 | poissonpdf( | TI-83 |
18 | poissoncdf( | TI-83 |
19 | geometpdf( | TI-83 |
1A | geometcdf( | TI-83 |
1B | normalpdf( | TI-83 |
1C | tpdf( | TI-83 |
1D | χ²pdf( | TI-83 |
1E | 𝙵pdf( | TI-83 |
1F | randNorm( | TI-83 |
20 | tvm_Pmt | TI-83 |
21 | tvm_I% | TI-83 |
22 | tvm_PV | TI-83 |
23 | tvm_𝗡 | TI-83 |
24 | tvm_FV | TI-83 |
25 | conj( | TI-83 |
26 | real( | TI-83 |
27 | imag( | TI-83 |
28 | angle( | TI-83 |
29 | cumSum( | TI-83 |
2A | expr( | TI-83 |
2B | length( | TI-83 |
2C | ΔList( | TI-83 |
2D | ref( | TI-83 |
2E | rref( | TI-83 |
2F | ►Rect | TI-83 |
30 | ►Polar | TI-83 |
31 | 𝑒 | TI-83 |
32 | SinReg | TI-83 |
33 | Logistic | TI-83 |
34 | LinRegTTest | TI-83 |
35 | ShadeNorm( | TI-83 |
36 | Shade_t( | TI-83 |
37 | Shadeχ²( | TI-83 |
38 | Shade𝙵( | TI-83 |
39 | Matr►list( | TI-83 |
3A | List►matr( | TI-83 |
3B | Z-Test( | TI-83 |
3C | T-Test | TI-83 |
3D | 2-SampZTest( | TI-83 |
3E | 1-PropZTest( | TI-83 |
3F | 2-PropZTest( | TI-83 |
40 | χ²-Test( | TI-83 |
41 | ZInterval | TI-83 |
42 | 2-SampZInt( | TI-83 |
43 | 1-PropZInt( | TI-83 |
44 | 2-PropZInt( | TI-83 |
45 | GraphStyle( | TI-83 |
46 | 2-SampTTest | TI-83 |
47 | 2-Samp𝙵Test | TI-83 |
48 | TInterval | TI-83 |
49 | 2-SampTInt | TI-83 |
4A | SetUpEditor | TI-83 |
4B | Pmt_End | TI-83 |
4C | Pmt_Bgn | TI-83 |
4D | Real | TI-83 |
4E | r𝑒^θ𝑖 | TI-83 |
4F | a+b𝑖 | TI-83 |
50 | ExprOn | TI-83 |
51 | ExprOff | TI-83 |
52 | ClrAllLists | TI-83 |
53 | GetCalc( | TI-83 |
54 | DelVar | TI-83 |
55 | Equ►String( | TI-83 |
56 | String►Equ( | TI-83 |
57 | Clear Entries | TI-83 |
58 | Select( | TI-83 |
59 | ANOVA( | TI-83 |
5A | ModBoxplot | TI-83 |
5B | NormProbPlot | TI-83 |
64 | G-T | TI-83 |
65 | ZoomFit | TI-83 |
66 | DiagnosticOn | TI-83 |
67 | DiagnosticOff | TI-83 |
68 | Archive | TI-83+ |
69 | UnArchive | TI-83+ |
6A | Asm( | TI-83+ |
6B | AsmComp( | TI-83+ |
6C | AsmPrgm | TI-83+ |
6E | Á | TI-83+ |
6F | À | TI-83+ |
70 | Â | TI-83+ |
71 | Ä | TI-83+ |
72 | á | TI-83+ |
73 | à | TI-83+ |
74 | â | TI-83+ |
75 | ä | TI-83+ |
76 | É | TI-83+ |
77 | È | TI-83+ |
78 | Ê | TI-83+ |
79 | Ë | TI-83+ |
7A | é | TI-83+ |
7B | è | TI-83+ |
7C | ê | TI-83+ |
7D | ë | TI-83+ |
7F | Ì | TI-83+ |
80 | Î | TI-83+ |
81 | Ï | TI-83+ |
82 | í | TI-83+ |
83 | ì | TI-83+ |
84 | î | TI-83+ |
85 | ï | TI-83+ |
86 | Ó | TI-83+ |
87 | Ò | TI-83+ |
88 | Ô | TI-83+ |
89 | Ö | TI-83+ |
8A | ó | TI-83+ |
8B | ò | TI-83+ |
8C | ô | TI-83+ |
8D | ö | TI-83+ |
8E | Ú | TI-83+ |
8F | Ù | TI-83+ |
90 | Û | TI-83+ |
91 | Ü | TI-83+ |
92 | ú | TI-83+ |
93 | ù | TI-83+ |
94 | û | TI-83+ |
95 | ü | TI-83+ |
96 | Ç | TI-83+ |
97 | ç | TI-83+ |
98 | Ñ | TI-83+ |
99 | ñ | TI-83+ |
9A | ´ | TI-83+ |
9B | ` | TI-83+ |
9C | ¨ | TI-83+ |
9D | ¿ | TI-83+ |
9E | ¡ | TI-83+ |
9F | α | TI-83+ |
A0 | β | TI-83+ |
A1 | γ | TI-83+ |
A2 | Δ | TI-83+ |
A3 | δ | TI-83+ |
A4 | ε | TI-83+ |
A5 | λ | TI-83+ |
A6 | μ | TI-83+ |
A7 | π | TI-83+ |
A8 | ρ | TI-83+ |
A9 | Σ | TI-83+ |
AB | Φ | TI-83+ |
AC | Ω | TI-83+ |
AD | p̂ | TI-83+ |
AE | χ | TI-83+ |
AF | 𝙵 | TI-83+ |
B0 | a | TI-83+ |
B1 | b | TI-83+ |
B2 | c | TI-83+ |
B3 | d | TI-83+ |
B4 | e | TI-83+ |
B5 | f | TI-83+ |
B6 | g | TI-83+ |
B7 | h | TI-83+ |
B8 | i | TI-83+ |
B9 | j | TI-83+ |
BA | k | TI-83+ |
BC | l | TI-83+ |
BD | m | TI-83+ |
BE | n | TI-83+ |
BF | o | TI-83+ |
C0 | p | TI-83+ |
C1 | q | TI-83+ |
C2 | r | TI-83+ |
C3 | s | TI-83+ |
C4 | t | TI-83+ |
C5 | u | TI-83+ |
C6 | v | TI-83+ |
C7 | w | TI-83+ |
C8 | x | TI-83+ |
C9 | y | TI-83+ |
CA | z | TI-83+ |
CB | σ | TI-83+ |
CC | τ | TI-83+ |
CD | Í | TI-83+ |
CE | GarbageCollect | TI-83+ |
CF | ~ | TI-83+ |
D1 | @ | TI-83+ |
D2 | # | TI-83+ |
D3 | $ | TI-83+ |
D4 | & | TI-83+ |
D5 | ` | TI-83+ |
D6 | ; | TI-83+ |
D7 | \ | TI-83+ |
D8 | | | TI-83+ |
D9 | _ | TI-83+ |
DA | % | TI-83+ |
DB | … | TI-83+ |
DC | ∠ | TI-83+ |
DD | ß | TI-83+ |
DE | ˣ | TI-83+ |
DF | ᴛ | TI-83+ |
E0 | ₀ | TI-83+ |
E1 | ₁ | TI-83+ |
E2 | ₂ | TI-83+ |
E3 | ₃ | TI-83+ |
E4 | ₄ | TI-83+ |
E5 | ₅ | TI-83+ |
E6 | ₆ | TI-83+ |
E7 | ₇ | TI-83+ |
E8 | ₈ | TI-83+ |
E9 | ₉ | TI-83+ |
EA | ₁₀ | TI-83+ |
EB | ◄ | TI-83+ |
EC | ► | TI-83+ |
ED | ↑ | TI-83+ |
EE | ↓ | TI-83+ |
F0 | × | TI-83+ |
F1 | ∫ | TI-83+ |
F2 | 🡁 | TI-83+ |
F3 | 🠿 | TI-83+ |
F4 | √ | TI-83+ |
F5 | ⌸ | TI-83+ |
EF — TI-84 Plus extended tokens
48 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | setDate( | TI-84+ |
01 | setTime( | TI-84+ |
02 | checkTmr( | TI-84+ |
03 | setDtFmt( | TI-84+ |
04 | setTmFmt( | TI-84+ |
05 | timeCnv( | TI-84+ |
06 | dayOfWk( | TI-84+ |
07 | getDtStr( | TI-84+ |
08 | getTmStr( | TI-84+ |
09 | getDate | TI-84+ |
0A | getTime | TI-84+ |
0B | startTmr | TI-84+ |
0C | getDtFmt | TI-84+ |
0D | getTmFmt | TI-84+ |
0E | isClockOn | TI-84+ |
0F | ClockOff | TI-84+ |
10 | ClockOn | TI-84+ |
11 | OpenLib( | TI-84+ |
12 | ExecLib | TI-84+ |
13 | invT( | TI-84+ |
14 | χ²GOF-Test( | TI-84+ |
15 | LinRegTInt | TI-84+ |
16 | Manual-Fit | TI-84+ |
17 | ZQuadrant1 | TI-84+ |
18 | ZFrac1⁄2 | TI-84+ |
19 | ZFrac1⁄3 | TI-84+ |
1A | ZFrac1⁄4 | TI-84+ |
1B | ZFrac1⁄5 | TI-84+ |
1C | ZFrac1⁄8 | TI-84+ |
1D | ZFrac1⁄10 | TI-84+ |
1E | ⬚ | TI-84+ |
2E | ⁄ | TI-84+ |
2F | | TI-84+ |
30 | ►n⁄d◄►Un⁄d | TI-84+ |
31 | ►F◄►D | TI-84+ |
32 | remainder( | TI-84+ |
33 | Σ( | TI-84+ |
34 | logBASE( | TI-84+ |
35 | randIntNoRep( | TI-84+ |
37 | MATHPRINT | TI-84+ |
38 | CLASSIC | TI-84+ |
39 | n⁄d | TI-84+ |
3A | Un⁄d | TI-84+ |
3B | AUTO | TI-84+ |
3C | DEC | TI-84+ |
3D | FRAC | TI-84+ |
3F | STATWIZARD ON | TI-84+ |
40 | STATWIZARD OFF | TI-84+ |
Open questions and roadmap
The major ROM and hardware subsystems are mapped well enough to support focused follow-up work. This page records the audit boundary and the evidence needed to resolve the remaining behavior. Detailed reconstructions and emulator comparisons remain on the subsystem pages linked below.
Static-analysis work
Symbol types
The Ghidra model assigns TIKeyCode to kbdKey, kbdGetKy, and keyExtend
(0x8444–0x8446), and TIError to errNo (0x86DD). TIVarType remains
on curType and varType. stat_calc_command remains inside the typed
SystemFlags span. [confirmed]
Floating-point table semantics
The _SinCosRad recurrence is mechanically reconstructed. Phase 1 extracts one
redundant BCD digit per row of 02:7201 by non-restoring modulo-1 subtraction
or addition of the row aligned at $10^{-(k+1)}$. Phase 2 builds
$b_0\cdot\prod_k(1+10^{-2k})^{\lfloor(11-d_k)/2\rfloor}$ from the digits. The
row values approach $1 - s^2/3$ for the aligned scale $s = 10^{-(k+1)}$, but
they do not reduce to a clean rotation identity. This suggests tuned or
truncated constants. [confirmed]
Remaining: the closed-form interpretation of the phase-1 digit map — which
function of the reduced argument the digit string represents, and how phase 3
combines it with the phase-2 product and the residual to assemble the result.
A second traced input (e.g. sin(0.5), digits 3,9,9,3,8,4,4,2, residual
$3.81\times10^{-9}$) is available to constrain the fit. See
Floating point.
Graph raster details
Find a natural flag state that routes Circle( through _DrawCirc2, then
compare its 60 emitted segments with the statically decoded schedule. Separately,
find and trace a natural caller of _GrphCirc, adding a direct-call interval
boundary to the current _CircCmd trace reducer. Extend the function-mode
traces to thick, shade, animate, and dotted styles; Xres>1; multiple selected
equations; and polar, parametric, and sequence modes. The coordinate rounding,
two ordinary function witnesses, and the clear-flag page-33 Circle path are
pinned in Graphing.
TABLE evaluation
Driver 05:6205 loops over seven visible rows and calls bcall ID 4741h →
35:7C7C for each row. The _ParseInp region executes once per row; _StoX
does not execute during the fill. The Ask-mode bodies are also decoded.
Indpnt=Ask prompts through the editor at 05:7303, with an OPS continuation
at 05:7329, and finalizes the row through the value-cache shift at 05:6032.
Depend=Ask evaluates one requested cell through 05:637C, with an OPS
continuation at 05:644E. Both mode tests honor an override check at 05:74BE.
The TblRng validation at 38:72DA and 38:7260 reduces to parse-boundary
checking: the range variable must be followed by a legal statement delimiter.
[confirmed]
No remaining items for this subsystem.
Statistics command families
The regression r/r² cluster is byte-pinned: 3A:6845–3A:6891 forms
r = num/den, stores it to Corr (_Sto_StatVar id 0x12), accumulates a
column-weighted residual sum over the augmented matrix, and — when the
denominator is nonzero — stores r² (id 0x35, slot 0x8C05) or R²
(id 0x36, slot 0x8C0E). [confirmed]
The STAT-TESTS engine occupies 3A:4A00–3A:7E60. A raw operand scan finds
about 50 candidate PStat–SStat references in that window. Byte-pinned
evidence includes a T-Test output stage at 3A:5500 that stores TStat (id
0x24), the Zelen–Severo normal-tail coefficient table at
3A:554F–3A:5584, and the test-editor descriptor tables at
3A:7D00–3A:7E60. The normalcdf( evaluation reaches the page-39
floating-point core at 39:4A02–39:4F5B and its helpers. [confirmed]
Remaining: the per-test entry addresses (the parser’s execution dispatch into
the page-3A engine — the page-38 table is parse-side only), the
menu-slot mapping of the 3A:7DF4 pointer array, and the algorithm identity
of the page-39 core. See Statistics.
MathPrint runtime paths
The action byte entering eqdisp_layout_main (39:4F9A) is a raw TI key code:
kLeft opens the backward-walk path (CP 2 at 39:5048) and kAlphaDown
opens window advance (CP 8 at 39:507C), which loops CALL 39:5167. The
kAlphaUp/kAlphaDown codes come from a translator at 39:53A1 — get-key
variant bcall ID 4A68h, compare against 0xFB, state byte 0x8446 selects up
vs down. In-slot character scrolling bypasses eqdisp_layout_main entirely, as
does nested-template insertion. [confirmed]
Remaining: make get-key return 0xFB inside a template editor state — neither
sequential ALPHA-then-arrow keystrokes nor overlapping press/release chords do.
Once it does, 39:5167, its callees 39:5949/39:5B10/39:5B1D, and the
saved-operand dispatch through 39:59E0/39:59F9 to _FindAlphaUp/
_FindAlphaDn become traceable; arbitrary VAT sequences and the two extension
bytes in the page-07 11-byte OP scratch registers also remain open. See
Equation display.
Matrix and list paths
Plain augment( enters the partial-pivoting engine at 02:4663 but never
eliminates: the 0x91 branch sets carry (02:6361 SCF, restored by the
POP AF at 02:6378), and the engine gates its elimination body on that flag
(46DA POP AFJR C,46EF). The elimination pass belongs to the statistics
regression path, which enters the same dispatcher through 3A:6398. [confirmed]
The randM( fill is decoded: 02:5CC1–02:5CE6 computes int(19·rand)−9 per
cell, drawing from _Random (36:7DC9) through the page 0 banked-call stub
at ram:392D. The ref( driver dispatches at 02:609A via bcall ID 4B85h
→ 35:7995; the rref( executor runs through bcall ID 4B88h → 02:7C23
from page-38 stubs at 38:514F/38:5157; SortA(/SortD( share one body at
02:652F with direction discriminators 0x0E/0x10. The seq( collection is
traced per element: entry 37:6E87, expression eval through the standard
parser, element append via 02:69BC → 37:4260–37:4285, list growth via page-07
VAT routines, final _CreateRList through 37:70DC. [confirmed]
No remaining items for this subsystem.
Parser and archive residuals
The Asm(/AsmPrgm setup before the ram:9D95 payload handoff is
byte-pinned at 07:5762–57D4: _ChkFindSym, size checks against 0x2000,
_InsertMem growth of userMem, LDIR payload copy, USB port-0x20 state
save, cleanup handler 0x5800, and the 07:57FD jump to 0x9D95. The entry
gate compares the second body byte to 6D (07:5772, FE 6D), while working
fixtures emit AsmPrgm as BB 6C and still reach the payload — reconciling
the gate byte remains open. [confirmed]
Remaining: the meaning of the loop-record state word (it varies per fixture:
0012h in one trace, 0007h in another) and the per-iteration split between
parse_end_ops_record re-entry and direct continuation jumps for While/
Repeat. The record shapes themselves are pinned: all three loops share the
5-byte form 00 | continuation word | state word, with For( continuations
38:5836/38:587D and the While/Repeat runtime continuation 38:57E7.
[confirmed]
Also open: a direct assembly-to-TI-BASIC program-call entry beyond VAT lookup
and the cooperative Ans callback. The generated ZZRUN negative probe
resolves prgmOO, sets the observed parser interval, and enters 38:6910, but
the carry-guarded run terminates at _ErrSyntax (ram:2700) with the cursor
inside the target body. An 80-byte layout of the same probe ended at
_ErrArgument (ram:2711), so the terminal error depends on state outside the
copied name and cursor interval. The remaining gap is the native caller’s
stack, error-handler, FPS/OPS, and run-state setup around that private entry.
[confirmed]
The group receive path is resolved. Receiving a .8xg stores each member as an
individual variable through the standard link variable-receive loop; no
0x17 object or page-07 guard is involved. A mixed group confirms that the
receiver honors the archive attribute per member. HELLO lands on Flash page
08, while FACTOR remains in RAM. Invoking the archived program takes the
page-byte guard to ERR:ARCHIVED. See
Variables, archive and unarchive.
[confirmed]
Resident-runtime experiments
The compiled Asm( launcher, its 0x2000 internal-size cap, pointer repair,
archived lookup, scratch-buffer observations, and normal bank-A bcall restore
are documented. The following cases remain unresolved:
- Repeat the completed direct, unarchived compiled-launch heap snapshot under
_ExecAsm, archived OS paths, and shell paths with boundary-size programs. - Isolate scratch-buffer writes by bcall. Complete a successful
_DisableApd/_DelResguard run through_GetKey, ON-key handling, an OS error, APD, archive collection, link/USB, and shell interrupts. - Run RAM-selector
0x83guards through editor, graph, table, statistics, App, archive-GC, and transfer contexts. Probe selectors0x84–0x87on identified 48 KiB and 128 KiB calculators. - Measure execution-protection ports and reset behavior on each ASIC. Recover Fullrene from an original artifact and test instruction fetch, operand read, stack access, and block copy at the same physical addresses.
- Repeat the direct-
Asm(maximum-AppVar measurement with representative VAT states. Measure the same limit under shell move loaders and one- and multi-page Flash Apps. - Force Flash-page, sector, and garbage-collection crossings while streaming an archived object. Reacquire its VAT result after each moving operation.
- Run one instrumented self-modifying payload under Ion, MirageOS, Doors CS, and zStart. Record peak RAM, self-lookup bytes, normal/error/forced-exit writeback, interrupt state, page selectors, and scratch restoration.
- Interrupt a two-slot AppVar update at each create, write, archive, garbage-collection, and delete boundary. Verify that startup selects the last committed generation.
Physical-hardware work
The emulator pages distinguish ROM behavior from TilEm, Wabbitemu, and MAME behavior. The items below require calculator measurements; emulator agreement does not close them.
Flash commands and interrupted collection
The Flash workers, top-boot geometry, archive-sector rotation, certificate-sector journal, and all six ROM-written collector phases are reconstructed. Cold TilEm and pinned Wabbitemu restart tests cover each phase, but do not establish physical timing or power-loss guarantees. [confirmed]
On physical calculators:
- measure legal and illegal
0→1programming, DQ toggle cadence, program and erase durations, erase suspend, and busy reads at all four top-boot boundaries; - force DQ5 failure with
DEfirst in Flash and then in restoring scratch RAM to test the worker’s undocumented reset write; and - cut power at each phase boundary and during active program and erase commands.
The reconstructed paths and emulator results are in Flash memory and Variables, archive and unarchive.
Two-wire link and USB
Determine why the legacy backup path normalizes the restored system-flags word
at 0x89F0 to 0x0063. The destination, section bounds, checksum coverage, and
affected indexed bits are pinned; the unresolved question is whether the value
is a canonical post-restore state or a compatibility state. [confirmed]
On physical calculators:
- measure port-
0x00pull-ups, thresholds, rise times, both CPU-speed timeout durations, and the both-low pulse’s voltage and duration; - run the read-only USB snapshot on identified TA2 and TA3 units, connected and
disconnected, before testing ports
0x49,0x51, and0x52; - test the FDRC-family register hypothesis, port
0x4B, and the port-0x4F/0x50setup sequence; - capture port-
0x5Apresentation traffic to test the proposed two-byte endpoint-2 LCD packets and host-mode dependency; and - exercise endpoint payload transfer and the connected boot receive path.
The prepared raw two-wire link probe measures digital settling but not analog voltage or pull-up behavior. See Two-wire link port hardware, Link transfer, and USB ASIC and link assist.
MD5 accelerator
Run the MD5 edge probe on TA2 and TA3 units. Add reset-retention and I/O wait-state measurements. The valid 64-step path, boot API, TilEm and Wabbitemu models, and MAME’s missing port block are already reconciled in MD5 accelerator and boot API.
Memory-mapper overlays
Determine whether ports 0x27 and 0x28 remain active in paired mode, whether
the 0xFB64 cutoff exists in the ASIC, and whether forced
execution-protection overlays
follow the underlying window or a forced RAM page. The boot transition, selector
modes, and emulator differences are in Paging.
Bus timing and LCD controller
On TA2 and TA3 controller revisions:
- measure every port-
0x2Eaccess class, CPU-speed readback, and actual clock frequency; - characterize port-
0x2Dlow-power behavior and the port-0x2Fmode-3 timer prescaler; - measure LCD read-versus-write ready timing and controller-specific minimum delays; and
- test hidden-column bounds and status-read pointer behavior.
Use the memory-bus timing probe, prefix-M1 probe, and programmable-timer probe. The established decode and emulator differences are in Bus timing and wait states and LCD controller and display bus.
ASIC identity, RAM, protection, and GPIO
Run the battery-level probe and
raw battery-selector probe
through upward and downward voltage sweeps on TA1, TA2, and TA3 units. Correlate
the RAM alias probe results and port-0x15
byte with PCB date and ASIC marking rather than assuming that a TA label fixes
RAM capacity.
Also test protected-register readback and high-value behavior, violation and
warm/cold reset behavior, the execution-fetch suite,
port-0x39 direction polarity, and port-0x3A electrical signals. See
ASIC status, identity, protection, and GPIO and
Execution protection.
Timers, keypad, and interrupts
On TA2 and TA3 units:
- distinguish timer divisors
33/328/3277from the emulator values32/327/3276; - test the port-
0x2Fprescaler, counter zero, first- versus second-expiry status bit 2, programmable-timerHALTbehavior, disabled RTC reads, and rollover coherence; - run the keypad settling probe with worst-case chords, then measure switch bounce and ON-key edges separately;
- determine which ON and link transitions wake low power; and
- test whether simultaneous legacy requests are coalesced by the port-
0x03clear-on-zero sequence and which timer configurations wakeHALT.
See Clock, timers, and power, Keypad and ON-key hardware, and Interrupts.
Closed audit boundary
The ROM-wide I/O census is complete. It classifies all 35 aligned
non-descriptor immediate candidates and all 37 register/block-I/O opcode pairs;
no unresolved immediate or computed-C candidate remains. [confirmed]
Reproduction paths
Static work can extend the headless pipeline in tools/ and rebuild with
tools/build.sh. Regions the decompiler leaves unanalyzed should be reduced
from raw bytes and reconciled with the generated database. Hardware work should
use the restoring probes in Hardware probes and record the
calculator revision, PCB date, and ASIC marking with each result.