Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 8287 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:

MechanismRole
Paging and bcallsReach code and data outside the current 64 KiB address space.
Floating-point engineStore real and complex values in the OP1OP6 registers and perform arithmetic.
Variable Allocation Table (VAT)Catalog named reals, lists, matrices, strings, programs, and AppVars.
Tokenizer and parserStore 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.

PageSubsystem
Memory mapAddress space, ports, and RAM layout
Flash memoryFlash geometry, protection, command sequences, boot write APIs, archive traces, and emulator differences
PagingPaired and independent Flash/RAM mapping, extended selectors, boot transition, and forced overlays
Bus timing and wait statesCPU-speed-selected Flash, RAM, LCD, and timer wait-state registers
ASIC status, identity, protection, and GPIOASIC status and identity, battery comparison, protection mode, and GPIO
The bcall mechanismrst 28h system calls and the jump table
InterruptsIM1 entry, USB and legacy routing, masks, status, acknowledgement, priority, and wake
Clock, timers, and powerClock domains, programmable timer API, RTC, APD cadence, and power-off
MD5 accelerator and boot APIMD5-assist ports, boot digest API, round descriptors, and Rabin hash transformation
Variables and the VATVariable Allocation Table and object types
Floating-point engineBCD floating-point format and OP registers
Tokenizer and TI-BASIC tokensToken tables, parser, and interpreter
Display and LCDLCD ports and screen buffers
Keyboard and link portKeyboard and link overview
Keypad and ON-key hardwareMatrix electrical behavior, scan timing, debounce, repeat, ON interrupts, and wake
Subsystem mapBcall API surface and the system through-line
Boot, contexts, and errorsBoot, the context system, _JError, and onSP
Memory managementRAM heap, VAT, userMem, Flash archive, and garbage collection
Flash page mapContents of each of the 64 Flash pages
RAM pagesRAM page selectors, page 83, and restore rules
Open questions and roadmapPrioritized 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~bcallsRepresentative 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 cryptography5_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

  1. Interrupt keeps time, scans the keypad into kbdScanCode, runs APD.
  2. _GetKey turns scan codes into key codes (TIKeyCode), driving menus and the homescreen.
  3. The parser reads tokenized input/programs, dispatching each TIToken.
  4. Number tokens → FP engine (OP1–OP6, BCD); name tokens → VAT (_FindSym).
  5. Results land in OP1 and are rendered by the display subsystem.
  6. 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

  1. System overview introduces the machine, OS, and evidence model.
  2. Subsystem map shows the major services and their dependencies.
  3. Memory map, Paging, The bcall mechanism, and Interrupts cover the shared architecture.
  4. 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.
  5. Glossary for any unfamiliar term.

Address notation

  • pp:addr — Flash page pp (003F) and logical address addr. Banked pages run in the 0x40000x7FFF window, so _PutS at 01:5C39 means page 01, address 0x5C39.
  • ram:addr — page 0 (the always-mapped kernel) and the RAM window; Ghidra keeps page 0 in its ram space, so ram:229E00:229E.
  • Ghidra’s overlay space writes flash addresses as page_pp:addr (e.g. page_38:4000); the wiki normalizes these to the short pp:addr form, so page_38:4000 is written 38:4000.
  • A bare 0x…. (no page) is a RAM data address or an unpaged value (e.g. flags 0x89F0, the bcall-ID ranges 0x4xxx/0x8xxx, a page number like 0x3B).
  • 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:

FlagMeaning
[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 (from ti83plus.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 as findsym_scan or fp_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.txt contains function entries. Its importer disassembles the entry and creates a function.
  • tools/symbols/labels.txt contains ROM data and internal code-entry labels. Rows marked entry seed and preserve disassembly without creating an overlapping function.
  • tools/symbols/ram.txt contains RAM symbols, including official SDK equates and carefully named inferred state.
  • tools/symbols/ports.txt contains I/O-port symbols.
  • tools/symbols/poffsets.txt contains reviewed base-plus-offset references. These make an operand such as mathprintArenaState + 0x0D render 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 at 4000), 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 --check validates the exact ignored base-ROM and AppVar hashes without writing output. Its reusable tools/ti84re/rom/assembly.py library 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 the D84PBE1.8Xv page-3F payload byte for byte. Only D84PBE2.8Xv, installed at page 2F, 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 the 0x4xxx table — 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. 0x8xxx bcall IDs index 87 populated retail boot-table entries on page 3F; several USB entries target page 2F. The local rom.bin is assembled from the patched base plus the retail D84PBE1.8Xv and D84PBE2.8Xv payloads. tools/symbols/bcalls8x_targets.txt contains the 83 byte-resolved bodies with public SDK names; the remaining four entries have project-inferred names. The resolver rejects these targets when page 3F has 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 0x000x2E and 0x300x3E are identical. Their pages 0x2F and 0x3F provide different boot support. [confirmed]

ImageSHA-256Page 0x2FPage 0x3F
Canonical retail analysis image7d9a7d96d89fc552ebee6afdbdd011fdc6047be9c16d308245dff07eb1f7bd6dD84PBE2 USB boot pageRetail boot 1.03
BootFree runtime-trace imagedbb47afae091ab36f9abe74e32083013fbeff3d7e0516bbf5d1abf4ee57adc09Patched-base pageBootFree 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 0x000x3E. 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]

StepRetail boot 1.03BootFree 11.259
Reset stubWrites ports 0x04, 0x06, and 0x0E, then jumps to 0x812CMaps page 0x3F through ports 0x06 and 0x07, then jumps to 0x812C
Installed-OS testScans the keypad; DEL and STAT select recovery; otherwise tests byte 0x0038 and marker 0xA55A at 0x0056Tests only marker 0xA55A at 0x0056; it does not scan a recovery key
Missing or rejected OSEnters serial or USB-assisted recovery and can receive an OSDisplays No OS Loaded and halts
Boot servicesCertificate, validation, serial receive, USB receive, installer display, and error pathsSmaller Flash/certificate utility set; signature, receive, USB, and most installer-display entries are stubs

The retail reset and installed-OS branches are at 3F:40003F:400C and 3F:420B3F:4308. The corresponding BootFree branches are at 3F:40003F:4006 and 3F:412C3F: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

TermMeaning
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.
bjumpOS-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 shortcutA 1-byte rst NN vector that fast-paths a hot routine (rst 10h=_FindSym, rst 30h=_FPAdd, rst 28h=the bcall dispatcher).
contextThe 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 / bankingThe Z80 sees 64 KiB; ports 6/7 swap which 16 KiB flash/RAM page is visible in the two middle slots. See Paging.
APDAuto Power Down — the standard-timer-driven idle shutoff. See Clock, timers, and power.
RTCReal-time clock — a 32-bit seconds counter with an epoch of 1 January 1997, exposed through ports 0x400x48.
programmable timerOne of three independent source/mode/counter blocks at ports 0x300x38; distinct from the two standard interrupt timers.
MathPrintThe 2D “pretty-print” rendering of expressions; on this OS the engine is on page 0x39.

Floating point

TermMeaning
BCDBinary-Coded Decimal — numbers stored as decimal digits (2 per byte), the format of all TI floats.
TIFloatThe 9-byte float: 1 type/sign byte, 1 biased exponent, 7 bytes = 14 BCD mantissa digits. See Floating-Point Engine.
OP1OP6The six 11-byte floating-point accumulator registers in RAM at 0x8478+. OP1 is the primary accumulator; binary ops use OP1+OP2, result in OP1.
FPSFloating-Point Stack — a software stack (pointer at 0x9824) for spilling OP registers during nested evaluation.
guard digitsThe 2 extra mantissa bytes past the 9-byte number (OP1EXT/OP2EXT), used for rounding during math.

Variables and memory

TermMeaning
VATVariable Allocation Table — the RAM catalog of every named object, growing down from symTable (0xFE66). See Variables & the VAT.
object typeThe 1-byte type tag of a variable (RealObj=0, ListObj=1, ProgObj=5, AppVarObj=0x15…), modeled as the TIVarType enum.
archiveVariables relocated to Flash to save RAM; the VAT entry’s page byte then points into Flash. See Variables, archive & unarchive and Flash memory.
Flash pageA 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 sectorThe 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 collectionCompacting 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 heapThe dynamic region from userMem (0x9D95) up to the VAT; managed by _InsertMem/_DelMem. See Memory Management.

Registers and RAM symbols

SymbolAddrMeaning
IY(reg)Held at flags (0x89F0) almost everywhere, so (IY+off) indexes the SystemFlags bitfield.
flags0x89F0The IY-indexed system flag area (SystemFlags struct).
OP10x8478Primary FP accumulator.
FPS0x9824Floating-point stack pointer.
onSP0x85BCSP saved at context/parse start; _JError unwinds to it (try/catch).
symTable0xFE66Top of RAM; the VAT grows down from here.
kbdScanCode0x843FLast keypad scan code (filled by the ISR, read by _GetCSC).
plotSScreen0x9340The 768-byte graph/display buffer (96×64).
parsePtr / parseEnd0x965D / 0x965FThe TI-BASIC parser’s token-stream cursor.

Conventions

  • Addresses: written pp:addr where pp is the flash page (003F) — e.g. 3D:6745. Page 0 (the always-mapped kernel) is also written ram:addr since Ghidra keeps it in the ram space. A bare 0x…. 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 are snake_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 0x050x07 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)

RangeSlotContentsNotes
0000-3FFFWindow 0Flash page 0 (fixed)Boot/kernel: RST vectors, dispatcher, FP/VAT core. Never swapped. [confirmed]
4000-7FFFWindow APort 0x06 in independent mode; even half of the port-0x06 pair in paired modePaged bcall targets run here after the dispatcher maps their page. [confirmed]
8000-BFFFWindow BPort 0x07 in independent mode; odd half of the port-0x06 pair in paired modeNormally RAM page 81; boot executes page 3F here in paired mode. [confirmed]
C000-FFFFWindow CPort 0x05 RAM in independent mode; port 0x07 in paired modeNormally 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)RoleEvidence
00Boot/kernel core, mapped at 0000RST vectors, bcall_dispatcher, FP/VAT/mem routines [confirmed]
01OS routines (display, homescreen text, menus)_PutC,_PutS,_ClrLCDFull,_NewLine resolve here [confirmed]
06OS routines (key input, parser-ish)_GetKey06:491E [confirmed]
2FUSB boot support pagevalidated local D84PBE2.8Xv supplies this page; retail page 3F maps _AttemptUSBOSReceive2F:4145, _ReceiveOS_USB2F:48CA, _InitUSB2F:52A4, _KillUSB2F:5961 [confirmed]
3Bbcall jump tablehighest-scoring page for the 0x4xxx bcall ID table; first entry _JErrorNo00:2799 [confirmed]
3CLink code, archive GC, and OS version string ("2.55MP")page starts 32 2E 35 35 4D 50; collector entry 3C:7733 [confirmed]
3ETwo 8 KiB certificate sectors; the inactive half also carries the transactional GC journal_GetCertificateStart (8057) and the GC command trace [confirmed]
3FRetail boot pagethe 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

AddrNameTypePurpose
0x8478-0x84B9OP1OP6TIFloat slot (9B body + 2B …EXT guard, 11B-spaced)Floating-point accumulators [confirmed]
0x89F0flagsSystemFlags (74B)IY-indexed system flag bitfield [confirmed]
0x844B/0x844CcurRow/curColbyteHomescreen text cursor (16 cols) [confirmed]
0x8447contrastbyteLCD contrast [confirmed]
0x843F0x8446kbdScanCode through keyExtend8 bytesscan mailbox, release filter, repeat state, and cooked-key workspace; see Keypad and ON-key hardware [confirmed]
0x84480x844AapdSubTimer/apdTimer/curTime3 bytesAPD low/high countdown and cursor timer [confirmed]
0x82590x82A1MD5 state73 bytes, with gapsworking words, bit length, compact length prefix, and digest; see MD5 accelerator and boot API [confirmed]
0x83A50x83E4MD5Buffer64 bytespartial message block or transformed-hash output [confirmed]
0x9C0C0x9C12timer API state7 bytesprogrammable timer-1 state, durations, and expiry count [confirmed]
0x9340plotSScreenbyte[768]Graph/display buffer (96×64/8) [confirmed]
0x86ECsaveSScreenbyte[768]Saved screen buffer [confirmed]
0x9824FPSFloating-point stack pointer [standard]
0x85BConSPSP 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).

PortNamePurpose
00linkActive-high pull-low controls on write and physical high-line levels on read; see Two-wire link port hardware
01keypadActive-low matrix group select/read; see Keypad and ON-key hardware
02hwStatusBattery comparator, LCD-ready, Flash-lock, and family status; see ASIC status, identity, protection, and GPIO
03intMaskLegacy interrupt enable/acknowledgement and low-power-on-HALT control; see Interrupts (IM1)
04intStatus / memMapModeRead = legacy pending state, ON level, and programmable completion; write = mapping mode, standard-timer rate, and battery selector; see Interrupts (IM1)
05mapBankCRAM selector for window C in independent mode
06mapBankAFlash/RAM selector for window A in independent mode or the A/B pair in paired mode
07mapBankBFlash/RAM selector for window B in independent mode or window C in paired mode
080Dusb/link assist84+ hardware byte-assist control/status/data/FIFO ports; see USB ASIC and link assist
0E/0FmapBankAHigh/mapBankBHighHigh two Flash-page bits for ports 0x06/0x07; no page effect on this 64-page TI-84 Plus
10/11lcdCmd/lcdDataLCD controller
181FMD5 assistSix serial operand registers, rotate/mode control, and four result bytes; see MD5 accelerator and boot API
20cpuSpeed0=6 MHz, 1=15 MHz (set in ISR)
15asicIdentityPublic ASIC/RAM/USB revision value; this ROM has no immediate or statically resolved literal-C access; see ASIC status, identity, protection, and GPIO
21flashGroup/ramExecProtected 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
2226execution boundsProtected Flash-page and RAM-chunk bounds; see Execution protection
27/28forced RAM overlays64-byte-granularity page-80/81 subranges; OS 2.55MP writes only zero, and paired-mode hardware behavior remains open
2DcrystalControlQuartz and programmable-timer behavior in low power
292CspeedDelaySpeed-selected LCD instruction delays and Flash/RAM wait-state gates; see Bus timing and wait states
2EmemoryDelayPer-access Flash/RAM one-T-state additions; see Bus timing and wait states
2FlcdTimerAdjustLCD-ready timing and programmable mode-3 prescaler; see Bus timing and wait states
3038programmable timersThree source/mode/counter triplets; see Clock, timers, and power
39/3AgpioConfig/gpioDataBattery-comparison and USB GPIO configuration/data; exact electrical signals remain open; see ASIC status, identity, protection, and GPIO
4048RTCControl, staged set value, and current 32-bit seconds count
4DusbLineStateUSB 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/56usbIntStatus/usbLineEventsUSB 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 0x050x07 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

EvidenceWhat it establishesConfidence
ROM bytes at 00:0000029C, 3F:40004210, and the paged-RAM helpersexact selector writes, boot transitions, and OS restore values[confirmed]
Resolved TilEm boot and homescreen tracesexecuted page transitions and logical-to-physical page resolution[confirmed]
TilEm x4_io.c and x4_memory.cone emulator’s paired mode, selector masks, forced overlays, and protection order[standard]
Wabbitemu 83psehw.c and core.can independent implementation, including extended Flash pages and different overlay rules[standard]
Guarded Wabbitemu mapper runinitialized-core reset, selector readback, fixed-page handoff, paired mapping, and overlay routing[standard]
MAME 0.287 ti85.cpp and ti85_m.cppa third implementation’s bank arithmetic, reset latch, mapped I/O, and backing ranges[standard]
Guarded MAME mapper runfresh-reset latch qualifiers, selector masks, safe RAM banks, absent overlay ports, and read/write/fetch routing[standard]
Public port descriptionsintended family-wide contracts for ports 0x040x07, 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 0x40000x7FFF 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 rangeWindowBase selector in independent modeNormal OS use
0x00000x3FFF0fixed Flash page 00reset vectors, interrupts, and kernel code
0x40000x7FFFAport 0x06, extended by 0x0E for Flashpaged Flash code or temporary banked RAM
0x80000xBFFFBport 0x07, extended by 0x0F for Flashnormally RAM page 81
0xC0000xFFFFCport 0x05normally 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.

WindowSourceResult
A, 0x40000x7FFFports 0x06 and 0x0Eone Flash or RAM page
B, 0x80000xBFFFports 0x07 and 0x0Fone Flash or RAM page
C, 0xC0000xFFFFport 0x05one 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]

WindowSourcePage rule
A, 0x40000x7FFFport 0x06selected physical page with bit 0 cleared
B, 0x80000xBFFFport 0x06the adjacent page with bit 0 set
C, 0xC0000xFFFFport 0x07the 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 0x800x86, but does not wrap higher RAM values.

SelectorPhysical page
bit 7 clearFlash page, low six bits on this 64-page calculator
bit 7 setRAM page `0x80

The hardware-facing 0x800x87 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]

DetailTilEmWabbitemuMAME 0.287jsTIfied 20170706a
Mapper ports0x040x07, 0x0E, 0x0F, 0x27, 0x28sameonly 0x040x070x040x07, 0x0E, 0x0F, 0x27, 0x28
Declared driver statususable mapperusable mapperMACHINE_NOT_WORKINGbrowser emulator source model
port 0x05 writestores low four bits; maps low threereduces low seven bits by RAM-page countstores low three bitsselects window C on TI-84 Plus
TI-84 Plus Flash selectorlow six bitsextended formula, then Flash-size masklow six bits for values below 0x80low selector plus ports 0x0E/0x0F extensions
RAM selectorlow three bitslow bits masked by RAM-page countraw value 0x800xFF becomes the bank number0x80 flag plus low three-bit page
paired Aport-0x06 page with bit 0 clearsamesameeven member selected from port 0x06
paired Bport-0x06 page with bit 0 setsee expression bug belowport-0x06 page with bit 0 setadjacent odd member
paired Cport-0x07 pagesamesameport-0x07 page
paired reads from 0x050x07stored register valuesactive C/A/B page valuesstored register valuesstored selector state
forced-RAM overlaysboth modesindependent mode onlyabsentimplemented

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 0x050x07. [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 0x0000000x0FFFFF and RAM at 0x2000000x21BFFF. The latter is seven, not eight, 16 KiB pages. Selectors 0x800x86 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]

ImplementationReset PCInitial visible pages 0/A/B/CFixed-page handoff
TilEm0x8000Flash 00/3E/3F/3Fnone; page 0 is already fixed
Wabbitemu0x0000Flash 3F/00/00, RAM 80first qualifying opcode fetch in A, or B while paired, changes fixed page 3F to 00
MAME 0.2870x0000Flash 3F/00/01/00a read from A, or from B while paired, clears the boot latch before returning the byte

MAME’s reset initializes selectors 0x050x07 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:000000: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]

PortForced logical rangePhysical page
0x270x10000 - 64n through 0xFFFFRAM page 80
0x280x8000 through 0x8000 + 64n - 1RAM 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]

DetailTilEmWabbitemuMAME 0.287
Overlay active in paired modeyesno; checks !boot_mappedno overlay model
Port 0x28 rangecomplete formula abovecomplete formula above in independent modeport unmapped
Port 0x27 rangecomplete formula abovealso requires the logical address to be at least 0xFB64port unmapped
Read, write, and instruction fetchall resolve through the overlaydata reads/writes use the overlay; execution checks retain some underlying-bank logicunderlying 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 0x050x07, 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 0x050x07 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 0x040x07, 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 0x210x26 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, and 0x28 have 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-83 helpers 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 0x06 or 0x07 can restore the wrong physical page. [standard]
  • Restore normal TI-84 Plus RAM windows with port 0x07 = 0x81 and port 0x05 = 0x00 when 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 0x27 and 0x28 in 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 0x210x26; record whether protection follows the underlying window or the forced RAM page.
  • Test port 0x27 below 0xFB64 to determine whether Wabbitemu’s additional cutoff models hardware or is an emulator-specific restriction.
  • Read ports 0x0E and 0x0F after writing values with upper bits set on TA2, TA3, and larger-Flash family members.
  • Select an even Flash page through port 0x06 in paired mode and verify that window B exposes the adjacent odd page rather than Wabbitemu’s duplicate.
  • Select RAM page 87 through ports 0x05, 0x06, and 0x07; this confirms the physical page independently of MAME’s seven-page backing-map defect.

Sources

SourceUse
OS 2.55MP rom.bin, especially 00:0000029C, 3F:40004210, 37:44AE, and 37:6D33executed selector sequences and reset values
TilEm x4_io.c and x4_memory.cmapping modes, 64-page masks, overlays, and protection order
Wabbitemu 83psehw.c and core.cextended selectors, paired mode, overlays, and independent comparison
MAME 0.287 ti85.cpp and ti85_m.cppmapped ports, bank backing, selector writes, reset mapping, and read-latch behavior
jsTIfied deployed 20170706a artifact and readable mirrorfourth 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 0x28historical 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

EvidenceWhat it establishesConfidence
boot_bus_timing_init at 3F:41BD41D3exact OS values for ports 0x290x2C, 0x2E, and 0x2F[confirmed]
Resolved cold-boot traceall six writes execute in order before normal CPU speed is selected[confirmed]
Whole-ROM immediate-port scanno second control-flow-verified write to these registers in the analyzed ROM[confirmed]
TilEm and Wabbitemu sourceindependent decode of speed selection, LCD instruction delays, and memory wait bits[standard]
Native Wabbitemu executionreset state, speed masks and frequencies, all seven delay latches, wait-gate selection, and port-0x2D side effects[standard]
MAME 0.287 sourcebinary CPU-speed selection and absence of the delay-register block[standard]
Guarded MAME ASIC-control runraw speed readback, measured 6:15 instruction throughput, absent delay ports, and soft-reset retention[standard]
Public hardware testsintended 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

PortRoleSelected by
0x20CPU-speed modesoftware; low two bits form mode 0–3
0x29LCD instruction delay and memory-wait gates for speed mode 0port 0x20 & 3 = 0
0x2Asame controls for speed mode 1port 0x20 & 3 = 1
0x2Bsame controls for speed mode 2port 0x20 & 3 = 2
0x2Csame controls for speed mode 3port 0x20 & 3 = 3
0x2Dquartz and low-power controlindependent block; see Clock, timers, and power
0x2Eone-T-state Flash and RAM access selectorsgated by bits 0–1 of the active 0x290x2C register
0x2Fhigh-speed LCD-ready interval and documented mode-3 timer prescalerfield selected by port 0x20

TilEm reads back the last byte written to ports 0x290x2C, 0x2E, and 0x2F. Wabbitemu registers one generic latch handler across the complete 0x290x2F 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 0x290x2C 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 0xFC0xFF. 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 0x290x2C, 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 0x290x2F both before and after the writes. [standard]

LCD instruction delay — ports 0x290x2C

TilEm and Wabbitemu add [standard]

$$ T_{\mathrm{LCD}} = D_s >> 2 $$

T-states to each Z80 IN or OUT instruction targeting LCD ports 0x100x13. The two low bits do not contribute to this count; they gate memory waits.

The OS bytes decode as follows:

Speed modeActive portOS byteAdded LCD T-statesLow-bit gates
00x290x175Flash and RAM
10x2A0x279Flash and RAM
20x2B0x2F11Flash and RAM
30x2C0x3B14Flash 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 bitMemoryAccess classEmulator placement
0Flashopcode/M1 fetcheach fetched opcode or prefix byte
1Flashnon-opcode readoperands, data, and stack reads
2Flashattempted writeevery CPU write routed to Flash
3unused by the documented delay blockstored on readback
4RAMopcode/M1 fetcheach fetched opcode or prefix byte
5RAMnon-opcode readoperands, data, and stack reads
6RAMwriteevery CPU write routed to RAM
7unused by the documented delay blockstored 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.

AccessFlash additionRAM addition
opcode/M1 fetch1 T-state0
non-opcode read00
write1 T-state1 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 modeFieldWidth
0no high-speed ready hold
1bits 0–12 bits
2bits 2–43 bits
3bits 5–73 bits

For a selected field $f$, TilEm and Wabbitemu use [standard]

$$ T_{ready} = 48 + 64f $$

The boot value 0x4B produces:

Speed modeField valueReady holdNominal interpretation
0noneCPU and per-access delay provide the low-speed spacing
13240 T-states16 µs at 15 MHz
22176 T-statesmode not used by the traced OS path
32176 T-statesmode not used by the traced OS path

TilEm restarts this ready timer on every modeled access to ports 0x100x13, 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

BehaviorTilEmWabbitemuMAME 0.287jsTIfied 20170706a
Port-0x20 writelow two bits select modes 0–3; nonzero runs at 15 MHzdefault TI-84 Plus state clamps modes 2–3 to mode 1; external extraSpeed enables 20/25 MHzstores the raw byte; zero selects 6 MHz and any nonzero value selects 15 MHzselects the browser emulator’s CPU-speed state
Active 0x290x2C registerindexed by port 0x20 & 3indexed by the accepted CPU-speed moderegisters absentdelay values are stored
LCD instruction additionactive byte shifted right by twosameabsentLCD uses its own busy interval
Memory gates and 0x2E bitsall six access classesall six access classesabsentno source-equivalent per-access wait insertion identified
Port 0x2Dlow-power control outside this blockraw fifth delay latch; no timer or low-power transitionabsentstored control state
High-speed ready startevery LCD-port read or writelast successful LCD writeprogrammable interval absentLCD readiness uses randomized controller timing
LCD controller rejectionready bit and controller modelalso has a separate fixed 60-T-state controller-access guardT6A04 device behavior without the ASIC delay blockcontroller transfers use the jsTIfied LCD timer
Mode-3 timer prescalernot modelednot modeled in the compared timer pathnot modelednot 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 0x290x2F. 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, 0x290x2C, 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 0x2E effects.
  • Measure the interval from LCD writes and reads to port-0x02 bit 1 becoming ready. This distinguishes TilEm’s every-access restart from Wabbitemu’s write-based model.
  • Find the lowest reliable ports-0x290x2C values for each LCD controller revision without assuming the published 0x0C threshold is universal.
  • Compare the HWTMR port-0x2F results across CPU-speed modes and ASIC revisions.
  • Compare nominal and measured T-state wall times on TA2 and TA3 ASICs.

Sources

SourceUse
OS 2.55MP boot_bus_timing_init and resolved boot traceboot register values, write order, and later CPU-speed transitions
WikiTI ports 0x29, 0x2A, 0x2B, and 0x2Cspeed selection, gate bits, LCD instruction delay, and published failure thresholds
WikiTI port 0x2Esix memory-access classes and prefix observation
WikiTI port 0x2FLCD-ready intervals and mode-3 timer prescaler
TilEm x4_io.c, x4_memory.c, and x4_init.cdelay decode, cycle placement, ready timer, and reset defaults
Wabbitemu 83psehw.c and core.cindependent delay decode, cycle placement, and readiness comparison
MAME 0.287 ti85.cpp and ti85_m.cppmapped I/O ports, raw speed readback, binary clock selection, and absent delay block
jsTIfied deployed 20170706a artifact and readable mirrorfourth 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.

SourceWhat it establishes
Retail OS 2.55MP and boot 1.03 bytesPort operations, masks, branch conditions, bcall targets, and return values [confirmed]
Resolved TilEm boot and archive tracesValues 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 pagesPublic bit names, port-0x15 identity values, and port-0x21 size tables [standard]
TilEm commit f56ad63One executable model for battery comparison, Flash grouping, and RAM execution masks [standard] for the implementation; [hypothesis] for physical equivalence
Wabbitemu commit 48c2dc0An independent status and protection model, including implementation defects described below [standard] for the implementation; [hypothesis] for physical equivalence
MAME 0.287A third implementation with fixed status and identity values, incompatible port-0x21 masking, and no GPIO ports [standard]
Guarded Wabbitemu --asic-edge-probe runInitialized-core status, identity, protected-write, internal-field, readback, and GPIO-map observations [standard]
Guarded Wabbitemu --protection-port-probe runShared write gate and internal-field behavior for ports 0x220x26 [standard]
Guarded MAME ASIC-control runRaw 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, 3537, 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 locationsApparent ports and directionsClassification
01:4304, 01:446A, 01:446E, 01:4C5A, 01:556D, 01:6E55, 01:6E95, 01:7CD60x49 OUT; 0x4E IN/OUT; 0x5E IN ×2; 0x6E IN; 0x70 OUT; 0xFF OUTtable-shaped data ×8
03:630B, 03:6323, 03:634F, 03:6367, 03:656F0xFE IN ×2/OUT ×2; 0x65 INtable-shaped data ×5
03:6DE10x9C INoperand overlap in LD HL,0x9CDB at 03:6DE0
07:40760xD1 OUTtable-shaped data
33:40100x6B OUTtable-shaped data
34:6CF5, 34:6CF7, 34:73AB, 34:73AD0x6D OUT ×2; 0x73 IN ×2table-shaped data ×4
37:6A9C, 37:6B140x6B IN ×2table-shaped data ×2
38:6A000xDC INtable-shaped data
3A:7D81, 3A:7FED0x5E IN; 0xDB INtable-shaped data ×2
3B:47B9, 3B:4F45, 3B:52AE, 3B:535C, 3B:54670x6F OUT; 0x51 OUT; 0x6E IN; 0x5D IN; 0x6D INtable-shaped data ×5
3F:40FC, 3F:4111, 3F:56F7, 3F:671B, 3F:67F70x5E OUT; 0x63 IN; 0xD1 IN; 0xE7 OUT; 0xE6 INtable-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 pairsClassificationEvidence
37:58A9 (INI), 37:5944 (OUTI)resolved instructions ×2Straight-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:6C90operand overlaps ×27Each 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:567Breviewed data ×8Rebuilt 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]

BitROM use or public meaningEvidence
0Battery 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
1LCD-ready state. LCD wait loops continue while this bit is zero.ROM wait helpers and the dynamic 0xE10xE3 transition [confirmed]
2Flash-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
3No meaning established. No direct status consumer in this ROM tests it.[hypothesis]
4No meaning established. No direct status consumer in this ROM tests it.[hypothesis]
5Publicly documented as USB-capable. Both emulators set it for their TI-84 Plus model, but this ROM does not test it directly.[standard]
6Publicly 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
7Advanced-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
ValueBit-level interpretation
0xE1Comparator high, LCD wait active, Flash locked, and bits 5–7 set [confirmed]
0xE3Comparator high, LCD ready, Flash locked, and bits 5–7 set [confirmed]
0xE7Comparator 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 maskSitesROM role and anchors
0x01 — bit 08battery comparisons, including _Chk_Batt_Low at 00:0D20 and _Chk_Batt_Level at 33:4E9F and 33:4EE6
0x02 — bit 13LCD-ready waits at 00:0CC4, 00:0CDC, and 3F:744F
0x80 — bit 744family-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]

BcallIDBodyPort-0x04 comparison value
_Chk_Batt_Low_B80F03F:61710x86
_Chk_Batt_Low_B280F33F:61630x46

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]

ResultPath
0The initial port-0x02 bit-0 test is low, so the routine returns before enabling the GPIO sequence.
4The comparison after port 0x04 = 0xC6 is high.
3The 0xC6 comparison is low and the 0x86 comparison is high.
2The first two comparisons are low and the 0x46 comparison is high.
1All 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 valueSelectorTilEm threshold
0x0603.3 V
0x4613.9 V
0x8623.6 V
0xC634.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 voltageComparator maskROM bcall result
below 3.3 V0x00
3.3–3.5 V0x11
3.6–3.8 V0x53
3.9–4.2 V0x73
4.3 V and above0xF4

The native mask transitions match the four source constants. The reusable model then applies the byte-verified decision tree at 33:4E9B4EDA. 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.

ValuePublic ASIC referenceUSB driver familyReported RAM
0x3383PL2M/TA2noneexternal 128 KiB
0x4483PLUSB/TA2old128 KiB
0x4584PLUSB/TA3new128 KiB
0x5584PLC/TA1new48 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 sitesConsumer
00:02AE, 00:1831, 00:2B32, 00:2B5BAND 0x03
2F:4DD5, 2F:511D, 36:5E90AND 0x03
3C:6BA8, 3C:7F0C, 3D:7392AND 0x03

The remaining three raw pairs overlap other instructions: [confirmed]

Raw pairOwning instructionWhy it is not I/O
06:5A10DB 2106:5A0D: LD (IX-1),0xDBThe DB byte is the stored immediate; 21 begins the following LD HL instruction.
05:6C96D3 2105:6C95: JR Z,05:6C6AThe D3 byte is the relative displacement; 21 begins the following LD HL instruction.
3C:5B91D3 213C:5B90: JR 3C:5B65The 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]

FieldPublic sizeHighest boot page
01 MiB0x3F
12 MiB0x7F
24 MiB0xFF
38 MiB0x1FF

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 directionRaw pairsReviewed instructionsOther raw pairs
IN (0x39)141302:5142 is table-shaped data with no function or xrefs.
OUT (0x39)1616none
IN (0x3A)211906:5A8D and 3C:7365 overlap operands.
OUT (0x3A)1717none

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]

  1. Set port-0x3A bit 7.
  2. Run the port-0x04 comparator tests.
  3. Set port-0x39 bit 4.
  4. Set port-0x3A bit 4, delay, and clear it.
  5. Clear port-0x3A bit 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]

LocationOperation
2F:5330Clear port-0x3A bit 1, then set port-0x39 bit 1.
2F:5353Clear port-0x39 bit 1.
2F:538CSet the low data bits to binary 100, then set port-0x39 bits 0–2.
2F:53ABClear port-0x3A bits 0–2, then set port-0x39 bits 0–2.
2F:53D5 and 2F:593BClear port-0x39 bits 0–2 during cleanup.
2F:521BTest 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 0x220x2F and 0x390x3A return zero before and after patterned writes. A 50-T-state counter continues executing from RAM while port 0x21 reads 0x03 and ports 0x220x26 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.

AreaTilEm f56ad63Wabbitemu 48c2dc0MAME 0.287jsTIfied 20170706a
Port 0x02dynamic comparator, LCD-ready, and Flash lock; family bits 5–7 setsame layout, with the TI-84 Plus comparator fixed high`0xC3(raw gate << 2)`, truncated to a byte
Port 0x15fixed 0x45model and RAM-revision dependentfixed 0x33model-dependent identity value
Port 0x21 accepted readbackvalue & 0x33, subject to Flash unlockonly bits 0–1 survive its read defectvalue & 0x0F, without protected-write gatingstored while Flash-unlocked and used for page-level execution groups
GPIOport 0x39 fixed at 0xF0; no meaningful TI-84 Plus port 0x3Aport 0x3A latch; port 0x39 absentboth ports absentsoftware latches without physical GPIO modeling
Driver statususable model with unmeasured battery thresholdsusable model with implementation-specific defectsTI-84 Plus marked MACHINE_NOT_WORKINGbrowser 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 0x15 on known TA1, TA2, and TA3 units and compare the result with package markings and installed RAM.
  • Program each port-0x21 field 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-0x39 direction polarity and port-0x3A electrical state with battery and USB paths active. Preserve the OS configuration and avoid driving externally forced pins against the ASIC.

Sources

SourceUse
Retail OS 2.55MP and boot 1.03 ROM bytesStatus branches, battery routines, port-0x21 boot setup, and GPIO operations
WikiTI port 0x02Public status-bit names
WikiTI port 0x15Public ASIC identity table
WikiTI port 0x21Public Flash/RAM size tables and execution-page description
WikiTI ports 0x39 and 0x3AHistorical GPIO interpretation, with the contradictions identified above
TilEm x4_io.c at f56ad63Battery table, status read, identity constant, protection mode, and fixed GPIO read
Wabbitemu 83psehw.c at 48c2dc0Independent port models and the port-0x21 read defect
MAME 0.287 ti85.cpp and ti85_m.cppshared I/O map, fixed status and identity reads, port-0x21 mask, missing GPIO, and driver status
jsTIfied deployed 20170706a artifact and readable mirrorfourth 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 0x210x26 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

EvidenceWhat it establishesConfidence
Retail boot page 3Fprotected writes, register values, and _SetFlashLowerBound behavior[confirmed]
Complete-ROM static I/O scanone 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 sourcethree executable software models, including their disagreements[standard]
Guarded TilEm boundary tracesfetch, return, warning, and reset sequences at pages 07, 08, 29, and 2A[confirmed] for the pinned emulator run
Guarded Wabbitemu boundary runsfetch, return, marker, and instrumented reset sequences at pages 07, 08, 09, 29, and 2A[confirmed] for the pinned emulator run
Guarded RAM execution runschunk-edge and mode disagreements under pinned TilEm and Wabbitemu[confirmed] for the pinned emulator runs
Guarded Wabbitemu protected-port runregistered-port gate, readback, high-field handling, and 16-bit RAM-bound storage[standard]
WikiTI port pagespublic inclusive-bound descriptions and the larger-device port-0x24 extension[standard]
Physical TA2/TA3 behaviorlower-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]

PortBoot valueModeled role
0x210x00bits 4–5 select the repeating RAM address mask; bits 0–1 also select the Flash protection group
0x220x08lower Flash no-execute page
0x230x29upper Flash no-execute page
0x250x10lower executable RAM chunk in 1 KiB units
0x260x20upper 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 0xB00000xBFFFF or 0xF00000xFFFFF. 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 0x210x26 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 0x080x29, inclusive. Pages 0x000x07 and 0x2A0x3F 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]

PageRecorded sequence after ram:9DBDTilEm warning countOutcome
0707:7FF0 at +8, 07:7FF2 at +23, return ram:9DC0 at +470returned
08attempted 08:7FF0 at +8, reset entry at +15; no 08:7FF2 or return1violation reset
29attempted 29:7FF0 at +8, reset entry at +15; no 29:7FF2 or return1violation reset
2A2A:7FF0 at +8, 2A:7FF2 at +23, return ram:9DC0 at +470returned

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]

PageProbe SHA-256Trace SHA-256
0787c11964b6cf67624b2eff46e1a962c56f1684dd48db931a5cb68e08c1b84b4e250cc9d2b8b3c85f5edb6391e847993e27e6c308c4a70d62dd5cfc8168af8e68
08ddd023d522d301315c0f4929f348499faca08c708e96c1333bf85e32505f9534f9c1f142430aafc47b514ef220a707be01de02678e6cd22fcb1f6e5fb024eeac
29f671bdb62e6bad19f33402eb919e70631cf7cc8f00b9f7f52114d052f86cea78ee3dac7ec1843c2a82ee321c0a3a16c95bc5898d3c70fb97296127dbf2020007
2Ad5f72f96562ef5e96f4ddaa12954548d210650d9ca6bec365f75f1bb6f3bad1bb9db26bc7ef69d97907118d0124213603632d9e2f3d9ebb56680b87d8644636d

This dynamically confirms the inclusive 0829 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]

GroupReset value
Z80 register pairs except PC0xFFFF
PC, R bit 70x8000, 0x80
IFF1, IFF2, IM, interrupt requests, HALTzero
Mapper windowspage 00, certificate page 3E, boot page 3F, boot page 3F
CPU speed6 MHz
Protection ports 0x210x23, 0x25, 0x2600, 08, 29, 10, 20
Flash command gate, state, and busy flaglocked, array-read mode, idle
LCD controllerinactive, contrast 32, 8-bit mode, increment 7, row stride 16
Link output and assist, keypad, MD5, programmable timerscleared 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 0x090x0F, 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]

PageSequence from ram:9DBDProbe instructionsMarkerOutcome
0707:7FF0, 07:7FF2, return ram:9DC05407returned
0808:7FF0, 08:7FF2, return ram:9DC05408returned
09attempted 09:7FF0; no 09:7FF2 or return; one reset52A0violation reset
29attempted 29:7FF0; no 29:7FF2 or return; one reset52A0violation reset
2A2A:7FF0, 2A:7FF2, return ram:9DC0542Areturned

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]

PageFixture ROM SHA-256Probe SHA-256
07ed2372b459cddd89deea6a27d00cd6f757d612c4f63db4feaf134665ad2e78cf87c11964b6cf67624b2eff46e1a962c56f1684dd48db931a5cb68e08c1b84b4e
08b0d32c8f3af1f87c8fce8f7966ab45d588a8ed42ed9ce7708de38b4d7dc57934ddd023d522d301315c0f4929f348499faca08c708e96c1333bf85e32505f9534
097f2443e3aecceaa8c1ad60e0de4e2316caad3d17802ec3a719567a05e25a244cf121bae475d56947bec80090bb3047fab478cc86db4dec897e4161f78df14584
291590ddf2681c3636e119df3759909c43b62a49a9dbc74f5a4f00d6500ae9017df671bdb62e6bad19f33402eb919e70631cf7cc8f00b9f7f52114d052f86cea78
2A1ee90aef8e9795ef56b668ae36560ad6a4c99938055cd0e5763b930b0f585d2ad5f72f96562ef5e96f4ddaa12954548d210650d9ca6bec365f75f1bb6f3bad1b

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]

GroupReset value
PC, SP0x0000
Interrupt mode1
Interrupt, EI block, IFF1, IFF2, HALT, and I/O flagscleared
Prefix statezero
Ports 0x27 and 0x28 remap countszero
RAM execution bounds0x00000x03FF
Mapper windowsboot page 3F, Flash page 00, Flash page 00, RAM page 00
Boot-map and page-0-change flagscleared
Legacy protected_page[4] array and selected groupzero

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 0x00000x03FF, 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 0x80000x83FF 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 0x800x87. “Chunk 0” means the first 1 KiB at page offset 0x00000x03FF. [standard]

ModeTilEm maskRepetitionFully executable pagesPartly executable pages
00x7C0032 KiB0x81, 0x83, 0x85, 0x87none
10xFC0064 KiB0x81, 0x85chunk 0 of 0x82 and 0x86
20x1FC00128 KiB0x81chunk 0 of 0x82
30x3FC00256 KiB0x81chunk 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]

ModeWabbitemu fully executable pagesPartly executable pages
00x81, 0x83, 0x85, 0x87chunk 0 of 0x82
10x81chunk 0 of 0x82
20x81chunk 0 of 0x82
30x81chunk 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 0x400xFF therefore wrap modulo 0x10000. For example, writing 0x40 to both ports produces the implemented interval 0x00000x03FF, not 0x100000x103FF. 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]

ModePhysical targetTilEmWabbitemuPredicate detail
0page 0x82, offset 0x03F0violation resetreturnedWabbitemu includes page-2 chunk 0 through its global range; TilEm’s mode-0 mask maps it below the lower bound
0page 0x82, offset 0x0400violation resetviolation resetfirst target in chunk 1
1page 0x82, offset 0x03F0returnedreturnedtarget lies wholly inside chunk 0
1page 0x82, offset 0x0400violation resetviolation resetfirst target in chunk 1
1page 0x85, offset 0x3FF0returnedviolation resetTilEm repeats the full-page window after 64 KiB; Wabbitemu uses one global range
1page 0x86, offset 0x03F0returnedviolation resetTilEm 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]

ModeReturned targetsViolation-reset targets
0page 0x81 offset 0x3FF0; page 0x82 offset 0x03F0; page 0x83 offset 0x3FF0page 0x82 offset 0x0400; page 0x84 offset 0x0000
1page 0x81 offset 0x3FF0; page 0x82 offset 0x03F0page 0x82 offset 0x0400; page 0x85 offset 0x3FF0; page 0x86 offset 0x03F0
2page 0x81 offset 0x3FF0; page 0x82 offset 0x03F0page 0x82 offset 0x0400; page 0x83 offset 0x0000
3page 0x81 offset 0x3FF0; page 0x82 offset 0x03F0page 0x82 offset 0x0400; page 0x83 offset 0x0000

A separate Wabbitemu run configured both chunk ports to 0x40. The native report recorded the wrapped bounds 0x00000x03FF. 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 targetTilEm probe SHA-256TilEm trace SHA-256Wabbitemu probe SHA-256
mode 0, 0x82+0x03F0a0853c1ea1f900a7b8b4c26d1091e5696265b993a214d20836c182743ae330c30bde946b277f0c3fe7c6040931ea1df6c265aa11d4fad6394cfeea5955dfe18b783d757f767b0d89df7c68881413e0cb47a6652da2c94829f90972e3eb2a64cb
mode 0, 0x82+0x04009f82e2df6960cc6e0658c1db4b19e755bd544105be73fa476f8b17e34527a11666853fc88e6a934577f1e70916df012f77e02ea7ab7e77b2caa4a9cda0a5e602d068ef192978d9fbddada76d6f55320263315ad2b3344e64751cc33f9aa58d5f
mode 1, 0x82+0x03F07ef4086cf9fe4e938215cf3592435d13fcd0874a239e58fbdc50c78719531ff231601907572c2060adaa76d2031a138e225a5e73bdc8f84c50505178e1e871eef0843119d9a19ab5f5578f61160a8cb5ce723d12ed2b3ea13a5d9cdfc8857ce7
mode 1, 0x82+0x04001531839a1d11895ad14ddada9974da4c307eb1ba09b5b660b3f1858bf2659a7f8a9ded0bb3479a86587579ae647a4c2604689f3f0dea9beba96b7fb1117415a9d2ba9523c63f7645f61cfad45677b9afc3bc47a64b251f2d8f3daafadc8525b0
mode 1, 0x85+0x3FF04996653aca01db9c7ce67d7a367810cf4a07ee42fa65991920232a81d6b3074ca76f11d993e1b5cf6e19feca1af4321637670a9f1c7f5232159096ea2ba0839f857a38aa6ccf163ebac779c775d812e9ad9df844c870a9bbc42fdad7932da959
mode 1, 0x86+0x03F08851278b8f3b54b7a7e7a0ff206b03f98bbec6528a3d705598a054dcf5f501a0e976f06992278db20f1b00c6faf2caf4140d7635de1af180f6491d103e0719ce3c989491f4031cfac972cd72af55824d6a1ca8f384315ac3fbbd5ed0ad15a3c0

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 bytePort-0x25 readInternal lower fieldPort-0x26 readInternal upper field
0x3F0x3F0xFC000x3F0xFFFF
0x400x000x00000x000x03FF
0x410x010x04000x010x07FF
0xFF0x3F0xFC000x3F0xFFFF

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 0x220x28. 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 0x220x28; all seven ports still read zero. With port 0x21 = 0x33, writes CC DD AA 10 20 to 0x220x26 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, and 20 to ports 0x21, 0x22, 0x23, 0x25, and 0x26 through protected byte sequences. [confirmed]
  • _SetFlashLowerBound writes port 0x23, despite its official name. [confirmed]
  • TilEm denies the inclusive Flash interval. Four guarded TilEm traces execute pages 07 and 2A and reset on attempted fetches from pages 08 and 29. Wabbitemu allows its programmed lower page: guarded native runs execute pages 07, 08, and 2A and reset on attempted fetches from pages 09 and 29. [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 0x40 wrap case. [standard] for source behavior; [confirmed] for the pinned emulator runs.
  • The retail ROM has no statically resolved port-0x24 access. [confirmed]
  • jsTIfied implements page-level Flash and RAM execution groups, but its stored ports 0x25 and 0x26 do not affect instruction fetches. [standard]
  • A guarded initialized-core Wabbitemu run verifies the common protected-write gate across ports 0x220x26, the port-0x24 high-field clearing defect, and 16-bit RAM-bound wrap at 0x40 and 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_step through 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

SourceUse
OS 2.55MP and boot 1.03 ROM, especially boot_execution_protection_init and _SetFlashLowerBoundprotected writes and bcall body
TilEm calcs.c, z80.c, x4_init.c, and x4_memory.cfull reset sequence, TI-84 Plus reset fields, Flash and RAM fetch predicates, and post-opcode exception handling
TilEm x4 I/O model at f56ad63protected register writes and mask updates
Headless TilEm fork at 8da54573ac49fe271fa22c60924b4c6a7cb9639fboundary execution traces; binary SHA-256 1c1f7dbe04fe074c2b9aca1657d0eb5ac5cfd1f7cbd480725eb7fb39b8126f33, x4_memory.c SHA-256 ddaa1e45330e3e4ad49486bd5c3675a0a0dff01bfda4d01817ba3387e309ac89
TilEm xc memory model at f56ad63port-0x24 high-bound bits
Wabbitemu core.c at 48c2dc0Flash and RAM fetch predicates, CPU_reset, and execution-violation control flow
Wabbitemu calc.c and lcd.cfrontend reset scope and LCD reset fields
Wabbitemu device.c at 48c2dc0global protected-port write gate
Wabbitemu 83psehw.c at 48c2dc0port handlers and port-0x24 implementation
MAME ti85.cpp and ti85_m.cpp at mame0287absent execution-protection ports and unused Flash-unlock state
jsTIfied deployed 20170706a artifact and readable mirrorprotected writes, page-level run_lock, violation reset, and unused stored RAM-bound ports
WikiTI port 0x22, 0x23, 0x24, 0x25, and 0x26public register descriptions, treated as secondary evidence
Crabcake release archive, SHA-256 84f6660c86f715e09e03637b19df47abe46b86906ed34791bc4281959186f71e6 MHz protected-port path and TI-84-family page-swap path
zStart 1.3.013 release archive, SHA-256 7a1b7c69c85030b412bb6ea11ae71ac608b9882a9de3ab7dbef1faf69519c5e9persistent Execute >C000 configuration and ON-script restoration
Swords 2 source release, archive SHA-256 830878e3449221664b85eb3996992ad0f8b46b7e57183c337930b7e78e5a3397; FULLRENE.8xv SHA-256 327ea2ce2a603febc46490d9758cffc12c9fc926fc1d773a0d53a5ccdf5d4ec3original 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:

  1. Read the 2-byte ID dw from the caller’s return address.
  2. Decode the ID’s high bits: bit15/bit14 select the address class; the low bits form the table offset.
  3. Bank the bcall table page into slot A (via the helper at ram:181c, which sets port_mapBankA).
  4. Read the 3-byte table entry: target address (2) + target page (1).
  5. Bank the target page into slot A (port_mapBankA = page), save the previous page.
  6. call the 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 offset 0x3B*0x4000 = 0xEC000).
  • 3-byte entries: addr_lo, addr_hi, page. IDs step by 3 from 0x4000, so entry for ID X is at table offset X-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; page 0x3B scored highest (the page-selection heuristic uses a conservative validity filter chosen only to pick the table). Once 0x3B is 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 — _PutS01:5C39, _GetKey06:491E, _ClrLCDFull01:60E4, _GetCSC00:04B2, _CreateReal00: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:

  • 0x4xxx0x7FFF (bit 14 set): the main table on flash page 0x3B, entry at offset ID − 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 page 3F, indexed by ID & 0x7FFF. Its real entries occupy IDs 0x80180x80D2 and 0x80E40x8129; bytes 3F:40D53F:40E3 between those ranges are executable dispatch-stub bytes, not five table entries. D84PBE1.8Xv supplies the retail page 3F; D84PBE2.8Xv supplies the companion USB boot support page 2F. Most entries resolve to 3F:addr; USB entries such as _AttemptUSBOSReceive (80E4) and _InitUSB (8108) resolve to 2F:addr. tools/ti84re/rom/resolve_bcalls.py refuses 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:

OpcodeVector → targetRoutine
rst 08h0008→1A2F_OP1ToOP2 (copy FP reg)
rst 10h0010→0E65_FindSym (VAT lookup)
rst 18h0018→155C_PushRealO1 (push OP1 to FPS)
rst 20h0020→1B01_Mov9ToOP1 (copy 9 bytes → OP1)
rst 28h0028→2A2Fbcall dispatcher
rst 30h0030→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:3B01ram: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 uses cross_page_jump to 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

EvidenceScopeConfidence
Page-0 bytes from im1_vector at ram:0038 through ram:0244IM1 entry, USB and legacy gates, source-test order, handlers, acknowledgement, and exit[confirmed]
Power-cycle trace from tools/macros/power-cycle.macroOS mask writes, low-power HALT, ON wake, status read, debounce, and restoration[confirmed]
WikiTI ports 0x03 and 0x04Bit-level enable, status, clear-on-zero, timer-rate, mapping, battery-selector, and low-power contract[standard]
TilEm commit f56ad63 and Wabbitemu commit 48c2dc0Two executable interpretations of the registers and their fidelity gaps[standard]
MAME 0.287 ti84pv3 driver and Lua I/O traceThird implementation, headless ON-wake execution, and explicit MACHINE_NOT_WORKING gaps[standard]
Guarded TilEm direct-core interrupt probeStored-mask readback, internal policy, acknowledgement, ON/link edges, timer callbacks, and reset ordering[standard]
Guarded TilEm direct-core link probeRaw link-activity and assist idle, receive, and error interrupt transitions[standard]
Guarded Wabbitemu interrupt edge probeInitialized-core mask, timer, acknowledgement, completion, and low-power transitions[standard]
Guarded MAME legacy-interrupt probeCPU-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 + 0x02interrupt_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.

BitMeaningEffect of writing zeroEvidence
0ON interrupt enableddisable and acknowledge the ON requestPublic register contract; OS writes and both emulators [standard]
1standard timer 1 enableddisable and acknowledge timer 1Public register contract; OS writes and TilEm [standard]
2standard timer 2 enableddisable and acknowledge timer 2Public register contract; OS writes and TilEm [standard]
3write control: one keeps hardware powered during HALT; zero selects low power on HALTselect low power for the next HALTPublic register contract; OS shutdown sequence and both emulators [standard]
4legacy link-activity interrupt enableddisable and acknowledge link activityPublic register contract; OS shutdown mask and TilEm [standard]
5–7no documented functionPublic 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.

ValueEnabled legacy sourcesHALT behaviorOS use
0x08nonepoweredcommon clear-on-zero acknowledgement and shutdown cleanup
0x09ONpoweredtransient standard-timer-1 acknowledgement path
0x0Astandard timer 1poweredtransient ON acknowledgement path
0x0BON and standard timer 1powerednormal mask
0x0FON and both standard timerspowerednormal exit when (IY+0x16) bit 0 requests timer 2
0x11ON and link activitylow powershutdown 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]

BitRead meaningOS useEvidence
0ON request pendingbranch to on_irq at ram:015BROM test at ram:00D2ram:00D5 [confirmed]; latch role [standard]
1standard timer 1 pendingbranch to standard_timer1_irq at ram:0167ROM test at ram:00D6ram:00D9 [confirmed]; pending role [standard]
2standard timer 2 pendingbranch to ram:01F1ROM test at ram:00C8ram:00CB [confirmed]; pending role [standard]
3one when ON is released, zero while presseddebounce reads at ram:0975ROM interpretation [confirmed]; electrical level [standard]
4legacy link activity pendingbranch to legacy_link_irq at ram:01E0ROM test at ram:00CDram:00D0 [confirmed]; pending role [standard]
5programmable timer 1 finishedtest timer-1 mode at port 0x31ROM tests at ram:0041 and ram:013A [confirmed]; completion role [standard]
6programmable timer 2 finishedpage-35 handler with A = 0x0BROM tests at ram:0046 and ram:0154 [confirmed]; completion role [standard]
7programmable timer 3 finishedtest timer-3 mode at port 0x37ROM 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]

BitsWrite meaningEvidence
0zero selects independent mapping; one selects paired mappingOS writes and mapper behavior [confirmed] for use; public contract and emulators [standard] for hardware
2–1standard-timer rate index 03, fastest to slowestOS writes 0x06; public formula and emulators [standard]
5–3unused in the public contract[standard]
7–6raw battery-comparator selectorOS 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]

PriorityStatus bitCandidateAdditional gate
17programmable timer 3port 0x37 bit 1
25programmable timer 1port 0x31 bit 1
36programmable timer 2handler selected through ram:0154
42standard timer 2none in the dispatcher
54legacy link activitynone in the dispatcher
60ON requestnone in the dispatcher
71standard timer 1none 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 0x300x38. 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.

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 0x55 as an active-low USB gate and port 0x04 as legacy/completion status. Do not interpret port-0x04 bit 3 as a pending source.
  • Acknowledge legacy sources by clearing their port-0x03 bits, 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-0x03 acknowledgement does not clear port-0x55/0x56 state.
  • 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

BehaviorTilEm x4Wabbitemu 83+SE/84+jsTIfied 20170706aConsequence
Port 0x03 readreturns stored maskreturns stored maskstores the interrupt maskmask reads agree [standard]
Legacy clear-on-zeroclears ON, timer 1, timer 2, and link pending state on port-0x03 writesclears ON directly; disabling an overdue standard timer catches its phase up in the same port-0x03 handler; port 0x02 can also catch it uptracks standard-timer and ON latches in emulator stateOS-style acknowledgement is modeled with different internal policies [standard]
Link statusimplements port-0x04 bit 4omits bit 4 from port-0x04 readslink state participates in the interrupt modelsoftware agreement does not establish electrical wake behavior [standard]
Standard timersexplicit pending interrupt bits when enabledderives status from elapsed phase while enabledschedules timer state in emulator cycle counterssimultaneous-source and latch tests can differ [standard]
Programmable completionexposes finished bits 5–7 independently of interrupt modeexposes timer-underflow bits 5–7retains per-timer completion and loop stateall separate completion from mode, with different timer cores [standard]
HALT behaviorport-0x03 bit 3 selects powered/low-power behavior; standard-timer mask controls programmable wake suppressionapproximates low power by changing LCD activity and suppresses programmable-timer requests while haltedhalted CPU state is part of the browser schedulerno model proves physical ASIC power domains [standard]
USB gatedisconnected fixed values 0x55 = 0x1F, 0x56 = 0partial Fake USB event modelfixed disconnected valuesconnected 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 0x0A0x080x0A 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]

AreaMAME ti84pv3 behaviorDifference from the public contract
Port 0x03 readcalls the same status reader used by port 0x04returns status and ON level instead of the stored mask
Port 0x03 writemasks ON and standard-timer pending fields with the written enable bitsmodels clear-on-zero for bits 0–2, but omits link bit 4 and low-power bit 3
Port 0x02 writewrites ON and standard-timer status through a handler whose comment says it is being ignoreddoes not match the documented port-0x03 acknowledgement ownership
Standard timersallocates fixed 256 Hz and 512 Hz callbacksport-0x04 rate writes do not select the published 107.79–512 Hz range
Programmable timersrequests an interrupt when mode bit 1 is clear and sets the port-0x04 completion field on that same branchreverses the documented interrupt-enable polarity and loses independent completion visibility
Link and low powerno legacy link-pending field or ASIC power-domain transitioncan execute the ROM wake path but cannot test physical link wake or low-power behavior
USBreturns fixed 0x1F and zero from ports 0x55 and 0x56disconnected 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/0x56 remain separate from ports 0x03/0x04.
  • [confirmed] The port-0x04 test 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 → 0x0B or 0x0F.
  • [confirmed] Shutdown writes 0x11 and executes HALT; the trace wakes through port-0x04 value 0x01 after 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-0x02 status 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

SourceUsed for
WikiTI port 0x03enable mask, clear-on-zero acknowledgement, normal value, and low-power-on-HALT contract
WikiTI port 0x04read-status fields, write controls, timer formula, and programmable completion distinction
TilEm x4_io.c, x4_init.c, keypad.c, link.c, and timers.clegacy latches, reset ordering, ON/link edges, timer completion, HALT policy, and disconnected USB values
Wabbitemu 83psehw.cindependent standard-interrupt, mapping, ON, timer, and low-power implementation
MAME 0.287 ti85.cpp and ti85_m.cppTI-84 Plus machine status, I/O map, interrupt masks, standard timers, programmable timers, and fixed USB reads
jsTIfied deployed 20170706a artifact and readable mirrorfourth interrupt-mask, timer, ON, link, halted-state, and fixed-USB implementation
Local OS 2.55MP page-0 bytesentry, gates, test order, handlers, acknowledgement, and exit
/tmp/tilem-power-cycle.traceshutdown 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.

LayerMain evidenceWhat it establishes
TI-OS kernelram:0038ram:04B2 and ram:09B5ram:0A5Finterrupt routing, standard-timer consumers, APD counters, and shutdown [confirmed]
TI-OS banked code33:5E1E33:5F69 and 37:535937:5950programmable-timer API and RTC conversion/access [confirmed]
TI-OS dynamic executiontools/macros/power-cycle.macro and resolved TilEm tracesstandard-timer cadence and the explicit shutdown/HALT path [confirmed]
Public hardware notesWikiTI ports 0x03, 0x04, 0x20, 0x2D, 0x2F, 0x300x38, and 0x400x48register semantics and oscillator-derived rates [standard]
Emulator modelsTilEm commit f56ad63, Wabbitemu commit 48c2dc0, MAME 0.287, and jsTIfied 20170706aindependent timer decode, scheduling, status, interrupt, and RTC policies [standard]
Native emulator executionguarded TilEm, Wabbitemu, and MAME timer/interrupt runssource 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]

BlockRegistersResolution or sourceOS use
Standard hardware timersport 0x04 rate; port 0x03 mask/ackfour crystal-derived rateskernel tick, keypad scan, cursor, APD
Programmable timers 1–3triplets 0x300x38crystal or divided CPU clocktimer bcall API and USB timeouts
Real-time clock0x400x48one-second, 32-bit counterdate/time bcalls and TI-BASIC clock commands

CPU speed

Port 0x20 selects CPU speed. Value 0 selects the nominal 6 MHz mode; values 13 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 0x290x2C 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]

BitSourceOS branch from the dispatcher
0ON keyon_irq at ram:015B
1standard hardware timer 1standard_timer1_irq at ram:0167
2standard hardware timer 2ram:01F1
3ON key level, active lowtested as state rather than a source
4link activitylegacy_link_irq at ram:01E0
5programmable timer 1 completestatus check at ram:013A; handler 33:5EB4
6programmable timer 2 completeram:0154 path
7programmable timer 3 completestatus 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:5EB4 continues the OS timer API’s programmable-timer-1 countdown.
  • 35:4792 stops programmable timer 3 and services a USB timeout/event structure through ports 0x8E, 0x91, and 0x92.
  • standard_timer1_irq handles 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 periodTimer-1 frequencyTimer-2 frequency
0001.953125 ms512 Hz1,024 Hz
0114.39453125 ms227.555556 Hz455.111111 Hz
1026.8359375 ms146.285714 Hz292.571429 Hz
1139.27734375 ms107.789474 Hz215.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]

ConsumerGate or counterCode
Run indicatorindicCounter at 0x8476run_indicator_tick at ram:027B
Keypad scan and repeatstate at 0x84400x8443kbd_tick_debounce_repeat at ram:03B4kbd_scan_matrix at ram:0406
Cursor blinkcurTime at 0x844Acursor_blink_tick at 06:7C45 through the ram:3FCF bjump
General countdownword at 0x9C24apd_timer_tick at ram:0355
APDapdSubTimer/apdTimer at 0x8448/0x8449ram:036Cram: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.

QuantityMinimumMaximum
Timer ticks29,44129,696
Seconds273.134277275.500000
Minutes4.5522384.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]

_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]

TimerSource/frequencyMode/statusCounterPort-0x04 completion bit
10x300x310x325
20x330x340x356
30x360x370x387

Source and divisor

The high two frequency-register bits choose the clock family. The low bits encode a family-specific divisor. [standard]

Value or familyResult
0x00timer off
0x4032.768 kHz divided by 3
0x4132.768 kHz divided by 33
0x4232.768 kHz divided by 328
0x4332.768 kHz divided by 3,277
0x44, 0x45, 0x46, 0x4732.768 kHz divided by 1, 16, 256, or 4,096
0x80, 0x81, 0x82, 0x84, 0x88, 0x90, 0xA0CPU clock divided by 1, 2, 4, 8, 16, 32, or 64
0xC0 familyCPU 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 0x400x47 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 bitMeaning
0loop after expiry
1request a maskable interrupt on expiry
2overflow: 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 0x300x35. 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 526C5281 are absent. The ABI below is reconstructed from 33:5E1E33:5F69. [confirmed]

Entry points

BcallIDBodyInputsSuccess result
_InitTimer526C33:5E38noneB=0x70, A=0, carry clear
_KillTimer526F33:5E4EA=0x70stops hardware and clears all state
_StartTimer527233:5E58A=0x70, DE duration, C!=0 for auto-restartstarts or completes immediately
_RestartTimer527533:5E9Dsame duration/restart inputsreplaces the current run
_StopTimer527833:5F42A=0x70stops hardware and clears running
_WaitTimer527B33:5EA4A=0x70, DE durationstarts once and busy-waits for finished
_CheckTimer527E33:5F16A=0x70HL expiry count; Z if unfinished, NZ if finished
_CheckTimerRestart528133:5F27A=0x70returns 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

AddressSizeMeaning
0x9C0C1bit 0 initialized; bit 1 running; bit 2 finished; bit 3 auto-restart
0x9C0D2original DE duration for auto-restart
0x9C0F2remaining chunk word
0x9C112saturating 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]

PortsAccessMeaning
0x40read/writebit 0 enable; rising edge on bit 1 commits a new count
0x410x44read/writestaged set value, least-significant byte first
0x450x48readcurrent 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 0x84990x849C. 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 0x010x03 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

BcallIDBodyRole
_chkTmr514337:54C1clock-value conversion/check entry
_getDate514F37:550Bdate into the floating-point stack
_GetDateString515237:55E8format the current date into DE buffer
_getDtFmt515537:5581return date-order setting 1, 2, or 3
_getDtStr515837:55A9date-string wrapper using current format
_getTime515B37:5551seconds, minutes, and 24-hour hour values
_GetTimeString515E37:567Eformat current time into DE buffer
_getTmFmt516137:5593return 12- or 24-hour setting
_getTmStr516437:55CFtime-string wrapper using current format
_SetZeroOne516737:5359helper for clock-setting parser state
_setDate516A37:536Evalidate and set a date
_IsOneTwoThree516D37:5438validate the three date formats
_setTime517037:540Dvalidate and set a time
_IsOP112or24517337:5413validate 12/24-hour selection
_chkTimer0517637:557Ejump directly to rtc_read_seconds
_timeCnv517937:56C4clock/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]

AddressOperationEffect
ram:0A4BOUT (0x04),0x06map mode 0 and slow standard-timer rate
ram:0A4FOUT (0x03),0x11ON and link interrupts enabled; both standard timers disabled; low-power-on-HALT selected
ram:0A51clear shift2ndremove the [2nd] modifier
ram:0A55clear onRunningmark the OS as powered down
ram:0A5BEIallow the selected wake interrupt
poweroff_halt_loopHALT
JR 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_vectorint_entry_save_alt_regson_irqon_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]

AreaDocumented contractTilEm f56ad63Wabbitemu 48c2dc0MAME 0.287jsTIfied 20170706a
Crystal divisors for 0x400x433, 33, 328, 32773, 33, 328, 32773, 32, 327, 32763, 32, 327, 32763, 33, 328, 3277
CPU familiesCPU clock divided by 1–64implementedimplementedall nonzero values instead use 32.768 kHz and the low-three-bit crystal tableimplemented with divisors 1–64
Mode-3 sourceadditional port-0x2F divisorordinary CPU-family decodeordinary CPU-family decodesame fixed-crystal decode; port 0x2F is unmappedordinary CPU-family decode
Counter 0recurring 256-count timer without completionimplementedreaches ordinary underflow after 256 decrementsnever decremented by the callbackscheduled by the same countdown path as other reload values
Mode bit 1set requests interruptset requests interruptset requests interruptclear requests interruptset requests interrupt
Mode/status bit 2missed acknowledgement/overflowset on a second unacknowledged expiryset on the first underflownever exposed; mode writes retain only bits 0–1completion/loop state is held in emulator timer fields
RTCports 0x400x48host wall time plus offsetemulated elapsed time plus baseunmappedimplemented

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 0xC0 family uses the ordinary CPU-family decode, so port 0x2F does not prescale it.
  • Port 0x2D stores its low two bits but does not pause the oscillator or programmable timers in low power.
  • An internal NO_HALT_INT flag suppresses programmable-timer interrupts during HALT when neither standard timer is enabled at port 0x03.
  • The RTC uses host time_t plus 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 0x290x2F 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 0x400x48 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 0x300x38 from this block. Ports 0x2D0x2F and RTC ports 0x400x48 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 0x2D0x2F and 0x400x48 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_irq drives APD, keypad scanning, cursor blink, and the run indicator.
  • [confirmed] 33:5EB4 is the programmable-timer API interrupt handler; 35:4792 is 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/3277 crystal divisors; pinned Wabbitemu and MAME sources use 32/327/3276.
  • [standard] TilEm, Wabbitemu, MAME, and jsTIfied all omit the published port-0x2F prescaler from their 0xC0-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 HWTMR image 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-0x04 writes 0x00 and 0x06, 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-0x2F prescaler, first-versus-second-expiry meaning of mode/status bit 2, counter-zero edge, and precise reason programmable timers fail to wake HALT need 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

SourceUsed for
WikiTI interrupt overviewsource bits, masks, acknowledgement, and HALT notes
WikiTI port 0x04standard-timer rates and programmable completion bits
WikiTI port 0x20CPU-speed settings and physical measurements
WikiTI ports 0x2D and 0x2Flow-power crystal control and mode-3 prescaler
WikiTI programmable timerstimer triplets, divisors, modes, overflow, and HALT quirk
Bad Apple application source at 111dcf1 and companion encoderthird-party timer setup, ISR output, tracker cadence, and note-counter constant
WikiTI RTC control, set registers, and current registersRTC protocol and 1997 epoch
WikiTI hardware historyASIC integration, quartz oscillator, and TI-84 Plus RTC
Datamath TI-84 Plus hardwareTA2/TA3 identification, ASIC/PCB photographs, and 15 MHz specification
TilEm x4_io.c, x4_init.c, and timers.cemulator timer, RTC, interrupt, and power policy
TilEm calcs.c and z80.creset sequencing and scheduler-state retention
Wabbitemu 83psehw.c, 83psehw.h, core.c, and calc.cindependent source decode, catch-up, underflow, HALT, RTC, and reset-retention policies
MAME 0.287 ti85.cpp and ti85_m.cppmapped ports, scheduling, callback polarity, standard timers, and driver status
jsTIfied deployed 20170706a artifact and readable mirrorfourth 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:000000: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 0x80B73F:477C) and _getHardwareVersion (bcall 0x80BA3F:4781). The USB boot support entry points route through the same table but land on page 2F, for example _AttemptUSBOSReceive (0x80E42F:4145) and _InitUSB (0x81082F: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 (0x80000x9BC3, then 0x9BD00xFFFF, leaving the 0x9BC40x9BCF 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 — recall cxCurApp is 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, sets flags.appFlags, and saves cxPage = 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 in cxPage first.
  • _PutAway (ram:08AF) calls the current context’s PutAway handler (cxPPutAway) to suspend/clean up — used on APD, when switching apps, or on 2nd+QUIT. [confirmed]
  • _PowerOff (5008, body ram:09E6) performs context/display cleanup and joins poweroff_shared_tail at ram:0A24. The shared tail disables the standard timers, enables ON/link wake, and enters poweroff_halt_loop at ram: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):

OffAddrFieldMeaning
+0858DcxMainmain/event handler ptr
+2858FcxPPutAwayputaway handler ptr
+48591cxPutAwayputaway
+68593cxRedispredisplay/repaint handler ptr (the inc’s cxRedisp bcall, id 0x4C6C, body ram:08D0, reads this slot via LD HL,(8593) and dispatches it)
+88595cxErrorEPerror entry point ptr
+108597cxSizeWindwindow-size handler ptr
+128599cxPageflash page the handlers live on
+13859AcxCurAppcurrent context id — equals a key code (cxGraph=kGraph, cxCmd=kQuit, cxPrgmEdit=kPrgmEd …)
+14859BcxPrevbase of the 14-byte shadow of cxMaincxCurApp (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 cxPrevcxMain (0x859B0x858D) and copying a 15th byte into the app-flags, and a matching save path (the LDIR at 07:5A8C) copies cxMaincxPrev. 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 in A (the TIError enum: E_Domain, E_DivBy0, E_Memory, … each ORed with E_EDIT=0x80 if re-editable). _JError stores the code to errNo (0x86DD); the sibling entry _JErrorNo (ram:2799) raises the already-stored errNo without taking a new code.
  • The handler restores the stack from errSP (0x86DE, LD SP,(errSP) at ram:27BB), restores a sane state, and displays the error screen (ERR: + message, with 1:Quit 2:Goto). errSP is the current error frame; _resetStacks seeds it from onSP (0x85BC, the context-level saved SP) at context/parse start.
  • The E_EDIT bit (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 0x4D41ram: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]

CodeTIErrorMessage @ page_07
1E_OverflowOVERFLOW (6B3C)
2E_DivBy0DIVIDE BY 0 (6B45)
3E_SingularMatSINGULAR MAT (6B51)
4E_DomainDOMAIN (6B5E)
5E_IncrementINCREMENT (6B65)
6E_BreakBREAK (6B6F)
7E_NonRealNONREAL ANS (6B75)
8E_SyntaxSYNTAX (6B81)
9E_DataTypeDATA TYPE (6B88)
10E_ArgumentARGUMENT (6B92)
11E_DimMismatchDIM MISMATCH (6B9B)
12E_DimensionINVALID DIM (6BA8)
UNDEFINED, MEMORY, INVALID, ILLEGAL NEST, BOUND, WINDOW RANGE, ZOOM, LABEL, STAT, SOLVER, …
31–35link-error aliasesLINK (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 and cxPage offsets are pinned by tracing _AppInit (ram:0936): LD DE,0x858D / LD BC,0x000C / LDIR then IN A,(6) / LD (0x8599),A. See Context block layout above for the full offset table and _AppInit body. _AppInit installs the block; it is not the sole writer — _POPCX (bcall 0x49E107:6D1C) restores a saved context into cxMain, and a save path at 07:5A8C copies cxMain into the cxPrev shadow.
  • Boot RAM-init trace — raw-disassembly trace. Emulator reset starts at logical 0x8000 on page 3F and reaches boot_os_entry; the page-0 restart vector at ram:0000ram:028C reaches the same continuation. The RAM clear/re-init is ram_reset_wipe (35:719F): two LDIR zero-fills (0x80000x9BC3, 0x9BD00xFFFF) preserving a few flag bytes, then JP 0x0BD9 (ram_init_after_reset: port 0 = 0xC0, stack reset in the raw trace, CALL 0x3EC1). The ram:0BD9 entry 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) to 3F:4C8F, _WriteFlashUnsafe (8087) to 3F:4CA6, _WriteAByte (8021) to 3F:4C9F, and _EraseFlash (8024) to 3F:4C2A. Their program and erase loops are copied to ramCode at 0x8100. A successful archive trace executes archive_write_record at 3D:64AA, three _WriteAByte calls, 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

EvidenceWhat it establishesConfidence
OS 2.55MP page 3F bytespage layout, instructions, direct branch targets, table entries, and validation checks[confirmed]
Rebuilt Ghidra databasefunction boundaries and cross-references within page 3F and the USB payload on page 2F[confirmed]
Four reset-origin TilEm tracesordinary, DEL-held, STAT-held, and MODE-held startup behavior in the pinned emulator[confirmed] for those runs
Full 2007 ti83plus.incofficial names for 83 of the 87 callable table entries[confirmed]
Physical calculator with a sending peerelectrical 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 rangeContents
3F:40003F:400Ereset stub
3F:400F3F:4017NUL-terminated version string and header data
3F:40183F:40D463 bcall entries, IDs 0x80180x80D2
3F:40D53F:40E3bank/return dispatch stub, not bcall entries
3F:40E43F:412B24 bcall entries, IDs 0x80E40x8129
3F:412C3F:7E4Dexecutable code and data
3F:7E4E3F:7FFF434 erased bytes (0xFF)

The table therefore has 87 populated three-byte entries in two ranges, not one continuous range. Treating 3F:40D53F: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.

IDBodyInferred role
0x804Ecertificate_reconcile_id_fields at 3F:4924reconcile calculator-ID certificate fields and rewrite the certificate/validation data
0x8066certificate_find_matching_field_data at 3F:4F91find matching data under certificate field 0x0310 and subfield 0x0610
0x8069certificate_count_matching_fields at 3F:4EFFcount or match certificate fields beginning with field 0x0300
0x810Busb_set_port81_bit0_delay at 2F:62C5set 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 keyFirst scanObserved endpoint within the trace
none0x00ram:0053, then ram:0C4F
DEL0x38boot_link_receive_wait at 3F:63B2
STAT0x20_AttemptUSBOSReceive at 2F:4145
MODE0x37ram: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_wait at 3F:63B2. [confirmed]
  • STAT first sets bit 5 of the boot flag byte at IY + 0x1B. The receive dispatcher observes that flag and calls _AttemptUSBOSReceive at 2F: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

EvidenceWhat it establishesConfidence
OS 2.55MP page 3F bytesinstructions, branch targets, port values, safety checks, OS-validity tests, and RAM-test pattern[confirmed]
Rebuilt Ghidra databasefunction boundaries and cross-references for the keypad, display, OS-validation, and recovery helpers[confirmed]
Full-reset TilEm instruction traceone 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 probeactual 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 tablesinstruction timing used for the reset-delay calculation[standard]
Public port descriptions and emulator implementationsproposed 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 measurementsoscillator 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 0xC00xFF;
  • adding eight to the saved stack pointer does not carry;
  • port 0x06 & 0x3F is page 0x3F or one of pages 0x2C0x2F;
  • port 0x07 equals 0x81;
  • 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]

StageWrites in execution orderROM evidence
Link and low-power setup0x2D = 0x02; 0x00 = 0x00; 0x09 = 0x97; 0x0A = 0xB4; 0x0B = 0xB4; 0x0C = 0xB4; 0x08 = 0x80; 0x08 = 0x003F:41B641BA; boot_link_assist_init
Bus timing0x29 = 0x17; 0x2A = 0x27; 0x2B = 0x2F; 0x2C = 0x3B; 0x2E = 0x45; 0x2F = 0x4Bboot_bus_timing_init at 3F:41BD41D3
Execution controls0x21 = 0x00; 0x22 = 0x08; 0x23 = 0x29; 0x25 = 0x10; 0x26 = 0x20boot_execution_protection_init at 3F:41D54206
Runtime mapping0x0E = 0; 0x0F = 0; 0x05 = 0; 0x06 = 0x3F3F:42074210
GPIO and USB control0x39 = 0xF0; 0x4A = 0x203F:42124218
Gate and final RAM windowprotected 0x14 = 0; 0x07 = 0x803F:421A422B

The bytes and their execution order are [confirmed]. Their subsystem meanings have separate evidence limits:

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 0x8A520xFFFF. The caller then clears port 0x27 and tests RAM pages selected by port 0x05 = 2 through 7, each from 0xC0000xFFFF. 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]

OrderVisible screenConstruction
10x81 in every visible byte, with rows 0 and 63 set to 0xFFone boot_lcd_fill_pattern call plus two boot_lcd_write_row calls
2all 0xFFequal-byte fill
3all 0x00equal-byte fill
4alternating 0x55 and 0xAA rowsalternating-byte fill
5alternating 0x00 and 0xFF rowsalternating-byte fill
6all 0xAAequal-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 1115, 2126, 3134, the five-entry rows 4145 through 9195, and 102105. 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_initialize emits seven commands and falls through boot_lcd_restore_contrast into boot_lcd_write_contrast;
  • boot_lcd_fill_pattern emits 24 command and 768 data writes for alternating 0x55/0xAA rows;
  • boot_lcd_write_row emits 24 command and 12 data writes, changing all 12 bytes of row 63 to 0xFF;
  • an explicit A = 0x27 call to boot_lcd_write_contrast emits 0xFF, 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:413F on 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 0x8000 and 0xC000.
  • 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]:

PtrAddrRole
tempMem0x9820base of the temporary area
fpBase0x9822floating-point stack base
FPS0x9824FP stack pointer (grows; _PushReal/_PopReal)
OPBase0x9826base of OP/symbol scratch
OPS0x9828OP/symbol scratch stack pointer (top)
pTemp0x982Etemp-variable pointer
progPtr0x9830currently-executing program pointer
pagedBuf0x983Apaged 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 of HL bytes at address DE by shifting all memory above it up. It calls insertmem_setup (ram:0F8B), which does the LDDR block move (at ram:0FA1), then delmem_fixup_tail (ram:1398) to fix up pointers. _InsertMem does not check free space itself — callers must ensure room first via _EnoughMem (the wrapper _ErrNotEnoughMem at ram:1735 calls _EnoughMem then jumps to _ErrMemory at ram:2721 on 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 from pTemp down to OPBase) 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:9D95ram: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 (id 5017 → body 3D: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_collect at 3C:7733 rewrites live records in 64 KiB sector units and erases the old sectors. gc_show_screen at 3C:7E0D displays "Garbage" and "Collecting..." from page 01. The collector also journals its phase in the inactive 8 KiB half of page 3E. [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 at 0x8100. ram_worker_launcher at 3D:678C installs that worker. The same launcher also runs the internal certificate-page program worker. [confirmed]
  • archive_find_free_span (3D:62C2) scans upward from page 08 to the dynamic App boundary from archive_app_boundary (3D:6413). The OS-only trace returns boundary 0x29 and selects 08:4000. [confirmed]
  • archive_write_record (3D:64AA) writes record states 0xFE then 0xFC; the helpers at 3D:7C8F, 3D:7C93, and 3D:7C97 implement 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_Unarc dispatches on the FindSym page byte B: B==0/in-RAM → 6107 archive, B≠0/in-Flash → 61F4 unarchive.)

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]

AddrFieldMeaning
0x83EEarcInfo.pagepage byte of the data (Flash page if archived; RAM marker otherwise)
0x83EFarcInfo.data_ptr2-byte data address (in Flash window 0x40000x7FFF, or RAM)
0x83F1arcInfo.vat_ptrpointer to the VAT entry’s type byte (the symbol record)
0x83F3arcInfo.dest_ptrdestination data pointer (RAM target on unarchive)
0x83F5arcInfo.data_sizea header/record-size component (loaded from BC after CALL ram:0FDE)
0x83F7arcInfo.sizethe variable’s data byte count (from _DataSize; 07:614B does CALL ram:1485LD (83F7),DE)
0x83F9arcInfo.size_fullsize + header overhead
0x83FBarcInfo.unknown_tailtwo bytes included in the saved tail; semantics unresolved
0x8406savedArcInfo12-byte save slot for arcInfo.vat_ptr through unknown_tail[1]

RAM-heap pointers used by the mem checks (cluster at 0x98200x983A, 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 BB 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 token N, findsym_scan reads page at N+1, the high/low data-address bytes at N+2/N+3, skips version and T2 at N+4/N+5, and reads type at N+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:2042ram: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:

  • _DataSize at ram:1485ram:14BB sends both types through the same leading size-word case and includes the same two-byte header in its result.
  • _Arc_Unarc at 07:6248 branches on the returned page byte in B. Its only post-lookup object-type rejection is GroupObj (0x17) at 07:6263. 07:614B07:6158 obtains either program type’s size through _DataSize.
  • The archive writer masks the VAT type with 0x1F at 07:616F07:6178 and preserves that low-five-bit value in the record. Copy length and page crossing do not depend on whether the preserved value is 0x05 or 0x06.
  • _FlashToRam at 3D:6745 receives 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:524D07:5251. The parser accepts either value and joins the same program path at 38:601238:601C. _ExecuteNewPrgm instead requires ProtProgObj at ram:2670ram: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:

UtilityArchive SHA-256Source member SHA-256Packaged member SHA-256
programs/prgmhide.zip6342d57b18a1277aa3ce13ba514d986fc3c485dfc4b747a157972fad61756103source/PRGMHIDE.z80: 10b7b0bfd902ebdef63f70120ef08d0fcb0fa3144885e8daf53e4880572648a1PRGMHIDE.8xp: 16e0ad05b138ccd15cde0648312b5032bd1f450faa544cd3b6ab1b882d4f0a43
programs/programtoappvar.zip4e27be8774fca769f26f1ce9984026f250fc8c4e9222277d3458a67e7fb25dc9Hide.asm: 65d290071a4ca2837f2e7ad08b3614939ec024a946057c75452f7b174b081a57HIDE.8XP: 0b03d6d7c97322140eb051844458adba1acf95009bf28d827c510e38f545e756
programs/prgmappv.zip33ba32795488b17e316f2efe556f5b323b6ca5b9fa9a1bf08db8dc59bac44ddfPRGMAPPV.z80: 5d75239635d4791b08519e366276aebef3de51d1fe81f68c8cd5ee59f65f97bbPRGMAPPV.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:839Fram: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:839Fram: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.
  • 5F45 resolves/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 _InsertMem and 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_Unarc direction logic. [hypothesis]

Recall. _RclVarSym (38:67B1) and rcl_var_push (3A:5D07):

  • _RclVarSym calls the wrapper at 00:17A6. It runs _FindSym, raises ERR:UNDEFINED on a miss, then tests the returned page byte B; a nonzero page jumps through 00:2779 to ERR:ARCHIVED. It then checks the name token at ram:8479. For a list recall (63/2A) it sizes the data with _DataSize (00:1485) and copies it into a work buffer at ram:91E0, using _LdHLind and cross-page helpers; it ends with JP _OP4ToOP1.
  • _DataSize (00:1485): returns the variable’s data byte-count in DE from the type byte — real=9, list/cplx-list read the word count header, matrix uses cols×rows, and named types (0x15 AppVar, 0x16, 0x17 Group) read the leading word size.
  • Flash is memory-mapped read-only in the 0x4000 window, 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 _FlashToRam remain 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 n
RET 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 the 2729 (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) into A and 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.

TrampolineTargetRole
ram:2FDF3D:61AF archive_prepare_scanprepare archive accounting and scan state
ram:2FF73D:62C2 archive_find_free_spanscan records for a span large enough for the new object
ram:2FF13D:64AA archive_write_recordwrite the record marker, header, name, data, and final status
ram:30033D:6440 unarchive_record_to_ramcopy 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 0x200130x2427C 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_byte
AND C
):

RoutineMask in CBit clearedState after
flash_op_fe (3D:7C97)0xFEbit 0record in-progress (newly begun)
flash_op_fd (3D:7C8F)0xFDbit 1(intermediate / “swap” marker)
flash_op_fb (3D:7C93)0xFBbit 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 0x000x1F 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.

UtilityArchive SHA-256Source member SHA-256Packaged member SHA-256
programs/archive_utility.zip04ca940aacb229a65378450c0c673644bea6fce723215396d195552487e2a7e8archutil.z80: 152f96d1d1b3eee178cd728527c10bacc5cdaa3498941ef750e4df79e2d2b2b2ARCHUTIL.8XP: 0b74c5a8eb6a2daa4e6b598d307cd6cca24948d7a7aa707dcc446bcabbf87569
programs/mirageos/arcrecov.zip991c0324521ac9099276a3de1bc795e87b272cd4c56eba276268b6f100c7d19carcrecov.asm: 1b30935bad150965b42fc75cf786570469d3df63a7ddd33816d664c46acb0a68ARCRECOV.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 testTop App page from 3D:726ECertificate page from 3D:738B
port 0x02 bit 7 clear0x150x1E
port 0x21 & 3 equals zero0x290x3E
remaining branch0x690x7E

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]

ClockOperationMeaning
325020849erase sector containing 0C:4000create the 64 KiB destination sector
328027494program 0C:4000 = 0xFEmark page 0C’s sector as the scratch destination
334678845program 0C:4000 = 0xFCadvance the destination-sector phase before record copy
334829015334924553program 0C:40010C:4015copy and finalize the surviving B record
334939256program 08:4016 = 0xF8mark the old B record as moved
335005172program 0C:4000 = 0xF8advance the destination-sector phase
335063060program 08:4016 = 0xF0retire the old B record
335227372erase sector containing 08:4000reclaim the original 64 KiB sector
338253448program 08:4000 = 0xFEmake page 08 the next empty scratch sector
338293984program 0C:4000 = 0xF0commit 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:60003E:7FFF. It copies the used tail at 3E:7DD23E:7FFF into that half, programs its base byte through 0x8F to 0x00, and erases the old half at 3E:40003E:5FFF. After archive relocation it reverses the operation: it copies 3E:5DD23E: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 0x1DEA0x1E4F. Mode 4 replaces that block and the validity tail at 0x1FE00x1FFF. [confirmed]

The GC block has model-dependent RAM mirrors beginning at 0x837B or 0x82A5. The helper addresses pin its first fields: [confirmed]

Block offsetCertificate offsetHelperRAM mirrorsROM use
+0x000x1DEA3C:7E780x837B, 0x82A5Control flags tested during preparation and recovery.
+0x010x1DEB3C:7E830x837C, 0x82A6The archive App boundary from 3D:6413, incremented once.
+0x020x1DEC3C:7E8E0x837D, 0x82A7Selected 64 KiB archive-sector page.
+0x030x1DED3C:7E990x837E, 0x82A8Master recovery phase.
+0x040x1DEE3C:7EA40x837F, 0x82A9Page erased by the phase-0xF8 recovery branch.
+0x050x1DEF3C:7EBA0x8380, 0x82AAOptional second page erased by the phase-0xFC branch.
+0x060x1DF03C:7EAF0x8381, 0x82ABStart 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 08. 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]

PhaseBranchRecovery action visible in the ROMJoin
0xFF3C:7C43Run the phase-0xFE initializer.gc_run_phase_machine at 3C:7CFB
0xFE3C:7C48Inspect pending sector slots, repair scratch-sector setup, and resume phase processing.3C:7CFB after internal repair branches
0xFC3C:7CC6Erase the selected recovery page and an optional second page through _EraseFlashPage = 8084h.3C:7D0A, after the phase-0xFC write point
0xF83C:7CDAErase the page stored at block offset +0x04.3C:7D1B, after the phase-0xF8 write point
0xF03C:7CE3Search 0xFC and 0xF8 archive-sector headers, then repair or erase the remaining sector.finalization at 3C:7D25 or 3C:7D2B
0xE03C:7D30Run 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]

ValueLoad and callCondition
0xFE3C:7ACF → 3C:7AD1Always after optional scratch-sector header programming.
0xFC3C:7D05 → 3C:7D07Journal flags bit 3 is clear.
0xF83C:7D10 → 3C:7D12Journal flags bit 3 is clear.
0xF03C:7D20 → 3C:7D22The archive-sector consistency check returns carry.
0xE03C:7D2B → 3C:7D2DAlways 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 phaseOriginal-trace triggerInput image SHA-256Cold-restart pathRecovery command shapes
0xFF3345776784e484ad4b99f07a333ae3845ee795b36cb6181e9a829261b2d52ff7931ac8f053C:7BC7 → 3C:7C1F → 3C:7C43 → 3C:7CFB → 3C:7D30582 programs, three erases, 36 resets
0xFE334587331b59cb47398bd186e2eaf7791ad42729e6f29f670da6b1854497eb7fbdbc362a83C:7BC7 → 3C:7C1F → 3C:7C48 → 3C:7CFB → 3C:7D30581 programs, three erases, 35 resets
0xE03382627329c85a13be6d123443457eb772a16664a4a49f06a3d1dc0340b8b8d96a9b12b6b3C:7BC7 → 3C:7C1F → 3C:7D30551 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 phaseOriginal-trace triggerInput image SHA-256Recovery branchRecovery command shapes
0xFC340858598f88f242026c8ae633764573f6dce0e2ef322668dbd149c36a8fb0732987da4913C:7BC7 → 3C:7C1F → 3C:7CC6 → 3C:7D30554 programs, four erases, 23 resets
0xF834096627977b7671e1bdd287022e1863de50a324b9818487be4f05403016e7f4e57b3f7823C:7BC7 → 3C:7C1F → 3C:7CDA → 3C:7D30553 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]

PhaseReference trigger clockInput image SHA-256Recovery branch
0xF0333006337df49d6ec77483e33944fdbcee969084fc065b01a4e44327f83246a9de363fcb23C: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 phaseWabbitemu dispatcher visitsChanged input bytesOutput SHA-256
0xFF7BC7 → 7C1F → 7C43 → 7CFB → 7D30748c857701d7da118d5c5f4c240ee21af91a10b95539059e74fb5e423368a683f9
0xFE7BC7 → 7C1F → 7C48 → 7CFB → 7D30758c857701d7da118d5c5f4c240ee21af91a10b95539059e74fb5e423368a683f9
0xFC7BC7 → 7C1F → 7CC6 → 7D30140dcf62f7445f5bc44b93effb7fd4cdf90d1cf813ad5ea55dd1f7445e0c14003f
0xF87BC7 → 7C1F → 7CDA → 7D30140dcf62f7445f5bc44b93effb7fd4cdf90d1cf813ad5ea55dd1f7445e0c14003f
0xF07BC7 → 7C1F → 7CE3 → 7D30131,08239113ee67921340b8817e35576a8f8fda467122af7713b099f399512d65d9bc3
0xE07BC7 → 7C1F → 7D30128c857701d7da118d5c5f4c240ee21af91a10b95539059e74fb5e423368a683f9

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, else count (INC HL ⇒ off-by-one inclusive). OPS is the top of the upward data heap; the gap to the downward VAT is the real free RAM (see _InsertMem collision 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 from pTemp(982E) down toward OPBase(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 (61F4 calls it before allocating).
  • _InsertMem (00:0F81) / _DelMem (00:1368) — open / close a gap at HL by block-moving everything above; _InsertMem fails E_Memory if it would collide with the VAT.
  • Free archive is computed inside the page-3D archive layer. archive_prepare_scan at 3D:61AF prepares its accounting state, archive_find_free_span at 3D:62C2 searches for placement, and archive_app_boundary at 3D:6413 supplies the dynamic exclusive upper page. The catalog MEM path runs through 3C:7121. [confirmed]

Routine index

space:addrnamewhat
07:6248_Arc_Unarcarchive/unarchive entry; toggles arc flag, dispatches RAM↔Flash
07:628Barc_chk_namearchivable-name validator
07:6107arc_ram_to_flashRAM→Flash archive worker (programs Flash, frees old RAM)
07:61F4arc_flash_to_ramFlash→RAM unarchive worker (carves RAM, copies from Flash)
07:6331arc_size_setupstash vatPtr, compute dataSize into arcInfo
07:61DCarc_save_infosave the 12-byte tail from arcInfo.vat_ptr into savedArcInfo; 07:61E8 is an inferred restore candidate
07:565Ffindsym_scanthe real _FindSym VAT scanner
00:0E65_FindSymRST10 trampoline → findsym_scan
00:0E60_ChkFindSymtype-check OP1 then FindSym
00:1485_DataSizevariable data byte-size by type
38:62A9_StoOtherstore value into named var
38:67B1_RclVarSymrecall var by symbol
3A:5D07rcl_var_pushrecall var, push to FPS
3D:6745_FlashToRamcopy archived data Flash→RAM (page-aware); ti83plus.inc sibling _FlashToRam2 (ID 8054h) maps to 3F:4888
3D:678Cram_worker_launchercopy a length-prefixed worker to 0x8100 and execute it; used by _FlashToRam and certificate-page programming
3D:61AFarchive_prepare_scanprepare archive accounting and scan state
3D:64AAarchive_write_recordprogram a complete archive record; executed in the archive trace
3D:6440unarchive_record_to_ramcopy an archived record to RAM and retire its Flash record
3D:62C2archive_find_free_spanscan from page 08 to the dynamic App boundary for space
3D:6413archive_app_boundaryreturn the first page below the installed App run in B
3D:726Emodel_app_top_pagemodel-specific App scan start (0x15/0x29/0x69)
3D:738Bmodel_certificate_pagemodel-specific certificate page (0x1E/0x3E/0x7E)
3D:727Dinit_flash_page_counterset appSearchPage (0x82A3) to top App page + 1
3D:7C97 / 3D:7C8F / 3D:7C93flash_op_fe/fd/fbclear status bit (0xFE/0xFD/0xFB AND-mask)
3D:7DEAflash_find_nonffscan 13-byte header for all-0xFF (free slot)
00:1837 / 00:182Fprobe_hw_model_keep_a / probe_port21_keep_amodel bits: port 2 bit7 / port 0x21 low
3D:6B6D / 3D:6B9Bflash_write_bounds_check / flash_write_byte_bounds_checkenforce page 08 and dynamic App-boundary limits before block or byte writes
3C:71F8gc_commanddisplay the Garbage Collecting screen, run recovery preflight, and call the collector
3C:7219gc_recovery_preflightinspect persistent GC state and enter recovery only when needed
3C:7733archive_gc_collectnormal collector entry and Flash-unlock wrapper
3C:7768gc_check_archive_sectorsscan four-page archive sectors for a valid starting state
3C:77B5gc_prepare_journalinitialize the RAM phase table and inactive certificate half
3C:781Agc_process_sector_statesdispatch ordinary-sector copy, erase, and finalization work
3C:7BC7gc_check_interruptedtest persistent journal bits at startup
3C:7C1Fgc_recover_by_phasedispatch interrupted states FF/FE/FC/F8/F0/E0
3C:7CFBgc_run_phase_machinerun the normal sector pass and advance persistent phases
3C:7E0Dgc_show_screendisplay "Garbage" and "Collecting..." from page 01
00:0E20_MemChkfree RAM = OPS − FPS
00:0FA6_EnoughMemensure N bytes; reclaim temps
00:0F81_InsertMemopen a RAM gap
00:1368_DelMemclose a RAM gap
00:12D9_DelVarArcdelete var incl. archived copy
00:1308_DelVardelete 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 08 to the exclusive App boundary from 3D:6413. On the traced OS-only TI-84 Plus, that interval is pages 0828; the sector header is at 08:4000, and the first record begins at 08:4001.

  • Hardware Flash path. [confirmed] archive_write_record at 3D:64AA invokes _WriteAByte and _WriteFlashUnsafe; the boot worker runs at 0x8100, 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 0xF00000xFFFFF.

  • Record-status bytes. [confirmed] The record-status byte uses monotonic bit clearing: 0xFF erased → 0xFE in-progress → 0xFC valid via flash_op_fe/fd/fb (3D:7C97/3D:7C8F/3D:7C93) AND-masking. The delete and GC paths write 0xF0 directly to the status byte; flash_find_nonff (3D:7DEA) treats an all-0xFF header as free.

  • Garbage collection. [confirmed] archive_gc_collect at 3C:7733 moves live records in 64 KiB sector units and uses the inactive 8 KiB certificate half as a persistent journal. The ordinary GCFLASH trace copies the surviving B record from the page-08 sector to page 0C, erases the old sector, and rotates the empty scratch sector back to page 08. TilEm and pinned Wabbitemu cold restarts exercise all six ROM-written journal phases. Five converge byte-for-byte with uninterrupted execution; 0xF0 converges after the uninterrupted result performs deferred 0xE0 cleanup 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 (type 0x17) carries a leading word-size header. _Arc_Unarc’s CP 0x1726E0 reject is on the Flash-backed branch before worker 07:61F4. Its only bcall site at 38:56E2 is a wrapper gated by the Archive/UnArchive statement handler at 38:56E8. That handler checks tStore (5Fh) and rejects 16h. Page 07 also contains the group guard cluster: the reject at 07:6266, type-class checker at 07:62C8, insertion guards at 07:7338 and 07:739B, and group-aware VAT walker at 07:73B7.

    A two-program .8xg fixture contains HELLO and FACTOR. It concatenates standard variable records — a 000Dh header-length word, 13-byte header, echoed size, and payload — followed by a 16-bit checksum and no end marker. Libtifiles rejects a trailing 80h byte. A headless TilEm trace compares this fixture with a single-variable HELLO baseline:

    • The members land as individual variables. The RAM dump shows FACTOR in a VAT record with type byte 05h, not a 0x17h Group object. No group blob remains in the final RAM image.
    • The coverage difference contains no new page-07 addresses. Neither trace executes the guard cluster at 07:6266, 07:62C8, 07:7338, 07:739B, and 07:73B7. Those guards belong to the Archive/UnArchive statement 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:785838:790F, the checksum verifier at 3C:6356, and the receive-and-store sequence at 3C:6994. The finalization block resets flags, performs _ChkFindSym-adjacent stores, and reloads VAT pointers from ram:96EEram:96F0 into ram:8588ram:858B. The receive-and-store sequence is documented in Link transfer. The .8xg framing 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 HELLO followed by ordinary FACTOR; the complete link transfer succeeds. In the final VAT, HELLO has page 08, data address 0x4001, and type 05h, while FACTOR has page 00 and type 05h. The PRGM menu marks only HELLO as archived. This confirms that the receiver honors each member’s 80h attribute independently and still creates no type-17h object. [confirmed]

    Invoking that archived HELLO reaches ERR:ARCHIVED. A bounded dynamic trace records 00:179D00:17A1 with B=08, followed by JP NZ,00:2779. The target loads error code 0xAF (E_Archived) and enters _JError at 00:2793. The observed path does not call _FlashToRam automatically. [confirmed]

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:576207:57D1:

  1. 07:5766 loads the internal data size into BC.
  2. 07:576A and 07:5771 check the BB 6D marker.
  3. 07:577B subtracts the internal size from 0x2000 and raises an error on borrow.
  4. _ErrNotEnoughMem at ram:1735 checks that the complete internal size fits in free RAM.
  5. _InsertMem at ram:0F81 opens that many bytes at ram:9D95.
  6. 07:579C adds the allocation size to the saved source pointer because the insertion moved the source variable upward.
  7. The LDIR at 07:579D copies to ram:9D95.
  8. 07:57FD jumps to ram:9D95 through the error-context wrapper at ram: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]

QuantityMaximum
Internal program-data size0x2000 = 8,192 bytes
BB 6D marker inside that size2 bytes
Bytes after the marker inside the variable0x1FFE = 8,190 bytes
Execution allocation0x2000 = 8,192 bytes
Last allocated byteram:BD94
Full ram:9D95ram:BFFF span0x226B = 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 sizeBytes after markerTilEm result
0x1FFF8,189Accepted
0x20008,190Accepted
0x20018,191Rejected 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:571707: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:573407: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:57C407:57CB, then calls _DelMem with HL=ram:9D95 at 07:57CE07:57D1. The error handler at 07:580007: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]

RegisterPost-instruction RET record at ram:9D95
AF0x01BB
BC0xFCCD
DE0xFFEC
HL0x57B4
SP0xFFCB

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]

CheckpointFPSOPSpTempprogPtrSP_MemChk
_ExecutePrgm entry0x9FFA0xFCBA0xFCCE0xFD340xFFD70x5CC1
First payload instruction0xA1710xFCBA0xFCCE0xFD340xFFC90x5B4A
Nested _MemChk entry0xA1710xFCBA0xFCCE0xFD340xFFC30x5B4A
Final payload RET (post-instruction)0xA1710xFCBA0xFCCE0xFD340xFFCB0x5B4A
Cleanup entry0xA1710xFCBA0xFCCE0xFD340xFFD70x5B4A
Cleanup return0x9FFA0xFCBA0xFCCE0xFD340xFFD90x5CC1

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 iMathPtr1iMathPtr5, 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:11E8ram:128A conditionally adjusts 24 OS pointer slots. Named slots include iMathPtr1iMathPtr5, 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:

  1. Keep the AppVar name and expected type in program-owned storage.
  2. Rebuild OP1 and call _ChkFindSym immediately before access.
  3. Require carry clear and B=0. DE points to the two-byte data-size field; the payload begins at DE+2.
  4. Do not retain DE, the payload base, or an interior pointer across a call that can move variables or reclaim temporaries.
  5. Reacquire the base afterward. Store internal references as offsets from the payload base.
  6. 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]

ResultMeaning
Carry setNo matching VAT entry
Carry clear, B=0DE is a RAM pointer to the two-byte data-size field
Carry clear, B!=0B: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:

StrategyFull object must fit free RAMPage crossingBank-A restoration
Direct page mappingNoCaller handles itCaller handles it
Chunked _FlashToRamNoOS handles itOS restores port 0x06
_Arc_Unarc, then RAM accessYesOS handles itNot 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

LauncherRAM-resident inputArchived inputArchive writebackError cleanupEvidence
Ion 1.6Moves the original bodyUnarchives the original, then moves its bodyAlways rearchivesNo Ion-owned error handlerIdentified original source; runtime untraced
Plasma 1.4Copies the body into the current userMem execution allocationCopies the body from Flash with _FlashToRamNone; clients can save to an AppVarNo Plasma-owned error handlerByte-matched release source; release entry traced, client paths untraced
TSE 1.5/1.6Moves the active body and task state between the variable and userMemUnarchives the original, then uses the RAM task pathLeaves the program in RAMCooperative switch and exit paths onlyByte-matched release source; infrastructure loader traced, task switching untraced
MirageOS 1.2Uses a symmetric move loaderCreates a named TempProgObj RAM copy, then moves its bodyRewrites only if changedInstalls an OS error handlerIdentified release-binary disassembly; runtime untraced
Doors CS 7.4Moves the original bodyCreates a complete RAM variable under a derived temporary nameCompares the temporary variable with the archive; replaces the archive only if changedRoutes OS errors through reverse-swap cleanupIdentified source commit; runtime untraced
zStart 1.3.013Moves the original bodyCopies the body into a raw userMem allocationUses a 16-bit checksum; replaces the archive only if changedRoutes OS errors through local cleanupIdentified 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:

  1. Copy at most 768 bytes of the variable body to a screen buffer.
  2. Call _DelMem to remove that source chunk.
  3. Call _InsertMem to open space at the destination.
  4. Copy the buffered chunk into the new space.
  5. Repeat until the body is at 0x9D95.
  6. 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 0x75CF0x76C0. 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 0x78990x78FD 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 0x77D50x7870 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 0x71760x71E9 install IM2. The path writes tasker state beginning at 0x8A3A, code at 0x8A4F0x8A88 and 0x8A8A0x8AFE, and an IM2 handler at 0x8C010x8C1B. It also builds the 257-byte IM2 vector table at 0x8B000x8C00. 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 swap1swap4 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 TempProgObj named Z,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 _ChkFindSym does 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;
  • _ChkFindSym results 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

ArtifactExact identitySource
Ion 1.6 release archiveSHA-256 b5a5ba97f325f8779aa35cda23e38152087930298ff8b7b8573905710230e6e6ion.zip
Plasma 1.4 release archiveSHA-256 62965a41fe071902043ebcbbd1254f710d29729bf86a78f20b6f14d6974f5d5aplasma141.zip
TSE 1.5/1.6 matched release archiveSHA-256 d640729fcb4ebf2a166fe37f3ae59741a50a571578e5091863295bb08dba6a3btsekrnl.zip
TSE matched source archiveSHA-256 d16407c2125133b24a86ad8e88819b3ae0fcc826a55ded5cb4155c19e6239592tsesrc.zip
MirageOS 1.2 release archiveSHA-256 38dc70173818972de8c5eb78099e8870c7acb9ad4c62d290f6c6f5840c71d43bmirageos.zip
Doors CS 7.4 release archiveSHA-256 3a16161ce1d091438b0ea9f5e72774f8e8b4fdfba9ab1024bad0b55569555230dcs7.zip
Doors CS source repositoryCommit 33af4f5ede199eee77cf2f89b5463a0a6ec9a1afDoors CS 7 commit
zStart 1.3.013 release archiveSHA-256 7a1b7c69c85030b412bb6ea11ae71ac608b9882a9de3ab7dbef1faf69519c5e9zstart.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 OP1OP6, iMathPtr1iMathPtr5, 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.

BufferAddress rangeLaunchInteractive inputClassification
OP1OP6ram:8478ram:84B9218 / 44824 / 44Unsafe across ordinary parser, VAT, and floating-point calls
iMathPtr1iMathPtr5ram:84D3ram:84DC80 / 888 / 10Unsafe across VAT, graph, table, and link activity
textShadowram:8508ram:8587442 / 1281,097 / 21Unsafe with ordinary text display
saveSScreenram:86ECram:89EB2,304 / 7683,072 / 768Unsafe in the normal launch state
statVarsram:8A3Aram:8C4C0 / 00 / 0Candidate only after _DelRes, with statistics and shell interrupts excluded
Table/solver workspaceram:91DCram:93010 / 00 / 0Candidate only while table, solver, finance, and graph-table contexts are excluded
plotSScreenram:9340ram:963F0 / 00 / 0Unsafe when graph or buffered-display routines remain available
appBackUpScreenram:9872ram:9B710 / 00 / 0Candidate 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:4F7007: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 appBackUpScreen and a parser hook to appBackUpScreen + 500. It calls _EnRawKeyHook and _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 bcall 4F66h, and launches its loader while that hook can receive key and ON events.
  • Remote Control copies a key hook to its KEYLOC equate at appBackUpScreen, installs it with bcall ID 4F66h, and returns. The hook sends bytes through _SendAByte when TI-OS invokes it.
  • ONBLOCK fills 0x99000x99FF with an IM2 vector, copies its handler to 0x9A9A, selects IM2, and returns while both ranges remain live inside appBackUpScreen. Its handler clears port-0x03 bit 0 before calling the TI-OS IM1 entry at ram: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]

HookTarget recordActive flagSetterClearer
raw keyram:9B84bit 5 of IY + 0x34_SetGetKeyHook = 0x4F66, body 3B:7D00_ClrRawKeyHook = 0x4F6F, body 3B:7B88
tokenram:9BC8bit 0 of IY + 0x35_SetTokenHook = 0x4F99, body 3B:7D0B_ClearTokenHook
parserram:9BACbit 1 of IY + 0x36_SetParserHook = 0x5026, body 3B:7D6E_ClearParserHook = 0x5029, body 3B:7C3B
silent linkram:9BD0bit 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:8600 and ram:8700, places interrupt code at ram:8686 and ram:8787, and alternates display buffers from the handler. Both code ranges overlap saveSScreen.
  • Weird places a signature at ram:86EC, an ISR at ram:8888, and an IM2 table at ram:8700. Its handler also reads apdTimer and writes the LCD through ports 0x10 and 0x11.
  • 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.

ContextResultEvidence and boundary
Direct TI-OS 2.55MPAll 531 bytes passOne TilEm x4 guard with _DelRes, statistics excluded, and IM1; no physical run
MirageOS 1.2 with tasker disabledCandidate onlyThe setup routine returns while tasker flag bit 6 at 0x9689 is clear; no client guard run
MirageOS 1.2 with tasker or custom interrupt activeUnsafeThe original binary installs timers, handler code, and an IM2 vector table inside statVars before client execution
Doors CS 7.4Unsafe as general client storageSource reserves the block for shell state; its Mirage-compatible interrupt also installs code and vectors there
ViewRegs interrupt installedUnsafeThe 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.6UnresolvedSource review pins no Ion-owned interrupt in this block; no client guard run
zStart 1.3.013UnresolvedThe 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 0x71760x71E9 writes these ranges:

RangeMirageOS owner
0x8A3A0x8A3Ethree timer counters and two reload values
0x8A4F0x8A88relocated interrupt code
0x8A8A0x8AFErelocated interrupt dispatcher
0x8B000x8C00257-byte IM2 vector table built by _MemSet = 4C33h
0x8C010x8C1Brelocated timer worker

The timer worker at mapped 0x71400x715A 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 0x8A3A0x8A73. 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 0x8B000x8C00, and optional timers at 0x8A3A0x8A3E. _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 0x8B000x8C00, 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:

ScenarioWritesTouched range
Direct resident launch2,30483:5A7E83:5D7D
Interactive resident input3,07283:5A7E83:5D7D
Guarded _GetKey wait interrupted by ON3,89383:437383:4390, 83:577E83:5794, and 83:5A7E83: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 rangeOwner
83:400083:4080App base-page staging [standard]
83:410083:433AUSB communication buffers [standard]
83:437383:4390Expression-path block copy [confirmed]
83:43D983:44BDBoot/home block copy [confirmed]
83:577E83:5A7DMathPrint previous-entry history [confirmed]
83:5A7E83:5D7DLCD/home-display capture [confirmed]
83:5D7E83:5DF2Additional 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 0x820x87 alias one physical RAM page on 48 KiB ASICs. Pages 0x840x87 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 0x40000x7FFF, 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

SourceUse here
OS 2.55MP ROM and tools/ti84re/trace/analyze_scratch.pyROM ownership and trace write attribution
tools/data/scratch-ram-observations.csvlaunch scenarios, selector assumptions, and write counts
tools/data/scratch-guard-results.csvguard trace identity, shell-owned ranges, and evidence limits
TI-83 Plus Developer Guidedocumented saveSScreen, statVars, _DisableApd, and _DelRes conditions
WikiTI RAM pages, revision 11670public page-0x83 owners and 0x820x87 alias behavior
MirageOS 1.2 release archive, SHA-256 38dc70173818972de8c5eb78099e8870c7acb9ad4c62d290f6c6f5840c71d43btasker setup and client-launch control flow
Doors CS source at 33af4f5shell state, ALE vectors, and Mirage-compatible interrupt ownership
Ion 1.6 release archive, SHA-256 b5a5ba97f325f8779aa35cda23e38152087930298ff8b7b8573905710230e6e6source review for the unresolved Ion row
zStart 1.3.013 release archive, SHA-256 7a1b7c69c85030b412bb6ea11ae71ac608b9882a9de3ab7dbef1faf69519c5e9source review for the unresolved zStart row
NoExec release archive, SHA-256 dc3ddf2dd4de8a802a2862d6aaf671a4ff5e618eb98377844eb711b90a443a84; member noexec.z80, SHA-256 de323ead58eea7b9590865da2694905b775b8f900c798fa438b4aa9b035d58b5static raw-key and parser hook placement in appBackUpScreen
Plasma 1.4.1 release archive, SHA-256 62965a41fe071902043ebcbbd1254f710d29729bf86a78f20b6f14d6974f5d5a; member Plasma/plasma.asm, SHA-256 b424980285adf3f16225239c3ba3f133a42efb38d0666d968eee4b1fe24b810fstatic raw-key hook placement in appBackUpScreen
Remote Control release archive, SHA-256 9eb1d4bb9beabe0ae31e49756c2a23938c6301a27f3d553a5d3381651262e591; member RemoteC.z80, SHA-256 19eb8c5b8b20a1f9139ac89c8603727f76977ddb9548c8ff318ef5eec07285c4static key-hook placement and link-send behavior
ONBLOCK release archive, SHA-256 40a5139d378608a303691fb34f3edf79ae4968bf39801b75bc311371b66f69d2; member ONBLOCK.asm, SHA-256 3023dc7654db87f8f2ea60f54a4b61beba1ca1252cc3fff975409631384ed750static 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 a4f66bc84f2f7d17e2dbfa5603fb3b65ed57f311e20dbb7143ad95bde20d2cf7static 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 2891c83665a136c02fd43df5a5db05ef9be229a5f98263539c4596458f211fa8static IM2 table and interrupt-code placement overlapping saveSScreen
Weird source, SHA-256 881cb1b39c41da3e2629e8cc39765f4cc8e337e6e5a2b65d0539df2cb9fd8ca4static persistent IM2 ownership inside saveSScreen
LCD2 release archive, SHA-256 46532d795aadfff782a83ca52001da87ad73cef9e2013c7800291f4b26af94ab; member lcd2.asm, SHA-256 01033202eb0439a7a6dcdb1b28abf62f2c52aeecc630c4688c8603de75b97780static 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 nibblefollowing length bytes
0xD1 byte
0xE2 bytes
0xF4 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:

fieldmeaningobserved payload
800master Flash-variable field800F with a four-byte app length at the start of every sampled app
801developer/signing key0104, the TI-83+/84+ freeware/shareware app key
802program revisionone-byte revision, usually 1
803build numberone-byte build number, usually 1; MirageOS uses 2
804app nameup to 8 bytes; examples include Axe, MirageOS, USBDRV8X, and zStart
808page countone byte; matches the decoded page count for Axe and CtlgHelp’s two-page apps
809disable TI splash screenusually zero-length when present; zStart uses a 15-byte app-owned payload
80Clowest basecodeusb8x uses 02 1E, decoded as basecode 2.30
032date stampsix-byte payload: nested 09 04, then a four-byte count of seconds since 1997-01-01
020date-stamp signature / unchecked payloadusually 64 bytes; Axe stores executable helper bytes here
807final fieldterminates 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 samplepages field / decoded pagesfinal field endentry bytes at 4080header-area note
Axe2 / 2407000 18 09 C3 97 40 C3 48020 payload contains the 4037 helper; then padding
MirageOS1 / 14070C3 D3 65 C3 D9 47 C3 D6padding to 4080
Omnicalc1 / 14070C3 8C 40 C3 E5 79 C3 70padding to 4080
CalcSys1 / 14070C3 89 40 21 AA 98 CB DEpadding to 4080
Symbolic1 / 1407018 2E 3A 4A 42 4A 4D 4Apadding to 4080
BatLib1 / 14070C3 25 61 C3 6E 43 C3 DEpadding to 4080
BatLib-modified Celtic 3 / Grammer / Omnicalc1 / 14070app-specific jump/vector bytessame boundary; nonzero 807F size bytes are ignored
zStart 1.3.013 / zStart831 / 1408018 11 83 C3 ...809D0F carries a 15-byte Z80 helper at 406B
CtlgHelp / zChem from zStart2 / 2 or 1 / 14070app-specific bytespadding to 4080
usb8x1 / 1402900 00 00 00 00 00 00 96mostly zero padding, plus JP 4180h
JP 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 loops app_find_next_page (5FB1) + a header-match step until done, returning the app’s start page and a found/not-found flag via RST 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 scanning
    
  • app_find_next_page (3D:5FB1) — appSearchPage (0x82A3) -= 1; stops at page 7 (low boundary of the app region); bjumps appSearchPage:0x4000 to inspect the header.
  • init_flash_page_counter (3D:727Dmodel_app_top_page at 3D:726E) — initializes appSearchPage at 0x82A3 to 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 walker app_5de7 (3D:5DE7). app_5de7 keeps two counts in BC (apps before/after) and tracks the current name in OP3.
  • _FindAppNumPages (ID 509Bh) maps to 3D: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:9D95ram: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 0x40000x7FFF. An App therefore avoids the temporary copy that the compiled Asm( launcher creates at ram:9D95. The conventional ram:9D95ram: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 0x40000x7FFF 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:

  1. Save each OS setting that the App changes.
  2. Open or validate persistent AppVars.
  3. Call _AppInit with a cxPutAway handler that reaches the normal cleanup routine.
  4. Install an error frame around each command dispatched by the App.
  5. On explicit quit, PutAway, or a handled error, close mutable variables, persist state, restore settings, and restore the default context.
  6. Call _ReloadAppEntryVecs, then return through _JForceCmdNoChar or _PutAway as 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

ArtifactExact identitySource
RPN83P sourceCommit e2ad0bff98c94a13f34ae461b13f79384a75c17fRPN83P commit
TruVid release archiveArchive SHA-256 ea61474625bc56ef1397fd67f978e29e8bd026ffd4ffc9c2f17c3bdc17f25ca9; member TruVid/source/truVid.z80, SHA-256 2a9a042177197583dae5af51367cfe906e2d7e84f0d15d1e5859a5dd20ee7953truvid.zip
SPASM-ng used for the reference buildCommit 5f0786d38f064835be674d4b7df42969967bb73cSPASM-ng commit

Remaining measurements

The fixture still needs these extensions:

  • reserve generated-code RAM through a named AppVar;
  • record _MemChk, heap pointers, VAT endpoints, and SP before 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:9D95ram: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]

AddrString
01:4076Defragmenting...
01:4098Arc Vars Cleared
01:40A9 Apps Cleared
01:40B8Arc Vars & Apps Cleared
01:4109Resetting All...
01:4126+412EGarbage + Collecting...
01:4234Resetting...
01:742501:746Emenu titles: RESET MEMORY, RESET DEFAULTS, RESET ARC VARS, RESET ARC APPS, RESET ARC BOTH, RESET RAM
01:747Ethe 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):

keyExtendactionmessage shown
1reset archived varsArc Vars Cleared (path 720B)
2reset archived appsApps Cleared (path 7267)
3reset both arc vars+appsArc Vars & Apps Cleared (path 7275)
4reset 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:

  1. System RAM: the half-open interval [appData, 0x9BC4), corresponding to 0x80000x9BC3.
  2. User RAM: [restartClr, 0x10000), corresponding to 0x9BD00xFFFF (0x6430 bytes).

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
bcalladdrbit setflag (inc)
_SetFuncM36:7D11bit 4 (|0x10)grfFuncM (Function)
_SetPolM36:7D2Cbit 5 (|0x20)grfPolarM (Polar)
_SetParM36:7D39bit 6 (|0x40)grfParamM (Parametric)
_SetSeqM36:7D1Fbit 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:

bitnamemeaning
0fmtExponent1 = show exponent (Sci/Eng), 0 = Normal
1fmtEng1 = Engineering, 0 = Scientific (when exponent on)
2-4fmtBaseMask (fmtHex/fmtOct/fmtBin)integer base (Dec/Hex/Oct/Bin)
5fmtRealreal display mode
6fmtRectrectangular complex display (a+bi)
7fmtPolarpolar 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:49E401: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.

LayerMain evidenceWhat it establishes
TI-OS and boot codetools/rom.bin, especially 3D:61AF3D:6BC4 and 3F:47843F:4E56bcall ABI, guards, RAM workers, archive allocation, and status handling [confirmed]
Dynamic executionarchive and GCFLASH TilEm traces plus guarded TilEm, Wabbitemu, and MAME runsROM worker paths, GC sector ordering, execution limits, and native command-state behavior [confirmed] for the pinned emulator runs
ASIC modelTilEm x4_memory.c, x4_io.c, and x4_init.cprotected-byte recognizer, port gates, execution limits, and modeled sector protection [standard]
Flash deviceDatamath’s March 2004 board photograph and Fujitsu MBM29LV800TA data sheetobserved package marking, sector geometry, command cycles, DQ status semantics, and rated limits [standard]
Emulator comparisonpinned TilEm, Wabbitemu, MAME, and jsTIfied sourcemodeled 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 fieldMeaning
8M (1M × 8/512K × 16)8 Mbit array, used here as one MiB of byte-addressable NOR Flash
TAtop-boot sector geometry
-7070 ns maximum read access
PFTN48-pin TSOP(I), normal-bend package
supply3.0 V-only read, program, and erase
program/erase enduranceminimum 100,000 cycles
byte program8 µs typical, 300 µs maximum
sector erase1 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 0x40000x7FFF 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]

OperationByte-mode command cycles
Read/resetF0, or AA 55 F0
AutoselectAA 55 90
Byte programAA 55 A0, then destination and data
Chip eraseAA 55 80 AA 55 10
Sector eraseAA 55 80 AA 55 30
Erase suspendB0 at any address during sector erase or its timeout window
Erase resume30 at any address while erase is suspended
Enter fast modeAA 55 20
Fast programA0, then destination and data; repeat in fast mode
Exit fast mode90, 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]

BitFujitsu data-sheet behavior
DQ7complements programmed data bit 7 while program is active; reads 0 during erase and the array value after completion
DQ6toggles during program, erase, and the sector-erase timeout window
DQ5indicates exceeded program/erase timing; it can also follow an attempt to program a nonblank location without erasing
DQ3distinguishes the open sector-erase command window from the active erase algorithm
DQ2toggles 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 bodyDirect unlock-address storesCommand use
Page-3D program worker at 3D:730A3D:7342, 3D:734B, 3D:7354AA 55 A0, then program data through LDI
Boot erase worker3F:4C48, 3F:4C51, 3F:4C5A, 3F:4C63, 3F:4C6CAA 55 80 AA 55 30
Boot program worker3F:4CFB, 3F:4D04, 3F:4D0DAA 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 rangeSizeLogical pages or page portion
0x0000000x0EFFFF15 × 64 KiBpages 003B, four pages per sector
0x0F00000x0F7FFF32 KiBpages 3C3D
0x0F80000x0F9FFF8 KiB3E:40003E:5FFF
0x0FA0000x0FBFFF8 KiB3E:60003E:7FFF
0x0FC0000x0FFFFF16 KiBpage 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 0xB00000xBFFFF or 0xF00000xFFFFF; 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 0xB00000xBFFFF and 0xFC0000xFFFFF. 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]

BcallIDBodyInputsIntended distinction
_WriteAByte80213F:4C9FA page, DE destination, B byteone byte; permits page 3E, rejects page 3F
_EraseFlash80243F:4C2AA page, HL address in the sectorraw sector selector; no page guard
_EraseCertificateSector80603F:4E3FH=0x40 or H=0x60; L uncheckedselect one 8 KiB certificate sector; hides erase result
_EraseFlashPage80843F:4C1EA pageuse 0x4000 in that page; rejects page 3E
_WriteFlashUnsafe80873F:4CA6A page, DE destination, BC length, HL RAM sourceblock write; permits page 3E, rejects page 3F
_WriteAByteSafe80C63F:4C9AA page, DE destination, B byteone byte; rejects pages 3E and 3F
_WriteFlash80C93F:4C8FA page, DE destination, BC length, HL RAM sourceblock write; rejects pages 3E and 3F
_SetFlashLowerBound80CF3F:4784A value for port 0x23change 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.

ClockCall and triggerReturn AFCondition
186,993,567_WriteFlash, input page 0x7E → masked page 3E0x3E42Z
186,995,033_WriteFlashUnsafe, input page 0x7F → masked page 3F0x3F42Z
186,996,552_WriteFlashUnsafe, input page 0x7D, BC=00x3DBBNZ
186,996,732direct CALL 3F:4CA6 from RAM, input A=0xA50xA591NZ

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.

ClockCall and triggerReturn AFBCDEHLOP1
187,804,393_WriteAByteSafe, page 0x7E3E0x3E420x22330x44550x66770x11 unchanged
187,806,001_WriteAByteSafe, page 0x7F3F0x3F420x00010x66770x84780x44 from B
187,807,587_WriteAByte, page 0x7F3F0x3F420x00010x77880x84780x55 from B
187,807,892direct CALL 3F:4C9F, A=0xA50xA5910x00010x88990x84780x66 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]

WorkerPrefixSource bytesRAM destination
sector eraseboot_flash_erase_worker_descriptor at 3F:4C3B, 0x0052descriptor + 2, at 3F:4C3D3F:4C8EramCoderamCode + 0x51
block programflash_program_worker_descriptor at 3F:4CC8, 0x007Cflash_program_worker_code at 3F:4CCA3F:4D45ramCoderamCode + 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]

StepMapped pageLogical writeValue
1020x6AAA0xAA
2010x55550x55
3020x6AAA0xA0
4targetDEbyte 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]

  1. XOR source and target, then test bit 7. Equal DQ7 means the byte completed.
  2. If DQ7 differs, restore that same target byte and test its DQ5 bit.
  3. Clear DQ5 repeats the first target read.
  4. Set DQ5 causes one final target read and DQ7 comparison.
  5. 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:4D3D3F: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:730A3D:738A. flash_program_worker_code contains 124 bytes at 3F:4CCA3F: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 offsetLengthRange
0x1DEA0x660x1DEA0x1E4F
0x1E500xC80x1E500x1F17
0x1F180xC80x1F180x1FDF
0x1FE00x200x1FE00x1FFF

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:52273D: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]

EntrySelected offsetDirect callers
3D:52270x1DD33D:42D4, 3D:7D7A
3D:522D0x1FE03D:42B3, 3D:4589, 3D:4654, 3D:47A8, 3D:521D, 3D:5448
3D:52330x1F183D: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:52410x1DEA3D:4274, 3D:4298, 3D:42A3
3D:5247model-selected3D:490F, 3D:5385, 3D:548F, 3D:5C0E
3D:52520x1FE03D: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 0x1E500x1F17, 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 0x1F180x1FDF. 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 0x1E500x1F17 to 0x8000 and 0x1DD30x1DDF to 0x80F0. [confirmed]

ModeBranchMode-specific replacement span
03D:423F0x1F180x1FFF (0xE8 bytes)
13D:41ED0x1E500x1F17 (0xC8 bytes)
23D:41DF0x1F180x1FFF (0xE8 bytes)
33D:41FB0x1DEA0x1E4F (0x66 bytes)
43D:42090x1DEA0x1E4F and 0x1FE00x1FFF
53D:421D0x1FE00x1FFF (0x20 bytes)
63D:422Bcomplete 0x1DEA0x1FFF tail (0x216 bytes)

Neither copy loop nor the dispatcher writes port 0x14. Five direct call sites enter the dispatcher: [confirmed]

ModeDirect callByte-pinned gate context
03D:66C7The 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.
13D:5774The 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.
23D:437EThe certificate receive path reaches 3D:4721; Flash App receive preparation reaches 3D:5094. Both inherit gate state.
53D:51D7The enclosing path opens at 3D:70DA; later exits relock at 3D:7194, 3D:71AA, or 3D:71E4.
63D: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]

ModePage-3C callCall chainRole
33C:75583C:7219 → 3C:724A → 3C:7544 → 3C:7558 → 00:2B77 → 3D:40F1Rewrite the 0x1DEA0x1E4F recovery metadata after an archive-sector operation in the recovery loop.
43C:73133C:7219 → 3C:72A5 → 3C:7313 → 00:2B77 → 3D:40F1Initialize 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 0x1E500x1F17. 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 0x1F180x1FFF 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 0x1F180x1FFF 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 0x1F180x1FDF. [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:72D13C: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]

BcallIDPage-3D entry
_SetAppRestrictions52F6h3D:7B9B
_RemoveAppRestrictions52F9h3D:7C1B
_QueryAppRestrictions52FCh3D:7CBA

certificate_tail.restriction_control occupies certificate offset 0x1DD2. The 13-byte restriction_record field at 0x1DD30x1DDF 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:

BitMaskClear-bit meaningEvidence
00x01Base 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.
10x02logBASE is disabled.Type 6 selects mask 0x02 at 3D:7CE3; the UI string at 37:4A42 and query at 37:4E43 name logBASE.
20x04The 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:

TypeRoleSetQueryRemove
0App named in OP1Resolve the App page and clear its bitmap bit.Test the resolved App’s bitmap bit.Unsupported.
113-byte restriction recordProgram 0x847A0x8486 into 0x1DD30x1DDF.Report whether any record byte differs from 0xFF.Replace the record with 0xFF.
2Base restriction controlClear control bit 0.Return 1 when bit 0 is clear.Set control bit 0.
3Aggregate restriction profileClear bit 0 and program the record.Derive an active-profile mask from the control and record bytes.Set bits 04 and replace the record with 0xFF.
4Bulk App bitmapProgram the control byte and 13 bitmap bytes from 0x848E0x849B.Count installed Apps whose bitmap bits are clear.Unsupported.
5App page in BUnsupported.Test the selected App’s bitmap bit.Unsupported.
6logBASE restrictionClear control bit 1.Return 4 when bit 1 is clear.Set control bits 1 and 2.
7Summation restrictionClear control bit 2.Return 8 when bit 2 is clear.Unsupported.

_SetAppRestrictions accepts types 04, 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]

BcallIDBodyBehavior
_MarkOSInvalid8093h3F:5209Stage 0x1F180x1FFF, set bit 0 in the staged 0x1FE0 byte at 0x836D, and erase/rebuild the certificate data.
_MarkOSValid8099h3F:51F5Read 0x1FE0, clear bit 0, and program the byte through _WriteAByte = 8021h.
_CheckOSValidated809Ch3F:52C6Read 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 0x1DEA0x1E4F 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 0x1E4E0x1E4F 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 08 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]

BehaviorPage-3D certificate workerBoot block worker
Prologuesaves target page at 0x9868, then saves the current port-0x06 valuemasks the target page to six bits and maps it directly
Crossing sentinelskips a page-select output when the next page is 0x7Eskips it when the next page is 0x3E
Success mappingrestores the saved port-0x06 valueforces page 0x3F
Failure mappingrestores the saved port-0x06 valueforces page 0x3F
Failure returnreturns the restored page in A; Z if that page is zeroreturns 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.

ClockWorker addressCPU write or read
186,985,124ram:8149attempt data 0x40 at 3D:7FFF after AA 55 A0
186,985,143ram:814Dread array byte 0x50; requested and observed DQ7 agree
186,985,240ram:816Battempt 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.

ClockCommandPhysical targetValue
186,446,349byte program3D:7FFF (0xF7FFF)0x40
186,446,829byte program3D:4000 (0xF4000)0xE0
186,447,016array reset3D: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 0xF7FFF0xF4000 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.

ClockWorker addressObserved result
186,668,556ram:8149program 0xD0 at 3D:7FFF (0xF7FFF)
186,668,575ram:814Dread 0x00; DQ7 differs and DQ5 is clear
186,668,646ram:814Dread 0x60; DQ7 differs and DQ5 is set
186,668,712ram:8159final read 0x20; DQ7 still differs
186,668,738ram:815Dtake the NZ branch to ram:8173
186,668,753ram:8175write array reset 0xF0 at 3D:7FFF
186,668,775ram:817AOR 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]

PageBcall sites and source HL
365E5C=82A5
3C630E=8000, 6AA0=82A5, 6AF5=983A
3D436C=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 0x080x29. 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 40007FFF window therefore aliases the destination page rather than retaining an independent source page. A source in the fixed 00003FFF 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]

ClockCopied-worker addressResolved write attempt and state
187,318,374ram:8149first LDI: 0x4D to locked Flash at 3D:7FFF; BC=1, DE=8000, HL=0069
187,318,708ram:8149second LDI: 0x50 to RAM 8000; BC=0, DE=8001, HL=006A
187,318,824ram:816Bterminal 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.

ClockCall and triggerReturn AFCondition
187,400,702_EraseFlashPage, input page 0x7E → masked page 3E0x3E42Z
187,400,886direct CALL 3F:4C2A from RAM, input A=0xA50xA591NZ
187,402,383_EraseCertificateSector, HL=0x5000, seeded AF=0xA5450xA545caller 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:4E553F:4E56:

POP AF
RET

[confirmed]

The erase worker issues the six-cycle AMD sector-erase command: [confirmed]

StepMapped page or targetAddressValue
1page 020x6AAA0xAA
2page 010x55550x55
3page 020x6AAA0x80
4page 020x6AAA0xAA
5page 010x55550x55
6target pageHL0x30

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]

CallDE evidence at the erase
3D:40A3The 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:60EEThe 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:6127The 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:486E3F: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:0D6500: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 0xF80000xF9FFF, 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 stateTarget-read valueCount
FLASH_BUSY_ERASE_WAIT0x003
FLASH_BUSY_ERASE_WAIT0x443
FLASH_BUSY_ERASE0x0812,245
FLASH_BUSY_ERASE0x4C12,245
array data after completion0xFF1

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.

SampleRelation to selected sectorBusy value and clockArray value and clock
3E:4000 (0xF8000)selected start0x08 at 187,143,4750xFF at 188,343,472
3E:5FFF (0xF9FFF)selected end0x4C at 187,143,5020xFF at 188,343,499
3E:6000 (0xFA000)adjacent 8 KiB sector0x08 at 187,143,5290xFF at 188,343,526
3D:7FFF (0xF7FFF)preceding 32 KiB sector0x4C at 187,143,5740x50 at 188,343,571
3F:4000 (0xFC000)boot sector0x08 at 187,143,6190x3E at 188,343,616
08:4000 (0x20000)distant 64 KiB sector0x4C at 187,143,6640xFF at 188,343,661

Only physical 0xF80000xF9FFF 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 0x150x1E. [confirmed]

3D:6413 starts at a model-selected top App page returned by 3D:726E: [confirmed]

Model branchTop App page
port 0x02 bit 7 clear0x15
port 0x21 & 3 equals zero0x29
remaining branch0x69

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]

TargetPhysical sector
3E:60000xFA0000xFBFFF
0C:40000x300000x3FFFF
3E:60000xFA0000xFBFFF
3E:40000xF80000xF9FFF
08:40000x200000x2FFFF
3E:40000xF80000xF9FFF
3E:60000xFA0000xFBFFF

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 _WriteAByte request can return Z under TilEm without changing the target when the requested and stored DQ7 bits already agree. Port 0x02 and the final array read confirm that the gate remained locked and the target remained 0x50. Physical ASIC behavior remains unmeasured. [confirmed] for the ROM and TilEm trace; [hypothesis] for hardware.
  • The internal page-3D certificate programmer returns the saved port-0x06 page in A after 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.
  • _EraseFlashPage also rejects page 3E with Z. The certificate-sector wrapper restores caller AF after 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 caller AF. [confirmed]
  • _WriteFlash’s page-3E crossing 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 undocumented DE as a reset-command pointer. Two internal certificate paths leave a Flash address there, while the 3D:60EE reset path leaves inherited DE, the 3D:71C3 path carries metadata, and the public bcall accepts arbitrary DE. A forced physical DQ5 test with DE in 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→1 transition. 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, and 0xE0 converge byte-for-byte with uninterrupted execution. Active 0xF0 has matching archive bytes and converges after the uninterrupted result performs deferred 0xE0 cleanup on its next boot. A deterministic eight-record constructor reproduces the record-authentic 0xF0 input 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:0D73 through the protected unlock at 3D:60A6, gc_check_interrupted at 3C:7BC7, public Flash bcalls and copied block workers, and the relock at 3D:5CEF. All six phase images take this path. [confirmed] for Wabbitemu; [hypothesis] for physical gate behavior.
  • A controlled _ReceiveOS_USB run shows that _DisplayOSProgress precedes validation of an installer record’s page byte. Seeding the already-displayed page to 0x3E immediately before that helper isolates the downstream page validator: page 0x3E reaches 2F: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

SourceUse
WikiTI certificate headersliterature labels for certificate-tail offsets, kept separate from ROM-derived ownership
Wabbitemu 83psehw.c at 48c2dc0independent port-0x02 family-bit implementation
WikiTI port 0x14Flash command lock and certificate read protection
WikiTI protected portsprivileged pages and protected-byte sequence
WikiTI _WriteFlash and _WriteFlashUnsafepublic ABI and RAM-source requirement
WikiTI _EraseFlashsector-erase ABI and granularity warning
WikiTI ports 0x21, 0x22, and 0x23chip selection and Flash execution limits
Datamath TI-84 Plus hardware and March 2004 PCB photographFujitsu vendor identification and photographed 29LV800TA-70PFTN marking
Datamath memory-component indexreported AMIC, Fujitsu, Spansion, and Macronix compatible families
Fujitsu MBM29LV800TA/BA data sheet, DS05-20845-4Eexact 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.cpinned 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.cpinned 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.cpppinned 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 56246a1deployed 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-0x14 gate 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_Low before 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 28h with the bcall ID. Do not call the page-3F body address. The raw write and erase cores reject a direct caller whose immediate return address is at or above 0x8000.
  • Keep the stack, source, and destination buffers away from 0x81000x817B. The launcher overwrites that range with the block-program worker. The erase worker occupies 0x81000x8151. The launcher also writes its saved IFF state at 0x82A2.
  • Keep IY at the OS flags base for the write calls. The accepted block path clears (IY+0x25).1; its unused low-source branch can set the same unnamed scratch bit. _WriteAByte additionally overwrites the first byte of OP1 at 0x8478.
  • Treat A, BC, DE, HL, flags, OP1, and the scratch locations above as clobbered when their selected path uses them. The launchers preserve IX and 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 return A=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

NeedEntryProgrammer-visible differences
Program a RAM block outside the certificate and boot pages_WriteFlashRejects starting pages 3E and 3F; still requires complete span validation.
Program a RAM block in certificate page 3E_WriteFlashUnsafePermits starting page 3E; intended only for an owner of certificate update policy.
Clear bits in one ordinary byte_WriteAByteSafeCopies B through OP1; rejects pages 3E and 3F.
Clear bits in one certificate byte_WriteAByteCopies B through OP1; permits page 3E and rejects page 3F.

The block worker expects DE in 0x40000x7FFF 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

PathReturned A and flagsOther visible state
block or byte program succeedsA=0, ZBC=0; HL/DE advanced; page worker ends on page 3F before bcall mapping restoration
block or byte program fails DQ pollingA=0x3F, NZHL/DE at failing byte; BC already decremented
safe write rejects page 3EA=0x3E, Zwrapper-specific scratch changes described above
unsafe core rejects page 3FA=0x3F, Zno worker; byte wrapper may already have changed OP1, BC, and HL
accepted zero-length blockmasked page, NZno worker; write scratch bit unchanged
erase succeedsA=0, Zcaller BC, DE, and HL retained by the erase worker
erase fails DQ pollingA=0xF1, NZwrites 0xF0 through incoming DE
_EraseFlashPage rejects page 3EA=0x3E, Zno worker
_EraseCertificateSector returnscaller’s original AFaccepted 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.

ObservationGuarded result
_WriteFlash return and readbackAF=0x0044; array and copied bytes both A5 5A
_WriteFlashUnsafe page-3E return and readbackAF=0x0044; array and copied bytes both 3C C3
_WriteAByteSafe return and readbackAF=0x0044; array and copied byte both FC
_WriteAByte page-3E return, scratch, and readbackAF=0x0044; OP1=0xF8; array and copied byte both F8
_EraseFlashPage return and readbackAF=0x0044; 0C:4000 array and copied byte both FF
_EraseFlash return and readbackAF=0x0044; 10:4567 array and copied byte both FF
_EraseCertificateSector return and readbackcaller AF=0xA545 preserved; 3E:6001 array and copied byte both FF
shared write scratch(IY+0x25).1 clear after the accepted paths
_SetFlashLowerBound resultport-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]

BehaviorTilEm f56ad63Wabbitemu 48c2dc0MAME 0.287jsTIfied 20170706a
Unlock addresseslow 12 bits 0xAAA, 0x555low 12 bits 0xAAA, 0x555accepts several AMD address conventions, including the ROM’s low-12-bit formlow 12 bits 0xAAA, 0x555
Byte mutationold &= requestedold &= requestedold = requestedold &= requested
Successful program7 µs real-time timer; 42 clocks at the 6 MHz reset speedimmediate array dataimmediate array dataimmediate array data
Illegal 0→1 requesterror stateone transient error readwrites the requested one bitleaves the zero bit unchanged without an error state
Sector erase50 µs command window, then 200 ms erase timer; 300 and 1,200,000 clocks at 6 MHzimmediateimmediate data mutation followed by a timerimmediate; protected sector-table entries are skipped
Autoselectincompletemodeled AMD manufacturer 0x01, device 0xDAIDs at offsets 0/1; no compatible protection readmanufacturer 0xC2 and device 0xDA; each recognized read exits ID mode
Chip erasewritable sectors only; final status follows the last sectorimmediate full-array fill, including bootimmediate full-array fill; stale/default busy rangeimmediate erase of unprotected sector-table entries
Fast programcommand flow present; fidelity unresolvedimplemented for TI-84 Plus Flash version 3entry accepted, but A0 excludes the AMD maker IDabsent
Erase suspend/resumeabsentabsentabsentabsent
CFI queryabsentabsentabsent for AMD_29F800Tabsent
Sector-protection autoselect readunavailable with missing autoselectoffset 4 always returns zerono data-sheet-compatible protection readabsent
ASIC write gateprotected-byte sequence, lock, and sector groupsprivileged-page port-0x14 gate and boot-page flagsno effective Flash-write gateprotected-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 0xB00000xBFFFF and 0xFC0000xFFFFF. 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 caseState after writeBusy readsState after callbackLater reads
legal FF → 50array read, program busy, 42-clock deadline80, C0array read, idle50
illegal 50 → D0error, program busy, 42-clock deadline00, 40error, idle20, 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 0x200000x2FFFF 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 overrideChanged bytesBytes left non-FFLast program address
group 0966,65681,9200xFA000
group 11,048,57600xFC000

Group 0 leaves 0xB00000xBFFFF and 0xFC0000xFFFFF 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.

InitialRequestedInitial DQ6StoredFirst readSecond read
FF5000505050
504000404040
800000000000
50D000502050
50D040506050
008000002000
00010000A000

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 requestFinal stored DQ7ROM result
legalmatches requested DQ7succeeds on the first array read
illegal 0→1 outside DQ7matches requested DQ7succeeds after the final read even though lower requested bits remain zero
illegal DQ7 0→1differs from requested DQ7fails 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.

InitialRequestedInitial DQ6Worker readsStoredResultAF
FF50005050success0044
000100A0, 0000success0044
20A00020, 2020failure3F2C
50D00020, 5050failure3F2C
50D04060, 5050failure3F2C

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:609C3D: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 entriesData writes at ramCode + 0x49 (ram:8149)_EraseFlash entries
0xFF33483
0xFE32473
0xFC20204
0xF819193
0xF030465,5603
0xE017172

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 pathNative observation
AutoselectAA 55 90 enters FLASH_AUTOSELECT; offsets 0, 2, and 4 return 01, DA, and 00
Array resetF0 returns autoselect and a partial AA sequence to FLASH_READ
Fast programAA 55 20 enters FLASH_FASTMODE; two A0 operations store F0 & 50 = 50 and AA & A0 = A0, returning to fast mode after each
Fast-mode exit90 enters FLASH_FASTMODE_EXIT; F0 returns to FLASH_READ
Sector eraseAA 55 80 AA 55 30 changes all 65,536 seeded bytes at physical 0x200000x2FFFF to FF; no byte outside the range changes
Chip eraseAA 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 query98 from array mode returns to FLASH_READ and changes no byte
Erase suspend/resumeB0 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 0x220x28. 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 readRuntime observation
Autoselectoffsets 0, 1, 2, and 4 return 01, DA, 00, and 00
Byte programFF → 50 stores 50; the illegal 50 → D0 request stores D0
Array reset and CFIF0 after a partial unlock restores array reads; 98 leaves the programmed D0 visible
Unlock bypassAA 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 erasethe selected 0xF80000xF9FFF array range changes immediately; reads at 0xF8000, 0xFA000, and 0xFC000 expose busy status, while 0xE0000 remains an array read
Timer completionselected 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 0xF9FE00xF9FE1 (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 rangeSource timerCompletion frameOut-of-sector read while busy
0xE00000xEFFFF1,000 ms500xF0000 = 00
0xF00000xF7FFF500 ms750xF8000 = 08
0xF80000xF9FFF250 ms880xFA000 = 08
0xFA0000xFBFFF250 ms1010xFC000 = 08
0xFC0000xFFFFF500 ms1260xFBFFE = 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 0x180x1F. 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.

LayerMain evidenceWhat it establishes
Retail boot ROM3F:68ED3F:6BF5 and 3F:723F3F:72EAbcall ABI, buffers, descriptor format, exact port order, padding, length accounting, and hash transformation [confirmed]
Dynamic executiontools/tibasic-samples/MD5TEST.8xp, a complete resolved TilEm trace, and guarded TilEm, Wabbitemu, and MAME probes64 valid operations for MD5("abc"), implementing-emulator edge semantics, and MAME’s live unmapped-port behavior [confirmed]
Independent calculationtools/ti84re/hardware/md5.pyevery recorded result agrees with the 32-bit operation derived from the ROM and RFC 1321 [confirmed]
Public hardware notesWikiTI port 0x18 and MD5 bcall pageshistorical port and ABI descriptions checked against the local ROM [standard]
Emulator modelsTilEm f56ad63, Wabbitemu 48c2dc0, and MAME 0.287shift-register policy, masking, reset policy, implemented undefined reads, and MAME’s missing port block [standard]
Algorithm specificationRFC 1321MD5 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 03 selects the Boolean function. [standard]

ModeMD5 nameFunction
0F$(B \mathbin{\&} C) \mathbin{\vert} ((\mathop{\sim}B) \mathbin{\&} D)$
1G$(B \mathbin{\&} D) \mathbin{\vert} (C \mathbin{\&} \mathop{\sim}D)$
2H$B \mathbin{\oplus} C \mathbin{\oplus} D$
3I$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 0x180x1D load six independent 32-bit serial registers. Four bytes are written to one port, least-significant byte first. Reads from 0x1C0x1F 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.

PortWrite behaviorRead behavior
0x18serial input for Aundefined on physical hardware
0x19serial input for Bundefined on physical hardware
0x1Aserial input for Cundefined on physical hardware
0x1Bserial input for Dundefined on physical hardware
0x1Cserial input for message word Xresult bits 7–0
0x1Dserial input for constant T, called AC by WikiTIresult bits 15–8
0x1Erotate count sresult bits 23–16
0x1FBoolean-function selectorresult 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 0x180x1B. 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]

CaseNative result
Fresh reads from 0x180x1B00 00 00 00; the fresh calculated result is 0x00000000
One byte 11 written to operand A0x11000000
Three bytes 11 22 330x33221100
Four bytes 11 22 33 440x44332211
Fifth byte 550x55443322; the former low byte 11 is discarded
Raw shift and mode writes FF, FF0x00000004, matching shift 31 and mode 3 for operands 1 through 6
Reads from 0x180x1B after operand loads00 00 00 00
Mutate A between result-byte readsold 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 0x1D0x1F. 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 0x180x1F. 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:6B7E3F: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]

OperandValue
A0x67452301
B0xEFCDAB89
C0x98BADCFE
D0x10325476
X0x80636261
T0xD76AA478
s7
modeF
result0xD6D117B4

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]

BcallIDBodyInputMain output
_MD5Init808D3F:68EDnoneinitial state and zero bit length
_MD5Update80903F:6907HL data, BC byte countbuffered input and updated state
_MD5Final80183F:6964initialized statepadded 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]

AddressSizeMeaning
0x825916working words copied from the current state
0x82698message length in bits; only the low four bytes are updated
0x82911compact-big-integer length prefix written by _MD5Final
0x829216state words and final digest bytes
0x83A564partial 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]

WordValue
A0x67452301
B0xEFCDAB89
C0x98BADCFE
D0x10325476

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 0x826D0x8270. [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]

RoundDescriptor baseModeMessage-word indexRotate cycle
13F:662DF$g(j)=j$7, 12, 17, 22
23F:66CDG$g(j)=(5j+1)\bmod16$5, 9, 14, 20
33F:676DH$g(j)=(3j+5)\bmod16$4, 11, 16, 23
43F:680DI$g(j)=7j\bmod16$6, 10, 15, 21

Each descriptor occupies ten bytes: [confirmed]

OffsetSizeMeaning
01MD5Temp offset for operand A and result destination
11offset for B
21offset for C
31offset for D
41byte offset of the 32-bit message word in MD5Buffer
51rotate count s
64additive 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]

EventPer stepWhole block
four-byte operand writes to 0x180x1D241,536
mode and rotate writes2128
result-byte reads4256
total301,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:6B7F3F:6BDB, the rotate write is at 3F:6BDF, and reads are at 3F:6A663F: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:72613F:7297 and the subtractor at 3F:7299 implement: [confirmed]

Selector representationOutput 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 4255, 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 0x180x1F. The signature transformation and modular square are software big-integer operations on page 3F. [confirmed]

Emulator comparison and fidelity limits

BehaviorTilEm f56ad63Wabbitemu 48c2dc0MAME 0.287jsTIfied 20170706a
Ports 0x180x1Fmappedmappedabsent; live reads return 00mapped
Operand writessix 32-bit sliding registerssameunmappedimplemented
Control writesshift masked to five bits; mode masked to twosameunmappedimplemented
Result readsrecalculated on each read from 0x1C0x1Fsameunmapped; live reads return 00implemented
Reads from 0x180x1Bzerozerounmapped; live reads return 00modeled by the port block
Reset and statefields cleared on reset and serializedfields serializedno MD5 stateemulator fields are reset and serialized
Driver statususable implementationusable implementationTI-84 Plus marked MACHINE_NOT_WORKINGbrowser 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 0x1C0x1F;
  • reads from 0x180x1B returning 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 0x180x1B after 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

SourceUse
RFC 1321MD5 algorithm, padding, constants, and test vectors
WikiTI ports 0x180x1Fhistorical port register description, checked against ROM and emulators
WikiTI _MD5Init, _MD5Update, and _MD5Finalpublic ABI and historical finalization-bug report
WikiTI _TransformHashhistorical Rabin transformation description, checked and narrowed against 3F:723F
TilEm md5.c and x4_io.cemulator arithmetic, shift registers, masks, reads, and reset
Wabbitemu 83psehw.csecond emulator implementation of the same port block
MAME 0.287 ti85.cppTI-84 Plus I/O map, absent MD5 ports, and driver status
jsTIfied deployed 20170706a artifact and readable mirrorfourth implementation of the ports 0x180x1F arithmetic block
Datamath TI-84 Plus hardwarecalculator 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 0007 and 333D; page 2F contains retail USB boot support, page 3E holds two certificate sectors, and page 3F is the retail boot page. Pages 082E and 3032 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)

PageFuncsRoleRepresentative routines
00928Kernel — mapped at 0x0000; RST vectors, bcall dispatcher, FP core, VAT, memory, integer math_JErrorNo, _LdHLind, _DivHLBy10, _FindSym, _FPAdd, _InsertMem
0184Text display / homescreen_PutMap, _PutC, _PutS, _DispHL, _NewLine, _ClrLCDFull
02271Float transcendentals & advanced math_SqRoot, _LnX, _RnFx, _RndGuard
0323Edit-buffer / small font_CloseEditBufNoR, _Load_SFont, _SFont_Len
0466Graph drawing (pixel/line)_DarkLine, _ILine, _IPoint, _DarkPnt
05118TABLE editor + Graph-Table split-screentable_editor_main, table_recompute, table_paint_grid
0649Key input & edit/cursor_GetKey, _CursorOn, _CursorOff, _PutTokString (note _GetCSC’s body is on page 00)
0744Archive / 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
3370Graph coordinate math and programmable timer API_SetXXOP1, _UCLineS, _InitTimer, _StartTimer, timer_irq
3624Mode setters (Func/Param/Polar/Seq)_SetFuncM, _SetParM, _SetPolM, _SetSeqM
3723Graph coordinate conversion, RTC, and date/time formatting_XftoI, _YftoI, _getDate, _getTime, rtc_read_seconds
38277TI-BASIC parser / evaluator_ParseInp, _Find_Parse_Formula, parse_init
39153Equation pretty-printer (2D MathPrint layout) + menuseqdisp_render_entry, eqdisp_emit_glyph, _DispMenuTitle
3A85Statistics (1/2-var, regressions) + TVM finance_OneVar, reg_gauss_solve, tvm_solve_iterate
3416Token/parser scanning_AHEADEQUAL, _PARSAHEADS, _PARSAHEAD, parse_scan_table
356USB controller paths, memory-reset engine, factorialusb_timeout_irq, mem_reset_dispatch, ram_reset_wipe, op1_factorial
3B39bcall jump table + mem utils(table data) _MemClear, _MemSet, _DrawCirc2
3C72Link / variable transfer_SendAByte, _RecAByteIO, _SendVarCmd, _Rec1stByte
3D61App 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):

PageVerified contents
082E, 3032Blank or unused in this OS image — 100% 0xFF in tools/rom.bin. No app headers.
2FRetail 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]
3439More OS code (parser scan, USB, graph, mode, menu, and RTC); fill 0.2–17% 0xFF.
3Bbcall jump table — starts 99 27 00 = entry 0 (_JErrorNoram:2799).
3CLink 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.
3ECertification 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.
3FRetail 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 8083, 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 8287 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 capacityPhysical backingConsequence at one page offset
128 KiBeight independent 16 KiB blocksselectors 8087 can retain eight different bytes
48 KiBblocks 80, 81, and one block shared by 8287the last write through any selector 8287 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 8487.

WindowPortSelector encodingNormal TI-OS value
4000-7FFF0x06Bit 7 clear selects Flash page value & 0x3F; bit 7 set selects RAM page 0x80 | (value & 7)Banked Flash page
8000-BFFF0x07Bit 7 clear selects Flash page value & 0x3F; bit 7 set selects RAM page 0x80 | (value & 7)81
C000-FFFF0x05The 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 selectorUseEvidence
80Normal C000-FFFF RAM pageThe boot/home trace restores it with OUT (5),0; WikiTI marks it execution-protected. [confirmed] for the restore; [standard] for the protection claim.
81Normal 8000-BFFF RAM pageThe traces access OS variables, OP registers, flags, graph buffers, the user heap, and the VAT window through this selector. [confirmed]
82Temporary half of an OS bank pairThe idle trace selects it through port 0x05 as part of a paged RAM helper, then restores selector 80. No page-82 store occurs. [confirmed]
83Shared OS scratch and stateOS 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]
84No use established hereWikiTI marks it execution-protected. [standard]
85No use established hereWikiTI describes it as unused under typical TI-OS execution. [standard]
86No use established hereWikiTI marks it execution-protected. [standard]
87No use established hereWikiTI 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 8287. 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 8487 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.

ImplementationSource-verified behaviorLimit
TilEm f56ad637x4_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 48c2dc0core.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.287ti85_m.cpp retains raw RAM selectors at ports 0x06 and 0x07. ti85.cpp maps banked RAM across 0x2000000x21BFFF, 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 selectorIdle trace writes2+3 ENTER trace writesInterpretation
80256227 writes, all page addresses touched345702 writes, all page addresses touchedNormal high RAM selected by port 0x05; contains stack, system, and user RAM activity in C000-FFFF. [confirmed]
8162947 writes, all page addresses touched72638 writes, all page addresses touchedNormal 8000-BFFF RAM; contains OS variables, flags, OP registers, the heap, the VAT window, and working buffers. [confirmed]
82No writes observedNo writes observedPort 0x05 briefly selects raw value 02, but the observed store uses selector 83 in bank B. [confirmed]
831882 writes to 43D9-44BD and 5A7E-5DF23467 writes to 4373-4390, 43D9-44BD, 577E-5790, and 5A7E-5DF2Shared OS scratch and state. See the range table below. [confirmed]

The traces never select 8487. That absence describes these scenarios; it does not establish how the selectors behave. Under the public 48 KiB contract, selectors 8287 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 8487. [confirmed]

Two longer direct-TI-OS scenarios add coverage without establishing a borrowable range:

ScenarioPage-83 resultLimit
Resident guard, _DisableApd, _DelRes, _GetKey, then ON3,893 writes to 83:437383:4390, 83:577E83:5794, and 83:5A7E83:5D7DThe 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 dialog3,418 writes to 83:43D983:44BD and 83:5A7E83:5DF2The 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/pathHow to hit itEvidence
80 high RAMRun 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 RAMRun 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 captureRun 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 historyRun home-2plus3.macro.The trace adds 577E-5790, advances lastEntryPTR from 577E to 5791, and sets numLastEntries to 01. [confirmed]
83 expression scratch copyRun 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 copyEnter 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 initializationEnter 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 restoreOpen 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]
8487 independent pagesUse 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 8487 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:

RangeUseEvidence
4373-4390Expression-path page-83 scratch copyAdded 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-44BDBoot/home page-83 scratch copyPresent in the idle trace. flash_copy_block+0x16 performs the LDIR, and 37:44D8 stores one additional byte. [confirmed]
577E-5A7DHomescreen previous-entry historyPage 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-5DF2LCD/home display capture areaPresent 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-4080App base-page staging before app executionWikiTI public note; the two traces on this page do not launch an app. [standard]
4100-433AUSB communication buffersWikiTI 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:

ScenariolastEntryPTR (0x8DA7)numLastEntries (0x8E29)
Idle home screen577E00
After 2+3 ENTER579101

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

SourceUse here
Datamath TI-84 Plus hardware and March 2004 board photographsThree-IC board inventory, ASIC-integrated RAM, and photographed 83PLUSB/TA2 package
WikiTI hardware history, revision 10880Reported 128 KiB design and later 48 KiB revision
WikiTI RAM pages, revision 11670Reported selector uses and 8287 alias threshold
TilEm x4_io.c and x4_memory.cIndependent-page emulator mapping
Wabbitemu core.c and 83psehw.cOptional reduced-RAM alias and model identity behavior
MAME 0.287 ti85.cpp and ti85_m.cppSeven-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.

ProbeProgramResult AppVarPhysical status
MD5 edge behaviorHWPMD5HWPMD511Not run on a recorded unit
RAM selector aliasingHWPRAMHWPRAM21Not run on a recorded unit
ASIC register snapshotHWASICHWPASIC1Not run on a recorded unit
Battery-level stabilityHWBATTHWBATT01Not run on a recorded unit
Raw battery selectorsHWBRAWHWBRAW01Not run on a recorded unit
Raw link readbackHWLINKHWLINK01Not run on a recorded unit
Keypad-matrix settlingHWKEYSHWKEYS01Not run on a recorded unit
Six memory wait classesHWBUSHWBUS001Not run on a recorded unit
Prefixed RAM M1 placementHWPFXHWPFX001Not run on a recorded unit
Programmable-timer edgesHWTMRHWTMR001Not run on a recorded unit
USB control snapshotHWPUSBHWPUSB01Not run on a recorded unit
Flash execution boundariesHWEF07HWEF2Amatching HWEF...01 namesNot run on a recorded unit
RAM execution boundariesHWER81HWER84matching HWER...1 namesNot 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:112900: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:

  1. Delete the probe’s result AppVar if it already exists.
  2. Run Asm(prgmHWASIC) for the read-only register snapshot.
  3. Disconnect the 2.5 mm link port, then run Asm(prgmHWLINK) for the raw-link sample with release-to-idle cleanup.
  4. Run Asm(prgmHWKEYS), release the launch key, and hold the recorded test key or chord until the program returns.
  5. Run Asm(prgmHWBATT) for the restoring battery-level sample.
  6. Run Asm(prgmHWBRAW) for the higher-risk direct-selector sample only after HWBATT succeeds and its result has been exported.
  7. Run Asm(prgmHWPUSB) for the read-only USB control snapshot.
  8. Run Asm(prgmHWPMD5) for the MD5 probe.
  9. Run Asm(prgmHWBUS) on OS 2.55MP for the guarded bus-timing measurement. Export its result before another mutating probe.
  10. Run Asm(prgmHWPFX) for the guarded prefix-M1 timing measurement. Export its result before another mutating probe.
  11. Run Asm(prgmHWTMR) for the guarded programmable-timer edge measurement. Export its result before another mutating probe.
  12. Run Asm(prgmHWPRAM) for the RAM probe only after the earlier transfer and run path works on that unit.
  13. Run at most one HWEF... or HWER... execution probe before exporting its result. A denied fetch may reset the calculator.
  14. Export the new result AppVar to the host.
  15. 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.

OffsetSizeField
04ASCII magic HWP1
41format version, currently 1
51probe ID
62payload length
81port-0x15 ASIC identity read
91port-0x02 status read
10variableprobe 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:112900:112F and the assembled probe listings.

MD5 edge probe

Probe ID 1 records five four-byte fields:

Payload offsetFieldOperation
0valid resultfirst MD5 compression step for "abc"; expected arithmetic result 0xD6D117B4
4undefined readsdirect reads from ports 0x180x1B
8fifth-write resultfour zero bytes and a fifth 0x12 byte written to operand A
12high-control result0xFF written to mode and rotate-count ports
16mixed resultoperand 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 0x180x1F. 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 8287 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 8287. 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 offsetSizeField
01target kind: 0 Flash, 1 RAM
11port-0x06 selector
22logical scan start
42scan length
62selected RET address, or 0xFFFF
81outcome code
97ports 0x04, 0x06, 0x210x23, 0x25, and 0x26
ProgramResult AppVarSelector and scan rangeTilEm x4Wabbitemu
HWEF07HWEF0701Flash 07, 0x40000x7FFFreturnedreturned
HWEF08HWEF0801Flash 08, 0x40000x7FFFviolation resetreturned
HWEF09HWEF0901Flash 09, 0x40000x7FFFviolation resetviolation reset
HWEF29HWEF2901Flash 29, 0x40000x7FFFviolation resetviolation reset
HWEF2AHWEF2A01Flash 2A, 0x40000x7FFFreturnedreturned
HWER81HWER8101RAM selector 81, 0x40000x7FFFreturnedreturned
HWER820HWER82A1RAM selector 82, 0x40000x43FFviolation resetreturned
HWER821HWER82B1RAM selector 82, 0x44000x47FFviolation resetviolation reset
HWER83HWER8301RAM selector 83, 0x40000x7FFFreturnedreturned
HWER84HWER8401RAM selector 84, 0x40000x7FFFviolation resetviolation reset

These outcomes assume the retail boot values: port 0x21 mode 0, Flash bounds 0829, and RAM chunk bounds 1020. They are predictions from the pinned emulator predicates, not physical results. The decoder reports ports 0x04, 0x06, 0x210x23, 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, 0x290x2C, 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 rangeContents
03pre-call ports 0x04, 0x39, and 0x3A, then (IY+0x18) traceFlags
41916 _Chk_Batt_Level results
2024post-call status, ports 0x04, 0x39, 0x3A, and traceFlags
2528readback after restoring the three ports and traceFlags
29final 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 bitPort-0x04 selectorSample order
00x06first
10x46fourth
20x86third
30xC6second

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:4EDC33:4EE8.

After each sequence, the probe reproduces the cleanup at 33:4EEB33: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 419 and post-sequence state at offsets 2024. 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, 0x4A0x4D, 0x4F0x52, 0x540x57, 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.

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 rangeContents
03pre-sequence ports 0x00, 0x03, 0x04, and 0x20
4259256 samples in write-major, trial-major, delay-major order
260263post-sequence ports 0x00, 0x03, 0x04, and 0x20
264port-0x00 read after writing zero to release both lines
265final 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 rangeContents
04pre-sequence ports 0x01, 0x02, 0x03, 0x04, and 0x20
5all-groups read that triggered the held-chord delay
6517512 samples in group-major, trial-major, delay-major order
518522post-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 0x290x2C have both memory-group gates set, and fixed Flash bytes 00:0CE600: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.

OutcomeMeaning
0all guards passed and 12 measurements completed
1timer-2 source was active
2timer-2 mode/status was nonzero
3Flash gate reported unlocked
4at least one Flash/RAM timing gate was disabled
5fixed-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.

CasePort-0x2E maskIterationsWait-sensitive accessesTimed operation
Flash M10x014,09620,480five opcode fetches per call to 00:0CE6
Flash read0x0216,38416,384one fixed-page data read
Flash write0x0416,38416,384one locked 0xF0 reset-command write
RAM M10x1016,38465,537four loop opcodes per iteration plus counter-read opcode
RAM read0x2016,38432,769one data read and one branch operand per iteration, plus counter operand
RAM write0x4016,38416,384one 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 rangeContents
012pre-sequence ports 0x02, 0x03, 0x04, 0x20, 0x290x2C, 0x2E, 0x2F, and 0x330x35
13outcome code
1449six baseline/enabled pairs of counter, mode/status, and port-0x04
5062post-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.

CaseBytesInstructionZ80 M1 fetches per iterationComplete-loop M1 count
Unprefixed00NOP161,441
CBCB 42BIT 0,D273,729
EDED 44NEG273,729
DDDD 7CLD A,IXH273,729
Repeated DDDD DD 7CLD A,IXH386,017
Indexed CBDD CB 00 46BIT 0,(IX+0)273,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 0x290x2C. 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 0x41 divides 32.768 kHz by 33, as published and modeled by TilEm, or by 32, as modeled by Wabbitemu and MAME;
  • whether the 0xC0 source family applies the speed-selected port-0x2F prescaler;
  • 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]

OutcomeMeaning
0all guards passed and all measurements completed
1timer-1 source was active
2timer-1 mode/status was active
3timer-2 source was active
4timer-2 mode/status was active
5a programmable-timer completion bit was pending
6a 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 rangeContents
012pre-sequence ports 0x02, 0x03, 0x04, 0x15, 0x20, 0x2D, 0x2F, and 0x300x35
13outcome code
1429four source-0x41/source-0x45 counter trials
3065four nine-byte mode-3 speed and expiry-count cases
6671counter-zero case
7277first- and second-expiry status case
7890post-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

PathPurpose
tools/probes/hardware/common.incOP1 setup, _CreateAppVar, and frame copy
tools/probes/hardware/asic-snapshot.asmread-only ASIC, timing, and GPIO register snapshot
tools/probes/hardware/battery-level.asmrepeated retail battery-level bcall and restoring state audit
tools/probes/hardware/battery-raw.asmrepeated raw comparator-selector sequence and restoring state audit
tools/probes/hardware/link-raw.asmdisconnected two-wire link readback and instruction-spaced settling matrix
tools/probes/hardware/keypad-settle.asmheld-key and chord matrix-settling measurements
tools/probes/hardware/bus-timing.asmsix-class Flash/RAM wait-state timing matrix
tools/probes/hardware/prefix-m1.asmprefixed-instruction RAM-M1 timing matrix
tools/probes/hardware/timer-physical.asmguarded programmable-timer divisor, prescaler, zero-counter, and expiry matrix
tools/probes/hardware/usb-snapshot.asmread-only low-USB control and status snapshot
tools/probes/hardware/md5-edge.asmcalculator-side MD5 measurements
tools/probes/hardware/ram-alias.asmcalculator-side RAM alias and restoration measurements
tools/probes/hardware/execution-fetch.asmparameterized read-only Flash and RAM fetch measurement
tools/ti84re/hardware/probe.pyreusable TI container, frame, and payload library
tools/ti84re/hardware/bus_timing.pytiming-register models and physical counter-pair decoder
tools/ti84re/emulators/prefix_fetch_models.pyhash-guarded emulator prefix-fetch source analysis
tools/ti84re/hardware/timer.pyreusable source, duration, RTC, and physical timer-result models
tools/ti84re/emulators/describe_prefix_fetch_models.pytext and JSON prefix-fetch comparison CLI
tools/ti84re/emulators/wabbitemu/run_prefix_m1_probe.pyexact-ROM guarded assembled-probe execution CLI
tools/ti84re/emulators/wabbitemu/run_timer_physical_probe.pyexact-ROM guarded assembled timer-probe execution CLI
tools/ti84re/hardware/battery.pyROM decision tree and emulator threshold-region model
tools/ti84re/hardware/describe_battery.pytext and JSON threshold/sample model CLI
tools/ti84re/hardware/build_probes.pySPASM runner, artifact validator, packager, and manifest CLI
tools/ti84re/hardware/decode_probe.pytext 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 _FindSym walk, 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]

ValNameValName
0x00RealObj0x0CCplxObj
0x01ListObj0x0DCListObj
0x02MatObj0x0EUndefObj
0x03EquObj0x0FWindowObj
0x04StrngObj0x10ZStoObj
0x05ProgObj0x11TblRngObj
0x06ProtProgObj0x12LCDObj
0x07PictObj0x13BackupObj
0x08GDBObj0x14AppObj
0x09UnknownObj0x15AppVarObj
0x0AUnknownEquObj0x16TempProgObj
0x0BNewEquObj0x17GroupObj

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:

RoutineAddrRole
_FindSym00:0E65find 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
_ChkFindSym00:0E60route AppVarObj, GroupObj, ProgObj, TempProgObj, and ProtProgObj directly to the length-prefixed scanner; other classes fall through _FindSym
_CreateReal00:10B8make a RealObj named by OP1
_CreateReal/_CreateCplx/_CreateRList/_CreateCList/_CreateRMat/_CreateStrng/_CreateProg/_CreateAppVar/…00:10B0-00:1153one 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/_DelVarArc00:1308/00:12D9delete (and handle archived copies)
_InsertMem/_DelMem00:0F81/00:1368public 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:1008LD 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:

TypeValdataAddrSize (bytes)
RealObj0one TIFloat9
CplxObj0x0Cone TIComplex (re, im)18
ListObj / CListObj1 / 0x0Dcount word + count×TIFloat/TIComplex2 + 9·n / 2 + 18·n
MatObj2columns,rows bytes + row-major TIFloat[] (index math in Matrices and lists)2 + 9·r·c
EquObj3size word + tokenized formula — system var, carries a selection/style byte, auto-evaluated (Graphing, Table)2 + size
StrngObj4size word + tokenized text — inert (see Strings)2 + size
ProgObj / ProtProgObj5 / 6size word + tokenized program (6 = edit-locked)2 + size
AppVarObj0x15size word + raw bytes (any binary, not tokens)2 + size
PictObj7a graph back-buffer image (plotSScreen snapshot)756-byte payload + 2-byte size word = 758 (_CreatePict passes payload size 0x02F4)
GDBObj8graph database: mode byte + window vars + selected equations + stylesvaries
GroupObj0x17an 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:0E65findsym_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 0x84790x847B. 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-2the name bytes (matched against OP1’s 0x84790x847B)
N+1data page (B; 0 ⇒ data in RAM)
N+2 / N+3data address — high byte, then low byte
N+4version metadata
N+5T2 metadata
N+6type — 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 (tVarLst 0x5D), [A]-matrices (tVarMat 0x5C), system vars, and the token-named strings (tVarStrng 0xAA + id) and equations (tVarEqu 0x5E + 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:55D1 reads 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 (Str1Str0) — 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 Str1Str0 are named by a 2-byte token: lead tVarStrng (0xAA) then tStr1tStr0 (0x000x09), so Str1 = AA 00Str0 = 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 (Y1Y0, 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 tokens BB 56 / BB 55t2ByteTok 0xBB then tStrngToEqu 0x56 / tEquToStrng 0x55) copy token bytes between a Str and a Y=/equation variable (string ↔ equation).
  • sub(, length( (_StrLength, id 0x4C3F36:7F91), and inString( operate on the token bytes; _StrCopy (0x44E300: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]

OP1OP6 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:

RoutineAddrShortcutEffect
_FPAddram:229ERST 30hOP1 ← OP1 + OP2
_OP1ToOP2ram:1A2FRST 08hcopy OP1 → OP2 (11 bytes, via copy_op11 ram:1a8e)
_Mov9ToOP1ram:1B01RST 20hload 9 bytes at HL → OP1 (a constant/var)
_CkOP1FP0 / _CkOP2FP0ram:1DE9 / ram:1DEEtest OP1/OP2 == 0 (sets Z)
_CkOP1Realram:1942type-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+3 run (home-2plus3.macro) enters _FPAdd and — signs equal — falls through the sign test to fp_add_mantissa (ram:1cb9), while the 5−2 run (fpsub.macro) negates OP2 and takes the opposite-sign branch into fp_sub_mantissa (ram:1d37). fp_sub_mantissa has 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 0x84810x8482/0x848C0x848Dfp_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).

HelperAddrRole [confirmed]
fp_shift_right_digitram:1beaMantissa 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_diffram:1fbfExponent difference OP1.value.exp − OP2.value.exp (signed). Drives how many fp_shift_right_digit steps are needed for alignment.
fp_add_mantissaram:1cb9BCD 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_mantissaram:1d37BCD 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_guardram:2627Zero 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:

RoutineAddrRole
_FPSubram:2297OP1 = OP1 − OP2
_FPMultram:238BOP1 = OP1 × OP2
_FPRecipram:253DOP1 = 1 / OP1
_FPDivram:2541OP1 = OP1 / OP2
_LnX02:6EFDnatural log
_EToX02:705C
_SinCosRad02:733Esin/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:6F806FEE) 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) steps A=00…07 then 08…0F (the coarse/fine split at the 6FAD AND 0x8 / 6FD3 BIT 4 tests), walking successive 02:7181 rows with a per-step shift-add, then fetches $\ln 10$ via LD A,6
CALL ram:2362 and multiplies. e^{1} (exp1.macro) drives _EToX, which consumes the same table in reverse (the inner step is fp_sub_mantissa 1d37, the accumulator add fp_add_mantissa 1cb9), selector sweeping 00…0F under the 710A CP 0x0F bound. On-screen results: .6931471806 and 2.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:6F8C6FEC 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:

  1. Mode/select flags. 0x8499 holds the trig-op selector — 0x01 (sin), 0x02 (cos), 0x04 (tan) — ORed with 0x80 when (IY+0) bit 2 is clear (BIT 2,(IY+0)
    JR NZ,+2
    OR 0x80). _SinCosRad itself enters with A=0x81, so it stores 0x81 regardless. fp_clear_guard and _ZeroOP3 initialize the work area.

  2. Exponent gate. LD A,(0x8479)
    SUB 0x80
    JP C,02:73D4
    CP 0x0C
    JP NC — tiny arguments (negative exponent) take a fast path at 02:73D4, and arguments with decimal exponent ≥ 12 are rejected to the slow/error path (_JError 0x84 for out-of-range), because reduction can no longer be done accurately.

  3. 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 (mantissa 62 83 18 53 07 17 96 = 6.2831853…), copied to the OP3 work reg via LD HL,02:7D81
      CALL ram:1AE2 (ram:1AE2/copy7_from_8490 copies 7 mantissa bytes to 0x8490).
    • 02:7D8E, 02:7D95, 02:7D96 — companion constants used in the quadrant-fixup / remainder comparisons (CALL ram:1D7B magnitude compare at 02:73B1/02:7447). The quadrant (0–3) is accumulated in B/bStack_1 (bits 0/3/6) and decides sin-vs-cos and the result sign (the XOR 0x1 / OR 0x8 / XOR 0x8 flag juggling at 02:742402:7464).
  4. Per-digit evaluation. The reduced argument enters transcendental_eval (02:7498), the shared engine used by $\ln$ and $e^x$. For sin(1), the reduced argument is $r = \pi/2 - 1 = 0.5707963267948966$. The engine computes $\cos r$, while the quadrant bits in OP5.value.type carry the sign and phase. The recurrence has three phases; the first two consume the trig tables:

    • Phase 1 — digit extraction (02:74A402:74E0). For rows $k=0,\ldots,7$, 02:74A8 sets DE = OP2M and calls the table-A entry at 02:731D. The selected row address is 0x7201 + 16*k + 8*v, where $v$ is bit 7 of OP5.value.type. ram:1A94 copies the eight bytes. fp_align_round_diff aligns the row into OP3 at scale $10^{-(k+1)}$. A non-restoring subtract/add sweep reduces the accumulator modulo 1 and stores one decimal digit in OP5.value.mantissa[k]. For sin(1), the digits are 6,3,8,8,2,4,3,6, leaving $u \approx 2.56\times10^{-9}$. [confirmed]
    • Phase 2 — correction product (02:74E602:7528). Entry 02:7312 loads trig_recurrence_table_b[0], where $b_0 = 0.9509852944837202$, into OP2M. 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. For sin(1), the product is $0.9704891777365256$. [confirmed]
    • Phase 3 — result assembly (02:752A onward). The engine walks the stored digits again with align/add-sub steps and exponent bookkeeping. For sin(1), it produces OP4 = 0.8414709848078931, with cos(1) = 0.5403023058681400 alongside. [confirmed]

    The closed-form identity of the phase-1 digit map remains open; see Open questions.

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 0x80
JP C,02:73D4
CP 0x0C
JP NC — neither branch taken, since the decimal exponent of 1 is 0), and the reduction multiply by the 02:7D81 constant (7372 LD HL,02:7D81
CALL ram:1AE2). The trace records all three recurrence phases and the on-screen result .8414709848. It also records eight phase-1 entries at 02:731D, with B = 07 and HL = 0x7201 + 16·B + 8 after each ram:1A94 copy, followed by one 02:7312 entry 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,n
CALL 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 OP1OP6 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.

RegAddrRole in a calc
OP10x8478primary accumulator / result. Unary ops take arg here, return here.
OP20x8483second operand for binary ops (OP1 ∘ OP2 → OP1).
OP3OP60x848Escratch; sign/exponent staging, complex pairs.
guard0x8481/8482 (OP1EXT), 0x848C/848D (OP2EXT)extended guard digits, zeroed by fp_clear_guard at the top of nearly every op.
FPS0x9824software 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:150F14F6), _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.

OpRoutineAddrNotes
+_FPAddram:229E (= RST 30h)sign-magnitude BCD add; see floating-point.md.
_FPSubram:2297flips OP2.value.type bit 7, then falls into the add path.
×_FPMultram:238Bram:250F adds exponents (→ _ErrOverflow on carry past 0x7F), then digit-by-digit BCD multiply accumulating into OP3.
÷_FPDivram:2541_CkOP2FP0 first → _JError(0x82) DIVIDE BY 0 if divisor 0; else restoring BCD long division.
1/x_FPRecipram:253Dsets OP1=1 then enters the divide loop (same body as _FPDiv).

Convenience / derived ops:

  • _FPSquare ram:238A = RST 08h (OP1→OP2) then _FPMult. [confirmed]
  • _Cube ram:237D = _FPSquare then _FPMult. [confirmed]
  • _Times2 ram:2282 = OP1+OP1; _TimesPt5 ram:2382 loads the constant 0.5 (9-byte BCD @ ram:2635) into OP2 then _FPMult. [confirmed]
  • _InvSub ram:227D = _InvOP1S then _FPAddOP2 − OP1 (reversed subtract). [confirmed]
  • Negation: _InvOP1S ram:24BD (XOR OP1.value.type with 0x80, guarding against −0), _InvOP2S ram:24CD, _InvOP1SC ram:24BA (both). _CkOP1Pos ram:1E5D ANDs OP1.value.type with 0x80. [confirmed]

Roots and integer parts [confirmed]

  • _SqRoot 02:6E38: _ErrD_OP1NotPos (→ DOMAIN if negative/complex-real), fp_clear_guard, _ZeroOP3, then a digit-by-digit BCD square-root extraction loop (ram:1C9C trial-subtract + ram:1D4A compare, halving the exponent up front). A classic long-hand sqrt, not Newton’s method.
  • _Int/_Intgr ram:2621/2263: floor. _Trunc ram:2279 drops the fractional part (toward zero); _Intgr truncates then subtracts 1 (_Minus1 ram:2294) when the original was negative, giving true floor.
  • _Frac ram:24E3: fractional part = x − trunc(x); shifts mantissa by the exponent and keeps the low digits.
  • _Round / _RndGuard ram:2623 / 02:6A57: round to the active display-digit count; _Round is a thin cross_page_jump wrapper (body banked off page 0).

Degree, radian, and polar conversions [confirmed]

  • _DToR ram:236B (deg→rad): multiply OP1 by $\pi/180$ (ram:235D loads the constant) then normalize via ram:249E.
  • _RToD ram:2374 (rad→deg): multiply by $180/\pi$ (ram:2361).
  • _PToR 02:50BD polar→rectangular; pairs with the complex trig below. These constants are the BCD floats π/180 = 1.745…e-2 and 180/π = 5.729…e1 noted 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:

  1. saves the current page (IN A,(6)),
  2. builds a RET-to-page-0 trampoline on the stack (bcall return frame, page restored on exit),
  3. reads a 3-byte {lo, hi, page} descriptor (page masked with 0x1F/0x3F for 83+/84+ via ports 2/0x21),
  4. OUT (6),A to bank the target page in at 4000, 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]

  • _LnX 02:6EFD: _CkOP1Pos; non-positive real → _ErrDomain. For a positive real it calls the real-log core (_CLN path, selector C=2); the generic entry handles complex args.
  • _LogX 02:6F16: same structure, base-10 selector C=0, guards _ErrD_OP1_0/_ErrD_OP1NotPos.
  • _CLN 02:6CCA / _CLog 02:6CE7 — complex log: _CAbs (magnitude) → real _LnX/_LogX for the real part, _ATan2Rad (02:76D4) for the imaginary part (the argument/angle). Uses _PushRealO1/_PopRealO2 to juggle the operand. This is why ln(-2) returns a complex result in a+bi mode but raises _ErrNonReal (0x87) in real mode.

Exponentials [confirmed]

  • _EToX 02:705C (e^x): loads the log10(e) constant through fp_mul_indexed_constant, then falls through into the local _TenX body.
  • _TenX 02:7066 (10^x): splits exponent into integer (digit shift) + fractional (16-slot table-driven evaluation through logexp_digit_table). Argument too large → _ErrOverflow.

Trigonometric functions [confirmed]

  • _SinCosRad 02:733E, _Sin 7342, _Cos 7346, _Tan 734A. Each loads a function selector byte into 0x8499 (1=sin, 2=cos, 4=tan; 0x80 bit set when the rad-special mode tested by BIT 2,(IY+0) is off; _SinCosRad forces 0x81).
  • 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 near 02:7D81 and runs the same table-driven digit recurrence as ln/eˣ over the two trig recurrence tables (one row per digit step, sign-variant picked by OP5.value.type bit 7) — the per-step bcd_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 of 02:7201/02:7281 is detailed in floating-point.md.

Inverse trig [confirmed]

  • _ASinRad 76DA, _ACosRad 76C9, _ATanRad 76CF, _ATan2Rad 76D4, plus the degree-mode _ASin/_ACos/_ATan/_ATan2 at 76F1/76DF/76E9/7749.
  • _ASin/_ACos call domain check 02:79D3; |arg| > 1 → _ErrDomain.
  • All inverse trig funnel into the shared arctangent CORDIC engine at 02:774B (B=0x20 seeds the octant/quadrant base written to 0x84A4; 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]

  • _SinHCosH 7626, _TanH 762A, _CosH 762E, _SinH 7632; _ATanH/_ASinH/_ACosH at 7909/7956/7964. Same 0x8499 selector mechanism; built from _EToX (sinh = (e^x−e^-x)/2, visible in the _EToX+_FPDiv sequence near 02: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).

  • _FormReal 06: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 at 0x89FA (active fixed/decimal-places setting; (IX-1) local holds the effective format byte).
    • Exponent thresholds drive Normal↔Sci switchover: it compares OP1.value.exp against 0x7D/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)).
  • _FormEReal 06:5799 — forces scientific/E notation by setting 0,(IY+0xc) then calling _FormReal. [confirmed]
  • _FormBase 06:57C0 — integer formatting in a base; requires _CkOP1Real (→ DATA TYPE / DOMAIN on non-real). [confirmed]
  • _FormDCplx 06:59D3 — complex a+bi / r∠θ formatting (calls _FormReal twice). [standard]
  • Exponent ↔ ASCII helpers on page 0: _ExpToHex ram:1E4E, _OP1ExpToDec ram:1E77, _DecO1Exp ram: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:

RaiserAddrA codeMessage
_ErrOverflowram:26E80x81OVERFLOW
_ErrDivBy0ram:26EC0x82DIVIDE BY 0
_ErrSingularMatram:26F00x83SINGULAR MAT
_ErrDomainram:26F40x84DOMAIN
_ErrIncrementram:26F80x85INCREMENT
_ErrNon_Realram:26FC0x87NONREAL ANS
_ErrSyntaxram:27000x88SYNTAX
_ErrModeram:27040x9EMODE
_ErrDataTyperam:27080x89DATA TYPE
_ErrArgumentram:27110x8AARGUMENT
_ErrDimMismatch/Dimensionram:2715/27190x8B/0x8CDIM MISMATCH / INVALID DIM
_ErrUndefined/Memoryram:271D/27210x8D/0x8EUNDEFINED / MEMORY

Domain pre-checks (page-0, set Z if OK else jump to _ErrDomain):

  • _ErrD_OP1NotPos ram:2119_CkOP1Pos; not >0 ⇒ DOMAIN (used by _SqRoot, _LogX).
  • _ErrD_OP1Not_R ram:2120_CkOP1Real; complex ⇒ DOMAIN.
  • _ErrD_OP1NotPosInt ram:2125_CkPosInt.
  • _ErrD_OP1_LE_0 ram:212A, _ErrD_OP1_0 ram:212D — zero/sign guards (e.g. ln(0)).

Where the calc engine raises what:

  • ÷ 0, 1/0: _FPDiv/_FPRecip0x82 DIVIDE BY 0.
  • ×/10^x/exponent overflow: ram:250F exponent-add → 0x81 OVERFLOW.
  • √(neg), ln/log(≤0), asin/acos(|x|>1), tan(π/2), |trig arg| ≳ 10^12: 0x84 DOMAIN.
  • Complex result requested in real mode: 0x87 NONREAL 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 21192131.


Worked flow: 2*sin(π/6)+ln(5) [hypothesis]

  1. Parser pushes 2 (OP1), evaluates sin(π/6): loads π/6 into OP1, _SinCosRad/_Sin (selector 0x8499), table-driven digit recurrence → OP1=0.5.
  2. ×: the saved 2 is in OP2 (or popped from FPS) → _FPMultOP1=1.
  3. ln(5): spill 1 to FPS (_PushRealO1), OP1=5, _LnX (_CkOP1Pos passes) → 1.6094….
  4. +: pop 1 to OP2 (_PopRealO2), _FPAddOP1≈2.6094.
  5. _FormReal renders per MODE; result stored as Ans.

Statistics

The statistics subsystem reads list data, accumulates moments, solves regressions, and writes named results such as , Σx, Sx, a, b, r, and . This page separates the CALC, STAT-TESTS, and DISTR command families.

The CALC paths read L1L6 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):

AddrName (.inc)User-facing varMeaning
8A3AStatNnsample count (Σ of frequencies)
8A43XMeanmean of x
8A4CSumXΣxsum of x
8A55SumXSqrΣx²sum of x²
8A5EStdXSxsample std dev of x (÷ n−1)
8A67StdPXσxpopulation std dev of x (÷ n)
8A70MinXminXminimum x
8A79MaxXmaxXmaximum x
8A82MinYminYminimum y (2-Var)
8A8BMaxYmaxYmaximum y (2-Var)
8A94YMeanȳmean of y
8A9DSumYΣysum of y
8AA6SumYSqrΣy²sum of y²
8AAFStdYSysample std dev of y
8AB8StdPYσypopulation std dev of y
8AC1SumXYΣxysum of x·y
8ACACorrrcorrelation coefficient
8AD3MedXMedmedian of x
8ADCQ1Q1first quartile
8AE5Q3Q3third quartile
8AEEQuadAaregression coeff a (highest order)
8AF7QuadBbregression coeff b
8B00QuadCcregression coeff c
8B09CubeDdregression coeff d
8B12QuartEeregression coeff e
8B1B8B50MedX1/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 F2FF). 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:4A003A: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 (F2FF) 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_command and steers everything afterward.
  • LD HL,0x8AEE (= QuadA) is the regression coefficient destination; the solver writes a,b,c,d,e there in descending order of power.
  • _ErrStat (00:2741, id 0x44C2, code 0x15 “STAT”) and _ErrStatPlot (00:2759, code 0x1B) are the STAT-specific error raisers; the _OneVar body jumps to 0x2741 on e.g. fewer than the required data points. _ErrDimMismatch (0x2715) is raised if L1 and L2/freq lengths differ (the 21bb length compare at 6584/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:

TokenValueCommandModel
tOneVarF21-Var Statsone variable
tTwoVarF32-Var Statstwo variable
tLRF4LinReg(a+bx)degree-1 (a+bx form)
tLRExpF5ExpRegy=a·bˣ (log-linear)
tLRLnF6LnRegy=a+b·ln x (log-x)
tLRPwrF7PwrRegy=a·xᵇ (log-log)
tMedMedF8Med-Medresistant line
tQuadF9QuadRegdegree-2
tLR1FFLinReg(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:68456891 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 for linear fits or 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 the 3A:67F7 call to ram:212D; the 0x35/0x36 calls at 3A:68883A:688E are 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 in QuadA(8AEE) downward. [confirmed]

  • Correlation r and 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 (the 6845/684c cluster) and stored to Corr (8ACA). The store offset is pinned: at 3A:684F the code does LD A,0x12
    CALL 0x213D, and 0x213D is _Sto_StatVar (the store counterpart of _Rcl_StatVar 00:2149 — both funnel through the 0x3E07 statVar dispatcher with the name id in A). Id 0x12 = tCorr = the Corr slot, so this single sequence is exactly r → Corr (8ACA). The preceding 3A:6845 _SqRoot/_FPDiv cluster forms the ratio; (and for higher-order fits) is the coefficient of determination derived by the following column-weighted pass. It is stored separately through IDs 0x35 and 0x36, at 0x8C05 and 0x8C0E respectively. [confirmed]

  • The fitted equation is also written to RegEQ (the Y=-style regression equation system var, recalled via token tRegEq=0x01) so RegEQ can 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/MaxX are tracked during the accumulation pass with running min/max compares.
  • The median/quartile path (3A:79B97A0B …) sorts a working copy via the internal sort stat_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 (the 7B30/7B4C/7B6E helpers walk the cumulative-frequency index, and 198d/238b interpolate 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]

  1. Parser pushes the list args, sets A = command token, bcall(_OneVar).
  2. _OneVar parses args → x-list ptr (84D3), y-list (84D5), freq (84DB); saves the model code to stat_calc_command.
  3. Accumulation pass: one walk of L1/L2 building n, Σx, Σx², Σy, Σy², Σxy and minX/maxX/minY/maxY into statVars, plus the 2×2 moment matrix.
  4. 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$).
  5. 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=interceptQuadA/QuadB; r,r²Corr; equation → RegEQ, pasted into Y1.
  6. Results displayed by the STAT-CALC report screen; all of x̄/Σx/…/a/b/r persist in statVars for 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:4A0239:4F5B, with helpers at 39:5D2D39:5E41, 39:6C6339:6D31, and 39:57CF39: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,0x29
JR 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 PStatSStat references. A ROM-wide byte-pattern scan (tools/ti84re/rom/scan_stat_writers.py, immediate or absolute operands landing in 0x8B5A0x8C37) finds about 50 opcode-shaped candidates on page 3A (3A:4B153A: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,0x24
CALL _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:

AddrValueRole
3A:554F0.2316419threshold p
3A:55581.330274429coefficient b5
3A:5561-1.821255978coefficient b4
3A:556A1.781477937coefficient b3
3A:5573-0.356563782coefficient b2
3A:557C0.319381530coefficient 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:7D003A: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:7DF43A: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:addrnamewhat
3A:6420_OneVarSTAT-CALC entry (1/2-Var + all regressions), id 0x4BA3
3A:6572onevar_accumulateone-pass power-sum accumulation loop
3A:6567onevar_powmulrunning power·freq product (OP1→OP2, ×)
3A:6345onevar_frame_teardownrestore stat error frame
3A:6352onevar_frame_teardown_tailon-error tail calling onevar_frame_teardown
3A:6984stat_stddev_poppopulation variance/σ finalize (÷ n)
3A:6989stat_stddev_sampsample variance/S finalize (÷ n−1)
3A:6998stat_var_core(Σx²−n·x̄²) variance core + √
3A:67C6reg_gauss_solveGauss-Jordan solve of normal equations
3A:69AFreg_store_coeffwrite a solved coefficient (matrix set)
00:3A8F/3AA1/3AA7/3AAD/3AB9stat_mtx_index/get/setRAM trampolines for sums-matrix element access by (row,col)
3A:6F6Astat_next_elemfetch next list element, advance ptr
3A:6F7D/6F90stat_freq_defaultdefault frequency = 1
3A:7935stat_sortstat-internal data sort (median/quartile, Med-Med)
3A:79B9stat_median_quartilemedian/Q1/Q3 + Med-Med medians
3A:760F/75E4medmed_partitionMed-Med 3-partition setup
3A:5500ttest_output_stageT-Test result store: ×StdPX, ÷SStat, _Sto_StatVar ID 0x24 (TStat)
3A:554Fnormal_tail_coef_tblZelen–Severo coefficients (p, b5b1) for PStat p-values
00:2385fp_mult_constOP1 ×= (HL)-pointed float constant
00:2532fp_div_constOP1 ÷= (HL)-pointed float constant
39:4A0239:4F5Bdistr_normal_core (unnamed)traced normalcdf( evaluation core on page 39
00:2149_Rcl_StatVarrecall a named statVar into OP1, id 0x42DC
00:2741_ErrStatraise STAT error (code 0x15), id 0x44C2
00:2759_ErrStatPlotraise STAT PLOT error (0x1B), id 0x44D1
00:2294_Minus1OP1 − 1 (n→n−1 for sample stddev)
33:65DC_ZmStatsZoomStat — fit window to plotted data, id 0x47A4
00:2715_ErrDimMismatchlist length mismatch (0x8B)

RAM: statVars=0x8A3A, stat_calc_command=0x8A36, work pointers 0x84AF0x84DB (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:684F does LD A,0x12
    CALL 0x213D (_Sto_StatVar, ID 0x12 = tCorr), i.e. r → Corr (0x8ACA); / is the coefficient of determination from the following column-weighted pass, stored through IDs 0x35/0x36 at 0x8C05/0x8C0E. See the annotated 3A:68453A:6891 listing under Regression solver. [confirmed]
  • DISTR numerical cores. The normalcdf( evaluation path is traced to the page 39 FP core (39:4A0239:4F5B and helpers) — see DISTR functions. The erf / incomplete-gamma / incomplete-beta continued fractions behind the remaining DISTR tokens are unnamed and untraced; the page 38 parse-side table is not the execution dispatch. The exact algorithm in the page 39 core (continued fraction versus polynomial or rational fit) remains [hypothesis].
  • STAT-TESTS (Z/T/χ²/F/ANOVA) fill PStat…SStat/anovaf_vars from their own engine on page 3A. 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 the 3A:7DF4 pointer array remain [hypothesis]. The _Sto_StatVar/_Rcl_StatVar stubs (ram:213D/ram:2149) funnel through the cross-page-jump table at ram:3E07 (one CALL 2B09 + inline addr,page descriptor 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 via rst 28h (the bcall site isn’t fully analyzed in the DB). The SortA(/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 by count × 9-byte TIFloat elements (18-byte complex elements if the list is complex, flagged 0x0C). Element $i$ (1-based) lives at $\mathrm{addr}(L_i)=\mathrm{data}+2+(i-1)\cdot 9$.

  • A matrix is byte columns
    byte rows followed by columns*rows × 9-byte TIFloat, 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 TIFloat through OP1/OP2 and 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 the count/dim header, after which all indexing is pointer arithmetic computed by _AdrLEle/_AdrMEle.

  • One shared Gauss-Jordan engine (02:42A6) implements matrix inverse [A]⁻¹ (flag 0x00) and det( (flag 0x40) 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) computes result = H * L (B=H
    HL=Σ L, a DJNZ add loop) — it computes the element count from the two dimension bytes. [confirmed]
  • The header stores columns,rows; the payload contains columns*rows floats row-major.

Dimension naming. _CreateRMat receives H=rows and L=columns. The common header writer at ram:10E0 recovers those bytes as B and C, then stores C before B at ram:10EEram:10F1. _AdrMEle reads the first byte as the stride, adds it B-1 times, and adds C-1. Its public register convention is therefore B=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]; _CkValidNum validates 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 via cplx_op_arrange (splits real/imag into OP1/OP2).
  • get_pos_list_elem (02:5BBB) — fetch by a positive-integer index with _CkOP1Pos bounds (loads A=0x15 = E_Stat and jumps to the error vector ram:2741 on 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 row B; it sets C=1 and enters _AdrMEle.
  • _GetMToOP1 (02:4044) — [M](r,c) → OP1 (_AdrMEle then RST4 = 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 at 84AF/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

RoutineaddrRole
_CreateRList00:10C4new real list: count*9+2 bytes; see Data layouts and creator routines [confirmed]
_CreateCList00:1109new complex list: count*18+2 [confirmed]
_IncLstSize07:4EF4grow 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]
_DelListEl07:4F43delete element(s): _HLTimes9(index) to size the gap (×2 if complex, & 0x1F == 0x0D), then _DelMem via a cross-page jump [confirmed]
_RedimMat/_ConvDim07:4D3B / 38:741Fre-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]) evaluates expr for var = lo..hi, pushing each result and finally _CreateRList-ing the collected floats; _SetSeqM 36:7D1F is the sequence-graph variant. A trace of seq(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 through cross_page_jump at 37:6E87. The parser setup at 38:5B3C evaluates the expression, with 34:5AA1 and 34:5BD8 computing X². The append path runs through 02:69BC and 37:426037:4285; it addresses list elements through 00:150F and 00:154F and compares them at 00:198D. Page 07 VAT routines at 07:565F, 07:5662, and 07:5683 grow the storage. After the last element, 37:70DC calls _CreateRList at 00:10C4. The trace contains one collection cycle per element, with a period of roughly 2,000 instructions repeated five times. [confirmed] The 02:5E1402:5F5D span is the command-executor dispatch shared by every evaluated command; it is not the seq( collection loop.
  • cumSum( is a running _FPAdd writing 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 on sum(/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 elementSort key
realthe value (sign → magnitude)
complexthe 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) (token 0xB4identity_build (02:4108)) [confirmed]: allocate n×n, then walk every cell writing 1.0 when row==col (the exp==type test) and 0 otherwise:
    _OP1Set1 ; for each (i,j): if i==j -> store 1.0 (mantissa[0]=0x10) else 0
    
  • Fill(value,[M]) / randM( stamp a constant / random values across all cells via a per-cell loop over the whole matrix. The 02:62D4 branch (CP 0xB5) is dim( (0xB5 = tDim), which creates the r×c result (5DBB_CreateRMat 110F) and stores the dims (631B/631C/4825) but performs no fill. For the decoded randM( fill see The randM( 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:5CC102: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 (0x4B7936: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 412A414E 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 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 414A4178 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 imm
JR/JP chain that runs 5E46/60C863xx, keyed on the token byte). Each command’s body and its single caller are byte-verified below.

Commanddispatch sitebodywhat the disassembly shows
Matr►list(0x8D @ 638802: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 5DD8
CALL 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 @ 60E902: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:635B02: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,H
CP 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 @ 62D4create + 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 L
JP 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 @ 61C102:7D19 + copyreshapes 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 / opsiteflag Ameaning
[A]⁻¹ (^ token 0x0C, operand = matrix)02:5F800x00inverse; singular ⇒ error
det( (token 0xB3)02:5FC00x40determinant; 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,0x40
CALL 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]

  • 461C mat_max_abs — compute the matrix’s max-abs element (numeric scale for the near-zero pivot test).
  • 41C1 abs_cmp_op1op2|OP1| vs |pivot| compare (1A0F/1987 abs+compare); 41D0 — scan a column for the largest-magnitude pivot (partial pivoting), calling 43B9 to swap rows as it goes.
  • 43B9 / 414E mrow_swap_loop / _AdrMRow — physical row swap / row scale (whole-row moves; 414E loads the column-count stride and swaps two complete rows via _AdrMRow×2 + 1DDA).
  • 4259 — swap two entries in the permutation vector at 84D5.
  • 4473 ele_sub_ref — the elimination element step ([M](i,k) − factor*[M](pivot,k): RST8
    CALL 403C
    JP 2297 = load + _FPSub).
  • 426D col_dot_accum / 426F col_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:43D802: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:701A7026 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 into OP1/OP2 (RST4 = load-9, _Mov9B, _MovFrOP1) and all arithmetic is the FP engine’s RST 30h(_FPAdd)/_FPMult/_FPDiv/_FPSub/_FPRecip. There is no SIMD; a matrix multiply makes thousands of these calls. Complex elements (lists/[i]) carry a 0x0C flag and use 18-byte (two-float) elements, split via cplx_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). The count/dim header is read first; then _AdrLEle/_AdrMEle do 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 codenameraised by
0x780-index reject (via ram:2793)_AdrMEle/_AdrMRow on a 0 row/col index
0x83E_SingularMat (ERR:SINGULAR MAT)42A6 inverse on a zero pivot (_ErrSingularMat 00:26F0)
0x85E_Increment_ErrIncrement 00:26F8 (bad seq/loop step)
0x89E_DataTypedet(/matrix ops on a non-matrix operand (chk_op_is_matrix (02:69B7))
0x8BE_DimMismatch (ERR:DIM MISMATCH)add/sub/multiply with incompatible dims (_ErrDimMismatch 00:2715)
0x8CE_Dimension (ERR:INVALID DIM)non-square det/inverse, out-of-range element store (_ErrDimension 00:2719, _StMatEl)
0x15E_Stat (via ram:2741)get_pos_list_elem bad index (_CkOP1Pos)

Routine index

space:addrnamewhat
00:10C4_CreateRListnew real list (count*9+2) [confirmed]
00:1109_CreateCListnew complex list (count*18+2) [confirmed]
00:1115_CreateRMatnew matrix (H*L*9+2, header columns,rows) [confirmed]
00:1EF6_HTimesLelement count = H*L (dims multiplied) [confirmed]
00:1930_HLTimes9×9 (real TIFloat stride) [confirmed]
02:4000_AdrMRowaddress of matrix row start [confirmed]
02:4002_AdrMElematrix element address: ((row-1)*columns+(column-1))*9 [confirmed]
02:4044_GetMToOP1[M](i,j) → OP1 [confirmed]
02:406C_PutToMatOP1 → [M](i,j) (validated) [confirmed]
02:40BAmatrix-multiply bodyO(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:4108identity_buildidentity(n): diagonal-1 fill (token 0xB4) [confirmed]
02:412Amat_transposetranspose [A]ᵀ body (token 0x0E, dispatched 60E9/called 60FE): per-cell copy dst(c,r)=src(r,c) via the swapped dest header [confirmed]
02:414Emrow_swap_looprow swap/scale (elimination) [confirmed]
02:4178mat_fill_type1live DB name; single-counter per-cell fill/apply loop in the 414A4178 block — not transpose [confirmed]
02:4539mele_copy9_d3bulk row-major float-payload copy (skip 2 dim bytes, LDIR); used by augment(/reshape [confirmed]
02:4663mat_gauss_enginelive 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:4773mat_to_list_colsMatr►list( 2-arg column-extract engine (only caller 63A0): nested col×row walk copying matrix columns into list element(s) [confirmed]
02:5264cplx_swap_dispatchlive 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:6238mat_augment_copyaugment( column-concat: allocate result (5DE0) + 4539 payload copy + re-point 84D3 [confirmed]
02:49E3lele_copy_until_eqlive DB name; list-element copy-until-length-match (21BB, RET Z); inner copy of the Matr►list( 1-arg/list path (6397) [confirmed]
02:41C1abs_cmp_op1op2absolute-value compare: OP1 vs pivot [confirmed]
02:41D0pivot_col_scanpartial-pivot: find largest absolute value in column [confirmed]
02:4259perm_swapswap two entries of the permutation vector (84D5) [confirmed]
02:426D/426Fcol_dot_accum/col_dot_accum_fromcolumn dot-product / back-substitution accumulate [confirmed]
02:42A6matrix_gauss_engineinverse(flag 0)/det(flag 0x40) Gauss-Jordan + partial pivot; square-only (H==L guard) [confirmed]
02:4473ele_sub_ref[M] − factor*pivot element step (_FPSub) [confirmed]
02:461Cmat_max_absmaximum absolute element (pivot tolerance) [confirmed]
02:47C5_AdrLElelist element address: data+2+(i-1)*9 [confirmed]
02:47EA_GetLToOP1list[i] → OP1 (complex-aware) [confirmed]
02:47FBrcl_list_elem_to_op1recall list elem to OP1 [confirmed]
02:47FErcl_list_elem_brecall list elem (B-indexed) [confirmed]
02:4829_PutToLOP1 → list[i] (validated, complex-aware) [confirmed]
02:49A7rcl_c_list_elemcomplex-list element → OP1/OP2 [confirmed]
02:49B5rcl_c_list_elem_bcomplex-list element (B-indexed) [confirmed]
02:5BBBget_pos_list_elemlist element by positive index (bounds) [confirmed]
02:5E46func_eval_dispatchsingle-byte function-token evaluator (0xB0–0xCD) [confirmed]
02:5F80mat_inverse_entry[A]⁻¹: flag 0 → matrix_gauss_engine [confirmed]
02:5FC0det_entrydet(: flag 0x40 → matrix_gauss_engine [confirmed]
02:6104list_fold_dispatchsum(/prod( higher-order list fold [confirmed]
02:69B7chk_op_is_matrixrequire operand type==2 else E_DataType [confirmed]
ram:21C4chk_type_lt_1aclassify element type width: AND 0x1F
CP 0x1A
CP 0x18
CCF — real-vs-complex (0x0C) element width [confirmed]
35:79E9list_idx_times9list index ×9 + dispatch [confirmed]
07:4D3B_RedimMatre-dimension matrix/list [confirmed]
07:4F07_InsertList/_IncLstSizegrow a list in place [confirmed]
07:4F43_DelListEldelete list element(s) [confirmed]
38:6C8F_StMatElparser store into [M](r,c) (bounds-checked) [confirmed]
38:741F/7422_ConvDim/_ConvDim00coerce a dim/index to real [confirmed]
00:26F0_ErrSingularMatE_SingularMat 0x83 [confirmed]
00:26F8_ErrIncrementE_Increment 0x85 [confirmed]
00:2715_ErrDimMismatchE_DimMismatch 0x8B [confirmed]
00:2719_ErrDimensionE_Dimension 0x8C [confirmed]

Resolved behavior and remaining questions

  • rref(/ref( use a separate driver, not 42A6. Xref proves 42A6 has exactly two callers (inverse 5F80, det 5FC0); rref/ref are 2-byte 0xBB-lead function tokens dispatched via the page-38 evaluator’s leaf_production_handler_table (38:7175). The ref( execution dispatch is byte-pinned in the page 02 command chain. It compares CP 0x2D at 02:609A and, with arguments present, executes RST 28h
    .dw 0x4B85. Bcall ID 4B85h resolves through the page 3B table to 35:7995; its port-encoded page byte 0x75 selects page 35. 35:7995 is an iterative FP reduction loop (_Minus1/_FPMult/OP-exchange primitives, back edge at 35:79C4) consistent with the row-reduction driver. [confirmed] The rref( execution dispatch lives on page 38, where two entry stubs (38:514F with carry set and B=1; 38:5157 with carry clear and B=0) converge on RST 28h
    .dw 0x4B88 at 38:515D. The ID resolves through the page 3B table to 02:7C23, a per-element driver that walks the pushed matrix data from the FPS pointer (LD HL,(9824) then a DJNZ loop), validates dimensions against the header bytes (8479/847A exponent checks raising through 26F4 on failure), and stores results back per cell. No CP 0x2E site exists on page 02, so the parser normalizes the rref( token before this dispatcher. The role of B and carry in distinguishing rref( from related calls remains [hypothesis]. The parse-side signature descriptors remain distinct (38:431E/0x5108 for ref( vs 38:4323/0x510C for rref().
  • det sign / pivot-product (42A6 tail 43D8-4470) and dimension labeling. The det sign = LSB of the permutation-swap count applied via _InvOP1S (24BD) at 43FB/442B; the magnitude is the 238B/RST 30h diagonal-pivot accumulate (43E3-43F6); 420F/4259 undo 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 _AdrMEle takes B=row, C=column. See Data layouts and Element access.
  • transpose, Matr►list(, and the augment( column-concat bodies. Each command’s page-02 dispatch site and body are byte-confirmed, every body having exactly one caller:
    • transpose [A]ᵀ (token 0x0E @ 60E9) → 02:412A (only caller 60FE): the dim header is swapped (60F5) and 412A copies dst(c,r)=src(r,c) over every cell. 02:4178 is a separate single-counter fill/apply, not transpose. [confirmed]
    • Matr►list( (0x8D @ 6388) → 02:4773 (2-arg column-extract engine, only caller 63A0) with 02:49E3 as the 1-arg/list inner copy. [confirmed]
    • augment( (0x91 @ 635B) → equal-rows guard (CP L
      JP NC,2719) + column-concat copy at 02:6238 (5DE0 allocate + 02:4539 LDIR payload copy). [confirmed]
    • dim( (0xB5 @ 62D4; 0xB5 = tDim, not randM() → creates the result and sets its dims (5DBB/5DEB). 02:5264 (cplx_swap_dispatch, only caller 62D0 in the 0xBD branch) is reached only from that complex branch, not here. [confirmed]
    • List►matr( 0x8E branch (61C1) → 02:7D19 + _DataSize copy (4539/453F) is unchanged [standard].
  • The augment( call to 02:4663 performs pivot-column setup but skips elimination because the engine tests the carry set by 02:6361. The statistics regression path enters the same dispatcher with carry clear. [confirmed]
  • The randM( fill loop at 02:5CC102:5CE6 computes $\operatorname{int}(19 \cdot \operatorname{rand}) - 9$ per cell. It calls _Random (36:7DC9) through the page 0 banked-call stub at ram:392D; no RST 28h bcall site is involved. See The randM( cell fill. [confirmed]
  • seq(/SortA(/SortD(/stats list-builders: confirm the collect-then-_CreateRList loop and the in-place float sort/compare. (Residual — comparator _CpOP1OP2 confirmed; 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.

Errorbcallpage-0 stubcodeMessage
_ErrSignChange0x44C5ram:2749_JError(0x98)0x98NO SIGN CHNG
_ErrIterations0x44C8ram:274D_JError(0x99)0x99ITERATIONS
_ErrBadGuess0x44CBram:2751_JError(0x9A)0x9ABAD GUESS
_ErrTolTooSmall0x44CEram:2755_JError(0x9C)0x9CTOL 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:

  1. _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 are ED 5B 06 93 = LD DE,(9306)).

  2. It installs the error handler at 39:46C7, then re-evaluates the stored equation through parse_inp_current_state_bjump (ram:391B). The stub’s inline descriptor targets parse_inp_current_state (38:5992), an interior entry in _ParseInp that preserves the already selected parser state.

  3. The error filter at 39:46C7 inspects the error code in A: codes below 0x86 (OVERFLOW/DIV BY 0/SINGULAR MAT/DOMAIN) and 0x87 (NONREAL ANS) are swallowed by these comparisons:

    CP 0x87
    JR Z
    CP 0x86
    JP NC,0x2799
    

    This x is treated as a point where f is undefined, so the solver can step past it, while 0x86 (BREAK) and codes ≥ 0x88 are 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 why solve( 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 A each pass (39:44BF), pushed on the stack. Two caps are compared with SBC HL,…:
    • LD HL,0x01F3 (= 499) at 39:4479/39:458B → exceeding it jumps to 39:45A0 LD A,0x99 … JP 2793 = ITERATIONS, and the early LD A,0x9A path (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)$ at 39: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) around 39:4488…44F2 compute 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 of f at the bracket ends; XOR 0x80 toggles it (39:44AB…44B3). If the two bounds never bracketed a sign change, the path at 39:45CD…45DA JP 2749 raises 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 the 1.0e-13 tolerance; const_solver_floor_1e99 (39:46E1) stores the 1.0e-99 (00 1D 10 …). On reaching tolerance the solver exits through the 39:4540 → 4553 branch (dynamically traced on an X²−2 = 0 solve that converged to √2 ≈ 1.41421356); 39:4547 is a CALL, not the converged return, and the observed path bypassed it. The tolerance tests at 446F/44D7/44F8 run under that trace; 45C7 is 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 to X = 1.4142135623… (√2) with left-rt = 0. The mem-write records show the guess at 0x8478 climbing 1.40898 → 1.41421335 → 1.4142135623645 → 1.4142135623731 (|err| ≈ 4.9e-15, crossing below the 1e-13 tolerance on the final step). solver_iterate (39:4413) ran 808×; the per-iteration re-parse (parse_eval_expr 38:5AB3) ran 834×; the secant-in-bracket-else-bisect test (39:44F8), the 499-cap compare (39:4479 LD HL,0x01F3), and the 1e-13/1e-99 constants (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:7F02 loads the pointer at (84D3) (iMathPtr1; ED 5B D3 84 = LD DE,(84D3)), 3A:7F0F the 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 (= 64 iterations 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 B budget falls to 3A:7206 JP 274D = ITERATIONS (0x99). Solving for N/PV/PMT/FV is 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 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 at 33:4D18 are 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_digit 1B65; not _OP2SetA, whose body is 1B24) — loading the scalar 0x60 = 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 offsets DE=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 _TimesPt5 halving refines the interval, the 97E7/84AF depth counters track subdivision depth, and the loop tail includes 33:4E81 LD DE,0x0024 … C3 CB 45, while 33:4E8C 3D F5 C2 57 4D decodes as:

    DEC A
    PUSH AF
    JP NZ,0x4D57
    

    It converges when the change in the estimate has exponent ≤ CP 0x74 (~10⁻¹², 33:4E74). Exhausting the refinement budget falls through to 33: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:

  1. Place the trial value in OP1 (_Mov9ToOP1 / arithmetic result).
  2. _MovFrOP1 (ram:1B0C) store it into the named variable the expression mentions (the solve var, the nDeriv/fnInt integration var, or the TVM var).
  3. 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 own parse_inp_current_state entry at 38:5992). The parser walks the same stored token stream each pass.
  4. Read the numeric result back from OP1, form the residual / difference, decide the next step. The error handler at 39:46C7, its (IY+7).2 state bit, and the _FixTempCnt cleanup 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:

  1. The evaluator hands the operand token to the page-0x02 dispatcher, which recognises the 0xBB group and the second byte: tFnInt at 02:68F3 (CP 0x24), tNDeriv at 02:6904 (CP 0x25), tRoot at 02:58AD/02:69BC (CP 0x22). [confirmed]

  2. The page-0x02 handler parses the comma-separated argument list and sets defaults. For example, the nDeriv/fnInt prologue at 02:6AF6 does:

    LD A,0x7D
    LD (0x8479),A
    

    This seeds the default tolerance exponent 0x7D (= 1e-3, the documented nDeriv ε) before the call. [confirmed]

  3. It then performs a paged call into page 0x33. The page-0x33 entry re-validates the token through the 33:504E bb_token_scanner (CP 0xBB, then CP 0x68 / 0xCF / 0xDB / 0xF6 to assign a small class index in C and CALL 0x50AC) and dispatches into the numeric bodies nderiv_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 is const_ln10x100, used with bcall _LnX to 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:4D1B is executable code:

    LD A,0x60
    CALL fp_set_digit
    
  • TVM _SinH (id 0x40CF). The TVM rate loop calls _SinH at 3A:710B (0x40C6/0x40CF/0x40ED are 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, and tRoot. The parser route is BB-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 by bb_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 at ram:2800, renamed fps_swap_active_frame; the store/load stubs are fp_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 0x462A in 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:

LeadGroup
5ChMatrices
5DhLists
5EhEquation variables
60hPictures
61hGraph databases
62hOutput/Y-variable group
63hSystem variables
7EhGraph-format group
BBhGeneral extended commands
AAhString variables
EFhTI-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:

  • _IsA2ByteTok answers 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 = 0x05DE = 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:

  • 3Fh ends a stored line even though no newline character is displayed in the token stream.
  • bytes resembling Then or End inside 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:

RoutineAddressOperation
parse_cur_tok38:72DAFetch the current byte and classify 00h, 3Eh, and 3Fh
parse_advance38:7248Increment nextParseByte, compare it with basic_end, and refill when needed
parse_expect_or_err38:5CD8Require one token or restore the fault position and raise syntax error
parse_scan_tokens38:4180Scan to a statement delimiter without splitting a two-byte token or quoted string
parse_init38:5B7BReset 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 CHandler familyRole
Other than 02h or 03hgrammar_handler_table (38:4000)Main grammar productions
02hcode at 38:478CPostfix/power production
03hleaf_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:

JumpSelector sourceValid destinations
38:439014 entry wrappers load literal continuations14
38:724449-class table at 38:4FDB; five rows are zero/invalid27 distinct
02:5675Five preceding token comparisons load literal targets5
33:4380Bounds-checked 13-row table at 33:438113

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 Ans through _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 classTypePayload used by BASIC
Real00hOne 9-byte TIFloat
Real list01h2-byte length, then 9-byte elements
Matrix02hTwo dimensions, then 9-byte elements
String04h2-byte length, then token/character bytes
Program05h2-byte length, then token bytes
Protected program06hProgram payload with protected edit semantics
Complex list0Dh2-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.

LoopRecord at EndContinuationState
For( first End00 36 58 07 00for_first_update (38:5836)0007h
For( later Ends00 7D 58 07 00for_steady_update (38:587D)0007h
Repeat End00 E7 57 23 0038:57E70023h

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]

EventEffect
prgmNAMEEnter the callee with a nested parser/control frame
ReturnUnwind one BASIC program frame and resume the caller
End of bodyReturn through the same program-frame machinery
StopTerminate 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:

CommandParse/dispatch evidenceDownstream operation
Disppage-38 statement handler_Disp at 37:51D3, then _NewLine
Output(38:6AE6, page-02 handler_OutputExpr at 03:4AF2
Input02:54EFentry editor, _ParseInp, variable store
Prompt02:562Frepeated named-variable entry and store
Menu(02:555D_DispMenuTitle at 39:4D21, then label transfer
Pause02:55E7display and key-wait loop
getKeyexpression token ADhnon-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:

ProgramOriginating guardPredicateError shim
Disp 1/000:2548–254Bdivisor in OP1 is zero_ErrDivBy0 at 00:26EC
Disp 10^10002:7076–7078, then 02:7053–7059positive exponent argument is at least 100_ErrOverflow at 00:26E8
Disp 1E99*1E9900:2513–251Dadjusted sum of biased decimal exponents overflows_ErrOverflow at 00:26E8
Disp ln(0)02:6F1E, then 00:212D–2131logarithm operand in OP1 is zero_ErrDomain at 00:26F4
Disp sin⁻¹(2) / cos⁻¹(2)02:76F1–76F5 / 02:76DF–76E2operand lies outside $[-1,1]$_ErrDomain at 00:26F4
Disp (-1)! / (-1) nCr 135:79CF–79D2 / 02:4FC8, then 00:2125–211Doperand fails the operation’s sign or integer check_ErrDomain at 00:26F4
Disp sqrt(-1)00:1B8F–1B93a complex result reaches the real-mode guard_ErrNon_Real at 00:26FC
Disp [[1,2][2,4]]⁻¹02:439C–43A5the 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–5876the 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 entryDirect-reference candidatesWitnessed callers
_ErrOverflow at 00:26E892
_ErrDivBy0 at 00:26EC21
_ErrSingularMat at 00:26F031
_ErrDomain at 00:26F4914
_ErrIncrement at 00:26F862
_ErrNon_Real at 00:26FC31

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

AddressRole
00:1FE8_IsA2ByteTok
38:4000Grammar-handler pointer table
38:4130Matching End/Else scanner
38:4180Token-aware skip scanner
38:41E5Natural For( production entry
38:4200Natural End record consumer
38:4870Goto/Lbl name scanner
38:5987_ParseInp
38:5AB3Recursive expression evaluator
38:6251_StoAns
38:6910Stored-program statement-body entry
38:6FB7Grammar-class validation and high-token fold
38:7010Production-family selector
38:7248Cursor advance/refill
38:72DACurrent-token fetch and delimiter classification
38:758A_Find_Parse_Formula
38:7600Store/label name scanning region
38:778FNested stored-program body evaluator
02:5676Command finalization gate
33:435FBounded 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 choiceInterpreter costExample
Keep loop bodies shortFewer statement dispatches and parser scans per iterationText animation
Prefer list or matrix primitivesOne parsed command can run an internal ROM loopTrace-backed list fixture
Cache repeated list elements in scalarsAvoid repeated VAT lookup and list-element address calculationDFS list stack
Keep graph drawing in the graph bufferAvoid repeated home-screen formatting and LCD updatesGraph-buffer visualization
Use structured loops instead of hot Goto pathsAvoid repeated label rescans through 38:7600Loop behavior
Include the optional For( closing parenthesisAvoid the documented implicit-close parser trapFor( 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]

BoundarySupported pattern
BASIC → BASICStore inputs, call prgmNAME, then read shared variables or Ans.
BASIC → ASMCall Asm(prgmNAME) and require the payload to return normally.
ASM → BASIC callbackStore 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

PatternTrace evidencePractical rule
Straight-line display (HELLO)page-38 statement parse plus _DispFine for status text; avoid using Disp as a frame loop.
Prompted arithmetic (FACTOR)loop-body reseed, FP multiply, displayKeep loop bodies short; store loop-invariant values before For(.
List built-ins (DATA)sum( reaches list_fold_dispatchPrefer built-ins when one parser setup can cover many elements.
Text animation (ANIMTXT)Output( plus LCD text paths on every loopPrecompute positions/strings and update the smallest region possible.
Graph drawing (GRAPHV)primitives draw into plotSScreen, then _PDspGrphBatch 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 argumentsStore 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 variablesTreat globals/lists/Ans as the calling convention.
List algorithms (BIGADD, BIGMUL, DFS)VAT lookup, element address, OP-register move per accessPreallocate 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:

NodeDFS valuePixel centerLabel position
1root(10,44)Text(16,8,"1")
2first edge target(35,54)Text(6,33,"2")
3second edge target(35,14)Text(46,33,"3")
4child 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 L1L4 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:691038:691438:778F sequence, reached after TIBasicParserState (0x9652) and the adjacent stack pointers have been populated:

RAM stateAddressRole in the private parser frame
TIBasicParserState.basic_prog0x9652current OP1-style program/object name
TIBasicParserState.basic_start0x965Bfirst token byte after the stored program size word
TIBasicParserState.next_parse_byte0x965Dcurrent parser cursor
TIBasicParserState.basic_end0x965Fparser end pointer
TIBasicParserState.num_arguments0x9661argument count/state byte used by parser helpers
chkDelPtr3 / chkDelPtr40x981C / 0x981Etemporary VAT/data pointers used during name and object setup
FPS / OPS / pTemp / progPtr0x9824 / 0x9828 / 0x982E / 0x9830live 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 partPractical conventionTrace evidence
InputsScalars, lists, and Ans are shared across caller and callee. The caller stores them before prgmNAME.CALLSUB stores A; ABICALL seeds L1 and Ans.
OutputsThe callee stores results back to globals, list elements, or Ans.SUBRT increments shared A; ABISUB writes A, L1(3), and Ans.
ScratchNo automatic save/restore exists. Routines must document scratch variables.The VAT and parser state are shared across caller and callee.
Return/StopReturn 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 stateprgmNAME runs with private parser/FPS state already set up by BASIC.The callee path reaches 38:691038:691438: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)=1 means node V has already been displayed and expanded.
  • L4(1..P) is the pending stack, with L4(P) popped next.
  • Edges are scanned from left to right, so pushing node 2 and then node 3 makes node 3 display before node 2.

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]

AddressOperation
07:5758Query application restriction selector 3; reject a disallowed caller.
07:5762Resolve the program named by OP1; reject an archived data page.
07:5766Distinguish compiled marker BB 6D from hexadecimal AsmPrgm source.
07:577BReject a machine image larger than 0x2000 bytes.
07:5785Insert an exact-sized gap at ram:9D95 and copy compiled bytes.
07:57D4For source form, call _GetAsmSize, allocate the result, and call _SquishPrgm.
07:5791Store the allocation length in asm_prgm_size (ram:89EC).
07:57B1Install the error cleanup at 07:5800.
07:57B4Call the JP ram:9D95 trampoline at 07:57FD.
07:57C4Clear the length and delete the allocation after a normal return.
07:5800Restore 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:691038:691438: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]

DirectionConfirmed mechanismCaveat
BASIC → ASMAsm(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 → BASICprgmNAME 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 callbackASM 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 returnASM 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 lookupASMFIND builds OP1={ProgObj,"ZZBASIC"} and bcalls _ChkFindSym.Lookup is not execution; the wrapper returns and ZZBASIC does not display CALLED.
Direct ASM → BASICNo 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:

  • _ExecutePrgm is the AsmPrgm executor reached by Asm(prgmNAME), not a general “run a BASIC program” entry.
  • _ExecuteNewPrgm (4C3C, target 00:265F) is not a drop-in BASIC runner from an arbitrary AsmPrgm either. It expects OS state beyond a name pointer.
  • _ParsePrgmName (4E82, target 38:40D4) only consumes a prgmNAME token from the current parser cursor and builds the name object used by Asm(.

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:691438: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.

FieldObserved 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 pointersvalid live pointers (0x9E94 / 0xFCB1 on one entry)
gate bitsBIT 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 A
CALL 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 0x9E780x9E7F 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 groupRangeContents
Parser and name0x96520x9662basic_prog, parser pointers, and numArguments
VAT and temporary stacks0x981C0x9831VAT scratch, FPS, OPS, pTemp, and progPtr
Parser flags0x89F00x8A39IY flags read by the page-38 parser
Error state0x86DDprior 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(_ExecutePrgmram: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

LayerEstablishesDoes not establish
ROM signatureThe modeled instructions are the expected OS 2.55MP bytesMeaning of every surrounding routine
Finite modelEvery state in one declared finite domain has an outcomeArbitrary streams or caller-owned RAM and stack state
Natural traceA stored TI-BASIC program reaches an outcome with ordinary parser stateFeasibility of an unobserved outcome
Public-bcall probeExact ROM execution reaches a public ABI boundaryNatural TI-BASIC reachability of the supplied register value
Internal-entry probeExact ROM execution distinguishes a selected internal stateA supported ABI or natural caller for that state
RAM or LCD assertionThe fixture produces its expected machine or visible resultWhich 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:

ModelExhausted statesOutcomesBoundary
Encoded token width2562Lead-byte membership, not second-byte validity
Statement delimiter2564Byte classification, not refill faults
Token scan step2564One step, not arbitrary stream length
Block matcher transition524,28810Every 16-bit depth over eight decision-equivalent token classes
Extended grammar fold2562CP F2h/ADD 12h, not later handlers
Precedence handler family65,5363Grammar class × selector byte, not recursive handler state
Command finalization gate2565First page-02 gate only
Control-flow table bounds25615Index 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:

ComponentPossibleAll evidenceNatural programs
Parser core1,956625619
Command arguments1625448
Page-33 control flow174130
Value storage154112112
Numeric and error checks256120119
Total2,702924898

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:

CaseDistinct behaviorOracle
hellostraight-line statement, quoted string, DispLCD text
factorialPrompt, scalar stores, For(/End, FP multiplicationLCD result 120
datatwo-byte list tokens, literal/store, built-in list foldlists and sum on the LCD
dfsnested While, If ... Then, For, and list-backed stacktraversal and visited list
callabinested BASIC call, shared scalar/list/Ans, Returnreturned scalar and list state
callstopnested BASIC call and nonlocal Stopabsence of the post-call line
branchmatrixElse, Repeat, nested blocks, and an omitted string quoteA5h 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]

CaseOrdered causal boundaryResult
divzero00:2548 → 00:254B → 00:26ECdivisor-zero guard, code 82h
overflow02:7076 → 02:7078 → 02:7053 → 02:7056 → 02:7059 → 00:26E810^x range guard, code 81h
muloverflow00:2513 → 00:2516 → 00:2517 → 00:2519 → 00:251B → 00:251D → 00:26E8exponent-add overflow, code 81h
lndomain02:6F1E → 00:212D → 00:1DE9 → 00:2130 → 00:2131 → 00:211D → 00:26F4logarithm zero guard, code 84h
increment37:4268 → 00:1DE9 → 37:426B → 00:26F8zero loop step, code 85h
asindomain02:76F1 → 02:76F4 → 02:76F5 → 00:26F4inverse-sine range guard, code 84h
acosdomain02:76DF → 02:76E2 → 00:26F4inverse-cosine range guard, code 84h
sqrtnonreal00:1B8F → 00:1B93 → 00:26FCreal-mode result guard, code 87h
singular02:439C → 02:439F → 02:43A1 → 02:43A2 → 02:43A3 → 02:43A5 → 00:26F0matrix-pivot guard, code 83h
lateincrement38:586D → 38:5870 → 38:5873 → 38:5876 → 00:26F8loop no-progress guard, code 85h
negfactdomain35:79CF → 35:79D2 → 00:26F4factorial sign/integer guard, code 84h
ncrdomain02:4FC8 → 02:4FA1 → 00:2125 → 00:1DFD → 00:1E00 → 00:1E02 → 00:2128 → 00:211C → 00:211D → 00:26F4combination 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:

  1. reject linear-disassembly candidates that are data or unreachable code;
  2. backward-slice one remaining executable caller to its input predicate;
  3. construct the smallest natural program and a RAM or value oracle;
  4. retain its trace only when it adds a guard path or CFG outcome; and
  5. 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

FormInstructionsClocksTrace ID
Explicit )145,7481,698,162d8348851f6ba…
Implicit close157,0521,790,338eef08147e170…

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:

FormFirst FPSLast FPSDistinct values
Explicit )0x9F020x9F021
Implicit close0x9EFF0xA04F25

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
PathMain entryBehavior
Large text_PutMap at 01:5A98draws one large-font glyph directly into controller RAM
Cooked character_PutC at 01:5B4Ccalls _PutMap, advances curCol, and handles newline/wrap
String_PutS at 01:5C39emits a null-terminated large-font string
Small text_VPutMap/_VPutSrenders variable-width glyphs using penCol/penRow
Graph clear_GrBufClr at 04:6071clears 768 bytes of plotSScreen; does not touch the LCD
Graph blit_GrBufCpy at 04:60A3copies selected graph-buffer rows to controller RAM
Physical clear_ClrLCDFull at 01:60E4writes zero to all 768 visible controller bytes
Save/restore_SaveDisp/_RestoreDispcaptures and restores the displayed image

Software display state

AddressNameSizeRole
0x8447contrast1 byteOS contrast level used to build controller command 0xC00xFF
0x844AcurTime1 bytetimer-driven cursor blink countdown
0x844B/0x844CcurRow/curCol2 bytes16×8 homescreen character cursor
0x845A0x8461lFont_record8 bytescurrent large-font render record
0x85080x8587textShadow128 bytes16×8 homescreen character shadow
0x86EC0x89EBsaveSScreen768 bytessaved display image
0x93400x963FplotSScreen768 bytesgraph/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.

  • 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.

LayerMain evidenceWhat it establishes
TI-OS page 0ram:0CC3ram:0CEA, ram:1890ram:18D1, and ram:20BFram:20CDASIC-side wait, block reads/writes, and movement commands [confirmed]
TI-OS display code01:5A5901:5B4B, 01:60E401:612D, and 01:693401:6955byte I/O, text drawing, and full-screen clearing [confirmed]
TI-OS graph code04:607104:620Agraph-buffer clear and LCD transfer loops [confirmed]
TI-OS initialization_LCD_DRIVERON at 06:4D0206:4D3Amode, enable, power, and contrast command sequence [confirmed]
Dynamic executionresolved home-2plus3 trace filtered to ports 0x100x13 and 0x2Fexact initialization and clear transactions in TilEm [confirmed]
Datamath module photographsMarch 2004 TI-84 Plus LCD module and controller attributionsource-attributed Toshiba T6K04 identity; the die itself is hidden under epoxy [standard]
Toshiba T6K04 data sheetexact controller block diagram, command table, and timing specification128×64 display RAM, 80-series bus, counters, read latch, busy formula, reset state, and analog-drive controls [standard]
Toshiba T6A04A data sheetcompatible earlier controller documentation120×64 display RAM and family comparison [standard]
Public hardware notesWikiTI ports 0x02, 0x100x13, and 0x2Fstatus bits, command meanings, controller variants, transfer timing, and hardware quirks [standard]
Emulator modelUpstream TilEm lcd.c, x4_io.c, and x4_init.c at commit f56ad63implemented video RAM, latches, delays, aliases, and fidelity limits [standard]
Emulator comparisonWabbitemu lcd.c and 83psehw.c at 48c2dc0; MAME t6a04.cpp, ti85.cpp, and ti85_m.cpp at mame0287row 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 011 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 063 vertically and byte column for the horizontal group selected by commands 0x200x3F.

QuantityCommand or transferVisible range
Rowcommand 0x80 + row0x800xBF
Byte columncommand 0x20 + column0x200x2B
Pixel within a bytedata bit 7 through bit 0left to right [standard]
Visible storage12 byte columns × 64 rows768 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

PortDirectionRole
0x10readcontroller status
0x10writecontroller command
0x11read/writelatched video-RAM data at the current address
0x12read/writesecond-chip-select mirror of 0x10 on documented ASIC revisions [standard]
0x13read/writesecond-chip-select mirror of 0x11 on documented ASIC revisions [standard]
0x02 bit 1readASIC LCD-wait timer ready at high CPU speed
0x2Fread/writeduration 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 DB0DB7 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]

BitMeaning
0increment direction when set; decrement when clear
1movement affects the byte column when set; row when clear
2fixed zero in the T6K04 status definition
3LCD-supply operational amplifier enabled when set
4controller reset state
5display enabled
68-bit transfer mode when set; 6-bit mode when clear
7controller 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.

CommandMeaningOS 2.55MP use
0x00select 6-bit transfersused by some text read/modify/write paths at 01:5AD3 [confirmed]
0x01select 8-bit transfers_LCD_DRIVERON and _PutMap [confirmed]
0x02disable display output while retaining video RAMlcd_disable at ram:0CD9; _PowerOff calls it [confirmed]
0x03enable display output_LCD_DRIVERON [confirmed]
0x04decrement row after each data transferdocumented controller mode [standard]
0x05increment row after each data transferOS vertical byte loops [confirmed]
0x06decrement byte column after each data transferdocumented controller mode [standard]
0x07increment byte column after each data transferOS horizontal row blits [confirmed]
0x080x0Bselect the duration of enhanced LCD-supply amplifier drive_LCD_DRIVERON selects 0x08 or 0x0B [confirmed]; T6K04 OPA2 behavior [standard]
0x0C0x0Fmirroring controls reported on newer controllersabsent from the T6K04 command table and unused by this ROM [standard]
0x100x17control LCD-supply amplifier state and ability; 0x140x17 keep it on_LCD_DRIVERON selects 0x16 or 0x17 [confirmed]; T6K04 OPA1 behavior [standard]
0x180x1FT6K04 test-mode selectToshiba 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]
0x200x2Fset T6K04 byte column in 8-bit modevisible OS range 0x200x2B [confirmed]; 16-column limit [standard]
0x200x35set T6K04 six-pixel group in 6-bit modeOS selects 6-bit mode in an edge-rendering path [confirmed]; 22-entry limit [standard]
0x400x7Fset displayed top-row offset_LCD_DRIVERON writes 0x40 [confirmed]
0x800xBFset rowfull 64-row range [confirmed]
0xC00xFFset controller contrast 063_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 VLC1VLC5 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 0x200x2F in 8-bit mode and six-pixel group commands 0x200x35 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 0x200x3F 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 0x100x13 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 0x290x2C 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 kHz70.03–140.06 µs
57.12 kHz35.01–70.03 µs
228.48 kHz8.75–17.51 µs
456.96 kHz4.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 conditionMinimum /CE cycleMinimum /CE pulseMinimum address setupMinimum write-data setupMaximum read-data delay
3.0 V ± 10%1,000 ns450 ns100 ns280 ns350 ns
5.0 V ± 10%500 ns220 ns60 ns60 ns160 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]

OrderCommandEffect
10x40top displayed row = controller row 0
20x05increment row after data transfers
30x01select 8-bit transfer mode
40x03enable display output
50x16 or 0x17select one of the upper LCD-supply amplifier abilities
60x08 or 0x0Bselect amplifier-enhancement duration
70xC0 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 0x200x2B, _ClearRow performs: [confirmed]

  1. send command 0x07 through lcd_mode_column_increment;
  2. restore the band-base row command;
  3. send command 0x05 through lcd_mode_row_increment;
  4. select the current byte column;
  5. write eight zero bytes while the row auto-increments;
  6. 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 0x07 selects byte-column auto-increment;
  • a command in 0x800xBF selects the row;
  • command 0x20 selects 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:

StateAddressRole
Controller video RAMexternal LCD controllercurrently scanned panel image
plotSScreen0x93400x963Fgraph/back buffer; copied explicitly
saveSScreen0x86EC0x89EBsaved 768-byte display image
textShadow0x85080x858716×8 homescreen character shadow
lFont_record0x845A0x8461current 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 0x4B to port 0x2F at 3F:41D3;
  • _LCD_DRIVERON writes 40 05 01 03 17 0B EF at 06:4D38;
  • _ClrLCDFull writes the eight-band, 12-column, eight-byte clear pattern through 01:5A95, 01:6945, and 01: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]

AreaTilEm f56ad63Wabbitemu 48c2dc0MAME 0.287jsTIfied 20170706a
Controller RAM1,024 bytes, 16 × 641,024 bytes, 16 × 64960 bytes, 15 × 64960 bytes, 15 × 64
8-bit column incrementaccesses 0–15, then normalizes to 0accesses 0–14, then wraps to 0counts modulo 32 without a RAM boundaccesses 0–14 around a 120-pixel row
Controller busy50 or 70 cycles; early transfers rejected60-T-state guard from the last accepted writeabsent; busy status is always zerorandomized 25–47-emulator-cycle interval
ASIC readyport-0x2F timer starts on every LCD read or writeinterval measured from the last accepted writeport-0x02 bit 1 is always setstatus derives from the emulator’s LCD timer
Ports 0x12/0x13aliasesabsentaliasesaliases
Data-read latchmodeledmodeledmodeledmodeled
Analog power and test modesignoredignoredvalues partly stored; no analog effectdrive fields and grayscale display physics modeled
Driver statusactive TI-84 Plus trace targetsource modelMACHINE_NOT_WORKINGbrowser emulator source model

TilEm behavior and fidelity gaps

TilEm models the controller and the ASIC wait timer as separate mechanisms. [standard]

AreaTilEm behaviorFidelity consequence
Video RAMfixed 1,024-byte array, 16 bytes × 64 rowscapacity matches T6K04, but not 120-pixel or no-extra-RAM variants
Ports 0x12/0x13aliases command/status and datamodels the documented second-chip-select mirrors
Controller busy50 emulated cycles after an accepted accessdirect too-fast accesses are ignored when delay emulation is enabled
ASIC waitport-0x02 bit 1 remains clear for the port-0x2F intervalreproduces the OS wait loop independently of controller busy
I/O overheadadds five emulated CPU cycles per LCD-port accessemulator policy, not a physical bus measurement
Read latchreturns nextbyte, then loads the addressed bytereproduces the dummy-read requirement
6-bit modepacks six-pixel writes into the internal byte arraysupports OS edge-rendering paths
Z addressdisplays (row + shift) mod 64reproduces vertical display rotation
Power/test/mirror commandsmostly ignoreddoes not model analog drive, blue test modes, or newer-controller mirroring
Status-read pointer quirknot modeledcannot reproduce late Novatek corruption from busy polling
Out-of-range columnswraps against the fixed 16-byte stride before transferdiffers from documented behavior on narrower controllers
Low powerframe output blanks when the LCD is inactive or the halted ASIC powers downapproximates 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 014; TilEm visits 015. A direct command can still select column 15. Wabbitemu indexes that direct coordinate modulo its 16-byte row. Commands 0x300x3F therefore alias columns 015 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 0x290x2C 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]

CaseNative observation
Controller guarda status read at 59 T-states returns busy 0x80; one at 60 T-states is accepted
Early writea data write at 59 T-states leaves the cell and pointer unchanged
Increment from column 14writes visit columns 14, 0, 1, and 2; column 15 remains unchanged
Direct hidden columnscommand column 15 accesses column 15; command column 31 aliases the same cell and wraps the pointer to 0
Read latchthree accepted reads return 0x00, 0x12, and 0x34 from cells containing 0x12, 0x34, and the following byte
Read timestampthree same-T-state reads advance the pointer without changing the last-successful-write timestamp
Port mapreads 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 0x100x13. 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 011, 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 0x290x2F. 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 0x040x07 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 0x290x2F 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-0x02 bit 1 and does not busy-poll port 0x10 in lcd_wait.
  • [confirmed] 01:5A59 and 01:5A60 write and read pixel data; they are not contrast helpers.
  • [confirmed] _LCD_DRIVERON emits 0x40, 0x05, 0x01, 0x03, hardware-dependent power commands, and a RAM-derived contrast command.
  • [confirmed] _ClrLCDFull covers all 768 visible bytes with eight vertical bands.
  • [confirmed] boot_lcd_keypad_diagnostic covers all 12 visible columns, but boot_diagnostic_gate is constant-false.
  • [confirmed] _GrBufClr changes only RAM, while _GrBufCpy performs the controller transfer.
  • [confirmed] _PowerOff disables 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

SourceUsed for
WikiTI port 0x02ASIC LCD-ready bit and high-speed behavior
WikiTI port 0x10status bits, commands, addressing, controller variants, busy-poll incompatibility, and power/test cautions
WikiTI port 0x11pixel data, output latch, dummy reads, 6-bit transfers, and transfer delay
WikiTI ports 0x12 and 0x13second-chip-select mirrors
WikiTI port 0x2FASIC wait-duration fields and defaults
Datamath March 2004 TI-84 Plus module and LCD photographsource-attributed T6K04 identity and the epoxy-covered module construction
Toshiba T6K04 data sheet, 2001-03-13exact 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 sheetcompatible earlier 120×64 controller and family comparison
TilEm lcd.c, calcs.c, x4_io.c, and x4_init.cemulator video RAM, command decode, latches, port aliases, wait timers, and reset state
Wabbitemu lcd.c, 83psehw.c, and calc.ccontroller RAM, pointer movement, transfer guard, ASIC-ready calculation, port registration, and frontend reset scope
MAME t6a04.cpp, ti85.cpp, and ti85_m.cppcontroller array and commands, TI-84 Plus port map, fixed ready bit, and driver status
jsTIfied deployed 20170706a artifact and readable mirrorfourth 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.

AddrNameMeaning
0x8F50Xminleft edge real X
0x8F59Xmaxright edge real X
0x8F62XsclX tick spacing
0x8F6BYminbottom edge real Y
0x8F74Ymaxtop edge real Y
0x8F7DYsclY tick spacing
0x8F86ThetaMin / 0x8F8F ThetaMax / 0x8F98 ThetaSteppolar/parametric range
0x900DXresOXres (pixel step between plotted columns)
0x9151Xres_intinteger copy of Xres
0x9152deltaX(Xmax−Xmin)/94 — real width of one pixel column
0x915BdeltaY(Ymax−Ymin)/62 — real height of one pixel row
0x9164shortXreciprocal X scale used as a multiplier
0x916DshortYreciprocal Y scale used as a multiplier
0x913FXFact / 0x9148 YFactZOOM IN/OUT factors

There is a second “u” copy block at 0x8E7E (uXminuXres 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 1BA7 zeroes the destination mantissa,
  • CALL 5F6A converts the binary value to packed BCD by repeated ADD 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:

bcallAddrCommand
_HorizCmd04:793EHorizontal y — draws a full-width horizontal line at real Y. See note below.
_VertCmd04:7955Vertical x — draws a full-height vertical line at real X. See note below.
_LineCmd04:796ALine(x1,y1,x2,y2)_PDspGrph, optionally draws via page 33, then JP 0x152A = _DeallocFPS1(0x24) frees the coord frame (the alloc happens upstream).
_UnLineCmd04:797CLine(…,0) — erase variant (same path, clear mode).
_PointCmd04:79B2Pt-On/Pt-Off/Pt-Change( — reads style from OP1.value.mantissa[0] & 0x20, dispatches set/clear/toggle.
_DrawCmd04:7B8Btop-level DRAW dispatch — grabs the pending count and cross-jumps to the per-command handler.
draw_zero_op104:620Bseeds 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)&1 is set (graphFlags.graphDraw, inc graphFlags=3/graphDraw=0; 1=redraw needed — this is the graphFlags bit at IY+3, distinct from grfDBFlags at IY+4 and SmartGraph at IY+0x17), calls _Regraph to recompute the whole plot,
  • otherwise checks the split-screen flag (_Bit_VertSplit) and copies the buffer to the LCD (graph_redraw_buf 04: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. Y1Y0 (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 observationY1=X²Y1=X⁻¹
post-entry _Regraph instruction span3,951,1854,316,730
sample advances, curInc=0949595
parse_init_findsym entries190190
completed recursive evaluations190188
divide-by-zero entries at ram:26EC02
post-dispatch _ILine calls3094
pixel-byte writes after the 768-byte clear296402
set pixels in final plotSScreen261266

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/curCol text cursor (see display-lcd.md). The graph screen is the pixel buffer plotSScreen rendered 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/_SetXXOP2 to 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 DRAW command (_DrawCmd) or Line(/Circle(/Pt-On( draws straight into plotSScreen over the current plot and persists across a SmartGraph redraw (it is not re-evaluated) until ClrDraw is issued. [confirmed]

Evidence summary and open items

  • The forward transforms, coordinate rounding, and _ConvOP1 boundary 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² and Y1=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. _DrawCirc2 has a byte-pinned static schedule, but no reset-origin trace has selected it. [confirmed]
  • _HorizCmd and _VertCmd build 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 byte 0x23 selected / 0x03 deselected) and style values 06 agree 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]

AddrNameMeaningToken
0x92B3TblMin (a.k.a. TblStart)first independent value in the tabletTblMin/TBLMINt = 0x1A
0x92BCTblStep (ΔTbl)increment between successive rowstTblStep/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:

BitNameMeaning
4 (0x10)autoFillIndpnt: 0 = Auto (fill X from TblStart/ΔTbl), 1 = Ask (prompt for each X)
5 (0x20)autoCalcDepend: 0 = Auto (compute Y immediately), 1 = Ask (compute a cell only on request)
6 (0x40)reTable0 = 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:

VarTokenVarToken
Y10x10Y60x15
Y20x11Y70x16
Y30x12Y80x17
Y40x13Y90x18
Y50x14Y00x19

(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:

bcallAddrRole
_PUT_INDEX_LST33:7066store a value in slot n at 0x84D9 + 2n
_GET_INDEX_LST33:707Aload the value in slot n through _LdHLind
_HEAP_SORT33:7097sort 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:

  1. 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 _StoX for each row.
  2. Evaluate each selected equation against the current X through bcall ID 4741h. Its body at 35:7C7C drives the page 38 parser cluster: parse_init (38:5B7B), fps_alloc_to_9652 (38:5B10), 38:5ADA, and the _ParseInp region at 38:5987. The result remains in OP1.
  3. 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):

AddrRole
0x918C XOutSym / 0x918E XOutDatX column: symbol + data pointer
0x9190 YOutSym / 0x9192 YOutDatactive Y column: symbol + data pointer
0x9194 inputSym / 0x9196 inputDatthe “Ask”/input column descriptor
0x9198 prevDataprevious-column data pointer
0x91DBunnamed Ask-mode row state
0x91DC / 0x91DDCurTableRow / CurTableCol
0x91E0table-top state; exact role remains open
0x91E2table_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:6DFF calls the Indpnt test at 05:6D4C and invokes the entry-line editor at 05:7303. The editor pushes continuation 05:7329 onto the OPS stack through ram:27DA and enters setup at 05:5F64 and 05:5F51. On success, 05:6032 shifts the table_value_cache band and enters row evaluation at 05:615C. [confirmed]
  • Depend = Auto (bit5=0): Y cells compute immediately during the fill.
  • Depend = Ask (bit5=1): the gate at 05:6DD1 tests bit 5 through 05:6D67 and 05:6D56. In Ask mode, 05:69D2 checks cell state at 0x91CE and 0x8D1B, then calls 05:637C for one deferred evaluation. That routine pushes continuation 05:644E onto the OPS stack through ram:27DA and 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:

SiteTrigger
02:7B35 bytesediting TblStart/ΔTbl/Indpnt/Depend in TBLSET
37:5F3Dtoggling Indpnt or Depend on the setup screen
38:6340, 38:4809, 38:54CDthe 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:64DERES 6,(IY+0x13)). [confirmed]


End-to-end example: tabulating Y1=X² + 1

  1. Y=: types X²+1 after Y1=. The editor tokenizes it and stores the bytes as the EquObj Y1 (token 5E 10) in the VAT, with its flags byte’s select bit set (the = is highlighted). The parser store path sets reTable.
  2. TBLSET (2nd WINDOW): sets TblStart=0 (TblMin 0x92B3), ΔTbl=1 (TblStep 0x92BC), Indpnt:Auto, Depend:Auto. Each edit sets reTable (the setup bytes around 02:7B35).
  3. TABLE (2nd GRAPH): enters context cxTableEditor (0x4A) on page 05. table_editor_main (05:5D0D) sees reTable=1table_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 ID 4741h35:7C7C and the page 38 parser cluster (_Find_Parse_Formula / _ParseInp) → OP1 = X²+1, format and stash into table_value_cache.band[0]/band[1],
    • advance to the next row (bound-checked at 05:65DC; X = TblStart + k·TblStep) and repeat,
    • clear reTable.
  4. The grid paints (05:7E45) the cached X and Y1 columns as large-font text; scrolling (05:6014) slides the cache and computes only newly exposed rows.
  5. Deselecting Y1 (or editing the formula, or changing ΔTbl) sets reTable again 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 tblFlags bit layout, and which sites set/clear reTable: [confirmed] (equates + byte-verified bit-ops).
  • Page 05 = TABLE subsystem, the recompute→clear-reTable structure, the running-X seed from TblMin and +TblStep advance, 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 ID 4741h35:7C7C equation dispatcher → page-38 parser cluster) and the once-per-row _ParseInp execution are [confirmed] by a headless TilEm trace of Y1=X². _StoX does not execute during the fill; the running X moves through OP registers and FPS slots. _PUT_INDEX_LST, _GET_INDEX_LST, and _HEAP_SORT are generic indexed-list helpers; their bodies do not prove that TABLE uses iMathPtr4 for its selected equations.
  • Y= selection bit (0x20) — flags byte 0x23 selected / 0x03 deselected — and the style byte 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 continuation 05:7329. Depend=Ask evaluates individual cells at 05:637C, with OPS continuation 05:644E. See Auto and Ask modes.
  • _Find_Parse_Formula’s TblRng (type 0x11) special-case is [confirmed] at two byte sites: 38:734D (CP 0x11
    CALL NZ, 38:72DA — validates the range variable’s data layout via 38:7260 before accepting it) and 38:7056 (CP 0x11 / CP 0x12 distinguishing TblRng from the following type in the header switch).
  • The validation body at 38:72DA performs generic parse-boundary checking. It calls 38:7260, which reads the parse stream through the parser cursor block and accepts statement delimiters as valid terminations. The companion filter at 38:72FF rejects token classes that cannot follow: 0xB5, 0xAB, 0xEB, 0xAA, and the 0x410x64 range except for a 0x21 second byte. Classification side effects land at 0x8479 and 0x847A. The TblRng special 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:

RepresentationWhat it preservesMain code
Native token streamCalculator tokens and the active gap-buffer split.Page 06 editor helpers
Live record graphExpression nesting, child order, active child, and per-record geometry.Page 34 construction and traversal
Editor layout stateToken classes, handler rows, argument slots, and focused cells.Page 39
Drawing streamPositioned 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 0x85DE0x85F2. The table below names the fields that matter for reading the page 0x39 code. [confirmed]

RAMRoleMeaning
0x85DEmode / classCaller mode at entry, then the current layout class.
0x85DFrow indexCurrent row inside the selected handler or template.
0x85E0slot indexCurrent argument or cell slot.
0x85E1row countNumber of rows in the current handler record.
0x85E2slot countNumber of cells or arguments in the active row.
0x85E30x85E6saved display stateSnapshot of shared display flags while the engine redraws.
0x85E7OP scratchSaved OP1 slot used while recursing into operands.
0x85E8template kindLow nibble selects descriptor-backed template UI.
0x85E9/0x85EAdescriptor originPacked pixel base used by descriptor cell mapping.
0x85EBrow heightPixel height for the current descriptor row.
0x85EC/0x85EDcell pointerPointer to descriptor cell data.
0x85EE/0x85EFfraction geometryMeasured numerator/denominator cell counts for fraction templates.
0x85F2OP scratchSecond saved OP1 slot.
0x86D7/0x86D8pen coordinatePixel coordinate staged before graph/small-font output.
0x844B/0x844Ctext row/columnShared OS cursor row and column; 844C also participates in overflow.
0x984Abaseline rowThe row restored around recursive operand emission.
0x9D27saved geometryCopy 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:

ClassRecordMeaning
0x0839:608BNumeric-calculus operator row, including nDeriv( and fnInt(.
0x0D39:60F9Fixed structural glyph rows, including direct Lintegral cells.
0x2939:6546Group/root-family control row.
0x2A39:654DRoot/power row containing the 00 10 payload cell.
0x3039:6030Fraction-context variant of the class-0x08 operator row.
0x3139:6433Stacked 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 0x1F0x2B 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 0x1F0x2B. 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:4F4233: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:

  1. Save display and OP state.
  2. Classify the current token into 0x85DE.
  3. Load the handler record from 39:5E45.
  4. Measure row and slot counts into 0x85E1/0x85E2.
  5. Recurse into argument slots when a handler cell represents an operand.
  6. 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:

ActionDecisionResult
0x03 at 39:51F1Argument index is nonzero.Walk backward through 39:523B.
0x03 at 39:51F1Index is zero and (IY+1Dh).0 is set.Emit the row-token tail.
0x03 at 39:51F1Index 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:51F1Count is at least eight.Begin the visible window at count - 8 + baseline.
0x04 at 39:52A5uint8((count - 1) - index) is nonzero.Walk once through 39:5167, then emit the row-token tail.
0x04 at 39:52A5The difference is zero and (IY+1Dh).0 is set.Emit the same row-token tail.
0x04 at 39:52A5The 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:50A439: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_main while the content scrolls. Its template dispatchers run during insertion transitions. In-slot horizontal scrolling uses a separate page-39 scroll set (39:530A39:539F, 39:53A139:53FE, 39:550039:5563, 39:560539:5632, 39:570939:572C, 39:57AC39:57FC, and 39:595539: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 not kAlphaDown, so the relayout jumps directly to 39: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 0x410x59 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:

DescriptorKindUse
39:686F0x10Fraction menu descriptor.
39:68800x11Root/function template menu descriptor.
39:6893descriptor familyTwo-row template descriptor.
39:689Cdescriptor familyTwo-row, six-column descriptor.
39:68A5descriptor familyTwo-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 0x240x28 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:

FindingEvidenceConfidence
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.

ConceptCell / glyphSource
fnInt( display name00 C8Class 0x08/0x30 operator records and page-1 token-name strings.
Fixed integral glyphLintegral 0x08Class 0x0D cells FC3F and 08 42, emitted through 39:4F1A.
Summation glyph0xC6 familyFixed 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:

  1. Place the tall integral glyph on the main axis.
  2. Walk the lower, upper, integrand, and variable slots in parser order.
  3. Update 0x844B by the row step from 39:5949.
  4. Emit slot markers through 39:4E0A.
  5. Emit the operand bodies through 39:5B10 and 39: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 0061 09 (GDB1GDB0), 60 0060 09 (Pic1Pic0), and AA 00AA 09 (Str1Str0). [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:

PathEntryUse
Generic cell emitter39:4E8EDispatches two-byte display cells.
Direct large glyph map39:4F1AMaps FC3CFC40, FE7DFE81, and xx42 cells to large-font codes.
String path39:6B66 + page 01:6D10Converts ordinary token cells to counted strings.
Display-byte remappage 07:44DERemaps FE, FC, and FB prefixed display bytes.
Small-font blitpage 01:6293_VPutMap; emits small labels and compact limits from 0x86D7.
Large-font blitpage 07:4588Copies one fixed large-font glyph record.
Rule / rectangle helpers39:6ABF, 39:6AF5, ram:3555Draw 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]

AddressMeaning
39:4A74Main token/action dispatcher.
39:4C27Class table lookup through 39:5E45.
39:4DCARow-cell pointer computation for handler records.
39:4DE6Row cell stream emitter.
39:4E8EGeneric two-byte cell emitter.
39:4F1ADirect large-glyph classifier.
39:4F08Text-column overflow check before marker handling.
39:4E0AArgument-index marker emitter used by the row compositor.
39:5167Multi-argument operand walker and tall-template row compositor.
39:5949Row-step classifier for one-row versus two-row argument advance.
39:5B10 / 39:5B1DSaved-E7 wrappers for ascending and descending alphabetic VAT searches.
39:59E0 / 39:59F9_FindAlphaUp and _FindAlphaDn dispatchers.
39:672ETemplate handoff for incoming 0x3D.
39:683DDescriptor cell-to-pixel mapper.
39:68AEGeometry action handler.
39:69C8Descriptor/fraction geometry selector.
39:6ABF / 39:6B1CFraction focus rectangle and endpoint helper.
39:6B66Generic string selector.
39:66E9 / 39:66FEReverse and forward argument-overflow cues.
39:6712Overflow marker path; resets curCol and emits :.
07:44DEDisplay-byte remapper.
07:4588Large-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:5B1039: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 30h39h and letters 41h5Bh; 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 modelProjected inputsPath classesBranch outcomesMinimum representatives
Structural scan-kind dispatch2567127
Structural-depth gate256222
Structural-insertion dispatch65,5366106
Raised extended-token classifier3,047122210
Five-byte raised-name loop493,112,577125104
Eight-byte raised-name loop24,977,631,672,3211,021104
Shared marker draw helper33,554,432142613
Settled render nesting tail16,777,216152411
Point mode and buffer routing2,04828225
Drawing-hook dispatch4342
Point style dispatch512585
Point bounds33,554,4327147
Thick-point expansion4,294,967,2968148
Shaded-point expansion3,145,7281,850308
Small-font pointer selection65,536163116
Token-hook dispatch1,048,5769105
Direct cell-to-large-glyph selection65,5369169
Display-byte remapper65,5367127
_KeyToString _sOK prefix1,024585
_KeyToString selector65,536354014
Page-39 cell-string selector131,07214168
Page-39 archived-token prepass196,60813146
Page-39 marker restriction gate262,144563
Page-39 marker row retouch2,048343
Page-39 cell-emission controller2,097,15239227
Glyph advance and delimiter padding131,0726104
_VPutMap byte-boundary gate56222
MathPrint _VPutMap right-edge gate3,584462
MathPrint _VPutMap row state1124102
_VPutMap aligned-byte composition917,504222
Large-glyph hook dispatch3214168
Metric marker-tail gate16585
Editor action 0x03 controller131,0721194
Editor action 0x04 controller131,072553
Reverse argument-overflow cue65,536222
Editor horizontal viewport17,179,869,184862
Editor vertical viewport17,179,869,184862
Editor vertical overflow cues4,294,901,760583
Editor left-overflow cue1,099,494,850,560585
Editor right-overflow cue281,474,976,710,6565104
Glyph vertical viewport1,099,511,627,77616226
Glyph viewport gates30,064,771,072343
logBASE counted-string viewport8,589,934,5922061
Embedded-record viewport gate4,294,967,296222
Record-allocation capacity36,893,488,147,419,103,232662
Saved-operand wrappers1612128
FindAlpha type normalization323184
FindAlpha key preparation19213144
FindAlpha record stepping8,1928124
FindAlpha candidate reducer288251710
FindAlpha endpoint2242
FindAlpha OP scratch transition33,554,432252

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 0x1F0x2B select the 13 render and allocator rows; class bytes 0x000x43 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]

ComponentReachable instructionsNatural / all-evidence outcomesOutcomes in CFGNatural / all-evidence instruction coverage
Settled construction991249 / 25040880.93% / 80.93%
Settled rendering1,898258 / 25930297.52% / 97.52%
Metrics and geometry47077 / 7780100.00% / 100.00%
Record allocator647 / 7898.44% / 98.44%
Alphabetic VAT search23617 / 179234.32% / 34.32%
Editor layout2,776255 / 2551,09833.03% / 33.03%
Small-font and LCD output41381 / 8112275.54% / 75.54%
Point and line primitives50850 / 5013459.65% / 59.65%
Large glyphs13016 / 163268.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]

InputReproduction macroTrace SHA-256Exclusive outcomes in the full branch cover
Nested derivative with tall body and valuetools/macros/mathprint-nested-tall-nderiv.macroe11c011b74df79165c55f7f64b699e3aa393bf8087f45ec89a73d616b73cdbb510
Depth-four log-base and power treetools/macros/mathprint-nested-depth4.macrob8d970906e63db96d36847dfcafed91d97e73fc7699294cc8debd08e7affdd93Omitted
Log-base marker insertiontools/macros/mathprint-logbase-boundary-insert.macroa49e4c13c93358662713da7f5e07862f42863d60a70ce18e141a90987914008bOmitted
Radical marker insertiontools/macros/mathprint-radical-nonspecial-insert.macroe7b79e37149f2b9b4a986bdbb114a89b03cd452bbecc6da20490edc972895e98Omitted
Integral marker insertiontools/macros/mathprint-integral-boundary-insert.macro328b8f52ebe939b35f79e676076984aa85ee59e05c06862647c4fc615069bb3c2
Mixed summation traversaltools/macros/mathprint-editor-summation-left-navigation.macro55fee4452906f94c2f3133961879ce4daec8fa0a98a5b69be1c27eae27190d3d3
Completed nDeriv and log-base traversaltools/macros/mathprint-editor-extra-structural-navigation.macrod77bdeb19c52dd1337db4ea0410c1d5970924a7a3bf6a589742280b508fda7762
Remaining insertable structural traversaltools/macros/mathprint-editor-remaining-structural-navigation.macro6263edce978d46750859f38c964ec4858b2c28fc8f6c914d510a8c332a01d85f19
Token-built matrix traversaltools/macros/mathprint-editor-matrix-navigation.macro78639019ccf6b1d01a62b2f88dc5ff619382c08fe81396886aa0c49bcfe962d4Omitted
Depth-two fraction RIGHTtools/macros/mathprint-editor-nested-fraction-right-navigation.macro15e6bccf136c7212fd36f7bf8ed570fd1ebbe161c8ef58584a439e891237d1acOmitted
Depth-two fraction LEFTtools/macros/mathprint-editor-nested-fraction-left-navigation.macro6cd38899f36e5a6398a0d1959557f8cb45172b4046db1f39cdfa298250066e6a1
Fraction nested in a radicaltools/macros/mathprint-editor-radical-fraction-navigation.macro99d813bdbb7102c9bd5ae608c0cc9eb64cd84c0410a06e4f2243e1768d86c5741
Y=/table/power round triptools/macros/mathprint-yequ-table-power-insert.macroac719f540d2adfca05d2ffa415f065b83eaf407f04fca42f5ae63c440a746b9d16
Y= equals-sign selection sweeptools/macros/mathprint-yequ-state-sweep.macro56733273b52ab4281ca2998ec2b89ece3083deb75c01160f97b936f30b73fe2fOmitted

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]

InputReproduction macroTrace SHA-256Complete eqdisp_draw_marker_primitive path
Absolute-value markertools/macros/mathprint-absolute-boundary-insert.macro103f3acc7f1ad13d1bf88af45ecacdc7e34133e66cc9c00fb57587674357cacfA=0x21 → display code 0x7C
$e^x$ markertools/macros/mathprint-e-power-boundary-insert.macroc927963c5db9a1f6f18652213764eabbf7a4fa9f2d2a74b7dae320fe882d7917A=0x25 → display code 0xDB
$10^x$ markertools/macros/mathprint-ten-power-boundary-insert.macroeb337f479d112e88537f0950fd7d2a917d101cfafda98447fb717a9a35f1e1e4A=0x26 → display code 0x1D
Summation markertools/macros/mathprint-summation-boundary-insert.macro980b2d17df5753223881090235fcca4bb4e8457a37c6cb05eef8f7a54314adf8A=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 editTopeditCursor and editTaileditBtm. 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 stateActive leafLogical gap payloadCursor pathSparse-state SHA-256Cursor-off LCD SHA-256
Empty fraction numerator9cursor, EF 1Enumeratorbcabb3961e1f37fe21b4e66c8bbfffb9a3812a162324e85273fdeed0beccc019450e82a31ced68ed319a1c2e8d18d3e2d3813f097de8be9dc2a89d48289cc4c9
Integral upper bound after 21032, cursorupper bound1dab216a05a2604bdb51eaa8a347a881f4dcccb7c8f19965503d73812422f8d539b937b16e32e4e07f6ffc2d6e60842c0249fd51d3aa661b23af2d2cb8708cea
Fraction denominator nested in an integral body1532, cursorbody → denominator7da6d7fdbb5ea848dda0afb1105237280a06b6dff994879df0a8c0b63e1a5f101297c2562d7c2fac9612aad6fc2e829ecb8f487606da6a079fb7d49d1c4c64d9
Immediately after a completed integral7EF 22 08 00 EF 2D, cursorroot sequence after integral89dd708b40f3c77f2cb5392783256576be8f0014beea2b411b6ba860dd441ef480cc504e3a7c6c773906f1e64ca6916e48594fd0c66c946151b3bc9849647f64

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 tokenTypeOrdered childrenInitial focus
EF2Eh0x20numerator, denominatornumerator
00B2h0x21enclosed expressionenclosed expression
0024h0x22lower bound, upper bound, body, variablelower bound
0025h0x23variable, body, evaluation valuevariable
00F1h0x24index, radicandradicand, with Ans as the index
00BFh / 00C1h0x25 / 0x26exponentexponent
00BCh0x27radicandradicand
EF34h0x28base, argumentbase
EF33h0x29variable, lower bound, upper bound, bodyvariable
00F0h0x2Aexponent; base precedes the markerexponent

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 insertionNew numeratorBytes retained after the markerSelected child
Blank rootEF 1Enonenumerator
After 11nonedenominator
Between 1 and 212denominator
Before 12EF 1E12numerator

The migrated leaf keeps editor-only header state: the leaf-end case retains word0F = 0 and word11 = 1. In a nested example, outer records 810 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:490034:4905. Initialization at 34:490834: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 0x200x2A. 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:479634: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 0x200x2B, 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 0x200x2A; 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 0x200x2B 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:610534: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:617834: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:61CE34: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 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:5B1039: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:510407: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:50C407: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:50BE07: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-6H-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:512C07: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]

ScenarioPage 0x39 editing hitsFinal stateSettled LCD replay
int(1,2,X^2,X)4CA4 ×1, 4DCA ×2, 4DE6 ×1, 4E8E ×7, 4F1A ×140x85E1=04, 0x85E8=0043×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 ×10x85E8=10, 0x85EB=06, 0x85EE=02, 0x85EF=0247×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 012 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:5FE7ram:34E9 (158 writes), 34:6CA8ram:3CE1 (96), 34:5DA2ram:3573 (78), 34:5EA3ram:3567 (24), and 34:5DBAram: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:

  1. Add the word-sized logical record origin.
  2. Subtract the horizontal or vertical viewport clip.
  3. Add the byte-sized physical screen origin and reject out-of-bounds points.
  4. Route each accepted point to the LCD, plotSScreen, or appBackUpScreen according 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:5E9834: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:6F8404:6FAC initializes the phase modulo four, while the graph update path keeps the step in 13. Styles 0 and 4FFh 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:42B504: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 40h7Fh therefore alias rows 00h3Fh before the column is added. _PointOn fixes D=1, so 04:424D04: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:424C04: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=15, 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:404204: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:62B434:62C3 reads a radical child’s width word, increments DE three times, and passes the resulting word endpoint to 34:5DA6. 34:620A34: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.

TypeConstructHandlerDistinctive ordered output
0x20Stacked fraction34:620ANumerator, denominator, then a rule sized from the wider child.
0x21Absolute value34:6347Two vertical bars, then child 1.
0x22Integral34:622FInclusive stem and four hook points; child placement comes from the record.
0x23nDeriv(eqdisp_render_handler_tableDerivative fraction, body, variable, evaluation bar, then repeated variable and value.
0x24nth root34:6315Index, root hook and stem, radicand, then vinculum.
0x25 / 0x26$e^x$ / $10^x$34:6381Fixed glyph, then exponent child.
0x27Square root34:62A1Root hook and stem, radicand, then vinculum.
0x28logBASE(34:63B2Prefix, base, opening shape, argument, then closing shape.
0x29Summation34:6504Sigma/equals forms, children 1–3, then delimited child 4.
0x2APostfix power wrapper34:6375Recursively renders child 1; emits no primitive itself.
0x2BMatrix34:65AALeft 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 0x00140x0017. [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:6BBA01: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 0x1F0x2B 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, 60h63h, 7Eh, AAh, BBh, and EFh select tables at 01:445201:47E8. The 5Eh second byte selects one of four banks. The BBh path clamps indices F6hFFh to F6h. [confirmed]

The raw D:E selector accepts more states than the native token grammar. Leads 01h5Ch 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 64h7Dh and 7FhBAh alias the AAh table. Leads BChFFh 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:677401: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:63CE01:641A. The large-font path emits all seven rows of its fixed cell. [confirmed]

34:6C3734: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:635401: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:636001: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:643101: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:70C17084 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:77AD77C1 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 10h11h 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:76A934: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:490034:491D initializes a new structural record to 1. 34:41BB34: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:76C234: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 classMeaningResult
D = 1FhCursor markerUpdate cursor state without drawing a glyph.
D = 82hIndexed string or titleEmit the selected string.
Counted-token case39:6B66 to _KeyToString (45CAh, implemented at 01:6D10)Emit each display code from the counted string.
Direct-glyph caseMapper at 39:4F1AMap the packed cell to one large-font code.

The direct mapper recognizes three ranges: FC3CFC40 becomes E - 3Ch + 5, FE7DFE81 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:

CheckComparesWhat it establishes
Pinned-byte differentialJavaScript transition against the corresponding ROM helper bytesClosed helper branches, flags, and wrap behavior.
Decoded-graph oracleRequested AST against the calculator’s RAM record graphThe calculator accepted the intended expression structure.
Accepted-write oracleOrdered LCD (column, row, value) tuplesConstruction and draw order, including accepted writes that do not change a byte.
Final-bitmap differentialGenerated 96×64 pixels against TilEmVisible parity, but not operation order by itself.
Fuzz runNative tokens through calculator RAM and screen against the translated graph and frameComposition 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:5A9934: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:5F8B34: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:67C834: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:684834:684C. An endpoint below the lower edge stores the explicit row count in 0x9B72 at 34:683A34: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:600034: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:664134: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:6C6B34:6C71 adds the advance to the pen. 34:6C7334: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:6CA8ram: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 0x000x03 preserve A and return with carry clear. Values 0x040xFE 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 0x1F0x2B; 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:56AC34:56B3. X^12 returns after both digit bytes. The 2^(X^(2³)) editor buffer contains explicit 10h11h 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:56DF34: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:57A134: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 pathInputOutputBoundary
Translated ROM pathSupported complete native expressionRecord graph, primitive stream, ordered LCD writes, and pixelsUntranslated source or constructor branches fail explicitly.
Live-arena decoderCaptured arena, active gap leaf, and cursorCursor-annotated semantic ASTDoes not predict every next key mutation.
hex: pathExplicit native bytesTranslated construction and render resultMalformed or unsupported forms report an error.
Fallback compositorPartial or unsupported preview textApproximate editable boxesNot 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, body ram:04B2, atomically reads and clears the one-byte kbdScanCode mailbox. It returns raw scan events and does not block. [confirmed]
  • _GetKey = 4972, body 06:491E, blocks, processes hooks and APD state, applies 2nd and ALPHA, and returns a cooked TIKeyCode. [confirmed]
  • _KeyToString at 01:6D10 maps 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/0xFF are not tokens — they’re the menu / context-switch return codes the main event loop branches on (see Boot, contexts & errors), so _KeyToString routes them out via cross_page_jump rather than translating.

So the input path is: keypad → ISR → kbdScanCode_GetKey (cooked kXxx + modifiers) → _KeyToString → token → parser (Tokenizer & TI-BASIC).

The 2.5 mm I/O link uses two open-collector lines. Port 0x00 drives and samples them directly; ports 0x080x0D 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.

LayerMain evidenceWhat it establishes
TI-OS kernelram:015B, ram:03B4ram:04BE, and ram:0964ram:0A5Dmatrix transactions, scan-code construction, release filtering, repeat, ON debounce, and low-power control [confirmed]
TI-OS banked code_GetKey = 4972, body 06:491Eblocking input, hooks, APD interaction, modifiers, and cooked key codes [confirmed]
TI-OS dynamic executiontools/macros/power-cycle.macro and /tmp/tilem-power-cycle.tracea complete [2nd]+ON shutdown/wake cycle and a live [2nd] matrix scan [confirmed]
Public hardware notesWikiTI ports 0x01, 0x03, and 0x04matrix wiring, capacitance, ghosting, bounce, and interrupt-port semantics [standard]
Emulator modelsTilEm commit f56ad63, Wabbitemu commit 48c2dc0, and MAME 0.287three different matrix algorithms and ON-edge policies [standard]
Native emulator executionguarded TilEm, Wabbitemu, and MAME keypad/interrupt runsmatrix 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:04100453 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 maskBit 0Bit 1Bit 2Bit 3Bit 4Bit 5Bit 6Bit 7
0xFE (0x01) (0x02) (0x03) (0x04)
0xFDENTER (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)
0xEF0 (0x21)1 (0x22)4 (0x23)7 (0x24), (0x25)SIN (0x26)APPS (0x27)X,T,θ,n (0x28)
0xDFSTO→ (0x2A)LN (0x2B)LOG (0x2C) (0x2D)x⁻¹ (0x2E)MATH (0x2F)ALPHA (0x30)
0xBFGRAPH (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 valueLow bitsKeys
0xF51 and 3+
0xF32 and 3+
0xFA0 and 2+
0xFC0 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.

BcallBodyRole
_AppStartMouse = 4D473B:78F9initialize the workspace, display the cursor, and wait for a supported key
_AppStartMouseNoSetup = 4D4A3B:78FCdisplay and wait without reinitializing the workspace
_AppMouseGetKey = 4D4D3B:78FFenable diagonal scans, halt until _GetCSC returns an event, and classify it
_AppDispMouse = 4D503B:77D9select display rather than erase, then enter the shared cursor renderer
_AppEraseMouse = 4D533B:77CFselect erase rather than display, then enter the shared cursor renderer
_AppSetupMouseMem = 4D563B:75B0set center coordinates and copy a 26-byte cursor workspace template to 0x8100
_AppUpdateMouse = 4D653B:7A56redraw and commit the pending coordinates, then wait for another key
_AppDispPrevMouse = 4D683B:76BDrestore or redraw the cursor around a pending movement
_AppUpdateMouseCoords = 4DA43B:7721apply the row delta before committing the coordinate word
_AppUpdateMouseXY = 4DCE3B:7724copy pending coordinates to the committed word and clear both mouse flag bytes
_AppMouseForceKey = 4E553B:7913classify a supplied scan value without waiting for _GetCSC
_AppSetupMouseMemCoords = 4E583B:78B7initialize the workspace with caller-supplied coordinates
_AppMoveMouse = 4E5B3B:78E6force 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]

AddressInstructionEffect
ram:0415BIT 0,(IY+0x2C)admit the four group-0 diagonal samples when set
3B:773BLD (IY+0x2C),0x00clear every mouseFlag1 bit after committing pending coordinates
3B:7907SET 0,(IY+0x2C)enable diagonal recognition immediately before EI, HALT, and _GetCSC
3B:791ARES 0,(IY+0x2C)disable the mode after the first nonzero event and before key classification
3B:7A8BRES 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 063; the column range is 095. 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 valueInputPending-coordinate change
0x01row + 1
0x02column − 1
0x03column + 1
0x04row − 1
0x09ENTERno 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]

AddressNameRole
0x843FkbdScanCodeevent mailbox consumed by _GetCSC
0x8440kbdLGSClast scan code accepted by the filter
0x8441kbdPSCprevious raw scan result
0x8442kbdWURwait-until-repeat countdown
0x8443kbdDebncCntstable-release countdown
0x8444kbdKeycooked-key workspace used by _GetKey
0x8445kbdGetKymost recent nonzero published scan code
0x8446keyExtendextended key-processing state

kbd_tick_debounce_repeat applies asymmetric filtering: [confirmed]

  1. kbd_scan_matrix first writes 0x00 to port 0x01 as an all-groups probe. It returns immediately when every read bit is one.
  2. A multi-key rejection sets kbdPSC = 0xFF and reloads kbdDebncCnt = 5; it publishes no event.
  3. A changed raw result is copied to kbdPSC, and kbdDebncCnt is reloaded to 5.
  4. A changed nonzero result proceeds immediately. The ROM does not require five equal pressed samples.
  5. 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 0x010x04;
  • DEL, scan code 0x38;
  • the diagonal-arrow raw values 0xF30xFC accepted 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:

APIWaitsValueModifiers and hooksRepeat source
_GetCSCnoraw scan event, or zerono cookingtimer scanner
_GetKeyyescooked TIKeyCode2nd, ALPHA, hooks, context policyevents 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]

BitEquateMeaning
3shift2nd2nd pending
4shiftAlphaalpha mode active
5shiftLwrAlphlowercase rather than uppercase
6shiftALockalpha lock
7shiftKeepAlphprevent 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:4B9606: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]

RegisterBitMeaning
port 0x03 write0one enables ON interrupts; zero disables and acknowledges the pending request
port 0x03 read0ON interrupt enable state
port 0x04 read0ON interrupt pending
port 0x04 read3live 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]

AddressOperationEffect
ram:0A29OUT (0x03),0x08acknowledge and temporarily disable interrupt sources while cleanup continues
ram:0A4BOUT (0x04),0x06select map mode 0 and the slow standard-timer rate
ram:0A4FOUT (0x03),0x11enable ON and link wake, disable standard timers, and select low power on HALT
ram:0A51clear shift2nddiscard the power-off modifier
ram:0A55clear onRunningmark the OS powered down
ram:0A5BEIaccept a selected wake interrupt
poweroff_halt_loop at ram:0A5CHALT
JR 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]

AreaTilEm f56ad63Wabbitemu 48c2dc0MAME 0.287jsTIfied 20170706a
Selected rowsactive-low write, all eight bitscomplements the write, then considers seven rowsactive-low write, seven rowsactive-low write and row scan
Ordinary combinationOR of selected rowsOR of selected row resultsXOR of each selected pressed positionselected key-state rows are combined into an active-low result
Ghostingiterated transitive closureone pairwise-overlap passnoneno electrical settling model
Same-column keys in two selected rowsremain lowremain lowXOR twice and cancel to highremain low
ON levelseparate active-low port-0x04 bit 3separate active-low port-0x04 bit 3separate active-low port-0x04 bit 3separate standard-interrupt state
ON request edgepress and releasepress onlypress onlypress only while not already latched
ON detectioninjected-state eventstandard-interrupt device evaluationfixed 256 Hz timer-1 callbackkey-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 mouseFlag1 bit 0.
  • [confirmed] The App mouse bcall family owns mouseFlag1 bit 0. It enables the mode only while waiting for _GetCSC and maps the four raw bytes to two-axis cursor movement.
  • [confirmed] _AppMouseForceKey stages coordinates at 0x8122; _AppUpdateMouseXY commits them to 0x986D within row 063 and column 095.
  • [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] _GetCSC is 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 HWKEYS probe 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

SourceUsed for
WikiTI port 0x01matrix map, active-low protocol, capacitance, settling, ghosting, bounce, and interrupt interference
WikiTI port 0x03interrupt enables, acknowledgement, and low-power-on-HALT behavior
WikiTI port 0x04ON pending and active-low level bits
TilEm keypad.cmatrix closure, instant key state, and ON edge policy
TilEm x4_io.cport 0x01, port 0x03, and port 0x04 model
TilEm scancodes.hinjected key identifiers
Wabbitemu keys.c and 83psehw.cpairwise matrix algorithm and press-edge ON latch
MAME 0.287 ti85.cpp and ti85_m.cppkeypad map, XOR scan, and timer-polled ON edge
jsTIfied deployed 20170706a artifact and readable mirrorfourth active-low matrix implementation and press-edge ON policy
WikiTI _AppStartMouse, _AppEraseMouse, and _AppUpdateMouseliterature names and published API synopsis; ROM bytes determine flag ownership and coordinate staging here
Local headless TilEm trace.c at commit 8da5457key-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:

SourceWhat it establishesConfidence
OS 2.55MP bytesValues written to port 0x00, values accepted on reads, bit order, acknowledgements, and error branches[confirmed]
TilEm and WabbitemuTwo independent digital models of port reads, local output latches, connected endpoints, and link assist[standard] where both match the public port contract
MAME 0.287A third raw-port implementation, optional link-bus devices, advertised assist state, and interrupt omissions[standard]
Guarded TilEm link edge probeDirect-core raw truth table, assist port map, byte transfers, status, interrupts, and reset retention[standard]
Guarded Wabbitemu link edge probeInitialized-core raw truth table, assist port map, byte transfers, status, and interrupts[standard]
Guarded MAME raw-link probeCPU-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 WikiTIOpen-collector electrical description and red/tip versus white/ring names[standard]
Physical measurementsRise 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 bitsLocal actionUnopposed low read bits
0release both lines3
1pull line 0 low2
2pull line 1 low1
3pull both lines low0

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 0xD00xD3 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 writeLine 0Line 1Idealized differential state
0released/high if unopposedreleased/high if unopposedzero
1driven lowreleased/high if unopposednegative
2released/high if unopposeddriven lowpositive
3driven lowdriven lowzero

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]

PhaseSender driveReceiver driveRead value for bit 0Read value for bit 1
Sender asserts1 for bit 0; 2 for bit 1021
Receiver acknowledgesunchangedthe other line00
Sender releases0unchanged12
Receiver releases0033

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 readSender droveReceived bitReceiver acknowledgement
2line 0 with write 10write 2
1line 1 with write 21write 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:

StatusTailROM condition
0x003C:6DA0No accepted raw or assist activity; the early no-assist return at 3C:6D6A also leaves A=0.
0x013C:6DDBPrefix and delimiter accepted; first following byte is 0x01.
0x023C:6DE2Ordinary receive did not produce 0xE0, or the required delimiter condition failed.
0xF93C:6D95Entry assist status has bit 6, but neither masked buffered-data/activity bit.
0xFA3C:6D8EEntry assist error has buffered data other than 0xE0.
0xFB3C:6D87Entry assist error has buffered 0xE0; cleanup and two additional reads follow.
0xFC3C:6DE9The first post-prefix byte is not 0x01.
0xFD3C:6DF0The legacy prefix receive returned nonzero low-level status.
0xFE3C:6DF7The assist prefix receive returned nonzero status with C != 0xE0.
0xFF3C:6DFEThe 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]

ConditionROM responseEvidence
Sender never sees both-low acknowledgementexhaust DE = 0xFFFF, then jump to _JErrorNo3C:4216423A [confirmed]
Peer never releases after acknowledgementexhaust DE = 0xFFFF, then share the same _JErrorNo edge3C:4241424F [confirmed]
Receiver waits too long for a non-idle statejump to _ErrLinkXmit3C:448644A7 [confirmed]
Receiver sees both lines low before acknowledgingjump to _ErrLinkXmit3C:448B449A, 3C:44F9 [confirmed]
Sender fails to release its selected lineexhaust DE = 0xFFFF, then jump to _JErrorNo3C:44B444C6, 3C:44E444F6 [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]

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:619D61AE 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]

DetailTilEm f56ad63Wabbitemu 48c2dc0MAME 0.287jsTIfied 20170706a
Raw write 1/2 reaches connectoryesyesno; both external lines are releasedyes, through the browser link endpoint
Disconnected read after 1/20x12/0x210x12/0x210x12/0x21modeled raw-line latch and peer state
Peer pull-low affects readsyesyesyesyes
Read bits 4–5local low-two-bit latchlocal low-two-bit latchlow write bits copied into PCR bits 4–5local output state
Link-assist advertisementyesyesyes, through port 0x02 = 0xC3yes
Assist ports present0x080x0D0x08, 0x09, 0x0A, 0x0Donly port 0x09, fixed read zero0x080x0D state machine
Assist byte transferimplementedimplementedabsentimplemented
Raw-line activity interrupttransition model presentno transition assertion in the raw port handlerabsent from mask, status, and port handlersmodeled through link state changes
Driver statususable link modelusable link modelMACHINE_NOT_WORKINGbrowser 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 0x090x0C 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 drivePeer 0Peer 1Peer 2Peer 3
00x030x020x010x00
10x120x120x100x10
20x210x200x210x20
30x300x300x300x30

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]

WriteMAME PCR after resetLocal latchDisconnected read
0x000x0000x03
0x010x1010x12
0x020x2020x21
0x030x3030x30

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 0x000x03 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 0x080x0D all return zero before and after distinct writes A8AD. 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] _SendAByte writes 1 for bit 0 and 2 for bit 1, least-significant bit first.
  • [confirmed] The receiver maps initial read 2 to bit 0 and read 1 to 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] _KeyboardGetKey deliberately accepts that error condition after prefix 0xE0, then consumes command 0x01 and one data byte while returning status 0x01.
  • [confirmed] The installed error callback reaches 3C:618D for 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 HWLINK probe 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 0xA5 transfers, 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 1 and 2 leave 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-first 0xA5 send and receive, data-register acknowledgement, and seeded-error read-to-clear behavior.
  • [confirmed] MAME reports port 0x02 = C3, while ports 0x080x0D remain zero before and after patterned writes.
  • [standard] MAME’s source map gives port 0x09 a 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:618D abort pulse.

External references

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:

AddrLabel (.inc)Meaning
8670ioFlagI/O state flags (bit4 tested on receive completion)
8672sndRecStatetransfer type / phase: 0x08 selects backup-send framing, 0x0A appears in backup receive/orchestration, 0x15 is variable DATA, and 0x0B is request/directory
8673ioErrStatelink error sub-state
8674headerpacket header byte 0 = machine-ID
8675header+1packet header byte 1 = command-ID
8676header+2packet length, word (LE) — also the running payload byte budget
8678(running)running 16-bit checksum accumulator (sum of payload byte values)
867DioDatascratch: built var-header length / data ptr setup
867Fthe variable header (type+name) copied from OP1 via _MovFrOP1
8688/8689ioNewData“new var arrived” status (bit7 of 8689)
868BbakHeadersaved 9-byte header for echo/ACK comparison (_Mov9B to/from 8674)
84DBiMathPtr5active data pointer during a streaming transfer
848E8492three backup-section lengths parsed from or written to the backup header
8494saved user-memory boundary used after backup restore
9834pagedCountbytes buffered in the 16-byte staging block (Flash-write batching)
9836pagedGetPtrwrite cursor into pagedBuf
983ApagedBuf16-byte staging block for received Flash-window data
9C86HW-assist TX timeout reload (0xFA)
9CACHW-assist TX/RX timeout down-counter (seeded from CPU speed, port 0x20)
85D9varClassvariable 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).


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:

cmdnameseen atmeaning
0x06VARlink_xfer_op reply check 4E86 CP 6variable header packet (type+name+size)
0x09CTS4199 (H=0x09)clear-to-send (receiver ready for DATA)
0x15DATA40DA/407C send, 426D CP 0x15 recvthe variable’s data bytes
0x2DDELheader-validate 4382 CP 0x2Ddelete / directory variants
0x36SKIP/EXITlink_xfer_op 4E7C CP 0x36peer refused this var → abort transfer
0x56ACKbuilt by 42FB (LD H,0x56); checked 418F CP 0x56acknowledge
0x5AERR/NAKbuilt by 6356/6385 (LD H,0x5A)checksum/length error reply
0x68RTS41BC (LD H,0x68)request-to-send
0x92EOT4195 (H=0x92)end of transmission
0xA2/0xB7requestlink_xfer_op 4E2B/4E2Frequest 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 at 3C:42D4, and the loop increments HL after each byte.
  • A Flash-window destination (HL < 0x8000) is buffered at 0x983A. 3C:42CF flushes each full 16-byte block through 3C:6AB1, and 3C:42EC flushes 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 inputSource at 3C:6AB1
A destination pagearcInfo.page at 0x83EE
DE destination addressiMathPtr5 at 0x84DB
BC lengthB=0, C=pagedCount from 0x9834
HL RAM sourcepagedBuf at 0x983A

The protected sequence at 3C:6AD93C: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 branchPage maskUpper bound, exclusivePages accepted by 3C:6AB1
TI-84 Plus0x3F0x2A0x080x29
legacy0x1F0x160x080x15
expanded0x7F0x6A0x080x69

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.


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 (0x0F0x14) 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 lengthDATA header lengthDATA payload
len <= 0x037Dlensource[0:len]
len > 0x037D with sndRecState = 0x08, varClass = 0x0A0x037D63 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 0x89F00x8D6C. [confirmed]

The bytes 63 00 are the normalized image of RAM 0x89F00x89F1, 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 bitSentPublic symbolDirect ROM bit operations
0x89F0.01inDelete10 BIT, 4 RES, 1 SET
0x89F0.115 BIT, 4 RES, 2 SET
0x89F0.20trigDeg13 BIT, 3 RES, 2 SET
0x89F0.30kbdSCR2 BIT, 2 RES, 2 SET
0x89F0.40kbdKeyPress1 BIT, 1 RES, 2 SET
0x89F0.51donePrgm1 BIT, 0 RES, 4 SET
0x89F0.61none
0x89F0.704 BIT, 1 RES, 2 SET
0x89F1.00none
0x89F1.10none
0x89F1.20editOpen39 BIT, 2 RES, 2 SET
0x89F1.30AnsScroll6 BIT, 5 RES, 3 SET
0x89F1.40monAbandon13 BIT, 12 RES, 8 SET
0x89F1.501 BIT, 1 RES, 1 SET
0x89F1.6–70none

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_op and _SendVarCmd install 3C:4F3E, which restores link state, the APD timer, and IY+0xC bit 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 to 3C:6136. That callback dispatches on sndRecState; for the applicable non-DATA states it calls the raw/USB-aware abort cleanup at 3C:618D, then records ioErrState=1 through stub 2F3107:7AC3. The raw branch drives both port-0x00 lines 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/62BB clear the link error sub-state byte (8A0B, the low bits of IY+0x1B-area flags).


Flash-object dispatch and error handling

TriggerAddressError
send/receive line timeout, bad echo, unexpected reply cmd_JErrorNo 00:2799E_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 mismatch6356→ sends 0x5A NAK → 2799E_LnkErr 0x9F
peer sent SKIP/EXIT (0x36)link_xfer_op 4E80/4E83E_LnkErr 0x9F
incoming variable-header type at 0x867F equals 0x223C:463D_JError 00:2793raw 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 0x220x25 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 typeROM behavior
0x223C:463D jumps to _JError with A=0x22, producing ERR:LINK.
0x23 — OS/AMS3C:45EA enters negotiation at 3C:45EE; its 3C:5735 branch checks the battery, initializes MD5 through _MD5Init = 808Dh, and calls _ReceiveOS = 8072h.
0x24 — Flash application3C: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 — certificate3C: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]

  1. Host (TI-Connect, machine-ID 0x95) opens the USB/DBUS link; calc detects it (IY+0x1B bit1).
  2. Host requests the directory or a specific var; calc’s receiver (4338) parses the request header, 6994/6298 classify it.
  3. To send a var: link_xfer_op/_SendVarCmd builds the VAR header (type byte + name from OP1, size) at 867F, sends it (41C3, cmd path), waits for CTS (0x09).
  4. 40DA streams the DATA (0x15) payload via _PagedGet_SendAByte (Flash-transparent), appends the 16-bit checksum, waits for ACK (0x56).
  5. _GetSysInfo (07:7345, id 0x50DD)-style metadata and an EOT (0x92) close the session.
  6. 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, NAK 0x5A on error) → ACK out → VAT store.

Routine index

space:addrnamewhat
3C:420D_SendABytesend one byte: HW-assist (port 0x09/0x0D) or bit-bang (port 0)
3C:6BB2lnk_send_byte_hwHW-assist send: poll port 0x09 bit5, OUT (0x0D)
3C:443F_RecAByteIOreceive one byte (blocking)
3C:444Alnk_rec_statusdecode low-level status and returned C; C=0xE0 is the TI-Keyboard prefix and re-arms or joins its exceptional delimiter path
3C:6D5E_KeyboardGetKeydecode the 0xE0, deliberate-error, 0x01, data sequence and return a status byte
3C:439C_Rec1stBytewait for first byte of a packet (APD + start-bit)
3C:43A3_Rec1stByteNCas above, no line-clear
3C:41C3lnk_send_headersend 4-byte header (ID, cmd, len-lo, len-hi)
3C:419Blnk_send_ctrl_pktsend a 0-length control packet (cmd in H)
3C:4195lnk_send_eotsend EOT (cmd 0x92)
3C:4199lnk_send_ctssend CTS (cmd 0x09)
3C:4338lnk_recv_headerreceive + validate 4-byte header
3C:620Alnk_local_machine_idpick local machine-ID from IY+0x1B mode
3C:42FBlnk_send_ackbuild+send ACK (cmd 0x56, fresh local machine-ID), restoring the saved header
3C:4292lnk_recv_datareceive DATA payload, 16-byte Flash batching, checksum
3C:6356lnk_verify_cksumverify count vs len; NAK 0x5A on mismatch
3C:6AB1flush_paged_flash_blockprogram one 1–16-byte staged Flash block through _WriteFlash and port 0x14
3C:4DD2link_xfer_opsilent-link variable send orchestrator (OP1=name)
3C:4EDD_SendVarCmdbcall _SendVarCmd (4A14) body; DI-wrapped send-by-name
3C:4763lnk_resolve_varresolve var class/size/ptr for sending (archive-aware)
3C:40DAlnk_send_datasend DATA payload (_PagedGet_SendAByte) + checksum + ACK wait
3C:4167lnk_send_cksum_tailappend 16-bit checksum, recv reply, expect ACK 0x56
3C:4F3Elnk_cleanuperror/abort cleanup (restore APD/timers/flags)
3C:6136lnk_error_cleanupinstalled state-aware error callback; reaches raw/USB abort cleanup where applicable
3C:618Dlnk_abort_transportclear USB busy state or issue the raw both-low abort pulse
3C:62B0lnk_clear_substateclear link error sub-state (8A0B)
3C:6994lnk_recv_storereceive var + VAT store sequence (expects 0x09 then 0x15)
00:278D_ErrLinkXmit_JError(0x9F) E_LnkErr
00:2799_JErrorNoraise 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; 0x080x0D = 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 setting donePrgm; the remaining gap is why active unnamed bits 0 and 1 and unreferenced bit 6 of 0x89F0 are set.
  • The prior USB target gap is now mapped in sub-usb-asic.md: link_xfer_op calls ram:2E0B, a cross_page_jump thunk to 35:4280, after sampling port 0x4D.

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:

LayerPort rangeRole
Legacy link0x002.5 mm raw bit-banged byte path; see Two-wire link port hardware. [confirmed]
Link-assist FIFO0x080x0DHardware byte send/receive assist used below _SendAByte and _RecAByteIO. [confirmed]
USB line / interrupt gates0x4D, 0x55, 0x56Line-state and event/status gates used before and during link handling. [confirmed]
USB controller / endpoints0x4A0x5B, 0x800xA2Page-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]

PortObserved use in OS 2.55MPEvidence
0x02Hardware/model gate before using assist paths. The link code tests bit 7 before touching ports 0x080x0D.3C:6C82, 3C:6CB8, 3C:6D15
0x08Link-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
0x09Link-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:6BB66BC5, 3C:444A, 3C:6BFA, 3C:6CCE, 3C:6D33; WikiTI port 09
0x0AAssist 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, 0x0CAssist 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
0x0DAssist TX FIFO/data register. _SendAByte writes the outgoing byte here after port 0x09 bit 5 becomes set.3C:6BBC6BBF
0x20CPU 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
0x4BController-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:4C764C80, 35:59AB; duplicated at 2F:59B6, 2F:59C359CD; WikiTI port 4B
0x4CUSB 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
0x4DUSB 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:4E4A4E6F, 35:42BF, 35:4B6A4B9F; TilEm x4_io.c
0x4F, 0x50Unnamed 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.
0x55USB interrupt status, active-low in the low five bits. The IM1 dispatcher tests (in(0x55) ^ 0xFF) & 0x1F first.00:006F0075
0x56USB 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:008500AE, 00:01130127
0x57, 0x5B, 0x4A, 0x54USB 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:40384060, 35:42C542EA, 35:4B6A4C14
0x5APresentation-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:58B258DE
0x800xA2Endpoint/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 0x800xA2. 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:

PortWikiTI descriptionEvidence limit
0x49Raw USB-transceiver status, including proposed D+ and D− bits 1 and 2The page calls several other bits only “something” or “possible.” No ROM read, emulator handler, datasheet, or physical sample confirms the bit map. [hypothesis]
0x51Delay between starting a separate 48 MHz crystal and enabling USB, counted in two-tick units from 32.768 kHzNo ROM write or cited primary source establishes the clock, unit, or enable effect. [hypothesis]
0x52Charge-pump enable timer with timing like port 0x51The 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 candidateActual bytes and role
00:3EDC — apparent OUT (0x51),AInline 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),ALow 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.

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 0x800x9B align with the Mentor Graphics MUSBFDRC register file. A Mentor-authored 2004 mu_fdrdf.h header assigns offsets 0x000x1F 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 portsFDRC namesROM cross-check
0x80FADDRThe control-transfer path defers a device-address write until the status stage at 35:4630. [confirmed]
0x81POWERInitialization 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
0x820x85INTRTX1/2, INTRRX1/2The protocol handler reads transmit and receive endpoint-event bytes at 35:4D03 and 35:4D57. [confirmed]
0x86INTRUSBHost 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
0x870x8AINTRTX1E/2E, INTRRX1E/2ESetup enables transmit events with 0xFF at 35:407B and receive endpoint events with 0x0E at 35:4084. [confirmed]
0x8BINTRUSBEThe 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
0x8C0x8DFRAME1/2Initialization waits for the low frame byte to become nonzero at 35:411B and 35:418D. [confirmed]
0x8EINDEXEndpoint setup and transfer routines select a pipe before using the shared endpoint registers. [confirmed]
0x8FDEVCTLThe 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
0x900x92TXMAXP, CSR0/TXCSR1, CSR02/TXCSR2Endpoint 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]
0x930x97RXMAXP, RXCSR1/2, COUNT0/RXCOUNT1/2Receive paths select an endpoint, test RXCSR1 bit 0, read the count, drain the matching FIFO, and clear the ready condition. [confirmed]
0x980x9BTXTYPE, TXINTERVAL/NAKLIMIT0, RXTYPE, RXINTERVALHost setup writes endpoint type/address and interval values before starting transfers. [confirmed]
0x9C0x9FTXFIFO1/2, RXFIFO1/2; FIFOSIZE/CONFIGDATA aliases at 0x9FThese 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].
0xA00xAFendpoint FIFOs 0–15Mentor’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 portRelative offsetFDRC candidateCommon HDRC candidateROM cross-check
0x860x06INTRUSBlow byte of INTRTXEThe 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
0x870x07INTRTX1Ehigh byte of INTRTXESetup 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]
0x890x09INTRRX1Ehigh byte of INTRRXESetup writes 0x0E, matching receive endpoints 1–3 in the FDRC low-byte register. [confirmed] for the value; [hypothesis] for the imported endpoint names
0x8B0x0BINTRUSBEINTRUSBEBoth layouts agree at this offset; the write masks do not distinguish them. [confirmed]
0x8F0x0FDEVCTLTESTMODEThe 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:

  1. Seed the inner retry counter at RAM 0x9C86 with 0xFA.
  2. Read port 0x09.
  3. If bit 5 is set, copy the outgoing byte from C to port 0x0D and return.
  4. If bit 5 is clear, call the timeout decrementer (3C:6BE4/lnk_timeout_dec) and retry until the outer counter at 0x9CAC expires, then fall into the link error path at 3C: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:6BF46D40.

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.
  • 0x08 is 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 state TILEM_LINK_ASSIST_READ_BUSY.
  • When the receive condition is accepted, the byte is read from port 0x0A into C.
  • The status masks 0x19 and 0x99 select error/activity cases before the code resets or re-arms the assist latch through port 0x08.

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:

PortCPU speed modeValueDivisor fieldWait field
0x090, 6 MHz0x97 (10010111b)100b → divide by 160x17
0x0A10xB4 (10110100b)101b → divide by 320x14
0x0B2, 15 MHz duplicate 10xB4 (10110100b)101b → divide by 320x14
0x0C3, 15 MHz duplicate 20xB4 (10110100b)101b → divide by 320x14

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 0x090x0C, 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]

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:

  • OP1 holds the variable type/name.
  • sndRecState (0x8672) is 0x15 for DATA-style receive.
  • IY+0x1B bit 0 selects USB-first behavior; reset means use the link port path.

The OS confirms that contract in the 4E354E73 gate:

  1. If IY+0x1B bit 0 is clear, it skips USB probing and sends through the ordinary link path.
  2. If bit 0 is set and either IY+0x1B bit 5 or bit 6 asks for USB handling, it reads port 0x4D.
  3. If port 0x4D bit 5 is clear, or bit 5 is set and bit 6 is clear, the OS sets IY+0x1B bit 5 and calls the page-0 bjump at ram:2E0B.
  4. ram:2E0B dispatches via inline descriptor 80 42 75, which is target 35:4280 after the normal page mask. That routine calls the public _InitUSBDevice body at 35:42B0, then accepts only TI vendor 0x0451 with product IDs 0xE003, 0xE008, or 0xE00F; success returns carry clear, while mismatch or init failure returns carry set.
  5. On carry set, link_xfer_op clears IY+0x1B bit 5 and continues into lnk_send_data_867d (3C:4055), which sends the same TI link request/VAR/DATA packets described in the link-transfer page.
  6. On carry clear, the USB path remains selected and the OS calls the bjump reached through ram:3FC3 with A=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 bitPage-0 dispatchPage-35 targetObserved role
400:0122ram:3FA535:4B6Aline/event settle path; waits on 0x4D bits 7 and 0, writes 0x57 = 0x22.
500:0127ram:3FAB35:4B9Fevent clear/re-arm path; may clear 0x4C, reset USBFlag2 bit 6, and write 0x57 = 0x50/0x93.
600:0113ram:3F9335:40B2USB setup path; sets IY+0x1B bit 5, initializes controller state, and waits for 0x4C = 0x1A/0x5A.
700:0118ram:3F9935:4C14cleanup/reset path; clears 0x5B, resets USBFlag2 bit 0, and jumps through the common controller reset.
100:011Dram:3F9F35:4031alternate 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 IDPublic nameBodyROM-grounded behavior
50F2_SendUSBData35:4DD3Sends from HL with byte count in DE; stores progress at 0x9C7E/0x9C81 and writes 64-byte chunks to port 0xA2.
50F5_AppGetCBLUSB3B:54C7Sets IY+0x1B bit 1, clears bit 2, then reaches _GetVarCmdUSB.
50F8_AppGetCalcUSB3B:54F0At 3B:54DE clears IY+0x16 bit 0 and sets sndRecState=0x15, then bcall 0x50FB (shared get-var path).
50FB_GetVarCmdUSB / link_xfer_op3C:4DD2USB-first variable command wrapper described above.
5254_InitUSBDeviceCallback35:4696Initializes device mode, stores callback page/address at 0x9C13/0x9C14, and returns 0xFC0xFF style error bytes with carry set on failure.
5257 / 5311_KillUSBDevice / _RecycleUSB35:46FC / 35:5B9BClears callback state and recycles through the same cleanup path.
525A_SetUSBConfiguration35:470BBuilds an 8-byte request block at 0x9C29 and writes it through port 0xA0.
525D / 5260_RequestUSBData / _StopReceivingUSBData35:48BA / 35:48D1Stores or clears the receive-buffer descriptor at 0x9C1E; receive records are read from port 0xA1.
528A / 528D_EnableUSBHook / _DisableUSBHook3B:7DC6 / 3B:7DD1Stores USBActivityHookPtr/page at 0x9BD4/0x9BD6 and toggles (IY+0x3A) bit 0.
5290_InitUSBDevice35:42B0Main controller/device initialization path; uses 0x4C/0x4D line handshakes and endpoint ports 0x800x9B.
5293_KillUSBPeripheral35:59CFPeripheral teardown; sets controller state 0x9C28 = 5 and manipulates ports 0x54/0x81.
530B_ToggleUSBSmartPadInput35:5B84Sets or clears bit 3 in 0x9C75 according to A == 1.
530E_IsUSBDeviceConnected35:5B92Preserves 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]

BcallIDTable bytesBodyObserved role
_AttemptUSBOSReceive80E445 41 2F2F:4145Wait for or dispatch a USB line event, initialize the controller, then enter the OS-receive pipeline. [confirmed]
_ReceiveOS_USB80F6CA 48 2F2F:48CANegotiate transfer records and write the received OS image through the Flash-control path. [confirmed]
_USBErrorCleanup810558 59 2F2F:5958Clear port 0x5B, restore controller line state, and re-arm according to port 0x4D. [confirmed]
_InitUSB8108A4 52 2F2F:52A4Initialize peripheral mode and return carry set after timeout cleanup. [confirmed]
unnamed entry810BC5 62 2F2F:62C5Set port 0x81 mask 0x01, then wait through the timer-3 delay helper. [confirmed]
_KillUSB810E61 59 2F2F:5961Run 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 0x360x38 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]

AreaTilEm f56ad63Wabbitemu 48c2dc0MAME 0.287jsTIfied 20170706a
Controller portsfixed reads at 0x4C, 0x4D, 0x550x57handlers at 0x4A, 0x4C, 0x4D, 0x550x57, 0x5B, and 0x80fixed reads at 0x55 and 0x56 onlyfixed reads at 0x4C, 0x4D, 0x550x57
Initial/disconnected 0x4C, 0x4D0x22, 0xA50x22, 0xA5unmapped0x22, 0xA5
Initial 0x55, 0x56, 0x570x1F, 0x00, 0x500x1F, 0x50, 0x000x1F, 0x00, unmapped0x1F, 0x00, 0x50
Line/event statefixedpaired-state latch and event bytenonefixed
FDRC blockunmappedonly device address at 0x80unmappedunmapped
Connected transferunavailableunavailableunavailableunavailable
Driver statusdisconnected traces runsource calls the block Fake USBTI-84 Plus driver is MACHINE_NOT_WORKINGfixed 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 0x55 twice. The first handler was written for port 0x54, so the port-0x54 PHY control model is unreachable.
  • GenerateUSBEvent does not consult the mask stored at port 0x57; it raises the CPU interrupt unconditionally.
  • From reset state, writing 0x08 to port 0x4A sets VBUS-high bit 6 without clearing VBUS-low bit 7. The line byte becomes 0xE5, in which both Wabbitemu VBUS state bits are set.
  • The same write records a D-minus-high event by changing the event byte from 0x50 to 0x58, but it does not set D-minus-high in the line byte. Repeated writes can therefore regenerate the event.
  • Port 0x4D tries to select one D+ state with BIT(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 0x4A0x5B outside that pair and the FDRC region at 0x800xA2 are absent from the TI-84 Plus map. A guarded native sweep reads zeros across 0x4A0x5B 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, 0x550x57, 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 0x4A0x5B and 0x800xA2, 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.

CaseControlled resultInstructions / T-statesPolls and boundaryReturn
initialization success0x4C = 0x5A, 0x8C != 05,923 / 62,1962 timeout ticks; 2 port-0x4C reads; 1 port-0x8C readcarry clear, A = 0x01
handshake timeout0x4C = 0x02783,929 / 7,739,78365,535 timeout ticks and port-0x4C readscarry set, A = 0x50
frame timeout0x4C = 0x5A, 0x8C = 03,012,144 / 28,842,346327,676 timeout ticks; 327,670 port-0x8C readscarry set, A = 0x50
event 0x40 dispatchsuccess inputs5,935 / 62,310reaches 2F:4170 oncestopped 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:

NeedOS surfaceROM support
Send or request a variable over USB/link_GetVarCmdUSB/link_xfer_op (50FB3C:4DD2) or _SendVarCmd (4A143C: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 (4EE53C:420D)Assist branch writes C to port 0x0D after port 0x09 bit 5.
Receive one byte on the active link transport_RecAByteIO (4F033C:443F)Status path checks port 0x09 and reads port 0x0A on the assist path.
Use the raw assist FIFOPoll 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.

The guarded TilEm direct-core probe maps all handlers from 0x08 through 0x0D. A fresh disabled engine reports 0x20. The read sides of ports 0x090x0C 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]

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+0x1B consistently before calling link_xfer_op. Bit 0 is the USB-first selector.
  • Do not write ports 0x080x0D while the OS link engine is active; the OS keeps state in IY+0x3E bit 0, 0x9C86, and 0x9CAC.
  • 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/0x56 events.
  • 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, a cross_page_jump thunk to 35:4280. Its carry-clear/carry-set result is decoded above.
  • The public 0x50xx/0x52xx/0x53xx USB APIs and the boot-page 0x8xxx USB entries are mapped above. The controlled harness executes _InitUSB, both timeout paths, _AttemptUSBOSReceive through 2F:4170, and a scripted _ReceiveOS_USB installer 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 0x4A0x5B. 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-0x4B writes and the port-0x4F/0x50 read-modify-write sequence. It does not identify their electrical effects. Port-0x5A bit 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

SourceUse
Retail OS 2.55MP and boot 1.03 ROM bytesMain and boot bcall tables, page-2F/35 bodies, ports, branches, and RAM state
tools/symbols/ti83plus.incHistorical public names and comments, checked against table entries and bodies
TilEm x4_io.c at f56ad63Link-assist implementation and fixed disconnected USB reads
Mentor mu_fdrdf.h revision 1.7 as preserved in lightcubeMentor-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 4327394Independent implementation that corroborates the compact FDRC byte ordering; not TI-84 Plus evidence
Linux musb_regs.h at db2ddb8Mentor/TI-copyrighted common HDRC/MUSB map used as the comparison candidate; not TI-84 Plus silicon documentation
Linky at 89586b0Independent calculator software that names MUSBFDRC and exercises the same ports
Wabbitemu 83psehw.c at 48c2dc0Partial line-state and interrupt model, with the implementation limits described above
MAME 0.287 ti85.cpp and ti85_m.cppFixed USB interrupt reads and absent controller/endpoint ports
jsTIfied project 42 and deployed 20170706a artifactfixed disconnected values matching TilEm and absence of an endpoint/FDRC model; artifact SHA-256 c7325a38f976f64eaa34182da17d838fe4831eece4650b92d5db710cf7a8fc5b
WikiTI port 0x09Historical link-assist timing-field interpretation, kept separate from ROM observations
WikiTI port 0x4BHistorical USB-power orientation. The page calls its own bit descriptions mostly speculative.
WikiTI port 0x49Historical raw-transceiver bit claims; no primary hardware source or ROM use found
WikiTI port 0x51Historical USB enable-timer claim; physical clock and units remain unverified
WikiTI port 0x52Historical charge-pump timer claim; physical behavior remains unverified
WikiTI port 0x5AHistorical presentation-mirroring description, treated as an unverified physical claim beyond the ROM-visible setup sequence
WikiTI _KeyboardGetKey revision 5510Historical 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.

bcallIDBody (page:addr)
app_5de753263D:5DE7
arc_593651AC07:5936
arc_59f14A6807:59F1
cplx_op_arrange464802:494F
c_log_prep50FE02:6F1B
disp_paged_str51CA01:7C4D
draw_zero_op1487304:620B
drw_5df153F504:5DF1
drw_5df4482504:5DF4
drw_638e487C04:638E
dsp_62404C4201:6240
dsp_65ea4D6B01:65EA
edt_5d6f546403:5D6F
edt_69f8546103:69F8
edt_6bd1545803:6BD1
fps_push_real4A8307:6365
fpx_4a7b518502:4A7B
fpx_5d70466902:5D70
fpx_5dbb466C02:5DBB
fpx_7069510102:7069
fpx_7d9d533B02:7D9D
fpx_7dfe533802:7DFE
get_pos_list_elem466602:5BBB
_GetVarVersion510A33:5023
_GET_INDEX_LST47C833:707A
_HEAP_SORT47CB33:7097
_PUT_INDEX_LST47C533:7066
grc_454b526337:454B
grc_4556526637:4556
grc_4575526937:4575
_DispAppRestrictions52FF37:4611
grc_51c2517F37:51C2
grc_522351A037:5223
grc_5d4451D637:5D44
grc_5f42520037:5F42
grc_60cb51FA37:60CB
grf_435f514033:435F
grf_5e06547633:5E06
lcd_blit_region4D2607:5431
link_xfer_op50FB3C:4DD2
list_idx_times953D135:79E9
lnk_62b051823C:62B0
mde_7da949DB36:7DA9
mnu_6ddb546739:6DDB
op1_int_part_neg489A04:74E8
push_zero_op1465102:49C0
rcl_c_list_elem464B02:49A7
rcl_c_list_elem_b464E02:49B5
rcl_list_elem_b463C02:47FE
rcl_list_elem_to_op1463902:47FB
rcl_var_push4B9A3A:5D07
screen_split522705:7712
scr_405651F105:4056
scr_461951E505:4619
sta_5d3c520335:5D3C
sta_5eef4B9D3A:5EEF
sta_760f4BA93A:760F
vert_split_draw48DC05:5D88
_AbsO1O2Cp410E00:1987
_AbsO1PAbsO2405A00:225B
_ACos40DE02:76DF
_ACosH40F002:7964
_ACosRad40D202:76C9
_AdrLEle462D02:47C5
_AdrMEle460902:4002
_AdrMRow460602:4000
_AHEADEQUAL4B4934:5A99
_AllEq487604:6218
_AllocFPS43A500:1534
_AllocFPS143A800:1537
_Angle410202:6A38
_AnsName4B5238:74B7
_ApdSetup4C9300:03AE
_AppGetCalc4C783B:54EC
_AppGetCbl4C753B:54C3
_AppInit404B00:0936
_Arc_Unarc4FD807:6248
_ArcChk50143D:61AF
_ASin40E402:76F1
_ASinH40ED02:7956
_ASinRad40DB02:76DA
_ATan40E102:76E9
_ATan240E702:7749
_ATan2Rad40D802:76D4
_ATanH40EA02:7909
_ATanRad40D502:76CF
_BinOPExec466302:53DD
_Bit_VertSplit4FA800:215D
_BufClear493600:222E
_BufClr507404:6074
_BufCpy507104:60A6
_bufInsert490906:42E5
_CAbs4E9702:6C47
_CAdd4E8802:6BA5
_CanAlphIns4C6900:04C6
_CDiv4E9402:6BF3
_CDivByReal4EBB02:6DAC
_CEtoX4EA902:6D1D
_CFrac4EC102:6DCF
_CheckSplitFlag49F000:2060
_CheckTimer527E33:5F16
_CheckTimerRestart528133:5F27
_ChkFindSym42F100:0E60
_chkTimer0517637:557E
_chkTmr514337:54C1
_Chk_Batt_Level522133:4E9B
_Chk_Batt_Low50B300:0D07
_CIntgr4EC402:6DDD
_CircCmd47D433:74CE
_CkInt423400:1E06
_CkOdd423700:1E0A
_CkOP1C0422500:1DE4
_CkOP1Cplx40FC00:193A
_CkOP1FP0422800:1DE9
_CkOP1Pos425800:1E5D
_CkOP1Real40FF00:1942
_CkOP2FP0422B00:1DEE
_CkOP2Pos425500:1E58
_CkOP2Real42DF00:214E
_CkPosInt423100:1DFD
_CkValidNum427000:1E9B
_CleanAll4A5007:52CF
_ClearParserHook50293B:7C3B
_ClearRect4D5C3B:6935
_ClearRow4CED01:6934
_CLine479833:6028
_CLineS479B33:6034
_CLN4EA002:6CCA
_CLog4EA302:6CE7
_CloseEditBuf48D305:5675
_CloseEditBufNoR476E03:4743
_CloseEditEqu496C06:4771
_CloseProg4A3507:4FB4
_ClrCursorHook4F693B:7AEA
_ClrGraphRef4A3807:4FD8
_ClrLCD454301:60F5
_ClrLCDFull454001:60E4
_ClrLp41D100:1BC4
_ClrOP1S425E00:1E68
_ClrOP2S425B00:1E63
_ClrRawKeyHook4F6F3B:7B88
_ClrScrn454901:6167
_ClrScrnFull454601:6162
_ClrTxtShd454C01:616F
_CMltByReal4EB802:6D94
_CmpSyms4A4A07:519E
_CMult4E8E02:6BB7
_Conj4EB502:6D8F
_ConvDim4B4338:741F
_ConvDim004B4638:7422
_ConvKeyToTok4A0207:44DE
_ConvLcToLr4A2307:4CFF
_ConvLrToLc4A5607:5368
_ConvOP14AEF38:7433
_COP1Set0410500:195F
_Cos40C002:7346
_CosH40CC02:762E
_CpHLDE400C00:21BB
_CPoint4DC804:43D8
_CPointS47F504:43DD
_CpOP1OP2411100:198D
_CpOP4OP3410800:197A
_CpyO1ToFPS1445C00:16D4
_CpyO1ToFPS2446B00:16ED
_CpyO1ToFPS3447700:1701
_CpyO1ToFPS4448900:172B
_CpyO1ToFPS5448300:171C
_CpyO1ToFPS6447D00:170B
_CpyO1ToFPS7448000:1712
_CpyO1ToFPST444A00:16B5
_CpyO2ToFPS1445900:16CF
_CpyO2ToFPS2446200:16DE
_CpyO2ToFPS3447400:16FC
_CpyO2ToFPS4448600:1726
_CpyO2ToFPST444400:16AB
_CpyO3ToFPS1445300:16C5
_CpyO3ToFPS2446500:16E3
_CpyO3ToFPST444100:16A6
_CpyO5ToFPS1445600:16CA
_CpyO5ToFPS3447100:16F7
_CpyO6ToFPS2446800:16E8
_CpyO6ToFPST444700:16B0
_CpyStack442900:167C
_CpyTo1FPS1443200:168D
_CpyTo1FPS1043F300:1617
_CpyTo1FPS1143D800:15CF
_CpyTo1FPS2443B00:169C
_CpyTo1FPS3440800:1647
_CpyTo1FPS4440E00:1651
_CpyTo1FPS543DE00:15DF
_CpyTo1FPS643E400:15EF
_CpyTo1FPS743EA00:15FE
_CpyTo1FPS843ED00:1608
_CpyTo1FPS943F600:1621
_CpyTo1FPST442300:1674
_CpyTo2FPS1442F00:1688
_CpyTo2FPS2443800:1697
_CpyTo2FPS3440200:163F
_CpyTo2FPS443F900:162B
_CpyTo2FPS543DB00:15DA
_CpyTo2FPS643E100:15EA
_CpyTo2FPS743E700:15F9
_CpyTo2FPS843F000:160D
_CpyTo2FPST442000:166F
_CpyTo3FPS1442C00:1683
_CpyTo3FPS2441100:1656
_CpyTo3FPST441D00:166A
_CpyTo4FPST441A00:1665
_CpyTo5FPST441400:165B
_CpyTo6FPS243FF00:163A
_CpyTo6FPS343FC00:1635
_CpyTo6FPST441700:1660
_CpyToFPS1445F00:16D7
_CpyToFPS2446E00:16F0
_CpyToFPS3447A00:1704
_CpyToFPST444D00:16B8
_CpyToStack445000:16BD
_Create0Equ432A00:1131
_CreateAppVar4E6A00:114B
_CreateCList431B00:1109
_CreateCplx430C00:10B0
_CreateEqu433000:113C
_CreatePair4B0D38:6785
_CreatePict433300:1140
_CreateProg433900:1153
_CreateProtProg4E6D00:114F
_CreateReal430F00:10B8
_CreateRList431500:10C4
_CreateRMat432100:1115
_CreateStrng432700:1123
_CRecip4E9102:6BE6
_CSqRoot4E9D02:6C84
_CSquare4E8B02:6BB4
_CSub4E8502:6BA2
_CTenX4EA602:6D08
_CTrunc4EBE02:6DBD
_Cube407B00:237D
_CursorOff45BE06:7C5F
_CursorOn45C406:7D34
_CXrootY4EAC02:6D3B
_CYtoX4EB202:6D5C
_DarkLine47DD04:4025
_DarkPnt47F204:43D6
_DataSize436C00:1485
_DataSizeA436900:1466
_DeallocFPS439F00:1526
_DeallocFPS143A200:152A
_DecO1Exp426700:1E6F
_DelListEl4A2F07:4F43
_DelMem435700:1368
_DelRes4A2007:72F5
_DelVar435100:1308
_DelVarArc4FC600:12D9
_DelVarNoArc4FC900:130E
_DisableApd4C843B:7AA8
_Disp4F4537:51D3
_DispDone45B501:69B0
_DispEOL45A601:689F
_DispHL450701:5BF6
_DisplayImage4D9B3B:6A72
_DispMenuTitle506539:4D21
_DispOP1A4BF704:7844
_DivHLBy10400F00:0269
_DivHLByA401200:026B
_DrawCirc24C663B:7171
_DrawCmd48C104:7B8B
_DrawRectBorder4D7D3B:68F5
_DrawRectBorderClear4D8C3B:692A
_DToR407500:236B
_EditProg4A3207:4F6B
_EnableApd4C873B:7AAD
_EnoughMem42FD00:0FA6
_EOP1NotReal427900:1F06
_Equ_or_NewEqu42C400:20FD
_EraseEOL455201:61C5
_EraseRectBorder4D863B:68F1
_ErrArgument44AD00:2711
_ErrBadGuess44CB00:2751
_ErrBreak44BF00:273D
_ErrCustom14D4100:2771
_ErrDataType44AA00:2708
_ErrDimension44B300:2719
_ErrDimMismatch44B000:2715
_ErrDivBy0449800:26EC
_ErrDomain449E00:26F4
_ErrD_OP1NotPos42C700:2119
_ErrD_OP1NotPosInt42CD00:2125
_ErrD_OP1Not_R42CA00:2120
_ErrD_OP1_042D300:212D
_ErrD_OP1_LE_042D000:212A
_ErrIncrement44A100:26F8
_ErrInvalid44BC00:2729
_ErrIterations44C800:274D
_ErrLinkXmit44D400:278D
_ErrMemory44B900:2721
_ErrNonReal4A8C38:42E1
_ErrNon_Real44A400:26FC
_ErrNotEnoughMem448C00:1735
_ErrOverflow449500:26E8
_ErrSignChange44C500:2749
_ErrSingularMat449B00:26F0
_ErrStat44C200:2741
_ErrStatPlot44D100:2759
_ErrSyntax44A700:2700
_ErrTolTooSmall44CE00:2755
_ErrUndefined44B600:271D
_EToX40B402:705C
_Exch943D500:15CA
_ExLp422200:1DDA
_ExpToHex424F00:1E4E
_Factorial4B8535:7995
_FillBasePageTable501100:2692
_FillRect4D623B:6939
_FillRectPattern4D893B:6814
_FindAlphaDn4A4707:50B8
_FindAlphaUp4A4407:50B5
_FindApp4C4E3D:5EE3
_FindAppDn4C4B3D:5DE6
_FindAppNumPages509B3D:4AA3
_FindAppUp4C483D:5DDA
_FindSym42F400:0E65
_Find_Parse_Formula4AF238:758A
_FiveExec467E02:69BC
_FixTempCnt4A3B07:4FEC
_FlashToRam50173D:6745
_FlashWriteDisable4F3C3C:66D5
_ForceFullScreen508F39:66D2
_FormBase50AA06:57C0
_FormDCplx499606:59D3
_FormEReal499006:5799
_FormReal499906:5ACF
_FourExec467B02:6889
_FPAdd407200:229E
_FPDiv409900:2541
_FPMult408400:238B
_FPRecip409600:253D
_FPSquare408100:238A
_FPSub406F00:2297
_Frac409300:24E3
_GetBaseVer4C6F00:0284
_GetCSC401800:04B2
_getDate514F37:550B
_GetDateString515237:55E8
_getDtFmt515537:5581
_getDtStr515837:55A9
_GetK474437:746D
_GetKey497206:491E
_GetKeyRetOff500B06:491A
_GetLToOP1463602:47EA
_GetMToOP1461502:4044
_GetStringInput24E6137:5194
_GetSysInfo50DD07:7345
_getTime515B37:5551
_GetTimeString515E37:567E
_getTmFmt516137:5593
_getTmStr516437:55CF
_GetTokLen459101:66E5
_Get_Tok_Strng459401:66EA
_GrBufClr4BD004:6071
_GrBufCpy486A04:60A3
_GrphCirc47D733:758D
_HLTimes940F900:1930
_homeup455801:6216
_HorizCmd48A604:793E
_HTimesL427600:1EF6
_IBounds4C6004:42EC
_IBoundsFull4D9804:4306
_ILine47E004:4029
_IncLstSize4A2907:4EF4
_InitTimer526C33:5E38
_InsertList4A2C07:4F07
_InsertMem42F700:0F81
_Int40A500:2621
_Intgr405D00:2263
_InvCmd48C704:7D6A
_InvertRect4D5F3B:693D
_InvOP1S408D00:24BD
_InvOP1SC408A00:24BA
_InvOP2S409000:24CD
_InvSub406300:227D
_IOffset4C6304:42B5
_IPoint47E304:4157
_IsA2ByteTok42A300:1FE8
_IsEditEmpty492D00:21A7
_IsOneTwoThree516D37:5438
_IsOP112or24517337:5413
_JError44D700:2793
_JErrorNo400000:2799
_JForceCmd402A00:0747
_JForceCmdNoChar402700:0746
_JForceGraphKey500501:6BFD
_JForceGraphNoKey500201:6BFB
_KeyToString45CA01:6D10
_KillTimer526F33:5E4E
_LCD_DRIVERON497806:4D02
_LdHLind400900:0033
_LineCmd48AC04:796A
_LnX40AB02:6EFD
_LoadCIndPaged501D00:029F
_LoadDEIndPaged501A3C:6B36
_LoadPattern4CB101:6267
_Load_SFont478303:4A8F
_LogX40AE02:6F16
_Max405700:224D
_MemChk42E500:0E20
_MemClear4C303B:7138
_MemSet4C333B:7139
_Min405400:2244
_Minus1406C00:2294
_Mov10B415C00:1A90
_Mov18B47DA00:192B
_Mov7B416800:1A96
_Mov8B416500:1A94
_Mov9B415F00:1A92
_Mov9OP1OP2417D00:1B06
_Mov9OP2Cp410B00:1982
_Mov9ToOP1417A00:1B01
_Mov9ToOP2418000:1B07
_MovFrOP1418300:1B0C
_NewLine452E01:5F4A
_NZIf83Plus50E000:1837
_newContext403000:077E
_OneVar4BA33A:6420
_OP1ExOP2421F00:1DD2
_OP1ExOP3421900:1DB7
_OP1ExOP4421C00:1DBC
_OP1ExOP5420D00:1DA0
_OP1ExOP6421000:1DA5
_OP1ExpToDec425200:1E77
_OP1Set041BF00:1BA4
_OP1Set1419B00:1B38
_OP1Set241A700:1B50
_OP1Set341A100:1B44
_OP1Set4419E00:1B3D
_OP1ToOP2412F00:1A2F
_OP1ToOP3412300:1A0F
_OP1ToOP4411700:19EC
_OP1ToOP5415300:1A80
_OP1ToOP6415000:1A78
_OP2ExOP4421300:1DAA
_OP2ExOP5421600:1DAF
_OP2ExOP6420700:1D93
_OP2Set041BC00:1B96
_OP2Set141AD00:1B60
_OP2Set241AA00:1B55
_OP2Set3419800:1B30
_OP2Set4419500:1B29
_OP2Set5418F00:1B22
_OP2Set604AB038:5DDC
_OP2Set8418C00:1B1B
_OP2SetA419200:1B24
_OP2ToOP1415600:1A88
_OP2ToOP3416E00:1AE7
_OP2ToOP4411A00:19F5
_OP2ToOP5414A00:1A68
_OP2ToOP6414D00:1A70
_OP3Set041B900:1B8A
_OP3Set1418900:1B16
_OP3Set241A400:1B4B
_OP3ToOP1413E00:1A4E
_OP3ToOP2412000:1A07
_OP3ToOP4411400:19E3
_OP3ToOP5414700:1A60
_OP4Set041B600:1B85
_OP4Set1418600:1B11
_OP4ToOP1413800:1A44
_OP4ToOP2411D00:19FE
_OP4ToOP3417100:1AEF
_OP4ToOP5414400:1A58
_OP4ToOP6417700:1AF9
_OP5ExOP6420A00:1D98
_OP5Set041B300:1B80
_OP5ToOP1413B00:1A49
_OP5ToOP2412600:1A17
_OP5ToOP3417400:1AF4
_OP5ToOP4412C00:1A27
_OP5ToOP6412900:1A1F
_OP6ToOP1413500:1A3F
_OP6ToOP2413200:1A37
_OP6ToOP5414100:1A53
_OutputExpr4BB203:4AF2
_PagedGet502300:17BB
_PARSAHEAD4B4F34:5AA1
_PARSAHEADS4B4C34:5A9D
_ParseInp4A9B38:5987
_PDspGrph48A304:7904
_PixelTest48B504:79E7
_Plus1406900:2285
_PointCmd48B204:79B2
_PointOn4C3904:4155
_PopMCplxO1436F00:14BC
_PopOP1437E00:14EA
_PopOP3437B00:14DA
_PopOP5437800:14CA
_PopReal439300:1512
_PopRealO1439000:150F
_PopRealO2438D00:150A
_PopRealO3438A00:1505
_PopRealO4438700:1500
_PopRealO5438400:14FB
_PopRealO6438100:14F6
_PosNo0Int422E00:1DF7
_PowerOff500800:09E6
_PToR40F302:50BD
_PushMCplxO143CF00:15A6
_PushMCplxO343C600:1594
_PushOP143C900:1599
_PushOP343C300:1581
_PushOP543C000:1573
_PushReal43BD00:155F
_PushRealO143BA00:155C
_PushRealO243B700:1554
_PushRealO343B400:154F
_PushRealO443B100:154A
_PushRealO543AE00:1545
_PushRealO643AB00:1540
_PutAway403900:08AF
_PutC450401:5B4C
_PutMap450101:5A98
_PutPS451001:5C73
_PutPSB450D01:5C52
_PutS450A01:5C39
_PutTokString496006:46FD
_PutToL464502:4829
_PutToMat461E02:406C
_RandInit4B7F36:7E8A
_Random4B7936:7DC9
_RclAns4AD738:679F
_RclGDB247D133:72D9
_RclN4ADD38:67A9
_RclSysTok4AE638:683E
_RclVarSym4AE338:67B1
_RclX4AE038:67AE
_RclY4ADA38:67A4
_Rcl_StatVar42DC00:2149
_Rec1stByte4EFA3C:439C
_Rec1stByteNC4EFD3C:43A3
_RecAByteIO4F033C:443F
_RedimMat4A2607:4D3B
_Regraph488E04:6764
_ReleaseBuffer477103:47AC
_ReloadAppEntryVecs4C363B:73E4
_RestartTimer527533:5E9D
_RestoreDisp487004:6176
_RName427F00:1F4C
_RndGuard409F02:6A57
_RnFx40A202:6A71
_Round40A800:2623
_RToD407800:2374
_RToP40F602:50DB
_RunIndicOff457001:6531
_RunIndicOn456D01:6518
_SaveDisp4C7B39:5DD8
_SendAByte4EE53C:420D
_SendPacket4ED63C:4139
_SendVarCmd4A143C:4EDD
_SetAllPlots4FCC38:49C7
_setDate516A37:536E
_SetExSpeed50BF00:0DCA
_SetFuncM484036:7D11
_SetGetKeyHook4F663B:7D00
_SetNorm_Vals49FC00:220F
_SetParM484936:7D39
_SetParserHook50263B:7D6E
_SetPolM484636:7D2C
_SetSeqM484336:7D1F
_SetSilentLinkHook50CE3B:7DBB
_SetTblGraphDraw4C0000:00F5
_SetTokenHook4F993B:7D0B
_setTime517037:540D
_SetupPagedPtr502000:17AC
_SetXXOP1478C33:5F7E
_SetXXOP2478F33:5F83
_SetXXXXOP2479233:5F9E
_SetZeroOne516737:5359
_SFont_Len478603:4ABD
_ShRAcc41D400:1BCB
_Sin40BD02:7342
_SinCosRad40BA02:733E
_SinH40CF02:7632
_SinHCosH40C602:7626
_SqRoot409C02:6E38
_SrchVLstDn4F1207:71D7
_SrchVLstUp4F0F07:707F
_SStringLength4CB43B:61A6
_StartTimer527233:5E58
_StMatEl4AE938:6C8F
_StoAns4ABF38:6251
_StoGDB247CE33:71AC
_StoN4ACB38:6274
_StoOther4AD438:62A9
_StopTimer527833:5F42
_StoR4AC538:6264
_StoRand4B7C36:7E06
_StoSysTok4ABC38:623B
_StoT4ACE38:629B
_StoTheta4AC238:625C
_StoX4AD138:62A3
_StoY4AC838:626C
_StrCopy44E300:2810
_StrLength4C3F36:7F91
_Tan40C302:734A
_TanH40C902:762A
_TanLnF48BB04:7A43
_TenX40B702:7066
_ThetaName427C00:1F48
_ThreeExec467502:64ED
_timeCnv517937:56C4
_Times2406600:2282
_TimesPt5407E00:2382
_TName428E00:1F69
_ToFrac465702:4BBE
_Trunc406000:2279
_UCLineS479533:6010
_UngroupVar50C839:764A
_UnLineCmd48AF04:797C
_UnOPExec467202:5E14
_VertCmd48A904:7955
_VPutMap455E01:6293
_VPutS456101:646D
_VPutSN456401:644D
_VtoWHLDE47FB04:4410
_WaitTimer527B33:5EA4
_XftoI480437:41EB
_Xitof47FE04:441E
_XName428800:1F61
_XRootY479E33:632E
_YftoI480137:41DF
_YName428B00:1F65
_YToX47A133:6340
_Zero16D41B000:1B6F
_ZeroOP41CE00:1BBC
_ZeroOP141C500:1BAF
_ZeroOP241C800:1BB4
_ZeroOP341CB00:1BB9
_ZmDecml484F36:7BA4
_ZmFit485B36:7A57
_ZmInt484C04:5F85
_ZmPrev485204:5FFE
_ZmSquare485E36:7ABE
_ZmStats47A433:65DC
_ZmTrig486136:7B36
_ZmUsr485504:601D
_ZooDefault486736:7BF9
_lcd_busy405100: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 0x80180x80D2 and 0x80E40x8129. 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]

bcallIDBody (page:addr)
_MD5Final80183F:6964
_RSAValidate801B3F:6CB4
_cmpStr801E3F:7195
_WriteAByte80213F:4C9F
_EraseFlash80243F:4C2A
_FindFirstCertField80273F:4D62
_ZeroToCertificate802A3F:4DAF
_GetCertificateEnd802D3F:4D53
_FindGroupedField80303F:4E8C
_ret_180333F:4867
_ret_280363F:4867
_ret_380393F:4867
_ret_4803C3F:4867
_ret_5803F3F:4867
_Mult8By880423F:7059
_Mult16By880453F:705B
_Div16By880483F:7146
_Div16By16804B3F:7148
certificate_reconcile_id_fields804E3F:4924
_LoadAIndPaged80513F:486E
_FlashToRam280543F:4888
_GetCertificateStart80573F:4D46
_GetFieldSize805A3F:4DB8
_FindSubField805D3F:4DFB
_EraseCertificateSector80603F:4E3F
_CheckHeaderKey80633F:4B4A
certificate_find_matching_field_data80663F:4F91
certificate_count_matching_fields80693F:4EFF
_Load_LFontV2806C3F:7C8A
_Load_LFontV806F3F:7C8A
_ReceiveOS80723F:5DCE
_FindOSHeaderSubField80753F:5018
_FindNextCertField80783F:4D5C
_GetByteOrBoot807B3F:5C64
_getSerial807E3F:442F
_ReceiveCalcID80813F:5EDC
_EraseFlashPage80843F:4C1E
_WriteFlashUnsafe80873F:4CA6
_dispBootVer808A3F:44F1
_MD5Init808D3F:68ED
_MD5Update80903F:6907
_MarkOSInvalid80933F:5209
_FindProgramLicense80963F:4B1A
_MarkOSValid80993F:51F5
_CheckOSValidated809C3F:52C6
_SetupAppPubKey809F3F:53CA
_SigModR80A23F:7225
_TransformHash80A53F:723F
_IsAppFreeware80A83F:52E1
_FindAppHeaderSubField80AB3F:500A
_WriteValidationNumber80AE3F:540B
_Div32By1680B13F:706E
_FindGroup80B43F:4E61
_getBootVer80B73F:477C
_getHardwareVersion80BA3F:4781
_xorA80BD3F:5C6D
_bignumpowermod1780C03F:6CBD
_ProdNrPart180C33F:6209
_WriteAByteSafe80C63F:4C9A
_WriteFlash80C93F:4C8F
_SetupDateStampPubKey80CC3F:548C
_SetFlashLowerBound80CF3F:4784
_LowBatteryBoot80D23F:5834
_AttemptUSBOSReceive80E42F:4145
_DisplayBootMessage80E73F:6127
_NewLine280EA3F:73DD
_DisplayBootError1080ED3F:5789
_Chk_Batt_Low_B80F03F:6171
_Chk_Batt_Low_B280F33F:6163
_ReceiveOS_USB80F62F:48CA
_DisplayOSProgress80F93F:62D0
_ResetCalc80FC3F:5ED3
_SetupOSPubKey80FF3F:5387
_CheckHeaderKeyHL81023F:4B4D
_USBErrorCleanup81052F:5958
_InitUSB81082F:52A4
usb_set_port81_bit0_delay810B2F:62C5
_KillUSB810E2F:5961
_DisplayBootError181113F:63DB
_DisplayBootError281143F:5789
_DisplayBootError381173F:5789
_DisplayBootError4811A3F:5789
_DisplayBootError5811D3F:5789
_DisplayBootError681203F:5789
_DisplayBootError781233F:5789
_DisplayBootError881263F:5789
_DisplayBootError981293F: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:

2ndTokenSince
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:

2ndTokenSince
00L₁TI-82
01L₂TI-82
02L₃TI-82
03L₄TI-82
04L₅TI-82
05L₆TI-82

5E — Equation variables (Y= functions, parametric, polar, sequence)

31 tokens on the 84+ (2.55MP). Second byte → token:

2ndTokenSince
10Y₁TI-82
11Y₂TI-82
12Y₃TI-82
13Y₄TI-82
14Y₅TI-82
15Y₆TI-82
16Y₇TI-82
17Y₈TI-82
18Y₉TI-82
19Y₀TI-82
20X₁ᴛTI-82
21Y₁ᴛTI-82
22X₂ᴛTI-82
23Y₂ᴛTI-82
24X₃ᴛTI-82
25Y₃ᴛTI-82
26X₄ᴛTI-82
27Y₄ᴛTI-82
28X₅ᴛTI-82
29Y₅ᴛTI-82
2AX₆ᴛTI-82
2BY₆ᴛTI-82
40r₁TI-82
41r₂TI-82
42r₃TI-82
43r₄TI-82
44r₅TI-82
45r₆TI-82
80uTI-82
81vTI-82
82wTI-82

60 — Pictures (Pic1Pic0)

10 tokens on the 84+ (2.55MP). Second byte → token:

2ndTokenSince
00Pic1TI-82
01Pic2TI-82
02Pic3TI-82
03Pic4TI-82
04Pic5TI-82
05Pic6TI-82
06Pic7TI-83
07Pic8TI-83
08Pic9TI-83
09Pic0TI-83

61 — Graph databases (GDB1GDB0)

10 tokens on the 84+ (2.55MP). Second byte → token:

2ndTokenSince
00GDB1TI-82
01GDB2TI-82
02GDB3TI-82
03GDB4TI-82
04GDB5TI-82
05GDB6TI-82
06GDB7TI-83
07GDB8TI-83
08GDB9TI-83
09GDB0TI-83

62 — Statistics, regression, and output variables

60 tokens on the 84+ (2.55MP). Second byte → token:

2ndTokenSince
01RegEQTI-82
02nTI-82
03TI-82
04ΣxTI-82
05Σx²TI-82
06SxTI-82
07σxTI-82
08minXTI-82
09maxXTI-82
0AminYTI-82
0BmaxYTI-82
0CȳTI-82
0DΣyTI-82
0EΣy²TI-82
0FSyTI-82
10σyTI-82
11ΣxyTI-82
12rTI-82
13MedTI-82
14Q₁TI-82
15Q₃TI-82
16aTI-82
17bTI-82
18cTI-82
19dTI-82
1AeTI-82
1Bx₁TI-82
1Cx₂TI-82
1Dx₃TI-82
1Ey₁TI-82
1Fy₂TI-82
20y₃TI-82
21𝑛TI-82
22pTI-82
23zTI-82
24tTI-82
25χ²TI-82
26𝙵TI-82
27dfTI-82
28TI-82
29p̂₁TI-82
2Ap̂₂TI-82
2Bx̄₁TI-82
2CSx₁TI-82
2Dn₁TI-82
2Ex̄₂TI-82
2FSx₂TI-82
30n₂TI-82
31SxpTI-82
32lowerTI-82
33upperTI-82
34sTI-82
35TI-82
36TI-82
37dfTI-82
38SSTI-82
39MSTI-82
3AdfTI-82
3BSSTI-82
3CMSTI-82

63 — Window and system variables

56 tokens on the 84+ (2.55MP). Second byte → token:

2ndTokenSince
00ZXsclTI-82
01ZYsclTI-82
02XsclTI-82
03YsclTI-82
04UnStartTI-82
05VnStartTI-82
06U𝑛-₁TI-82
07V𝑛-₁TI-82
08ZUnStartTI-82
09ZVnStartTI-82
0AXminTI-82
0BXmaxTI-82
0CYminTI-82
0DYmaxTI-82
0ETminTI-82
0FTmaxTI-82
10θminTI-82
11θmaxTI-82
12ZXminTI-82
13ZXmaxTI-82
14ZYminTI-82
15ZYmaxTI-82
16ZθminTI-82
17ZθmaxTI-82
18ZTminTI-82
19ZTmaxTI-82
1ATblStartTI-82
1B𝑛MinTI-82
1CZPlotStartTI-82
1D𝑛MaxTI-82
1EZ𝑛MaxTI-82
1F𝑛StartTI-82
20Z𝑛MinTI-82
21ΔTblTI-82
22TstepTI-82
23θstepTI-82
24ZTstepTI-82
25ZθstepTI-82
26ΔXTI-82
27ΔYTI-82
28XFactTI-82
29YFactTI-82
2ATblInputTI-82
2B𝗡TI-83
2CI%TI-83
2DPVTI-83
2EPMTTI-83
2FFVTI-83
30P/YTI-83
31C/YTI-83
32w(𝑛Min)TI-83
33Zw(𝑛Min)TI-83
34PlotStepTI-83
35ZPlotStepTI-83
36XresTI-83
37ZXresTI-83

7E — Graph-format and mode tokens

19 tokens on the 84+ (2.55MP). Second byte → token:

2ndTokenSince
00SequentialTI-82
01SimulTI-82
02PolarGCTI-82
03RectGCTI-82
04CoordOnTI-82
05CoordOffTI-82
06ConnectedTI-82
07DotTI-82
08AxesOnTI-82
09AxesOffTI-82
0AGridOnTI-82
0BGridOffTI-82
0CLabelOnTI-82
0DLabelOffTI-82
0EWebTI-82
0FTimeTI-82
10uvAxesTI-82
11vwAxesTI-82
12uwAxesTI-82

AA — String variables (Str1Str0)

10 tokens on the 84+ (2.55MP). Second byte → token:

2ndTokenSince
00Str1TI-83
01Str2TI-83
02Str3TI-83
03Str4TI-83
04Str5TI-83
05Str6TI-83
06Str7TI-83
07Str8TI-83
08Str9TI-83
09Str0TI-83

BB — Extended command page (2-byte commands)

232 tokens on the 84+ (2.55MP). Second byte → token:

2ndTokenSince
00npv(TI-83
01irr(TI-83
02bal(TI-83
03ΣPrn(TI-83
04ΣInt(TI-83
05►Nom(TI-83
06►Eff(TI-83
07dbd(TI-83
08lcm(TI-83
09gcd(TI-83
0ArandInt(TI-83
0BrandBin(TI-83
0Csub(TI-83
0DstdDev(TI-83
0Evariance(TI-83
0FinString(TI-83
10normalcdf(TI-83
11invNorm(TI-83
12tcdf(TI-83
13χ²cdf(TI-83
14𝙵cdf(TI-83
15binompdf(TI-83
16binomcdf(TI-83
17poissonpdf(TI-83
18poissoncdf(TI-83
19geometpdf(TI-83
1Ageometcdf(TI-83
1Bnormalpdf(TI-83
1Ctpdf(TI-83
1Dχ²pdf(TI-83
1E𝙵pdf(TI-83
1FrandNorm(TI-83
20tvm_PmtTI-83
21tvm_I%TI-83
22tvm_PVTI-83
23tvm_𝗡TI-83
24tvm_FVTI-83
25conj(TI-83
26real(TI-83
27imag(TI-83
28angle(TI-83
29cumSum(TI-83
2Aexpr(TI-83
2Blength(TI-83
2CΔList(TI-83
2Dref(TI-83
2Erref(TI-83
2F►RectTI-83
30►PolarTI-83
31𝑒TI-83
32SinReg TI-83
33Logistic TI-83
34LinRegTTest TI-83
35ShadeNorm(TI-83
36Shade_t(TI-83
37Shadeχ²(TI-83
38Shade𝙵(TI-83
39Matr►list(TI-83
3AList►matr(TI-83
3BZ-Test(TI-83
3CT-Test TI-83
3D2-SampZTest(TI-83
3E1-PropZTest(TI-83
3F2-PropZTest(TI-83
40χ²-Test(TI-83
41ZInterval TI-83
422-SampZInt(TI-83
431-PropZInt(TI-83
442-PropZInt(TI-83
45GraphStyle(TI-83
462-SampTTest TI-83
472-Samp𝙵Test TI-83
48TInterval TI-83
492-SampTInt TI-83
4ASetUpEditor TI-83
4BPmt_EndTI-83
4CPmt_BgnTI-83
4DRealTI-83
4Er𝑒^θ𝑖TI-83
4Fa+b𝑖TI-83
50ExprOnTI-83
51ExprOffTI-83
52ClrAllListsTI-83
53GetCalc(TI-83
54DelVarTI-83
55Equ►String(TI-83
56String►Equ(TI-83
57Clear EntriesTI-83
58Select(TI-83
59ANOVA(TI-83
5AModBoxplotTI-83
5BNormProbPlotTI-83
64G-TTI-83
65ZoomFitTI-83
66DiagnosticOnTI-83
67DiagnosticOffTI-83
68Archive TI-83+
69UnArchive TI-83+
6AAsm(TI-83+
6BAsmComp(TI-83+
6CAsmPrgmTI-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+
ADTI-83+
AEχTI-83+
AF𝙵TI-83+
B0aTI-83+
B1bTI-83+
B2cTI-83+
B3dTI-83+
B4eTI-83+
B5fTI-83+
B6gTI-83+
B7hTI-83+
B8iTI-83+
B9jTI-83+
BAkTI-83+
BClTI-83+
BDmTI-83+
BEnTI-83+
BFoTI-83+
C0pTI-83+
C1qTI-83+
C2rTI-83+
C3sTI-83+
C4tTI-83+
C5uTI-83+
C6vTI-83+
C7wTI-83+
C8xTI-83+
C9yTI-83+
CAzTI-83+
CBσTI-83+
CCτTI-83+
CDÍTI-83+
CEGarbageCollectTI-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+
DBTI-83+
DCTI-83+
DDßTI-83+
DEˣTI-83+
DFTI-83+
E0TI-83+
E1TI-83+
E2TI-83+
E3TI-83+
E4TI-83+
E5TI-83+
E6TI-83+
E7TI-83+
E8TI-83+
E9TI-83+
EA₁₀TI-83+
EBTI-83+
ECTI-83+
EDTI-83+
EETI-83+
F0×TI-83+
F1TI-83+
F2🡁TI-83+
F3🠿TI-83+
F4TI-83+
F5TI-83+

EF — TI-84 Plus extended tokens

48 tokens on the 84+ (2.55MP). Second byte → token:

2ndTokenSince
00setDate(TI-84+
01setTime(TI-84+
02checkTmr(TI-84+
03setDtFmt(TI-84+
04setTmFmt(TI-84+
05timeCnv(TI-84+
06dayOfWk(TI-84+
07getDtStr(TI-84+
08getTmStr(TI-84+
09getDateTI-84+
0AgetTimeTI-84+
0BstartTmrTI-84+
0CgetDtFmtTI-84+
0DgetTmFmtTI-84+
0EisClockOnTI-84+
0FClockOffTI-84+
10ClockOnTI-84+
11OpenLib(TI-84+
12ExecLib TI-84+
13invT(TI-84+
14χ²GOF-Test(TI-84+
15LinRegTInt TI-84+
16Manual-Fit TI-84+
17ZQuadrant1TI-84+
18ZFrac1⁄2TI-84+
19ZFrac1⁄3TI-84+
1AZFrac1⁄4TI-84+
1BZFrac1⁄5TI-84+
1CZFrac1⁄8TI-84+
1DZFrac1⁄10TI-84+
1ETI-84+
2ETI-84+
2F󸏵TI-84+
30►n⁄d◄►Un⁄dTI-84+
31►F◄►DTI-84+
32remainder(TI-84+
33Σ(TI-84+
34logBASE(TI-84+
35randIntNoRep(TI-84+
37MATHPRINTTI-84+
38CLASSICTI-84+
39n⁄dTI-84+
3AUn⁄dTI-84+
3BAUTOTI-84+
3CDECTI-84+
3DFRACTI-84+
3FSTATWIZARD ONTI-84+
40STATWIZARD OFFTI-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 (0x84440x8446), 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 4741h35: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/ cluster is byte-pinned: 3A:68453A: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 (id 0x35, slot 0x8C05) or (id 0x36, slot 0x8C0E). [confirmed]

The STAT-TESTS engine occupies 3A:4A003A:7E60. A raw operand scan finds about 50 candidate PStatSStat 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:554F3A:5584, and the test-editor descriptor tables at 3A:7D003A:7E60. The normalcdf( evaluation reaches the page-39 floating-point core at 39:4A0239: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 AF
JR 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:5CC102: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 4B85h35:7995; the rref( executor runs through bcall ID 4B88h02: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:69BC37:426037: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:576257D4: _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/_DelRes guard run through _GetKey, ON-key handling, an OS error, APD, archive collection, link/USB, and shell interrupts.
  • Run RAM-selector 0x83 guards through editor, graph, table, statistics, App, archive-GC, and transfer contexts. Probe selectors 0x840x87 on 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→1 programming, DQ toggle cadence, program and erase durations, erase suspend, and busy reads at all four top-boot boundaries;
  • force DQ5 failure with DE first 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.

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-0x00 pull-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, and 0x52;
  • test the FDRC-family register hypothesis, port 0x4B, and the port-0x4F/0x50 setup sequence;
  • capture port-0x5A presentation 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-0x2E access class, CPU-speed readback, and actual clock frequency;
  • characterize port-0x2D low-power behavior and the port-0x2F mode-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/3277 from the emulator values 32/327/3276;
  • test the port-0x2F prescaler, counter zero, first- versus second-expiry status bit 2, programmable-timer HALT behavior, 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-0x03 clear-on-zero sequence and which timer configurations wake HALT.

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.