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

TI-84 Plus OS — Reverse-engineering notes: system overview

Target: ti84plus.rom (1 MiB flash dump). OS self-identifies as 2.55MP. CPU: Zilog Z80 (16-bit address bus, 64 KiB logical space) with hardware flash/RAM paging. Ghidra project: ti84.gpr (rebuild: tools/build.sh).

Confidence is flagged: [confirmed] = verified in disassembly/decompiler; [standard] = matches documented TI-83+/84+ architecture and is consistent with the disassembly; [hypothesis] = inferred, not yet verified.

The big picture

The TI-84+ is a Z80 machine that can only see 64 KiB at once, but has 1 MiB of flash and 128 KiB of RAM. It bridges that gap with a 4-slot paging scheme and a system-call (“bcall”) mechanism that lets code on one 16 KiB flash page call routines on any other page. The OS is a single-tasking monitor: a boot/kernel core on flash page 0 (always mapped low), a large body of OS routines spread across the other flash pages and reached via bcalls, and a fixed RAM region holding the system state (flags, floating-point registers, display buffers, the variable table).

Everything the user interacts with — the homescreen, TI-BASIC programs, graphing, the catalog — is built on four pillars:

  1. Paging + bcalls — how code and data beyond 64 KiB are reached. (see 02-paging.md, 03-bcall-mechanism.md)
  2. The floating-point engine — 9-byte BCD reals/complex in the OP1–OP6 registers; all math flows through these. (06-floating-point.md)
  3. The variable system (VAT) — named objects (reals, lists, matrices, strings, programs, appvars…) catalogued in the Variable Allocation Table. (05-variables-vat.md)
  4. The tokenizer/parser — TI-BASIC is stored as 1- and 2-byte tokens; the parser executes them. (07-tokenizer-basic.md)

Around those sit the I/O subsystems: the IM1 interrupt that drives timing/APD/cursor/ON-key (04-interrupts.md), the LCD driver, the keypad scanner, and the link port.

Subsystem index

Each row maps a documentation page to the subsystem it covers and its analysis status.

DocSubsystem
01-memory-map.mdAddress space, ports, RAM layout
02-paging.mdFlash/RAM banking (ports 6/7)
03-bcall-mechanism.mdrst 28h system calls + jump table
04-interrupts.mdIM1 ISR, timers, APD, ON key
05-variables-vat.mdVariable Allocation Table, object types
06-floating-point.mdBCD float format, OP registers
07-tokenizer-basic.mdToken tables, parser/interpreter
08-display-lcd.mdLCD ports, screen buffers
09-keyboard-link.mdKeypad scan, link protocol
10-subsystem-map.mdbcall API surface, system through-line
11-boot-contexts-errors.mdBoot, context system, _JError/onSP
12-memory-management.mdRAM heap, VAT/userMem, Flash archive/GC
13-flash-page-map.mdWhat each of the 64 flash pages contains
14-ram-pages.mdRAM page selectors, page 83, and restore rules
99-open-questions.mdPrioritized future-work roadmap
sub-calculation.mdCalculation engine: FP ops, transcendentals, formatting, errors
sub-graphing.mdGraphing: window vars, coord↔pixel, draw primitives, Y= eval
sub-tibasic.mdTI-BASIC: program execution, control flow, I/O commands
sub-tibasic-tracing.mdTI-BASIC fixture traces, smoke runner, coverage anchors
sub-vat-archive.mdVariables, Sto/Rcl, Archive/Unarchive, Flash GC
sub-apps-mem-settings.mdApps find/launch, RAM-reset, MODE/format flags
sub-statistics.mdSTAT: 1/2-var, regressions, statVars
sub-matrix-list.mdMatrix/list element access, Gauss-Jordan inverse/det, matmul
sub-solver-numeric.mdSolver root-finder, nDeriv/fnInt, TVM finance
sub-table-yvars.mdTABLE generation/cache, Y= equation vars
sub-equation-display.mdEquation display / MathPrint layout (page 0x39 eqdisp_*)
sub-link-transfer.mdLink protocol: byte/packet/var-transfer (page 0x3C)
sub-usb-asic.mdUSB ASIC/link-assist ports and OS transport selection

(The sub-* docs are deep dives covering user-facing functionality and I/O internals: calculation, graphing, TI-BASIC, VAT/archive, apps, stats, matrices, solver, table, equation display, link, and USB/link assist.)

New to these notes? Start with Conventions & Methodology (how to read the addresses and confidence flags) and the Glossary; the bcall Index is the full alphabetical system-call reference.

The main 0x4xxx bcall table and the retail boot bcall table (0x8xxx, from the local complete ROM) both carry TI-OS types. Most boot bcall bodies are on page 3F; USB boot routines such as _AttemptUSBOSReceive, _ReceiveOS_USB, _InitUSB, and _KillUSB are on page 2F. Rebuild: tools/build.sh.

10 — Subsystem map (bcall API surface)

This page categorizes the ~600 named bcall entry points (the OS’s public API) by subsystem, so the whole OS surface is visible at once. This is the surface area user code and the OS itself program against.

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,_LinkXferOP
System / power~15_AppInit,_PutAway,_RandInit,_ApdSetup,_Chk_Batt_Low,_SetExSpeed,_JForceCmd
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

The dominant fact: this is a calculator — roughly two-thirds of the API is numeric. 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: bcall/paging (03), interrupts/APD (04), error handling (_JError + TIError codes), and the system flags (SystemFlags @ flags).

How the pieces connect (the 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 per-subsystem docs 0109 for detail.

Conventions & methodology

How to read these notes, and how they were produced.

Suggested reading order

  1. Overview — the four pillars and the system through-line.
  2. Subsystem map — see the whole API surface at once.
  3. Substrate: Memory mapPagingThe bcall mechanismInterrupts.
  4. Pick a core subsystem (Floating-point, VAT, Tokenizer/TI-BASIC, Display…), then its feature deep-dive (sub-*).
  5. Glossary for any unfamiliar term.

Address notation

  • pp:addr — flash page pp (003F), logical address addr. Banked pages run in the 4000–7FFF window, so e.g. _PutS at 01:5C39 means page 1, address 0x5C39. Example: 3D:6745.
  • 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, e.g. _FindSym = 42F4h) 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 the disassembly/decompiler of this ROM.
[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.

Some early deep-dive docs use shorthand [C]/[H]/[I][confirmed]/[standard]/[hypothesis]; read them against this three-tier scheme.

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 during this RE from a routine’s behavior (which named routines it calls, which RAM/ports it touches), e.g. findsym_scan, fp_normalize. Accurate in aggregate; any single low-level helper name is a best-effort guess.

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.

How this RE was produced

  • The Ghidra database is rebuilt from the ROM by tools/build.sh (a 10-stage headless pipeline). It loads all 64 flash pages (page 0 + overlays at 4000), then resolves and names routines from the main OS bcall table.
  • 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 a retail boot table on page 3F (with the USB boot entries on retail page 2F). This rom.bin is a BootFree image — page 3F carries the BootFree prefix 3E 3F D3 06 … and page 2F is blank (all FF) — so these 0x8xxx body targets do not resolve and are left unnamed (tools/bcalls8x_targets.txt has no body rows); only their SDK equate names are known.
  • Decompiler caveats. Ghidra’s Z80 decompiler mis-renders some idioms — SET b,(IY+d) flag ops, the CALL cross_page_jump (ram:2B09) trampolines, and register-passed arguments on banked pages. Where the decompiler is unreliable the notes are grounded in the raw disassembly (and several deep-dives used a small custom Z80 decoder over the ROM to verify addresses byte-exactly).
  • Parallel multi-agent passes. The feature deep-dives (sub-*) and the final 100%-naming pass were produced by multiple agents working on isolated copies of the database, each owning a disjoint set of pages, then merged.

See the repository README.md for the exact build pipeline and tooling.

Glossary

Quick definitions for the terms and key RAM symbols used throughout this 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 timer-driven idle shutoff.
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 & 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.
garbage collectionCompacting the archive flash when it fills (“Garbage Collecting…”). The GC-core candidate flash_gc_relocate@3C:7BD0 is a project-local inferred label, not a defined function in the current live DB nor a WikiTI or ti83plus.inc equate.
RAM heapThe dynamic region from userMem (0x9D95) up to the VAT; managed by _InsertMem/_DelMem. See Memory Management.

Registers & 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).

01 — Memory map

The Z80 sees a flat 64 KiB logical space, divided into four 16 KiB slots. Hardware paging (ports 6/7) decides which physical flash page or RAM page is visible in the two middle slots. See 02-paging.md for the banking detail and 14-ram-pages.md for RAM page 83 and restore rules.

Logical address space (what the Z80 sees)

RangeSlotContentsNotes
0000–3FFFFlash bank 0Flash page 0 (fixed)Boot/kernel: RST vectors, dispatcher, FP/VAT core. Never swapped. [confirmed]
4000–7FFFFlash bank ASwappable flash page (port 6)Paged bcall targets run here after the dispatcher banks their page in; page-0 bcall bodies instead execute in place below 4000 (e.g. _JErrorNo00:2799). [confirmed]
8000–BFFFBank BSwappable RAM/flash page (port 7)Usually RAM. [standard]
C000–FFFFRAMRAM page (MemC)Normally RAM page 0, but page-selectable via port 5 on the 84+ (not hard-fixed). Stack lives near the top. [standard]

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/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 pagesupplied by local D84PBE2.8Xv; 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 + OS version string ("2.55MP")page starts 32 2E 35 35 4D 50 [confirmed]
3ECertification page (per-calculator cert sector; effectively blank in this OS-only image — no certificate payload, only a few 00 bytes)84+ cert page is 3E, not 3F [standard]
3FRetail boot pagesupplied by local D84PBE1.8Xv; 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 (the Ghidra build input) is a BootFree image — pages 2F and 3F are blank or BootFree-substituted there. The 2F/3F rows above describe the retail USB/boot content from the local D84PBE2.8Xv / D84PBE1.8Xv segment files (the page-3F retail boot is also applied in ti84plus_patched.rom); those bodies are byte-decoded from those files, not from rom.bin.

Key RAM regions (named & typed)

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]
0x843F/0x8444kbdScanCode/kbdKeybyteLast key scan code / key [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 I/O ports [standard, cross-checked vs code]

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
00linkLink port lines
01keypadKeyboard matrix select/read
02hwStatusStatus (bit7 used at reset)
03intMaskInterrupt enable mask
04intStatus / memMapModeRead = interrupting-device ID + ON-held; write = memory-map mode + timer rate
05mapBankCRAM page in slot C000 (MemC) on the 84+
06mapBankAFlash page in slot 4000
07mapBankBPage in slot 8000 (0x81=84+ mode seen in ISR)
080Dusb/link assist84+ hardware byte-assist control/status/data/FIFO ports; see USB ASIC and link assist
10/11lcdCmd/lcdDataLCD controller
20cpuSpeed0=6 MHz, 1=15 MHz (set in ISR)
21asicVer/ramSizeASIC version & RAM-page count; read in the kernel (e.g. 00:02AE) and its low bits mask the slot-4000 page number before OUT (6)
4DusbLineStateUSB line-state gate sampled by _GetVarCmdUSB (id 50FB; Ghidra alias _LinkXferOP); bits 5/6 gate the ram:2E0B bjump to 35:4280
55/56usbIntStatus/usbLineEventsUSB interrupt state / line events (84+) — polled first in int_dispatch_sources; both read-only (port 0x56 is a read-only event bitmap, not a write mask)

02 — Memory paging

The Z80’s banked 16 KiB slots are windows onto physical memory selected by I/O ports.

SlotSelect portSelects
4000–7FFF (bank A)port 6 (mapBankA)a flash page (0–63, bit 7 clear) or a RAM page (bit 7 set, e.g. 0x83→RAM page 83); the bcall mechanism uses the flash case to bring routines into view
8000–BFFF (bank B)port 7 (mapBankB)a RAM or flash page; 0x81 observed = 84+ RAM mode
C000–FFFF (bank C)port 5 (memModeA)a RAM page; 0x00 observed = RAM page 80

0000–3FFF is hardwired to flash page 0. C000–FFFF is RAM in the static OS model and is normally the stack/user-RAM window; the 84+ hardware banks the high RAM slot through MemC/port 5. The dynamic trace in RAM pages confirms port-5 restores to RAM page 80 during normal OS execution. [confirmed]

How code uses it

  • bcalls to a paged body set port_mapBankA (port 6) to the target routine’s page, run it at 4000+, then restore the previous page; bcalls whose body lives on page 0 run it in place below 4000 (e.g. _JErrorNo00:2799). See 03-bcall-mechanism.md. The helper ram:181c is the page-set primitive used by the dispatcher.
  • A routine that runs banked into 4000 must therefore be written position-fixed for 4000 — which is exactly why every overlay page in Ghidra is based at 4000.
  • cross_page_jump (ram:2b09) is the common cross-page jump trampoline: many page-0 entries and inline OS calls use it with a following .dw addr; .db page payload to reach a body on another page. [confirmed]

Modeling in Ghidra

Each physical flash page 1–63 is an overlay block page_NN based at 4000, so the same logical 4000–7FFF window exists once per page without collision. Bank B/RAM is the single RAM block 8000–FFFF. This loses the runtime “only one page visible” semantics (intentionally) so all code is statically present.

03 — The bcall system-call mechanism

This is the heart of how the OS spans 1 MiB with a 64 KiB CPU. A routine on any page calls a routine on any other page by bcall 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 of the named .inc 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, all 535 .inc IDs plus 61 RE-named entries (596 total) resolve and are live-confirmed.
  • Validation: known bcalls land exactly where expected — _PutS01:5C39, _GetKey06:491E, _ClrLCDFull01:60E4, _GetCSC00:04B2, _CreateReal00:10B8.

tools/bcall_targets.txt holds 596 resolved main-table bcall rows. tools/bcalls8x_targets.txt holds the retail boot-table (0x8xxx) rows only when the local ROM has the retail page 3F and USB support page 2F installed; with the checked-in BootFree rom.bin it has no body rows — only skip comments (the resolver refuses to emit 0x8xxx bodies from a BootFree-substituted page 3F). tools/ApplyBcalls.java disassembles and names the confirmed bodies it can resolve.

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 (596 live-confirmed bcalls: 535 from the .inc + 61 RE-named).
  • 0x8xxx (bit 15 set): the retail boot table is on physical page 3F, indexed by ID & 0x7FFF. 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/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: CALL cross_page_jump (= CALL ram:2B09) followed inline by .dw addr; .db page. cross_page_jump reads the stacked return address (its caller’s, via an SP-relative load — it does not POP it), fetches the 2-byte target + 1-byte page from the inline descriptor there, rewrites the return frame past those 3 bytes, banks the page (& 0x3F), and returns into the target. The target’s RET then returns to the bjump’s caller, so it behaves like a call that consumes the 3 inline bytes.

There is a trampoline table in the page-0 address range ram:3B01–ram:3D0A: 87 packed 6-byte entries, each a bjump to a hot OS routine on another page (ram:3D0B already begins a separate CALL ram:2B49 table). The static Ghidra DB models it in the page-0/ROM address space; whether the table is copied to RAM at runtime is a hypothesis, not MCP-confirmed. Code invokes a routine by CALL ram:3Bxx into the table. tools/bjumps.txt lists every entry’s (offset → page:addr); tools/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, CALL cross_page_jump; .dw; .db appears in other packed dispatch tables (e.g. a page-3D dispatch table near ram:2B6B) and inline within OS routines. Because cross_page_jump consumes the 3 inline bytes and tail-jumps (the target returns to the bjump’s caller), the bytes after must be data and the call is non-returning. tools/FixInlineBjumps.java marks all 355 such sites in the complete local ROM, which substantially improved OS-wide disassembly coverage.

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.

04 — Interrupts (IM1)

The Z80 runs in interrupt mode 1: every maskable interrupt vectors to 0038h. There is no vector table — one handler services all sources by polling status ports.

Vector → handler [confirmed]

0038:  JR  0x006d        ; RST 38h vector
006d:  int_entry_save_alt_regs ; shadow-register save prologue
006f:  int_dispatch_sources    ; live interrupt-source dispatcher

int_dispatch_sources @ ram:006F runs after the two-byte prologue at ram:006D, with IY = flags (0x89F0), so (IY+off) reads/writes SystemFlags fields.

What it does [confirmed from decompiler]

Entry saves context (ex af,af' / exx — the Z80 shadow registers, the classic TI ISR convention) then polls:

  1. port_usbIntStatus (0x55) — the 84+ USB Interrupt State port. This OS overloads it as the ISR’s master “anything pending?” gate: (val ^ 0xFF) & 0x1F tests the 5 active-low sources.
  2. port_usbLineEvents (0x56) — the USB Line Events port; a read-only event bitmap whose bits select the timer/link sub-handlers. (Port 0x56 is read-only, so it is not an interrupt mask.)
  3. Branches per source:
    • ON key — sets an ON-flag; onSP (0x85BC) holds the SP to unwind to for the ON-break path.
    • First/second timer — drives the APD (auto-power-down) countdown and cursor blink; ACKs via the interrupt-mask port 0x03.
    • Link activity — services the link port.
  4. Hardware-mode housekeeping: checks port_mapBankB == 0x81 (84+ mode), and on one path sets port_cpuSpeed = 1 (15 MHz) and port_mapBankB = 0x81.
  5. Restores context and EI / RET.

(IY+off)SystemFlags fields the ISR touches [confirmed from disassembly]

int_dispatch_sources reads/writes these flag bits via BIT/SET/RES b,(IY+d). Offsets are confirmed against the standard ti83plus.inc group layout; the anchor apdFlags = IY+0x08 is confirmed in code (_DisableApd/_EnableApd @ 3B:7AA8/3B:7AAD do RES/SET 2,(IY+0x8)), curFlags = IY+0x0C is confirmed (_CursorOn/_CursorOff @ 06:7D34/06:7C5F).

(IY+off)bitfield / equatemeaning in the ISR
IY+0x031flag byte 0x03 bit1ON-key interrupt already latched (guards the ON-set path @ ram:00F5)
IY+0x030graphFlags·graphDrawredraw-graph flag the ISR sets @ ram:0109
IY+0x082apdFlags·apdAbleAPD enabled; toggled by _DisableApd/_EnableApd
IY+0x093onFlags·onRunningcalculator-running flag; tested before the 84+ USB-port path (ram:008B, ram:099E)
IY+0x094onFlags·onInterruptON-key interrupt-request flag; set @ ram:0A87
IY+0x0C3curFlags·curOncursor currently drawn (blink phase)
IY+0x0C2curFlags·curAblecursor-blink enabled (curLock is bit 4)
IY+0x0F7seqFlags bit7cleared @ ram:0A8C (RES 7,(IY+0Fh)) on the ON-key path
IY+0x123shiftFlags·shift2ndthe [2nd]-pending modifier flag; the ISR clears it at ram:01E0 (RES 3) so a held [2nd] does not linger — see the keyboard modifier state machine
IY+0x120indicFlags·indicRunrun-indicator-on flag (set by _RunIndicOn); the byte is shared — bits 0–2 are indicFlags, bits 3–7 are shiftFlags
IY+0x160speed/ACK selectchooses the value re-written to int-mask port 0x03 on exit (ram:00E6)
IY+0x161(same byte)link-busy sub-flag, reset @ ram:015E
IY+0x242link/transfer-activeguards the ON-break vs. link-restore decision (ram:09EE, ram:0AAB)
IY+0x287/3APIFlg·appRetKeyOff (b7)ISR tests BIT 7 (appRetKeyOff) @ ram:09DB and does SET 3 @ ram:09E1 on the ON-break path
IY+0x2C0mouseFlag1 bit0scanner-active flag tested by kbd_scan_autorepeat @ ram:0415 (the scan code itself is RAM kbdScanCode 0x843F, not an IY flag)
IY+0x335/0context-restore sub-flagsbranch selectors on the ON-break / restore path
IY+0x3A0hookflags5·usbActivityHookActivewhen set, the ISR runs the deferred USB-activity hook (ram:032A) and ACKs
IY+0x3F7RAM-clear controlmasked during the ON-key RAM wipe (ram:0B3C)
IY+0x442(uncharacterized)a restore-path branch clears this bit; no standard equate identifies it

The byte _GetCSC (00:04B2) clears is (IY+0) bit3 (*flags & 0xF7) — the kbdSCR/“new scan code ready” flag in the keyboard group.

Interrupt sources & ACK. The dispatcher polls the 84+ USB-interrupt ports first, then the two crystal hardware timers: it reads port 0x37 (crystal timer 3 status — IN A,(0x37) @ ram:012D, BIT 1,A @ ram:012F) and port 0x31 (crystal timer 1 status — IN @ ram:013B, BIT 1 @ ram:013D). (Per the WikiTI port map the three crystal timers are 0x30–0x32 = timer 1, 0x33–0x35 = timer 2, 0x36–0x38 = timer 3.) Each maskable interrupt is ACKed by rewriting the int-mask port 0x03. The common exit (ram:00E4) writes 0x0B, or 0x0F when (IY+0x16) bit0 is set (the second value re-enables the on-key + both timer sources at the higher CPU speed). The master ACK at ram:00DC writes 0x08 then the saved mask. Port 0x04 is loaded with 6 (ram:09B5/ram:0C8C) to re-arm the legacy 83+ timer line.

APD (auto-power-down) countdown. APD is a software down-counter decremented by the timer interrupt:

  • apdSubTimer (0x8448) / apdTimer (0x8449) hold the APD down-counter. _ApdSetup (00:03AE) writes the reload constant 0x74 to 0x8449 (apdTimer). The per-tick decrement is in page 0 at ram:036C (LD HL,0x8448; DEC (HL); RET NZ; INC HL; DEC (HL) — decrement apdSubTimer, and on its underflow apdTimer); a key press reloads the counter, so any key keeps the calc awake. The crystal-timer interrupt rate remains unreadable from this DB: when a timer-status bit is set the ISR dispatches to an unanalyzed handler — 35:4792 (via the ram:3FB1 bjump) for timer 3 / port 0x37, and 33:5EB4 (via ram:3FB7) for timer 1 / port 0x31 — so the wall-clock timeout cannot be derived from this DB. By the standard 83+/84+ design this yields the documented ~2–5 minute idle power-down. [reload constant + counter addresses + decrement site ram:036C confirmed; tick rate / absolute seconds [hypothesis]]
  • The unrelated indicCounter/indicBusy pair (0x8476/0x8477) drives the run indicator (the moving-dashes busy spinner), not APD: _RunIndicOn (01:6518) seeds it (0x8477 = 0xF0, 0x8476 = 1) and the tick routine at ram:027B (dec_apd_timer in the symbol map, though it decrements the run-indicator counter) does DEC (0x8476); RET NZ each timer tick, advancing the spinner via the expiry path (ram:3FE1) only on underflow. [confirmed]

Cursor blink cadence. The blink is the same kind of software down-counter, driven off the same timer interrupt:

  • 0x844A (curTime) is the blink down-counter; _CursorOn/_CursorOff (06:7D34/06:7C5F) reload it with 0x32 (50) (LD A,0x32; LD (0x844A),A).
  • The ISR tests curAble (IY+0x0C bit 2) at ram:019B and, if enabled, calls the cursor tick through the ram:3FCF bjump to 06:7C45, which decrements 0x844A; on underflow it toggles curFlags (IY+0x0C) bit 3 (curOn) to flip the glyph and reloads 0x32. So the cadence is “toggle every 50 timer ticks”; only the absolute rate (the crystal-timer tick frequency) is ungrounded, which by the WikiTI-documented rate is ≈ 2 blinks/second. [reload value 0x32, counter 0x844A, and tick site 06:7C45 confirmed; Hz [hypothesis]]

Notable details

  • This OS keys off the 84+ USB-interrupt ports 0x55/0x56 as the primary interrupt-state source, rather than the classic 0x03/0x04. Port 0x55 is the USB Interrupt State (read; (v^0xFF)&0x1F masks the active sources) and 0x56 is USB Line Events (read-only) — both are read sources, not a status/mask pair, despite the dispatch role. The legacy mask 0x03 is still written to ACK. [confirmed in code; ports per WikiTI]
  • The ISR is where APD (auto power down) and the blinking cursor timing originate — both are software down-counters (apdTimer 0x8449/curTime 0x844A) ticked by the crystal timers (ports 0x37/0x31). The separate run-indicator spinner uses indicCounter/indicBusy (0x8476/0x8477), seeded by _RunIndicOn. [confirmed]
  • _GetCSC (00:04B2) cooperates with the ISR: the ISR (or keypad path) updates kbdScanCode; _GetCSC atomically reads and clears it with interrupts masked, also clearing (IY+0) bit3. [confirmed]

Timer tick rate (ungrounded)

  • The crystal-timer tick period (and therefore the APD timeout in seconds and cursor blink in Hz) depends on the unanalyzed timer-status handlers 35:4792 (timer 3 / port 0x37, via ram:3FB1) and 33:5EB4 (timer 1 / port 0x31, via ram:3FB7), which are data in this DB, so the absolute tick rate is not derivable here. The reload constants (apdTimer 0x8449 = 0x74, curTime 0x844A = 0x32), the counter addresses, and the page-0 / page-06 decrement sites (ram:036C, 06:7C45) are confirmed; only the absolute tick rate is ungrounded. [hypothesis]

11 — Boot, contexts, and error handling

Three cross-cutting mechanisms that tie the OS together: how it starts, how it switches “modes” (contexts), and how it aborts on error.

Boot [confirmed, partial]

0000 reset:  IN A,(2); AND 0x80; JP 0x028C   ; test port 2 bit7, go to boot continuation
028c:        port_mapBankA = 0x1F             ; bank a flash page into 4000
             (cond) DAT_io_000E = 3; port_mapBankA = 0x7F   ; configure RAM/exec paging (port 0x0E)
             port_memMapMode = 7              ; OUT (4): memory-map mode 1 + slow timer rate
             JP 0x812C                        ; into the boot page (3F:412C) — BootFree substitute here; retail boot in D84PBE1.8Xv

Boot configures the paging hardware (OUT (6),0x7F selects flash page 3F, the boot page) and the memory-map/timer mode (OUT (4)), then ends JP 0x812c into the banked window where page 3F sits — 3F:412C. In this BootFree rom.bin page 3F is a substitute placeholder (3F:412C re-maps paging rather than running the retail sequence); the retail boot page carries the real continuation (D84PBE1.8Xv / ti84plus_patched.rom, 3F:412C = 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).

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 clear / re-init (ram_reset_wiperam: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 12-memory-management. The ram:3EC1 continuation (VAT/sysflag/LCD bring-up) is page-0 kernel code and is statically present (ram:3EC1 = CALL 0x2B09; …). Residual: JP 0x812c targets the boot page (3F:412C); this rom.bin carries a BootFree substitute there, with the retail boot code in D84PBE1.8Xv.

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 — how the OS implements “modes”/apps [confirmed — key concept]

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]

This is the backbone of the UI: 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, from ti83plus.inc + xrefs]

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   ; run handler on its page, control returns here
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.

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.

Error-message table [local data-table trace]

The error screen shows ERR:<MESSAGE> (the ERR: prefix is on 01:4008). A local data-table trace shows the handler masking the code (AND 0x7F), then for codes below 0x3A indexing a little-endian pointer table at 07:6ACC by (code) − 1 (LD HL,0x6ACC; ADD HL,DE; ADD HL,DE; CALL _LdHLind) to fetch each message pointer; the message strings themselves sit consecutively from 07:6B3C as null-terminated text. Codes ≥ 0x3A (and the special-cased 0x36/0x37/0x39) bypass the table and fall back to the ? message at 07:6C5A. The current MCP function/xref view does not prove this data-only table directly, so treat the addresses as a data trace rather than live function symbols:

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, … LINK (6C55)

The Code column is each error’s low 7 bits. Re-editable errors set the E_EDIT (0x80) bit on top — E_Overflow equ 1+E_EDIT, E_DivBy0 equ 2+E_EDIT, … — while non-editable ones (E_Label equ 20, E_Stat equ 21, …) carry no such bit. The handler masks the code (AND 0x7F) before indexing. So the whole error pathway is: a routine _JErrors a code → the handler restores SP from errSP → masks the code and looks up the message here → renders ERR:<msg>.

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. Reset (ram:0000) → 028c paging setup → JP 0x812c (boot page 3F:412C — BootFree substitute in this rom.bin; retail boot in D84PBE1.8Xv). 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 12-memory-management. See RAM clear / re-init.
  • Flash write/erase sector primitives. Page-3D anchors include flash_program_buf 3D:678C, the per-record status writers 3D:7C8F/7C93/7C97, and flash_erase_wait 3D:5ED3, with byte-poke loops copied to ramCode 0x8100. The candidate labels flash_program_core 3D:61AF and flash_write_record 3D:64AA are not defined functions in the disassembly; both names are project-local inferred labels, not WikiTI or ti83plus.inc equates. The public single-byte flash writers are _WriteAByte (bcall 0x8021) and _WriteAByteSafe (bcall 0x80C6) in ti83plus.inc; these name the public entry points, whose bodies are likewise not defined functions in the disassembly. See sub-vat-archive §6.

Residual: JP 0x812c targets the boot page (3F:412C); page 3F is a BootFree substitute in this rom.bin, with the retail boot code in D84PBE1.8Xv. The ram:3EC1 init continuation is page-0 kernel code and is statically present.

12 — Memory management (RAM heap & Flash archive)

How the OS allocates the ~24 KiB of user RAM between variables, temporaries, the FP stack, and the program being run — and how it offloads variables to Flash (“archive”).

The RAM heap [confirmed pointers, standard layout]

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 05-variables-vat.md.

Boundary/work pointers (clustered at 0x9820–0x983A) [confirmed addrs]:

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) (the pointers at 0x9828/0x9824) + 1, i.e. the span between the floating-point stack and the operand/symbol stack in the middle of the region (the conceptual picture above: user data grows up, the VAT grows down, free RAM in the middle). When a variable grows/shrinks, everything above it shifts.

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, etc. (see 05) — share a create body (_CreateReal at ram:10B8 jumps into ram:1011) that carves space via an internal gap routine at ram:0F0C — which does its own block move and updates the temp/FP-stack pointers, not the public _InsertMem — then registers the variable in the VAT.

Flash archive [confirmed location]

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 can’t be overwritten in place, so deleting one only marks it dead. When the archive Flash fills, a garbage collector rewrites the live vars to fresh sectors and erases the old ones — the Garbage Collecting screen (stored as two ROM strings, "Garbage"/"Collecting...", with three ASCII dots). That GC path is distinct from _CleanAll, but the older flash_gc_relocate@3C:7BD0 / gc_show_screen@3C:7E0D labels are not present as functions in the current live Ghidra/MCP DB.

_CleanAll is RAM cleanup (not Flash GC) [confirmed from disassembly]: _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 sector at a time but programmed byte-by-byte (the page-3D writer at 3D:64AA calls the single-byte bcall _WriteAByte, id 8021), via low-level routines through the flash-control port 0x14 (see Variables, Archive & Unarchive) [partly confirmed]:

  • Confirmed page-3D anchors include _FlashToRam (3D:6745), flash_program_buf (3D:678C), flash_erase_wait (3D:5ED3), flash_cmd_base (3D:738B), and the status-bit helpers flash_op_fd (3D:7C8F), flash_op_fb (3D:7C93), flash_op_fe (3D:7C97).
  • Further page-3D flash routines at 3D:61AF, 3D:6B9B, 3D:64AA, 3D:62C2, and 3D:6413 are unnamed in the disassembly.
  • 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 is byte-verified in Variables, Archive & Unarchive. Flash write/erase and GC routines on page 3D are partly unnamed [hypothesis].

Variables, archive & unarchive

TI-84 Plus OS 2.55MP — feature deep dive.

Deep-dive companion to 05-variables-vat.md and 12-memory-management.md, focused on what a program that manages memory touches: the VAT walk (_FindSym), variable Store/Recall, and the Archive / UnArchive path (RAM ↔ Flash), the Flash garbage collector, and the memory checks.

Every address here is read from the raw Z80 disassembly rather than the decompiler alone, which mis-renders the SET b,(IY+d) flag ops and the cross-page CALL 0x2b09-style trampolines. Page numbers are the masked flash page (rawpage & 0x3F); cross-page trampolines store lo hi rawpage in the 3 bytes after the CALL.


1. The arcInfo workspace and key RAM pointers [confirmed]

The archive engine keeps a 12-byte scratch block, labelled arcInfo (83EEh) in ti83plus.inc, plus a saved copy savedArcInfo (8406h). _Arc_Unarc’s reentrant inner mover at 07:61DC copies the 12 bytes starting at 83F1 (the vatPtr field onward, not the whole 83EE block) into 8406 (LD HL,83F1 / LD DE,8406 / LD BC,0C / LDIR); the matching 07:61E8 restore candidate is an inferred label, not byte-confirmed in the disassembly.

AddrField (this doc’s name)Meaning
83EEarcInfo.pagepage byte of the data (Flash page if archived; RAM marker otherwise)
83EFarcInfo.dataPtr2-byte data address (in Flash window 0x4000–0x7FFF, or RAM)
83F1arcInfo.vatPtrpointer to the VAT entry’s type byte (the symbol record)
83F3arcInfo.destPtrdestination data pointer (RAM target on unarchive)
83F5arcInfo.dataSizea header/record-size component (loaded from BC after CALL 0FDE)
83F7arcInfo.sizethe variable’s data byte count (from _DataSize; 614B does CALL 1485LD (83F7),DE)
83F9arcInfo.sizeFullsize + header overhead
8406savedArcInfo12-byte save slot for nested calls

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


2. _FindSym and the VAT walk [confirmed]

_FindSym (00:0E65, = RST 10h) is a page-0 trampoline that cross-page-jumps to the real scanner findsym_scan @ 07:565F. _ChkFindSym (00:0E60) first type-checks OP1 (_CkOP1Real) then falls into FindSym.

The scanner keys off OP1 at 8478: OP1.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 (single-char entries) / -= (6+nameLen) 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 05-variables-vat.md):

  • single-char (real/cplx/Ln/[A]/sysvars): high-address-first name token, page byte, data pointer, and type byte; findsym_scan reads these as page at name+1, data pointer at name+2/+3, and type at name+6.
  • named (prog/appvar/group/str/equ): high-address-first name bytes/length plus the same page/data/type fields; 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.


3. Store / Recall [standard]

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 in §4. [hypothesis]

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

  • _RclVarSym calls RST 10h (17A6, a _FindSym+error-check wrapper: RST 10h; JP C,271D), then checks the name token (8479). For a list recall (63/2A) it sizes the data with _DataSize (00:1485) and copies it into a work buffer (91E0), using _LdHLind and cross-page helpers; ends 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.
  • The recall code does not care whether the source is RAM or Flash for reading — Flash is memory-mapped read-only into the 0x4000 window. To use an archived program/var that must be modified or executed in RAM, the OS first copies it via _FlashToRam (§5). [standard]

4. Archive / unarchive — _Arc_Unarc (07:6248) [confirmed]

The headline. 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⇒not allowed → JP 26E0 (local error shim; LD A,0xB2 = E_Variable, ERR:VARIABLE → _JError)
  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 = page byte: 0 ⇒ currently in RAM, else ⇒ in Flash
      (Flash, B≠0) LD A,(HL); CP 0x17 ; Group? ⇒ JP 26E0 reject  [groups archive via a different path]
                   CALL 61F4   ; *** Flash → RAM:  unarchive ***
   6272 (RAM, B==0):  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.

4a. RAM → Flash (archive), 6107 [confirmed]

6107:  CALL 7866 ; DI
       CALL 614B                       ; size/accounting: (83F1)=vatPtr, _DataSize→83F7;
                                       ;   616C reserves the archive-Flash slot
       CALL 2FF1 (cross_page 3D:64AA)  ; *** program the data into the archive Flash ***  (see §6)
       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 2FDF(3D:61AF) / 2FF7(3D:62C2)

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). 3D:64AA is the Flash writer that lays down a fresh archived record plus a copy of the symbol header/name and data (status marker bytes — 0xFE=in-progress / 0xFC=valid / 0xF0=deleted, with 0xFF=erased/empty; the bit-clearing mechanism is confirmed in §6a). 3D:64AA is an inferred label, not byte-confirmed in the disassembly; the flash_write_record name for it is a project-local inferred label, not a WikiTI or ti83plus.inc equate. _Chk_Batt_Low (00:0D07) gates the Flash write — archiving aborts on low battery (61C5: CALL _Chk_Batt_Low).

4b. Flash → RAM (unarchive), 61F4 [confirmed]

61F4:  LD (83EF),DE ; LD (83EE),A      ; arcInfo.dataPtr/page = source (Flash page+addr from FindSym)
       CALL 6335                       ; 6331/6335: stash vatPtr (83F1), compute dataSize (83F5) via _DataSize
       CALL 32D3                       ; size accounting
       LD A,(HL) ; CALL 146C           ; add header overhead → 83F9 (sizeFull)
       EX DE,HL ; CALL _EnoughMem(0FA6); ensure there is RAM room for the unarchived copy
                JP C,_ErrMemory(2721)
       OR 1 ; CALL 0F0C                ; carve the RAM gap (internal create-gap routine)
       LD (83F3),DE                    ; destPtr = new RAM address
       CALL 3003 (cross_page 3D:6440)  ; *** page-3D unarchive worker: copy Flash→RAM, retire the 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). 3D:6440 shares the page-3D flash-control prologue (OUT (0x14)) and is an inferred label, not byte-confirmed in the disassembly.

4c. Errors raised on the path [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.

5. Reading archived data — _FlashToRam (3D:6745) [confirmed]

bcall(_FlashToRam) (id 0x5017 → real 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     ; bank A = the source Flash page into 0x4000 window
   loop LDI:  BIT 7,H → at 0x8000 wrap: IN A,(6); INC A; OUT (6),A; LD HL,0x4000  ; next page

Port 6 is the bank-A page-select; the read code itself runs from ramCode (0x8100). This is how an archived program/appvar is pulled back into RAM to be executed or edited. ti83plus.inc also names a sibling _FlashToRam2 (id 8054); the retail boot table maps it to 3F:4888.


6. Low-level Flash write / erase (pages 3C/3D, port 0x14) [mixed]

The Flash program/erase primitives live on flash pages 0x3C / 0x3D and are invoked through page-0 cross-page trampolines. The public bcall entry points for the byte writer are named in ti83plus.inc: _WriteAByte (id 8021) and _WriteAByteSafe (id 80C6) program a single Flash byte; _FlashToRam2 (id 8054) is the companion Flash→RAM copy of _FlashToRam (§5). The retail boot table maps those APIs to boot-page bodies (_WriteAByte3F:4C9F, _WriteAByteSafe3F:4C9A, _FlashToRam23F:4888), which then wrap the lower-level page-3C/3D flash machinery below. Several page-3C/3D targets below are reached by byte trace but are inferred labels, not byte-confirmed in the disassembly; their flash_* names are project-local inferred labels (not WikiTI or ti83plus.inc equates):

Trampoline (RAM)→ page:addrRole
00:2FF13D:64AA (inferred label)Flash program record candidate
00:2FDF3D:61AF (inferred label)Flash program/erase core candidate
00:2FF73D:62C2 (inferred label)Flash free-sector scan / allocate candidate
00:2FC13C:580EFlash command/menu entry
00:2FFD3C:7121 (inferred label)Flash command dispatcher candidate
00:32A905:4A6Ecomplex-list special-case helper

The program-core candidates 3D:64AA and 3D:6440 share this unlock prologue (3D:61AF starts differently — PUSH AF; PUSH HL; BIT 6,(IY+0x24)):

RES 7,(IY+0x24) ; LD A,1 ; DI ; IM 1 ; DI ; OUT (0x14),A ; DI ; CALL FUN_ram_02bf

OUT (0x14),A toggles the Flash control port (0x14) to enable write/erase; FUN_ram_02bf sets up the RAM-resident write stub (the actual byte-poke loops run from RAM at 0x8100/ramCode, because the CPU cannot fetch from a Flash chip mid-erase). 3D:6B9B/3D:6B6D are bounds-checked byte-program candidate calls (return carry → caller raises E_ArchFull); neither is byte-confirmed in the disassembly. The public byte-write API for this layer is _WriteAByte (8021) / _WriteAByteSafe (80C6), which resolve to boot-page wrappers. The free-slot scan reads sector status bytes and sums free space to decide whether a GC is needed before a write.

6a. Record-status byte — the one-way bit-clearing scheme [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/GC path (§7), 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.)

6b. Archive sector map / erase-block granularity [confirmed]

The physical Flash pages that form the archive pool are model-selected at runtime from the two model bits — port 2 bit 7 (probe_hw_model_keep_a 00:1837) and port 0x21 low bits (probe_port21_keep_a 00:182F):

Model testArchive base page (flash_page_select 3D:726E)Archive top page (flash_cmd_base 3D:738B)Page mask
port 2 bit 7 clear (1 MB)0x150x1EAND 0x1F (32 pages)
port 0x21 == 0 (mid)0x290x3EAND 0x3F (64 pages)
else (2 MB)0x690x7Eno mask (full 8-bit page; 3D:6745 skips both ANDs)

So on a 1 MB TI-84 Plus the user archive occupies roughly raw pages 0x15…0x1E, and the OS pages it into the 0x4000 window one 16 KB page at a time for both reading (_FlashToRam, masks 0x1F/0x3F via the same model check, 3D:6745) and erasing. flash_set_sector_cnt (3D:727D) loads (base+1) into the sector counter 0x82A3; the erase routine flash_erase_wait (3D:5ED3, whose loop jumps to 3D:5EF13D:5EE3 is the unrelated _FindApp) pages each sector to 0x4000 and issues the chip erase command via RST 0x28, decrementing 0x82A3 down toward the base page. The underlying Am29F-class chip uses 64 KB physical sectors (= 4 × 16 KB OS pages); the OS walks/erases at 16 KB page granularity. [64 KB physical-sector figure: hypothesis]


7. Flash garbage collector — “Garbage Collecting…” [mixed]

Distinct from _CleanAll (RAM/FP-stack cleanup, 07:52CF). When the archive Flash fills, dead (unarchived/deleted) records must be reclaimed by rewriting the live records to fresh sectors and erasing the old ones.

  • The on-screen prompt string "Garbage\0Collecting...\0" is at 01:4126; "Defragmenting...\0" at 01:4076. The display front-end candidate 3C:7E0D (LD HL,0x4126 ... CALL 3E85) is an inferred label, not byte-confirmed in the disassembly.
  • GC is driven from the command dispatcher candidate 3C:7121: 3C:71F9 = “show GC screen + relocate” (CALL 7E0D then CALL 7219 then CALL 7733), 3C:720D = relocate-only, and the archive-full auto-GC 3C:7204 runs 71FC (GC) then retries the write at 7F1C. 3C:7121 is an inferred label, not byte-confirmed in the disassembly.
  • The relocation/erase-core candidate 3C:7BD0–7BF4: tests a status flag, 7E6B/7C10 prepare the swap sectors (writes 0xF0 marker, sets 97A6 sector counter, 8477), 7BE3:CALL 7E0D shows the banner, 7C1F walks live VAT/Flash entries copying each valid (0xFC-marked) record to the new sector, and 7C04 finalizes (erases the old sectors, SET 2,(IY+0x25)). [standard] 3C:7BD0 is an inferred label, not byte-confirmed in the disassembly; the flash_gc_relocate/gc_show_screen names are project-local inferred labels, not WikiTI or ti83plus.inc equates.
  • GC is callable from the user catalog (Archive/the MEM menu “Garbage Collect?” — string at 01:76C9).

So: archive = append to Flash; delete/unarchive = mark dead; when Flash fills, GC compacts. Exactly the classic TI-83+/84+ behaviour, now pinned to addresses.


8. 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 (Flash) is computed inside the Flash layer. The free-space sum is at 3D:6413 and the catalog “MEM” read path runs through 3C:7121. Neither address is byte-confirmed in the disassembly.

9. Confident address 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 12-byte arcInfo into savedArcInfo; 07:61E8 (restore candidate) is an inferred label, not byte-confirmed in the disassembly
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:5D07_RclVarPushrecall var, push to FPS
3D:6745_FlashToRamcopy archived data Flash→RAM (page-aware); ti83plus.inc sibling _FlashToRam2 (id 8054) is named but its body is unmapped in the disassembly
3D:678Cflash_program_buflive-MCP Flash programming/buffer helper
3D:64AAflash_write_record (inferred label)program an archived record to Flash candidate; not byte-confirmed in the disassembly
3D:61AFflash_program_core (inferred label)Flash program/erase core candidate; not byte-confirmed in the disassembly
3D:62C2flash_alloc_sector (inferred label)scan/allocate next free archive sector candidate; not byte-confirmed in the disassembly
3D:6413flash_free_scan (inferred label)sum free archive space / decide GC candidate; not byte-confirmed in the disassembly
3D:726Eflash_page_selectarchive base page by model (0x15/0x29/0x69)
3D:738Bflash_cmd_basearchive top page by model (0x1E/0x3E/0x7E)
3D:727Dflash_set_sector_cntshared page counter 0x82A3 = base+1
3D:5ED3flash_erase_waiterase a 16 KB archive page, wait for completion
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:6B9Bflash_write_byte (inferred label)bounds-checked Flash byte program candidate; not byte-confirmed in the disassembly. Public byte-write bcalls _WriteAByte (8021) / _WriteAByteSafe (80C6) are named in ti83plus.inc, but the 0x8xxx table does not yet map either ID to this body
3C:7121flash_cmd_dispatch (inferred label)Archive/UnArchive/GC command dispatcher candidate; not byte-confirmed in the disassembly
3C:7BD0flash_gc_relocate (inferred label)GC core candidate; not byte-confirmed in the disassembly
3C:7E0Dgc_show_screen (inferred label)“Garbage Collecting…” display front-end candidate; not byte-confirmed in the disassembly
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.

10. Summary & open items

  • Sector map / erase-block — [confirmed], see §6b. The archive pool is model-selected: base page 0x15/0x29/0x69 (flash_page_select 3D:726E) up to top page 0x1E/0x3E/0x7E (flash_cmd_base 3D:738B); on a 1 MB TI-84 Plus that is raw pages ~0x15…0x1E. The OS pages the region into the 0x4000 window and erases one 16 KB page at a time (flash_erase_wait 3D:5ED3, sector counter 0x82A3 from flash_set_sector_cnt 3D:727D); the physical chip sector is 64 KB = 4 OS pages [hypothesis].

  • Record-status bytes — [confirmed], see §6a. Monotonic bit-clear: 0xFF erased → 0xFE in-progress → 0xFC valid via flash_op_fe/fd/fb (3D:7C97/7C8F/7C93) AND-masking; 0xF0 deleted is a direct write in the delete/GC path the status byte; flash_find_nonff (3D:7DEA) treats an all-0xFF header as free.

  • Lower-level flash helper bodies — address-keyed labels still inferred [hypothesis]. The public bcall entry points are canonical equates in ti83plus.inc and now resolve through the retail boot table: _WriteAByte (8021) → 3F:4C9F, _WriteAByteSafe (80C6) → 3F:4C9A, and _FlashToRam2 (8054) → 3F:4888. The address-keyed flash_* labels in §6/§9 stay inferred and body-undisassembled until the lower-level page-3C/3D helper graph is split cleanly.

  • Group archive path — partially pinned [hypothesis]. _DataSize (00:1485) confirms a Group (type 0x17, like AppVar 0x15/0x16) carries a leading word-size header, so a group can be stored as one Flash blob. In _Arc_Unarc the CP 0x1726E0 reject sits on the B≠0 (in-Flash) branch, immediately before the unarchive worker 61F4 — so an archived group is not unarchived through 61F4, and groups are handled by a separate routine that walks the group’s member list. That member-walk routine remains unidentified in the disassembly — _Arc_Unarc’s body past the entry CALL is not disassembled here (cross-page CALL flagged non-returning), and no group-archive function is named or xref-reachable. Confirming it would need a linear disassembly pass like the one behind §4.

Apps, memory reset & settings

TI-84 Plus OS 2.55MP — feature deep dive.

How the student-facing parts of the OS work: launching Flash Apps, the MEM → Reset menu (what “RAM Cleared” erases), and the MODE screen format/mode flags. Addresses are space:addr where ram/page_00=0000-3FFF, flash pages mapped at 4000-7FFF. Confidence flags follow conventions.md: [confirmed] = read from disassembly; [hypothesis] = strong inference, not yet verified (used below for both strongly-inferred claims and partial traces where the code is RAM-resident / cross-page and not fully traced).

Cross-references: doc 11 (contexts, _AppInit, event router), doc 12 (RAM heap, _CleanAll), doc 13 (flash page map). Flag bits use the ti83plus.inc equates; the SystemFlags base is IY = flags = 0x89F0, so e.g. (IY+0x0A) = flags + fmtFlags.


1. Flash Apps — find & 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.

1.1 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, and the low nibble of the second byte encodes the payload length. The decoder bytes are around 3D:7285, but the disassembly does not expose a live function there:

size nibblefield payload size
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 bytes; bytes 1-4 decode as 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 OUT (6),A; RET run from RAM after A has been set to the current bank-A page minus one. 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/app_header_re.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 (bcall 0x80AB) locates a field in an app header, and _FindOSHeaderSubField (bcall 0x8075) does the same for the OS header. Both build on the generic walkers _FindSubField (bcall 0x805D), _FindGroupedField (bcall 0x8030), and _GetFieldSize (bcall 0x805A), which decode the TLV length nibble shown above. These IDs sit in the boot-page bcall range (0x8000+); the 0x8040/0x8070/0x8080 helpers the OS also reaches are a distinct group in the same range and are not these field walkers. The body addresses behind these public entry points are not defined functions in the disassembly.

1.2 _FindApp / _FindAppUp / _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            ; flash_set_sector_cnt -> 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.
  • flash_set_sector_cnt (3D:727D → helper 726E) — initializes 0x82A3 to the model-selected page base plus one.
  • _FindAppUp (5DDA) / _FindAppDn (5DE6) — enumerate the previous / next app in flash (for the APPS-menu list), both wrapping the common walker _app_5de7 (5DE7). _app_5de7 keeps two counts in BC (apps before/after) and tracks the current name in OP3.
  • _FindAppNumPages is present in the bcall table (3D:4AA3), but the disassembly has no function record at that address.

State variables: appSearchPage = 0x82A3, 0x8497/0x8481/0x9C87 are search-mode scratch (0x9C87=‘i’ selects the in-RAM “temp app” search variant).

1.3 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 doc 11 §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, bcall 0x4C36) 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)doc 11).

Because cxCurApp (0x859A) is a key code, pressing a mode key selects the context to load (doc 11). The App quit restore-path candidate at 3B:7412 is not a defined function in the disassembly; the saved-context restore behavior stands as a byte-trace note (the label is project-local, not a WikiTI or ti83plus.inc equate).


2. RAM clearing / 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.

2.1 The user-facing strings (page 0x01) [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:7425..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

2.2 The reset dispatcher (mem_reset_dispatch @ 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)

2.3 What “RAM Cleared” (RAM reset) zeroes [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 0x8000–0x9BC3 (~7 KiB: OS scratch, the Context block, system buffers).
  2. User RAM 0x9BD0–0xFFFF (0x6430 = 25648 bytes, ~25 KiB: the VAT and all user variables/programs).

A handful of flag bits are explicitly preserved across the wipe (IY+0x3F bit7, IY+0x34 bit6, IY+0x35 bits0/1, and the word at 0x9B73) so the calculator knows it is mid-reset. 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 doc 11), 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.

2.4 Full reset (page_0/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.

2.5 _CleanAll / cleanup_temp_ram (07:52CF) — not a reset [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 doc 12). _FixTempCnt (07:4FEC) marks temps ≥ a count reclaimable then tail-calls the same compaction.

2.6 Flash archive GC — “Defragmenting…” / “Garbage Collecting…” [confirmed behavior; display-label addresses undisassembled]

Separate from RAM reset: when the Flash archive fills, the OS rewrites live archived vars to fresh sectors and erases the old ones. The display dispatcher sits around 3C:7E23 (shows Defragmenting... 0x4076) / 7E10/7E1C (shows Garbage Collecting... 0x4126+412E); 3C:7E00 is not a defined function in the disassembly (the label is project-local, not a WikiTI or ti83plus.inc equate). It clears 0x844B (curRow, the text-row cursor — reset before the banner draws) and runs with the screen frozen (DI). The actual sector erase/write primitives are RAM-resident (flash control port 0x14) — see doc 12.


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

3.1 Angle: Degree vs Radian — 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).

3.2 Graph type: Func / Param / Polar / Seq — 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).

3.3 Numeric format: Normal/Sci/Eng, Float/Fix, base — fmtFlags (IY+0x0A) [confirmed (bits from inc)]

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.

3.4 MODE screen plumbing

The MODE screen is a menu context (cxMode/kMode=0x45) reached via the event/key router (doc 11). Its row strings live as token names on page 0x01 (RadianN/DegreeO/NormalP/ Float at 01:49E4..4A06; trailing letters are token-id bytes) and full-caps menu labels on page 0x37 (DEGREE 4A85, RADIAN 4A8C). Selecting a row writes the flag bits documented above directly (SET/RES (IY+…), or stores into fmtDigits). [hypothesis] (partial) — the per-row write table itself is reached through the menu dispatcher and was not traced line-by-line, but every target bit/byte is confirmed from the setters and inc equates.


4. Confident space:addr index

3D:5EE3   _FindApp
3D:5DDA   _FindAppUp
3D:5DE6   _FindAppDn
3D:5DE7   _app_5de7
3D:5FB1   app_find_next_page
3D:727D   flash_set_sector_cnt
3D:7285   TLV-length candidate (inferred label); no defined function in live DB
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 0x8000-0x9BC3 and 0x9BD0-0xFFFF)
ram:0BD9       ram_init_after_reset
ram:0B27       full_reset_wipe        (zeroes all 0x8000-0xFFFF)
3C:7E00   archive-GC-display candidate (inferred label); no defined function in live DB
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 / 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, doc 11)

13 — Flash page map

What lives on each of the 64 physical flash pages (16 KiB each). OS code occupies pages 00–07 and 33–3D; pages 08–32 are blank in this image; page 3E is the certificate sector and 3F the boot page. On a retail unit the upper pages can also carry Flash Apps, but this dump is OS-only — the page scan below reports zero Flash-App headers. The page roles below are characterized by the named bcall routines that resolve to each page (tools/bcall_targets.txt) plus function counts; the 0x8xxx cert/boot/USB bcalls come instead from ti83plus.inc and the retail segment files (bcall_targets.txt does not carry the 0x8xxx targets).

OS pages (carry bcall entry points)

PageFuncsRoleRepresentative routines
00928Kernel — mapped at 0000; 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_SetXXOP1, _UCLineS (window↔pixel transforms)
3624Mode setters (Func/Param/Polar/Seq)_SetFuncM, _SetParM, _SetPolM, _SetSeqM
3723Graph coord convert_XftoI, _YftoI
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
3416Crystal timers / clock, token scan_CrystalTimerA, timer_scan_tbl
356Memory-reset engine, factorialmem_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

Page byte-scan notes (empty range, boot & 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
08–32Blank/unused in this OS image — 100% 0xFF in tools/rom.bin. Page 2F is the retail USB boot support page, but its code lives in the separate D84PBE2.8Xv segment (not in rom.bin); the retail page-3F boot table maps the 0x8xxx USB bcalls (_AttemptUSBOSReceive, _ReceiveOS_USB, _USBErrorCleanup, _InitUSB, _KillUSB) into page 2F. No app headers.
34–39More OS code (graph/mode/menu/timers); fill 0.2–17% 0xFF.
3Bbcall jump table — starts 99 27 00 = entry 0 (_JErrorNoram:2799).
3CLink code + the OS version string — page starts with ASCII 32 2E 35 35 4D 50 = "2.55MP".
3ECertification page — the per-calculator certificate sector (84+ cert page is 3E, not 3F). Blank (99% 0xFF) in this OS-only image, since the cert is written per-device. The OS reads this sector through the ti83plus.inc cert bcalls: _GetCertificateStart (bcall 0x8057) and _GetCertificateEnd (bcall 0x802D) bound the sector, and _FindFirstCertField (bcall 0x8027) / _FindNextCertField (bcall 0x8078) walk its TLV fields.
3FRetail boot page — supplied by local D84PBE1.8Xv; 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/hardware-version bcalls now resolve to _getBootVer 3F:477C (0x80B7) and _getHardwareVersion 3F:4781 (0x80BA).

The large-font glyph table is on page 0x07 (see Display / LCD). Alternate large fonts live on pages 1 and 0x36 (selected by (IY+0x35) bits 5/1). Page 7 is the busiest data page (archive code, list/matrix, error messages, and the large font). [confirmed]

Takeaway

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.

14 — RAM pages

The TI-84 Plus has banked RAM pages behind the Z80’s 16 KiB windows. TI-OS normally keeps RAM page 81 in 8000-BFFF and RAM page 80 in C000-FFFF, but ROM helpers temporarily map other pages for paged memory access. On OS 2.55MP, traces show page 83 writes during boot/home initialization and during homescreen expression entry. Programs that borrow page 83 must preserve or restore the OS-visible regions below.

Page selectors [confirmed]

The 84+ memory ports use two encodings:

WindowPortSelector encodingNormal TI-OS value
4000-7FFF6bit 7 clear selects Flash page value & 0x3F; bit 7 set selects RAM page 0x80 | (value & 7)banked Flash page
8000-BFFF7bit 7 clear selects Flash page value & 0x3F; bit 7 set selects RAM page 0x80 | (value & 7)81
C000-FFFF5low three bits select RAM page 0x80 | (value & 7)00 → RAM page 80

This rule matches the dynamic resolver, TilEm’s x4 memory mapper, and the OS trace. In the idle boot/home trace, the RAM-window writes are:

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 final restore values are therefore port 7 = 0x81 and port 5 = 0x00 for normal OS execution.

Page map [standard, cross-checked]

WikiTI’s RAM pages page is a useful public map, but OS 2.55MP needs the page-83 warnings to be read literally. The local dump at wikiti-dump/main/83Plus:OS:Ram Pages.wiki carries the same current page-83 notes. The local trace and disassembly support this table:

RAM pageUse
80Normal C000-FFFF RAM page. The boot/home trace restores this with OUT (5),0. WikiTI marks it execution-protected.
81Normal 8000-BFFF RAM page. This contains the visible TI-OS RAM variables, OP registers, flags, graph buffers, user heap, and VAT window documented in tools/ram.txt and ti83plus.inc.
82Not a general OS work page under normal execution. The idle trace maps it briefly through port 5 as part of a paged RAM helper, then restores page 80. WikiTI marks it execution-protected.
83Shared OS scratch/state page. OS 2.55MP maps it through port 6 for block copies and LCD capture, and through port 7 for a paged byte-store helper. Homescreen expression entry writes the previous-entry buffer at 577E.
84Not used by TI-OS under typical execution; WikiTI marks it execution-protected.
85Not used by TI-OS under typical execution on full-RAM hardware.
86Not used by TI-OS under typical execution; WikiTI marks it execution-protected.
87Not used by TI-OS under typical execution on full-RAM hardware.

On newer 48 KiB hardware, WikiTI says RAM pages 82-87 alias the same physical memory. WikiTI’s port 15 page identifies ASIC value 55h as the 48 KiB TA1 ASIC. Programs that use page 83 must not treat 82-87 as independent storage on that hardware. [standard]

Per-page trace coverage [confirmed]

The boot/home and 2+3 ENTER traces exercise startup, homescreen initialization, display capture, parsing, evaluation, and previous-entry storage. They do not exercise app launch, USB transfer, graph drawing, archive cleanup, or a 48 KiB ASIC. Within that scope, physical RAM-page writes are:

RAM pageIdle trace writes2+3 ENTER trace writesInterpretation
80256227 writes, all page addresses touched345702 writes, all page addresses touchedNormal high RAM page selected by port 5; contains stack/system/user RAM activity in the C000-FFFF window.
8162947 writes, all page addresses touched72638 writes, all page addresses touchedNormal 8000-BFFF RAM page; contains the documented OS variables, flags, OP registers, heap, VAT window, and working buffers.
82no writes observedno writes observedPort 5 briefly selects raw value 02, but the observed store is through page 83 in bank B. No page-82 storage is confirmed by these traces.
831882 writes to 43D9-44BD and 5A7E-5DF23467 writes to 4373-4390, 43D9-44BD, 577E-5790, and 5A7E-5DF2Shared OS scratch/state page. See the range table below.
84no writes observedno writes observedNo typical-use OS storage confirmed in these traces.
85no writes observedno writes observedNo typical-use OS storage confirmed in these traces.
86no writes observedno writes observedNo typical-use OS storage confirmed in these traces.
87no writes observedno writes observedNo typical-use OS storage confirmed in these traces.

The zero-write rows mean “not hit by these scenarios,” not a global proof that the page is never used. On 48 KiB hardware, pages 82-87 alias page 83, so writes intended for 84-87 would not be independent storage even if a program can select those page numbers. [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 hit pages 82 or 84-87. [confirmed]

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 at ram:1868/ram:187C. [confirmed]
83 split-screen/table copyEnter a split-screen/table workflow that calls _ScreenSplit.Ghidra shows _ScreenSplit 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]
84-87 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. Pages 84–87 are reachable through the helper but are not selected on any observed OS path [hypothesis]. The B = 1 caller pattern is confirmed for the decoded callers above. [confirmed]

Page 83 use [confirmed and 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. The block write is the LDIR at ram:187E in the page-83 copy helper (page 83 mapped via OUT (6),A at ram:187C); the caller is still unlabeled. [confirmed]
43D9-44BDBoot/home page-83 scratch copyPresent in the idle trace. The block write is the LDIR at ram:187E in the page-83 copy helper (page 83 mapped via OUT (6),A at ram:187C), plus one byte stored at 37:44D8. [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, not traced here]
4100-433AUSB communication buffersWikiTI public note; the two traces on this page do not exercise USB transfer. [standard, not traced here]

Ghidra identifies the page-83 block-copy helper at ram:1868. It saves the current port-6 value, writes 0x83 to port 6, runs LDIR, and restores the previous page through the page-set helper:

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, address confirmed]

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
python3 tools/analyze_ram_page_trace.py /tmp/page83-idle.trace --page 0x83
python3 tools/analyze_ram_page_trace.py /tmp/page83-2plus3.trace --page 0x83
python3 tools/analyze_ram_page_trace.py /tmp/page83-graph.trace --page 0x83

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

05 — Variables & 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 — generated tools/ty_vartype.txt, per ti83plus.inc]

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.

How a variable is named/found

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; returns ptr/page (also the RST 10h fast path: vector 00:0010JP 0E65)
_ChkFindSym00:0E60type-classify OP1 (via the helper at ram:2042, which calls _CkOP1Real 00:1942 then checks the findable var classes) then _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.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 from the _Create* family / DB types]

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  rows, cols;  TIFloat elem[/* rows * cols */];  }; /* MatObj 2, column-major; dim0=rows first (byte-confirmed in sub-matrix-list.md; the TIMatrixHdr DB type labels these cols,rows — reversed) */
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
MatObj2rows,cols bytes (dim0=rows) + TIFloat[], column-major (index math in Matrices & 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 12). 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.exp is the name’s token class — _CreateRList validates a list-name token 0x5D/0x24/0x3A/0x72).

The VAT entry [confirmed — byte-verified vs findsym_scan]

The VAT grows downward from symTable (0xFE66); _FindSym (00:0E65findsym_scan 07:565F) scans down, matching the name in OP1. Fixed-token names (reals, complex, L-lists, [A]-matrices, system vars) are matched by a short 1–3 byte compare against OP1’s 0x84790x847B; length-prefixed names (programs, appvars, groups) branch to a separate name scanner at 07:55D1 that compares the full name. On a match it reads the entry’s 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+6type — low 5 bits = TIVarType class, high bits flag archive state; copied to OP1 at 0x8478

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

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

06 — 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 at 0x8478, spaced 11 bytes (OP2=0x8483 …). The extra 2 bytes past the 9-byte number are extended guard digits used during math: OP1EXT/OP2EXT = bytes +9/+10 (seen in _FPAdd as 0x8481/0x8482). OP1 is the primary accumulator; most routines take their argument in OP1 (and OP2 for binary ops) and return in OP1.

Core operations [confirmed from disassembly]

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; 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}

This is the canonical sign-magnitude BCD add. 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/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.exp − OP2.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.

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.

Multiply / divide / transcendentals [confirmed — located]

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 are local page-0x02 code plus page-0x02 coefficient tables. The apparent LD A,n; CALL ram:2362 “page switch” sites are not banked series tails: Ghidra disassembly shows ram:2362: CALL ram:3DD1, and ram:3DD1 is a bcall-table entry whose inline descriptor is 1E 7D 02 (02:7D1E). The real banked-call helper is ram:2B09. ram:2362 fetches the page-0x02 coefficient indexed by A and then multiplies OP1 by it (it enters the _FPMult body at ram:2392). Therefore the preceding LD A,n is a coefficient-table index, not a target flash page. Raw ROM bytes at the supposed same-address page-0x03 targets are 0xFF, and Ghidra has no page-0x03/page-0x06 functions there.

The shared algorithm — digit-by-digit pseudo-division [confirmed]

The forward log and exp evaluators are a digit-by-digit pseudo-division recurrence — the algorithm BCD calculators have used since the 1960s. The table gives it away: 02:7181’s 16 rows are exactly $\log_{10}(1+10^{-k})$ for $k=0\ldots15$ (matched to 14 digits — [00] = log₁₀2, [01] = log₁₀1.1, [02] = log₁₀1.01, …), which is precisely the per-step increment such a recurrence consumes. The recurrence runs on shift-and-add alone: scaling a BCD number by $1+10^{-k}$ is $x + (x\text{ shifted right }k\text{ digits})$, one fp_shift_right_digit (ram:1bea) plus one BCD-add, so the digit-by-digit recurrence core needs no general multiply (only the base-conversion scaling via CALL ram:2362 enters _FPMult). (The traces show the shift 1bea is shared, but the running-add entry differs: _EToX uses fp_add_mantissa ram:1cb9, while _LnX uses the sibling BCD-add entry ram:1ca91cb9 fires 0× in the ln(2) loop, 1ca9 0× in the loop.)

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 code’s two passes — the AND 0x8 then BIT 4 stops at 02:6FAD/6FD3 — are the coarse digits ($k=0\ldots7$) and the fine digits ($k=8\ldots15$). The selector C chooses the base: $\ln x = \log_{10}x \cdot \ln 10$, with $\ln 10$ fetched from 02:7D42[06] by the LD A,6; CALL ram:2362 tail at 02:704A (_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}

So a single 16-row table at 02:7181 powers all of ln / log / eˣ / 10ˣ; the 02:7D42 block only supplies the base-conversion constants ($\log_{10}e$, $\ln 10$) and the trig reduction constants.

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.

Sin/cos (_SinCosRad) uses the same digit-recurrence shape on the range-reduced angle, but with its own near-unity scaling tables 02:7201/02:7281 (eight rows, two sign-variants each, picked by 0x84A4 bit 7) rather than Taylor coefficients — so it too is a table-driven recurrence, not a fixed polynomial. The exact rotation identity each trig row encodes is not pinned here.

_LnX — natural log (02:6EFD) [confirmed]

_LnX first calls _CkOP1Pos (ram:1E5D) and raises a domain error on x <= 0. The core (02:6F1B) splits x into mantissa × 10^exp; after a small pre-step near 02:6F4502:6F50 (a value formed with _FPAdd (RST 30h) / _FPSub / _FPDiv ram:2541), the pseudo-division loop described above (02:6F8C02:6FEC) steps through the shared 16-slot table at 02:7181 via 02:7301/02:7302; the first phase stops when the selector has bit 3 set (02:6FAB02:6FAF, coarse digits) and the second when it reaches bit 4 (02:6FD202:6FD5, fine digits). The 02:6F70: LD A,3; CALL ram:2362 site fetches constant-table index 3, and 02:704A: LD A,6; CALL ram:2362 fetches index 6 (ln(10), the base-conversion multiply), both from 02:7D42.

_EToX — eˣ (02:705C) [confirmed]

_EToX is local page-0x02 code. It clears guard digits, then 02:705F: LD A,3; CALL ram:2362 loads constant-table index 3 (log10(e)) and then 02:7064 JR +3 skips _TenX’s entry at 02:7066 (which does its own CALL ram:2627), joining the shared body at 02:7069. The body splits the decimal exponent/integer digit shift (02:706902:70B6), handles sign/reciprocal cases (02:70B902:70D9), then evaluates the fractional part with the shared 16-row table at 02:7181. The exact loop bound is 02:7106 LD HL,0x848E; 02:7109 LD A,(HL); CP 0x0F; JR Z,02:7140, so the table-driven exp evaluator has 16 selector slots (0..15).

_SinCosRad — sin/cos 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. After reduction (02:7475 onward, falling through 02:7488 LD A,B) the reduced argument in [0, pi/4) drives the same table-stepping digit recurrence as ln/e^x: a selector walks the coefficient table one row per step rather than unrolling a fixed polynomial. The loaders are local — 02:74AB: CALL 02:731D reads the signed table at 02:7201, 02:74EA: CALL 02:7312 reads the signed table at 02:7281 — and the selector advances under BIT 3,B / BIT 3,C in the tail (02:74DD02:74E0, 02:75C602:75C8), so the walk covers eight selector rows (0..7), each carrying two 8-byte sign/phase variants. Per-row decoding of 02:7201/02:7281 is the one piece still open: the rows climb toward 10 (e.g. 02:7201[00]=9.9668…, [06]=9.999999…), the shape of a half-angle / rotation factor consumed one digit at a time.

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 exp(1)=0 is in [0,12)), the reduction multiply by the 02:7D81 constant (7372 LD HL,02:7D81; CALL ram:1AE2), and the table-stepping loader 02:731D returning successive rows of 02:7201 (HL = 7209/7219/…/7279, 16-byte stride) all execute as described. On-screen result: .8414709848. (Per-row coefficient meaning remains the open piece.)

Coefficient tables [confirmed]

02:7D1E zeroes the OP2 type byte, indexes 02:7D42 + 9*A, then copies the selected constant image into OP2. The only LD A,n; CALL ram:2362 uses in this cluster are A=3 (log10(e)) and A=6 (ln(10)); the later trig reduction constants are loaded directly from the same nearby 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

02:7181 is the shared ln/e^x digit table loaded by 02:7301/02:7302/02:7305. It has 16 8-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

02:7201 and 02:7281 are the forward sin/cos signed recurrence tables (the per-row near-unity factors the digit loop steps through, per the shared algorithm above). Each row is 16 bytes: the first 8-byte variant is selected when 0x84A4 bit 7 is clear, and the second 8-byte variant is selected 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 run on the table-driven digit recurrence above — a selector that steps through a coefficient table one row at a time, on shift-and-add (ln/e^x loaded from 02:7181; sin/cos from 02:7201/02:7281). The separate arctangent engine behind inverse trig uses a base-10 CORDIC iteration, documented in Calculation Engine.

Calculation engine

TI-84 Plus OS 2.55MP — feature deep dive.

What happens between a college student typing 2*sin(π/6)+ln(5) and seeing a number. All arithmetic is BCD floating point in the OP registers (OP1OP6 @ 0x8478, 11-byte spaced) with a software FP stack (FPS @ 0x9824) for nested temporaries. See 06-floating-point.md for the TIFloat byte format and _FPAdd internals; this doc covers the rest of the engine: ×, ÷, ^, roots, transcendentals, formatting, and errors.

Address form below is page:addr (flash page in slot 4000) or ram:addr for the fixed page-0 core mapped at 0000. Page-0 routines are reached by RST/direct CALL; everything else through the bcall dispatcher. Confidence: [confirmed] = read from disassembly, [standard] = matches documented TI behavior, [hypothesis] = inferred.


1. Register & 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.

Example seen in the complex-log core (_CLN, §5): it does _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.


2. 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 06-floating-point.md.
_FPSubram:2297flips OP2.type bit7 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.type with 0x80, guarding against −0), _InvOP2S ram:24CD, _InvOP1SC ram:24BA (both). _CkOP1Pos ram:1E5D ANDs OP1.type with 0x80. [confirmed]

Roots & 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).

3. Degree/radian & 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 06-floating-point.md’s constant scan.

4. Cross-page dispatch (cross_page_jump @ 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. ram:2362 calls the bcall entry at ram:3DD1, whose inline descriptor is 1E 7D 02, so it invokes the page-0x02 coefficient fetcher at 02:7D1E. In LD A,3; CALL 0x2362 / LD A,6; CALL 0x2362, the 3 and 6 are coefficient-table indexes, not target pages. _EToX falls through locally into the _TenX body at 02:7069; the ln/e^x/sin-cos coefficient tables are on page 0x02 (7181, 7201, 7281, and the constant block near 7D42), not page 0x03/0x06/0x07.


5. 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 ram:2362/02:7D1E, 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 02:7181). Argument too large → _ErrOverflow.

Trig — sin/cos/tan [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 signed near-unity tables at 02:7201 and 02:7281 (one row per digit step, sign-variant picked by 0x84A4 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 06-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. This makes ^ the most FP-stack-heavy single operator a student hits.


6. Number entry & 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.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 08-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]


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

8. Routine index (confident, space:addr)

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.


9. Worked flow: 2*sin(π/6)+ln(5) [hypothesis, from the above]

  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

TI-84 Plus OS 2.55MP — feature deep dive.

What happens between a college student entering data into L1/L2, pressing STAT ▸ CALC ▸ 1‑Var Stats (or LinReg(ax+b), QuadReg, …), and seeing x̄, Σx, Sx, a, b, r, r² appear — and where every result is stored so it can be recalled by name (, Σx, RegEQ, …).

This doc covers the STAT CALC computations. The data source is the L1–L6 lists (VAT/05-variables-vat.md, sub-vat-archive.md); the arithmetic is the BCD FP engine (06-floating-point.md, sub-calculation.md). Stat plots and the DISTR menu are noted in §8/§9 — DISTR functions are parser functions, not part of the STAT‑CALC engine.

Address form: page:addr (flash page in the 0x4000 slot) or ram:addr for the fixed page‑0 core at 0x0000. The whole STAT‑CALC engine lives on flash page 0x3A. Confidence: [confirmed] = read from Z80 disassembly, [standard] = matches documented TI behavior, [hypothesis] = inferred.

The Ghidra decompiler mis-renders the Z80 SET/RES b,(IY+d) flag ops, the RST macros, and cross-page CALL 0x2b09 trampolines, so the algorithm here is read primarily from the disassembly.


1. The statVars result block (0x8A3A) [confirmed]

Every STAT‑CALC result is a 9‑byte TIFloat (see 06-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 a student recalls 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)

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 scope]. 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 noted in §3 — and are not dispatched through _OneVar (whose token map is only F2FF, §3). They fill the PStat…SStat/anovaf_vars block above directly. No PStat/ZStat‑writing routine appears among the page‑0x3A stat_* symbols (all of which are the _OneVar accumulate/variance/median/regression engine), confirming the tests live in their own command handlers reached from the parser’s command dispatch, separate from the STAT‑CALC engine documented here. The exact per‑test handler addresses are not exposed as named routines in this DB; those per-test handler addresses are [hypothesis]. [confirmed: separate from _OneVar]

A scratch byte 0x8A36 (immediately below statVars) holds the stat‑command discriminator (the model index, set from the command token — see §3) 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/_RclVarPush, 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). [confirmed/standard]


2. _OneVar — the STAT‑CALC engine entry (3A:6420, id 0x4BA3) [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, see §3) selects the behaviour.

_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                     ; *** the accumulation pass (§4) ***
  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 (§5)
  …
  SET 7,(IY+9)                  ; mark results valid
  CALL 67c1 …                   ; finalize / median (§6)

Key facts read from the disassembly:

  • The command byte is saved at (0x8A36) 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).

3. STAT command token map (STATCMD = 0xF2) [confirmed]

The parser passes the command token; _OneVar stores it at 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. [confirmed/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 (§6). The SortA(/SortD( command sort is a different routine on page 0x02 (≈02:5939, comparator _CpOP1OP2) — see Matrices & Lists.


4. The accumulation pass (3A:6572 …) [confirmed]

This is the heart of 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 then bcall _LnX at 3A:6F72 for model codes < 4 (ExpReg/LnReg/PwrReg); the back‑transform _EToX/_TenX lives on page 0x02 (see sub-calculation.md §5). This is the standard “linearize, fit a line, transform back” method; r is the correlation of the transformed data.

4a. Mean & 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]


5. The regression solver — Gauss‑Jordan on the normal equations (3A:67C6 …) [confirmed]

For a polynomial fit the moment matrix from §4 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
6845: CALL 3939 (_SqRoot) ; 6849 ; CALL 2541 (_FPDiv)  ; (forms r²/r from the fit)
684f: LD A,12 ; CALL 213d           ; r-related store/guard
6859..6876: row-reduce all other rows (3aa7 get, 238b _FPMult, RST 30 _FPAdd)
6880: CALL 2541 (_FPDiv) ; CP 2 ; LD A,0x35/0x36 ; CALL 213d  ; *** _Sto_StatVar stores into the
                                                              ; statVar slots id 0x35/0x36 — NOT a SingularMat raise.
                                                              ; the zero-pivot guard is the 67F7→212D path → 0x83.
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” (e.g. all x equal, or too few distinct points for the degree). The LD A,0x35/ 0x36 and CALL 0x213d are the in‑solver guards. [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 r·r / coefficient‑of‑determination derived from the same cluster and surfaced through the same Corr slot. [confirmed: the A=0x12 → _Sto_StatVar r‑store; standard formula]

  • 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. [confirmed path / standard]


6. Median, quartiles, min/max & the sort (3A:7935, 3A:79B9) [confirmed]

For 1‑Var Stats the five‑number summary needs the data sorted:

  • MinX/MaxX are tracked during the §4 pass (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). [confirmed path / standard quartile rule]

The five‑number summary (minX, Q1, Med, Q3, maxX) is what the MED/box‑plot stat plot reads back out of statVars.


7. Worked flow: 2‑Var Stats L1,L2 then LinReg(ax+b) L1,L2,Y1 [hypothesis, from §§2–6]

  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 (8A36).
  3. Accumulation pass (§4): 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 (§4a): $\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 (§5): 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).

8. Stat plots [standard / partially confirmed]

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. [confirmed addresses / standard behavior]


9. Distributions (DISTR menu) — not part of STAT‑CALC [confirmed scope]

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. [confirmed: not reachable from _OneVar; hypothesis: numerical method]

Grounding [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. So the erf / incomplete‑gamma / incomplete‑beta continued fractions are [hypothesis] — outside the STAT‑CALC engine, sitting behind the parser’s 2‑byte (0xBB‑prefixed) DISTR‑token function table; their exact page/address is not exposed as a named routine in this DB. [confirmed scope; address hypothesis]


10. Integration summary

  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 → 0x8A36                     │  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.


11. Confident address index (space:addr)

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
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, model‑discriminator 0x8A36, work ptrs 0x84AF–0x84DB (84D3 x/median ptr, 84D5/84D7 element ptrs, 84D9 sums‑matrix base, 84DB freq ptr, 84B1/84B2 loop counters, 84B3 element count). FP engine reused: RST 30h=_FPAdd, RST 08h=OP1→OP2, 00:238B=_FPMult, 00:238A=_FPSquare, 00:2541=_FPDiv, 00:2294=_Minus1, 02:6E38/3A:3939 =_SqRoot, 24BD=_InvOP1S.

12. Notes

  • r store offset. 3A:684F does LD A,0x12 ; CALL 0x213D (_Sto_StatVar, id 0x12 = tCorr), i.e. r → Corr (8ACA); / is the r·r/coefficient‑of‑determination from the same 6845 _SqRoot/_FPDiv cluster, surfaced through the same Corr slot (§5). (Residual: the 6845–6891 region is unanalyzed code in the DB, so only the A=0x12 store sequence was byte‑pinned, not every intermediate.)
  • DISTR numerical cores (erf/incomplete‑gamma/incomplete‑beta) are outside the STAT‑CALC engine: no distribution core is a named routine in this DB; they sit behind the parser’s 2‑byte DISTR‑token function table (sub-tibasic). Exact address [hypothesis] (§9).
  • STAT‑TESTS (Z/T/χ²/F/ANOVA) that fill PStat…SStat/anovaf_vars are separate command handlers, not reached through _OneVar (whose tokens are only F2FF); no PStat‑writing routine is among the page‑0x3A stat_* symbols (§1). Per‑test addresses [hypothesis].
  • 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 & Lists.

Matrices & lists

TI-84 Plus OS 2.55MP — feature deep dive.

How the TI-84 Plus OS (2.55MP) stores, indexes, and computes on lists and matrices — the routines a college student hits doing linear algebra (det(, [A]⁻¹, rref(, [A]*[B], identity(, T) and data work (L1+L2, dim(, sum(, seq(, SortA(). Companion to 05-variables-vat.md (where the data lives), 06-floating-point.md (how each element is computed), and sub-vat-archive.md (Store/Recall/Archive).

All page:addr are read from the raw Z80 disassembly, not the decompiler alone. Page numbers are the masked flash page (rawpage & 0x3F). The whole-OS image lives in one Ghidra program with address spaces ram (the page-0/RAM-resident 0x0000–0x7FFF window) and page_NN for each flash page mapped into the 0x4000–0x7FFF bank-A window.


0. TL;DR — the mental 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 dim0; byte dim1; (two 1-byte dimensions) followed by dim0*dim1 × 9-byte TIFloat, stored column-major. The element offset from the start of the data area (after the 2 dim bytes) is

    $$\mathrm{offset}=\big((\mathit{idx}_0-1)\cdot \mathit{dim}_0+(\mathit{idx}_1-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, doc 05): 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.


1. Data layouts & the 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(dimWord, dataPtrOut):
  _HTimesL()                    ; element count = H * L  (the two dims multiplied)
  var_alloc(2)                  ; carve  H*L*9 + 2  bytes
  header: LD (HL),C ; INC HL ; LD (HL),B   ; writes dim0 then dim1
  • _HTimesL (00:1EF6) is literally result = H * L (B=H; HL=Σ L, a DJNZ add loop) — it computes the element count from the two dimension bytes. [confirmed]
  • Header = two bytes dim0,dim1; data is dim0*dim1 floats column-major.

Dimension naming [confirmed]. Settled by disassembly. _AdrMEle (02:4002) reads the header’s first byte (LD A,(DE); LD L,A) and uses it as the major stride, looping (B−1) adds of it and then +(C−1) within a column (column-major). The major stride of a column-major array is the number of rows, so the first header byte (dim0) = #rows, and _AdrMEle’s B = idx0 = column, C = idx1 = row. _CreateRMat (ram:1115) confirms the layout: it is PUSH HL ; CALL _HTimesL (1EF6) ; LD A,2 ; JR 10DD_HTimesL returns H·L (the element count) and A=2 is the 2-byte dim header; the two dimension bytes are stored dim0 (rows) then dim1 (cols). The byte-confirmed index arithmetic ((idx0−1)·dim0 + (idx1−1))·9 is therefore a (column, row) register convention with a row-count stride.


2. Element access — the index→offset math [confirmed]

This is the heart of everything. 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).
  • _RclListElemToOP1 (02:47FB), _RclListElemB (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.
  • _RclCListElem (02:49A7), _RclCListElemB (02:49B5) — complex-list element via _CplxOPArrange (splits real/imag into OP1/OP2).
  • _GetPosListElem (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=column idx0, C=row idx1, DE=matrixDataPtr
  if B==0 or C==0 -> LD A,0x78 ; JP 0x2793 ; 0-index rejected (error vector)
  A = (DE)        ; A = dim0 (rows)        ; first header byte
  HL = 0
  repeat (B − 1) times:  HL += dim0        ; (idx0-1) * dim0     (column stride)
  HL += (C − 1)                            ; + (idx1-1)          (within column)
  DE += 2                                  ; skip both dim bytes
  CALL 1930 (_HLTimes9)                    ; HL *= 9
  HL += DE                                 ; final element pointer

Column-major offset: elem = data + 2 + ((idx0−1)*dim0 + (idx1−1)) * 9. The (B-1) adds of dim0 walk whole columns; the (C-1) steps within a column. The 8-bit adds track a carry into H so the address is a true 16-bit offset (matrices up to 99×99). Because the multiplied byte is dim0 and that is the row count (column-major major stride), B=idx0 is the column index and C=idx1 is the row index — see the [confirmed] dimension-naming note in §1. [confirmed]

Matrix element wrappers: [confirmed]

  • _AdrMRow (02:4000) — address of the start of column idx0 in the column-major buffer (loops (idx0−1) × dim0, no +(idx1-1)); whole-row operations layer their own iteration on top.
  • _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).
  • _ListIdxTimes9 (35:79E9) = _HLTimes9(idx) then a small dispatch (RST4) — the list analogue used in a few list-builder paths.

3. List operations [standard]

Create / resize / insert / delete

RoutineaddrRole
_CreateRList00:10C4new real list: count*9+2 bytes (§1) [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 + a column-major 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 — the per-element FP path is confirmed; the outer driver is the parser’s binary-op handler.]

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]

seq(, cumSum(, SortA(/SortD(, mean(/median(/stdDev( [hypothesis]

  • seq(expr,var,lo,hi[,step]) evaluates expr for var = lo..hi, pushing each result and finally _CreateRList-ing the collected floats (the generic list-builder loop; _SetSeqM 36:7D1F is the sequence-graph variant). [standard]
  • 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 comparator]
  • Stats (mean/median/sum/stdDev/variance) are list folds layered on sum(/sort. [hypothesis]

SortA( / SortD( — list sort [confirmed comparator]

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 sort body is on page 0x02 (around 02:5939); it is reached only through the parser’s computed command dispatch, so it is unnamed. Its comparator is _CpOP1OP2 (00:198D), confirmed by the call at 02:5939.

_CpOP1OP2 compares two TIFloats as real numbers [confirmed from disassembly]: 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. [dynamic run confirmed; list primitives above are confirmed where marked]


4. 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. randM( itself is a separate 2-byte token (tRandM = 0x20, 0xBB-prefix group); its per-cell random fill is the one residual still open (§4) — and notably it does not use the _Random bcall (0x4B79): a ROM-wide scan finds zero RST 28h; .dw 0x4B79 sites, so randM’s randomness comes from some other path. [standard]
  • Matrix copy/reshape = _DataSize-counted byte copy of the float payload (mele_copy9_d3 (02:4539)/mele_copy9_loop (02:453F)). [confirmed]

[A] + [B], [A] - [B], scalar·[A] — element-wise [standard]

Binary matrix add/sub apply the FP op per cell with a nested for col { for row { load [M](r,c)→OP1; op; store } } walk and require equal dims (_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 — body decoded from rom.bin; callers 02:5FFF/4605/5B39 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 (CP 0x02 ; JR NZ), swaps the two dimension bytes for the result header (60F5: LD A,H ; LD H,L ; LD L,A), 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=col,C=row) 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). Because the destination header carries the dims swapped (the 60F5 swap), _AdrMEle (4002) computes the column-major offset with the row/column roles exchanged, so the same linear walk lands element src(r,c) at dst(c,r) — a true transpose, which re-indexes both i and j. [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 nests a per-row loop (477B: LD B,1 …, reading via 4040 _AdrMRow/4068 mele_store_ckvalid) inside a column loop over (84AF), copying matrix columns into list element(s) (4051/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 @ 635B02:6238 copy [confirmed]; 02:4663 engine [standard]The 0x91 branch requires two operands (CP 0x02), reads the dims (5D98), and checks the two row counts with LD A,H ; CP L: equal rows fall through (JR Z) and H>L raises E_Dimension (JP NC,2719). It then runs the column-concatenation copy at 02:6238 [confirmed]: 6238 allocates the result (5DE05DE6_CreateRMat 110F) and bulk-copies the float payload with 02:4539 (mele_copy9_d3 — skip the 2 dim bytes, LDIR the column-major data), re-pointing 84D3←84D7. The branch then calls 02:4663 (6379, only caller; byte-verified CD 63 46), a partial-pivoting elimination engine: it computes min(H,L) (4672: LD A,H ; CP L ; JR C ; LD L,H), inits via 475E, and iterates from BC=0x0101 calling 461C (max-abs), 41D0 (pivot-column scan), 198D (compare), 471C (permutation swap) and 405E (store). The column-concat copy (6238/4539) is confirmed augment behaviour [confirmed]; 4663’s elimination pass on the concatenated result is byte-confirmed but its role for plain augment( is left open [standard]. (augment(L1,L2) list-concat is the 0x92 sibling at 637F, sharing the 6362 setup.)
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 2-byte token (tRandM = 0x20, 0xBB-prefix group) routed through the 0xBB dispatcher; its cell-fill body is unidentified — it uses no _Random bcall (0x4B79: a ROM-wide scan finds no RST 28h; .dw 0x4B79 site). [standard]
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 column-major payload copy. Each command’s dispatch site and body are byte-confirmed above; the single residual is randM(’s per-cell fill and 4663’s elimination role inside augment(. [confirmed for the bodies; standard for those two residuals]


5. The heavy ones — det(, [A]⁻¹, rref( / ref( [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 (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|,
                comparing |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 dim0 stride and swaps two whole 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.

Det sign / pivot-product bytes in the tail (02:43D8–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]

rref( / ref( — separate driver, not 42A6 [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 class-3 (function-token) handler-pointer table at 38:7175 (701A/7021/7026 select the 0x4000/0x478C/0x7175 tables by class; 703A: CALL 0x0033 = _LdHLind jumps 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 38:7175 2-byte handler table and was not byte-isolated in this pass (the table is unanalyzed data in the DB); the architectural fact that it is a separate driver, not 42A6, is confirmed by the two-caller xref. [confirmed for the “separate driver” conclusion; standard for the exact body address.]


6. How it ties to the FP engine and the VAT [confirmed]

  • Every element is a TIFloat (doc 06). 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 is literally thousands of these calls. Complex elements (lists/[i]) carry a 0x0C flag and use 18-byte (two-float) elements, split via _CplxOPArrange.
  • Where the data lives: the parser resolves the list/matrix name through OP1_FindSym/_ChkFindSym (doc 05/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.

7. Errors raised on these paths [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)_GetPosListElem bad index (_CkOP1Pos)

8. Confident address 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 dim0,dim1) [confirmed]
00:1EF6_HTimesLelement count = H*L (dims multiplied) [confirmed]
00:1930_HLTimes9×9 (real TIFloat stride) [confirmed]
02:4000_AdrMRowaddress of matrix column start (column stride) [confirmed]
02:4002_AdrMElematrix element address: ((column-1)*dim0+(row-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 (§4). 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 (§4) [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 (§4) [confirmed]
02:4539mele_copy9_d3bulk column-major float-payload copy (skip 2 dim bytes, LDIR); used by augment(/reshape (§4) [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 (§4) [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) (§4) [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 (§4) [confirmed]
02:6238mat_augment_copyaugment( column-concat: allocate result (5DE0) + 4539 payload copy + re-point 84D3 (§4) [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) (§4) [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:47FB_RclListElemToOP1recall list elem to OP1 [confirmed]
02:47FE_RclListElemBrecall list elem (B-indexed) [confirmed]
02:4829_PutToLOP1 → list[i] (validated, complex-aware) [confirmed]
02:49A7_RclCListElemcomplex-list element → OP1/OP2 [confirmed]
02:49B5_RclCListElemBcomplex-list element (B-indexed) [confirmed]
02:5BBB_GetPosListElemlist 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:79E9_ListIdxTimes9list 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]

9. Findings

  • 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 class-3 handler table at 38:7175 (§5). The exact rref/ref body sits behind that (unanalyzed-data) table and is the only residual: its start address was not byte-isolated, but it is confirmed not to be 42A6.
  • det sign / pivot-product (42A6 tail 43D8–4470) and dim labelling. 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 (§5). Row/col vs dim0/dim1 is now [confirmed]: dim0 (first header byte) = #rows, and _AdrMEle takes B=column, C=row (§1/§2).
  • 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 (§4):
    • 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].
  • OPEN — two residuals inside the confirmed branches (§4):
    • augment(’s 0x91 branch calls 02:4663 (mat_gauss_engine, only caller 6379) — a min(H,L) partial-pivoting elimination pass — after the column-concat copy. Its role for plain augment( is byte-confirmed as a call but not explained. [standard]
    • randM(’s per-cell random fill body is unidentified. randM( is a 2-byte token (tRandM = 0x20, 0xBB-prefix group), distinct from the 0xB5/dim( branch; the visible matrix-create/dim-convert code is dim(’s, not randM’s. randM does not go through the _Random bcall (0x4B79) — a ROM-wide scan finds no RST 28h; .dw 0x4B79 site. [standard]
  • 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 & numerical methods

TI-84 Plus OS 2.55MP — feature deep dive.

What happens when a calculus/algebra student uses the equation Solver / solve(, nDeriv(, fnInt(, or the TVM finance solver. All of these are iterative routines that repeatedly evaluate the user’s expression through the BCD floating-point engine (see sub-calculation.md, 06-floating-point.md) and the TI-BASIC parser (sub-tibasic.md).

Address form is page:addr (flash page banked at 0x4000) or ram:addr for the fixed page-0 core at 0x0000. Confidence: [confirmed] = read from disassembly, [standard] = matches documented TI behavior, [hypothesis] = inferred from surrounding code. Disassembly was recovered byte-exact from tools/rom.bin; the headless Ghidra project on the banked pages is only partially auto-analyzed, so addresses here were cross-checked against raw opcodes.


0. The four solver errors and the error-code table [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

The error message-name table is on page 0x07 starting at 07:6B81. It 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]


1. The equation Solver / solve( root finder — page 0x39 [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.)

1.1 The function-value evaluator f(x) [confirmed]

At the heart is a callback that, 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 an error trap (39:46D9 CALL 327F; RES/SET 2,(IY+7)) and re-parses / re-evaluates the stored equation formula (page-0x39 hosts a parse/token walker at 39:327F using parse_advance 7248 / parse_cur_tok 72DA-style cursors, mirroring the page-0x38 parser — the equation is re-tokenised and evaluated every iteration).
  3. The post-eval 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 (CP 0x87; JR Z then 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). This is why solve( can skip singularities inside the bracket without aborting. [confirmed]

The sign test 39:463A reads OP1.type (8478) and OP2.type (8483), masks 0x80 and XORs them: Z = same sign, NZ = opposite sign — the bracket sign-change predicate. [confirmed]

1.2 The 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. [confirmed for the op sequence; method name 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. The tolerance floor is the TIFloat constant 1.0e-13 at 39:46EA (00 73 10 …); the residual-zero floor is 1.0e-99 at 39:46E1 (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]


2. The TVM finance solver — page 0x3A [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]

2.1 The 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. [confirmed sequence; equation standard]

2.2 The 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 (LD HL,0x0005; _AllocFPS at 3A:70A2) and 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]

2.3 The TVM rate loop calls _SinH (3A:710B) [confirmed]

At 3A:710B the TVM body contains EF CF 40 = RST 0x28; .dw 0x40CF, and 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. [confirmed for I%; standard for the rest]

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. [confirmed token map; hypothesis for IRR sharing the loop]


3. nDeriv( and fnInt( — page 0x33 numeric calculus [confirmed core, method 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).

3.1 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. [confirmed it is a finite- difference with var save/restore; ε-default standard]

3.2 fnInt( — adaptive numeric integration [confirmed: adaptive bisection, no node table]

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 bytes]
  • 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; the 97E7/84AF depth counters track subdivision depth; the loop tail is 33:4E81 LD DE,0x0024 … C3 CB 45 and 33:4E8C 3D F5 C2 57 4D = DEC A; …; JP NZ,0x4D57), and 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 loop/tolerance]

Quadrature rule. A full byte scan of 33:4D00…4F00 finds exactly one floating-point constant in the body: the TIFloat at 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), 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 in §1.1 — the same “store var → parse_eval → read OP1” loop. [standard]


4. How the unknown is varied — the parser feedback loop [confirmed/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 page-0x39 token walker). 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. An error trap (39:327F / RES 2,(IY+7)) lets a DOMAIN/NONREAL error at one sample be caught and treated as “undefined here” instead of aborting the whole solve (§1.1).

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.

4.1 How tFnInt/tNDeriv/tRoot reach the page-0x33 bodies [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 bytes]
  2. The page-0x02 handler parses the comma-separated argument list and sets defaults — e.g. the nDeriv/fnInt prologue at 02:6AF6 does LD A,0x7D; LD (8479),A, seeding 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 path; trampoline hides the static edge]

5. Routine index (space:addr name) [confirmed unless noted]

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)
39:327F  solver_parse_formula       (page-39 token walker used by the evaluator)
39:46EA  const_tol_1e-13            (convergence tolerance, TIFloat 00 73 10..)
39:46E1  const_floor_1e-99          (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 4381)

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)  [renamed]
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.


6. Findings

Summary of the four sub-results:

  • fnInt( quadrature rule (§3.2). Not Gauss–Kronrod. The body has no node or weight table; its sole FP constant is ln(10)·100 at 33:4E92, 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) (§2.3). 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/tRoot (§4.1). Path 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 (§5). 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-0x33 control-flow handlers (33:4381 jump table) is not yet field-mapped; only the dispatch path is confirmed. (See sub-tibasic.md §4.)
  • bcall 0x462A in the TVM body is unmapped (adjacent to _AdrLEle; likely a finance-sysvar list/element accessor).

07 — Tokenizer & TI-BASIC

TI-BASIC programs are stored as tokens, not text: every command, function, and variable is a token of 1 or 2 bytes. The OS detokenizes (token→display string) to show a program and tokenizes (keypress/text→token) on entry; the parser walks tokens to execute.

Token encoding [confirmed]

  • Most tokens are one byte (tStore=0x04, tBoxPlot=0x05, operators, digits, letters, common commands). The Ghidra database models the byte values as the TIToken enum, built from the t-prefixed equates in ti83plus.inc.
  • Some bytes are lead bytes of a two-byte token: the first byte selects a table, the second byte the entry.

The 2-byte lead-byte set [confirmed]

_IsA2ByteTok (ram:1FE8) scans an 11-byte table at ram:1FF6 to decide if a byte starts a 2-byte token. The bytes are:

ByteMeaning (.inc)
5CtVarMat — matrix name ([A]…)
5DtVarLst — list name (L1…)
5EtVarEqu — equation variable (Y1, r1, …)
60tVarPict — picture
61tVarGDB — graph database
62tVarOut — Y-vars / output
63tVarSys — system var group (Xmin, …)
7Egraph-format token group
BBt2ByteTok — the general “extended commands” page (2.x additions)
AAtVarStrng — string variable (Str1…)
EFTI-84+-era extended token page

So e.g. 5D 00 = list L1; BB xx = an extended command. The second byte indexes that group’s name/handler table. (String variables Str1Str0 use lead AA; they are a distinct VAT object type holding tokenized text — see Strings.)

Detokenize / token length [confirmed]

  • _GetTokLen (01:66E5) returns the length of the detokenized display string for the token at HL: it calls smallfont_glyph_ptr (01:6702) to resolve the token’s string pointer, then reads the leading length byte (LD A,(HL)). It is _IsA2ByteTok, not _GetTokLen, that tests 1-byte vs 2-byte encoding.
  • _Get_Tok_Strng (01:66EA) returns the display string for a token (used by the program editor and Disp).

Parser / interpreter [located — page 0x38]

The expression parser/evaluator lives on flash page 0x38. Entry points:

  • _ParseInp (38:5987) — parse/evaluate the input (homescreen/entry line). It calls parse_init (38:5b7b) to reset parser state, clears editing flags, then resolves via _ChkFindSym. [confirmed]
  • _ParseInpLastEnt (38:5984) — public parser variant immediately before _ParseInp; the generated ASMPARSE.8xp/ZZPARSE.8xp fixture reaches it but fails with ERR:INVALID without running the named BASIC target. [confirmed negative probe]
  • _Find_Parse_Formula (38:758A) — _FindSym a variable then parse its stored formula (Y-vars, equations). The generated ASMFORM.8xp/ZZFORM.8xp fixture reaches it from an AsmPrgm payload but fails with ERR:UNDEFINED without running the named BASIC target. [confirmed negative probe]
  • parse_init (38:5b7b) — zeroes the parse-position/state bytes and clears a batch of parser flag bits (in the IY flag area). [confirmed]

The engine reads the token stream and dispatches each token to a handler; arithmetic tokens flow into the FP engine (06), variable tokens resolve via the VAT (05), and the busy indicator is driven by _RunIndicOn / _RunIndicOff. _BinOPExec applies a binary operator via OP1/OP2.

The handler dispatch table [confirmed]

Page 0x38 begins with the parser’s handler dispatch at 38:4000 — a flat array of 2-byte little-endian handler pointers. Raw bytes are 9F 41 F0 45 1C 42 … = entries 0x419F, 0x45F0, 0x421C, … (all in-window 0x4xxx/0x47xx code addresses), indexed by token class and dereferenced; the selector at 38:7010 loads LD HL,0x4000 and adds 2×index (see TI-BASIC Programs).

These handlers implement TI-BASIC statements/commands and operators. Sampling them by the routine they call:

  • indices 8–10, 17–19, 38 → bcall(_Regraph) — graph commands (DrawF, ZoomFit, etc.).
  • indices 14–16, 21–22 → bcall(_Disp) — display/output commands (Disp, Output).
  • the “no-bcall” handlers are the arithmetic/operator productions — they drive OP1/OP2 through the FP engine via the RST shortcuts (RST 30h _FPAdd, etc.), which is why a bcall scan doesn’t flag them; variable handlers go through _FindSym (05).

The first handlers: 38:419F, 45F0, 421C, ….

Parse-stream cursor [confirmed]

The evaluator walks the token stream via a cursor in RAM: parsePtr (0x965D = official nextParseByte, current position) and parseEnd (0x965F = basic_end, end). Named helpers on page 0x38:

  • parse_cur_tok (38:72DA) — fetch the token at the cursor.
  • parse_advance (38:7248) — parsePtr++ (advances the cursor and reloads BC from it). The bounds/refill check is the adjacent entry 38:7245, which calls 0x1FD6 before falling into the increment.
  • parse_expect_or_err (38:5CD8) — fetch a token and raise _ErrSyntax (recording the position in parsePtr) if it isn’t the expected one.

So the dispatch loop is: parse_cur_tok → index the pointer table at 38:4000 and call the selected handler (which may consume args via parse_advance) → repeat.

Main evaluator: parse_eval_expr (38:5AB3) is the big recursive-descent expression evaluator — it dispatches through handler function-pointers (code *) with operator precedence, reading via the cursor helpers and leaving the result in OP1. _ParseInpparse_initparse_eval_expr. parse_scan_tokens (38:4180) is a token-scan helper (skips to a delimiter, honoring 2-byte tokens via _IsA2ByteTok).

The region at 38:4000 is a flat array of 2-byte handler pointers (entries 0x419F, 0x45F0, 0x421C, …), not executable code — CALL 0x33AB (CD AB 33) appears nowhere on page 0x38. Each handler is itself recursive-descent code; the table selects which one to enter. See sub-tibasic.md for the execution model (eval_stmt_entry@38:59C5, the blockmatch_end_else@38:4130 End/Else matcher, goto_lbl_name_scanner@38:4870).

The handlers are recursive-descent grammar productions (not flat per-operator routines): each reads via parse_cur_tok, conditionally recurses, and some load sub-dispatch tables (e.g. 38:5110, 5127) for finer token classes — implementing operator precedence by nesting. So “the + operator” isn’t one table entry; it’s handled within the term/factor production that drives _FPAdd (RST 30h).

The precedence levels (term/factor/unary productions) and sub-dispatch tables are mapped in TI-BASIC Programs §3/§6.

Tokenized sample programs

The raw bodies below are the bytes stored after a ProgObj size word. They can be regenerated, along with loadable .8xp files, with tools/tibasic_samples.py --write-dir tools/tibasic-samples and traced with the workflow in tools/dynamic-tracing.md. The generated samples were run under headless TilEm on OS 2.55MP; see TI-BASIC programs for observed outputs and trace anchors.

ProgramSourceBody bytes
HelloClrHome / Disp "HELLO, WORLD"E1 3F DE 2A 48 45 4C 4C 4F 2B 29 57 4F 52 4C 44 2A 3F
FactorialPrompt N / 1->F / For(I,1,N) / F*I->F / End / Disp FDD 4E 3F 31 04 46 3F D3 49 2B 31 2B 4E 11 3F 46 82 49 04 46 3F D4 3F DE 46 3F
Data{3,1,4,1,5}->L1 / SortA(L1) / cumSum(L1)->L2 / sum(L1)->S / display results08 33 2B 31 2B 34 2B 31 2B 35 09 04 5D 00 3F E3 5D 00 11 3F BB 29 5D 00 11 04 5D 01 3F B6 5D 00 11 04 53 3F DE 5D 00 3F DE 5D 01 3F DE 53 3F
Asm( wrapperDisp "BEFORE" / Asm(prgmASMRET) / Disp "AFTER"DE 2A 42 45 46 4F 52 45 2A 3F BB 6A 5F 41 53 4D 52 45 54 11 3F DE 2A 41 46 54 45 52 2A 3F
ASM callback bridgeAsm(prgmASMSIG) / If Ans / prgmZZBASICDE 2A 42 45 ... 72 3F 5F 5A 5A ... (full body in tools/tibasic-samples/asmbridge.tok)
ASM return valueAsm(prgmASMVAL) / Ans+3->A / Disp ABB 6A 5F 41 53 4D 56 41 4C 11 3F 72 70 33 04 41 3F DE 41 3F
AnimationClrHome / For(I,1,8) / Output(1,I,"X") / End / Disp "DONE"E1 3F D3 49 2B 31 2B 38 11 3F E0 31 2B 49 2B 2A 58 2A 11 3F D4 3F DE 2A 44 4F 4E 45 2A 3F
Graph drawingClrDraw, window stores, visible axes/diagonal, Circle(47,31,10), Text(0,0,"DFS"), DispGraph85 3F 30 04 63 0A ... DF 3F (full body in tools/tibasic-samples/graphviz.tok)
Graph visualizationClrDraw, window stores, then Line(/Circle(/Text( drawing the DFS topology85 3F 30 04 63 0A ... DF 3F (full body in tools/tibasic-samples/graphdfs.tok)
List-driven graph visualizationedge endpoint lists L1L4, node lists L5/L6, looped Line(L1(I),...) and Circle(L5(I),...)85 3F 30 04 63 0A ... DF 3F (full body in tools/tibasic-samples/graphlist.tok)
BASIC subprogram0->A / prgmSUBRT / Disp A; callee Disp "SUB" / A+1->A / Returncaller 30 04 41 3F 5F 53 55 42 52 54 3F DE 41 3F; callee DE 2A 53 55 42 2A 3F 41 70 31 04 41 3F D5 3F
BASIC ABI fixturecaller initializes L1 and Ans, calls prgmABISUB, then displays A, L1, and Anscaller 08 32 2B 34 ... DE 72 3F; callee 72 70 5D 00 ... D5 3F (full bodies in tools/tibasic-samples/callabi.tok and abisub.tok)
Big integer addlist digits in L1/L2, carry C, indexed stores into L308 35 2B 34 ... DE 5D 02 10 36 11 3F (full body in tools/tibasic-samples/bigadd.tok)
Big integer multiplynested For( loops over digit lists, L3(I+J-1) accumulation, carry into L3(I+J)08 33 2B 32 ... DE 5D 02 10 34 11 3F (full body in tools/tibasic-samples/bigmul.tok)
DFSedge lists L1/L2, visited L3, stack L4, While/If Then/nested For08 31 2B 31 ... D4 3F DE 5D 02 3F (full body in tools/tibasic-samples/dfs.tok)

These examples show the main token categories the parser must walk: statement separators (3F), string delimiters (2A), store (04), list names (5D 00/5D 01), extended BB tokens (cumSum( = BB 29, Asm( = BB 6A, AsmPrgm = BB 6C), and command tokens such as Prompt (DD), Disp (DE), For( (D3), End (D4), ClrHome (E1), and SortA( (E3). The newer samples add Output( (E0), graph commands (85, 93, 9C, A5, DF), system/window variables (63 0A through 63 0D for Xmin/Xmax/Ymin/Ymax), Ans (72), Return (D5), list indexing with ( (10) / ) (11), int( (B1), arithmetic operators (70, 71, 82, 83), and the program-name token (5F) before the name characters. BIGMUL adds the loop variable J (4A) and indexed expressions such as I+J-1. DFS adds structured-control tokens While (D1), If (CE), Then (CF), and equality (6A). [confirmed token bytes from ti83plus.inc and token-tables.md]

Second-byte tables

Every 2-byte token group’s second-byte → token mapping (matrices, lists, Y-vars, system/window vars, the BB extended-command page, the EF 84+ page, etc.) is tabulated in 2-Byte Token Tables — 492 tokens, sourced from TI-Toolkit/tokens and filtered to the 84+ 2.55MP.

(The main parser loop, handler dispatch, and OP1-as-name handoff are covered in TI-BASIC Programs.)

TI-BASIC programs

TI-84 Plus OS 2.55MP — feature deep dive.

How a student-written TI-BASIC program is stored, parsed, and executed on OS 2.55MP. Builds on 07-tokenizer-basic.md (tokens, cursor helpers, the page-0x38 evaluator), 05-variables-vat.md (OP1 naming, _FindSym), and 11-boot-contexts-errors.md (contexts, onSP/_JError).

Confidence: [confirmed] = decompiled/byte-verified here, or multiple consistent signals (token compares, call shape) pin it even where the dense Z80 handler bodies don’t fully reduce in the decompiler; [standard] = documented TI-83+/84+ behavior consistent with what was seen; [hypothesis] = inferred, not yet verified.


1. How a program is stored [confirmed]

A program is a VAT object of type ProgObj (5) / ProtProgObj (6) (see doc 05). Its data is word size followed by size bytes of tokenized body — the exact byte stream the parser walks. No line numbers; lines are separated by the EOL/newline token 0x3F (tEnter, shown as cVar=='?' = 0x3F in the decompiled cursor code). Most tokens are 1 byte; the 11 lead bytes (5C 5D 5E 60 61 62 63 7E BB AA EF, the order of the ram:1FF6 table) introduce 2-byte tokens (doc 07).

Editing/detokenizing for the program editor uses the page-01 token helpers (doc 07, re-confirmed here):

  • _GetTokLen (01:66E5) — returns 1 or 2 for the token at HL (length of the token’s byte encoding), via smallfont_glyph_ptr (01:6702).
  • _Get_Tok_Strng (01:66EA) — returns the display string for a token (cross-page jump to the name table). The editor calls this per token to paint a line; Disp of a list/string also routes display text through related formatters.

2. The parse-stream cursor [confirmed]

The interpreter walks the token body through a RAM cursor (doc 07, all re-verified by decompilation):

HelperAddrBehavior
parse_cur_tok38:72DAfetch token at cursor (parsePtr); special-cases : token 0x3E (tColon)/end 0x00
parse_advance38:7248parsePtr (965D = nextParseByte) ++, bounds-check vs parseEnd (965F = basic_end); refill via deref_byte (38:5B79)
parse_expect_or_err38:5CD8fetch a token; if not the expected one, set parsePtr to the fault position and _ErrSyntax
parse_scan_tokens38:4180skip forward to a delimiter, honoring 2-byte tokens via _IsA2ByteTok; used by every block scanner
parse_init38:5B7Bzero parse position bytes (+6/+7), clear a batch of parser flag bits in the IY flag area

parsePtr is the OS RAM byte 965D (official equate nextParseByte); parseEnd is 965F (basic_end). parse_scan_tokens loop: parse_cur_tok; if token==0x2A (tString, the " delimiter it tests for here) stop, else _IsA2ByteTok then parse_advance (twice for a 2-byte token). This is the primitive every control-flow scanner is built on.


3. Top-level execution model [confirmed]

  • _ParseInp (38:5987) is the entry that parses/evaluates the entry line or a formula: it clears RAM 9305 (official equate EST, edit-screen height), calls parse_init, clears an editing flag (*(IY+0x1F) &= 0xF7), then tail-calls _ChkFindSym to resolve OP1. [confirmed]
  • _Find_Parse_Formula (38:758A) _FindSyms a named var then parses its stored formula; its body switches on var type (0x0F Window / 0x10 ZSto / 0x11 TblRng special-cased) before the cross-page parse. Used for Y-vars / equations and for running a program (resolve ProgObj, point the cursor at its body, run the statement loop). [confirmed]
  • The statement/expression evaluator is the big recursive-descent core parse_eval_expr (38:5AB3). The interpreter has several entry variants that all converge on the same shared inner loop (the code label at 38:59C8, inside parse_eval_expr) and the same precedence selector (the label at 38:7010): 38:59C5, 38:5826, 38:5CA7, 38:6963, 38:6F63. Each variant does statement-type-specific setup/teardown (FPS push, flag bits) and then runs the common token loop. [confirmed]

The shared inner loop (38:59C8) [confirmed]

loop:
  parse_advance_refill / err_if_not_real_86   ; refresh cursor / housekeeping
  class = chk_tok_end()          ; classify current token
  if (IY+9 & 0x80) class = set_split_rows()  ; alt classify for flagged tokens
  IY+9 &= 0x7F
  if class == 4: parse_pos=fault; _ErrSyntax     ; class 4 = syntax error
  if class <= 3:                 ; an operand/sub-expression
       cls = parse_cur_err_illegal()  ; map token -> grammar class (>0xF1 => +0x12)
       precLevel = 3
  38:7010:                       ; dispatch by precedence level
       if precLevel==2: handler = 0x478c        ; postfix/factorial+power production
       if precLevel==3: handler = 0x7175        ; (nop / leaf)
       else:            handler = 0x4000        ; the base dispatch block
       ... call handler, fold result via FP RSTs into OP1 ...

The param_2/handler pointer is one of 0x4000 (base/term), 0x478c (the postfix ^/! production — it reads +/^ (0x11), range-checks an exponent as a positive int, _JError(0x84) Domain otherwise; it is a raw code target within parse_eval_expr, not a defined function in the live DB), or 0x7175 (a leaf no-op). Selecting among these by precLevel (1/2/3) is how operator precedence is realized — nesting of productions, not a flat table (confirms doc 07’s “recursive-descent” claim). Results land in OP1; binary operators are applied via the FP RST shortcuts (RST 30h _FPAdd, …) and _BinOPExec.

Parser dispatch: page 0x38 begins with a handler-pointer table [confirmed]

Raw bytes at 38:4000 are 9F 41 F0 45 1C 42 CC 41 D9 41 … = a flat array of 2-byte little-endian handler pointers (entries 0x419F, 0x45F0, 0x421C, 0x41CC, 0x41D9, …), not executable code: CALL 0x33AB (CD AB 33) appears nowhere on page 0x38. The 38:7010 precedence selector indexes this table (LD HL,0x4000; add 2×class; deref), with raw-code alternates at 0x478C and 0x7175 for the postfix/leaf classes. Classification is done by chk_tok_end (38:72E0) / parse_cur_err_illegal (38:70F8).

Results / Ans [confirmed]

  • _StoAns (38:6251) stores OP1 into Ans (_CkOP1Real path; the bytes that follow are the Ans-var token table). _RclAns (38:679F) = _AnsName then _RclVarSym.
  • _AnsName (38:74B7): _ZeroOP1; (OP1+1)=0x72 (OP1 holds a name here, so the byte at 0x8479 is the name’s type/class tag rather than an exponent) — builds the OP1 name for the Ans variable (token class 0x72).
  • _StoSysTok/_RclSysTok (38:623B/683E) store/recall a system token variable (Xmin etc.) into/from OP1.

4. Control flow [confirmed]

Control-flow tokens (ti83plus.inc): tIf=CE tThen=CF tElse=D0 tWhile=D1 tRepeat=D2 tFor=D3 tEnd=D4 tReturn=D5 tLbl=D6 tGoto=D7 tPause=D8 tStop=D9.

Block matcher / End-Else scanner: blockmatch_end_else (38:4130) [confirmed]

This is the routine that skips a block to its matching End/Else (used to skip the not-taken branch of If / If…Then, and to bound For/While/ Repeat bodies). It keeps a nest-depth counter and walks via parse_scan_tokens:

depth = 0
loop:
  t = cur_token
  case t == tElse(D0):  if depth==0 -> stop, return 0xD0 (Else)   ; else skip
  case t == tEnd (D4):  if depth==0 -> stop, return 0xD4 (End)
                        else depth--                              ; close a nest
  case t in {tFor(D3), tWhile(D1), tRepeat(D2)}: depth++          ; open a nest
  case t == tIf (CE):   scan; if next == tThen(CF) depth++        ; If…Then opens
  default: parse_scan_tokens (skip token, honoring 2-byte)

Token compares verified at 38:4137 (CP D0), 414B (CP D4), 415D (CP D3), 4164 (CP D1), 4168 (CP D2), 416C (CP CE), 4179/41B3 (CP CF). Return value 0xD0 vs 0xD4 tells the caller whether it landed on Else or End.

If / Then / Else execution [confirmed]

  • The If statement handler evaluates the condition into OP1 (real). If the next token is not tThen, it’s a single-statement If (execute the one statement when true, skip it when false). If tThen, it’s a block.
  • The Else path is if_else_skip_handler (38:5826): on seeing tElse(D0) it repeatedly calls the block matcher blockmatch_end_else (38:4130) to skip the Else block to its matching End (the “condition was true, ran Then, now jump over Else” case), then rejoins the shared loop at 38:59C8. Other tElse compares at 38:57B3/58A6/58C6 handle the symmetric “skip Then, run Else” and nested cases.
  • if_isg_stmt_handler (38:6F63) is the per-statement entry that special-cases tIf (0xCE) and tISG (0xDA, IS>(): the second compare is 38:6F6C: CP 0xDA (tISG, the adjacent token to tStop 0xD9). For tIf it sets grammar class 0x5F and falls into the shared precedence loop to evaluate the condition; unknown leading tokens here raise _JError(0x88) (E_Syntax) for ordinary unknown tokens, or _JError(0x30) (E_Version, “ERR:VERSION”) for tokens above 0xF5 (the reserved/newer-token range — 0x30 is the message-table index, one below 0x31 ARCHIVE FULL). [confirmed bytes]

For( / While / Repeat / End [confirmed]

  • For(/While/Repeat push a loop-control record onto the FPS/loop stack recording the loop variable, limit, step, and the parsePtr of the loop top (so End can jump back). End pops/updates: increments the For variable, re-tests the limit, and either re-seeds parsePtr to the loop top or falls through. The block matcher blockmatch_end_else (38:4130) is what bounds these bodies during skips (e.g. While 0 skips straight to End).
  • Dispatch path (byte-traced). The For/While/Repeat/End/Return execution handlers live off page 0x38 — page 0x38 only has the tFor/tWhile/tRepeat/tEnd compares inside the blockmatch_end_else skip scanner (38:4130…4180). The live handlers are reached via the page-0x02 command dispatcher: 02:54BD loads a per-token handler pointer (LD HL,0x6A30 for tFor=CP 0xD3, 0x6A34 for tEnd=CP 0xD4, 0x6A2A for tReturn=CP 0xD5), and tWhile/tRepeat load a loop-type code (LD A,0x26/0x27) and JP 0x6400. 02:6400 and the 6A2A/6A30/6A34 stubs set a command index (0x28/0x29/0x2A) and invoke bcall 0x5140/0x513D, which both resolve to page 0x33 (_grf_435f, target 33:435F). 33:435F does SUB 0x20, bounds-checks, and indexes a 13-entry jump table at 33:4381 (0x47BB, 0x4A71, 0x4817, 0x4759, 0x47F5, 0x4AAA, 0x4B36, 0x4B4B, 0x45DE, 0x45D1, 0x459B, 0x4C93, 0x4CE8) — the actual For/While/Repeat/End/Return bodies. The default For step uses _OP2Set1 and the loop variable is stored via _MovFrOP1; End re-seeds the parse cursor from the loop-record’s saved position. [confirmed dispatch chain into page 0x33; exact FPS record byte layout not yet field-mapped — see residual]
  • if_else_skip_handler (38:5826)’s prologue calls _DeallocFPS1 then restore_982c_ctx (38:58DF, which sets pTempCnt/cleanTmp) — FPS bookkeeping consistent with pushing/popping a loop frame. [hypothesis]
  • A trace-backed performance trap exists when For( omits its optional closing ) and the first loop-body statement is a single-line false If: the implicit-close path at 02:5676 interacts with the false-If skip path and repeatedly advances temporary parser storage. See TI-BASIC For( optional paren trap.

Goto / Lbl: goto_lbl_name_scanner (38:4870) + scanner at 38:7600 [confirmed]

  • Lbl/Goto use a name scanner. goto_lbl_name_scanner (38:4870) reads the label name after tGoto(D7)/tLbl(D6): it advances over the (possibly 2-byte) label token(s) until EOL ('?'=0x3F) / end, records the position in parsePtr, then does a cross_page_jump(0x14) to the search routine. Token compares for tLbl(D6) at 38:4870 and 38:7626; tGoto(D7) at 38:762A. [confirmed]
  • Goto resolves by rescanning the program body from the top for a matching Lbl name, then setting parsePtr there — the classic TI-BASIC behavior that makes Goto O(program size) and makes Goto out of a loop leak the loop’s stack frame. [inferred — standard, consistent with the rescan call shape]
  • Return/Stop (tReturn=D5/tStop=D9) terminate execution at different scopes: Return exits the current BASIC program and resumes the caller, while Stop exits the whole BASIC program chain back to the homescreen context. CALLSUB/SUBRT and CALLSTOP/STOPSUB are run-confirmed fixtures for those two cases. [confirmed]

5. I/O commands

The display primitives live on pages 01/04/37; the command (token) handlers that parse arguments live mostly on page 0x02 (the TI-BASIC command-execution page) and page 0x39, reached from the page-38 evaluator via cross-page jump (RST2/bjump). Token-compare sites located by ROM scan:

CommandTokenHandler site(s)Display primitive used
DisptDisp=DEdispatch → _Disp (37:51D3), bcall site 38:45A4_Disp, _NewLine, _DispDone
Output(tOutput=E038:6AE6 (CP E0), 02:673E, 01:7D3D_OutputExpr (03:4AF2) at row,col
InputtInput=DC02:54EF, 02:56AB, 02:5917, 01:7DEFprompt + entry-line editor + _ParseInp of typed text
PrompttPrompt=DD02:562F, 02:5786, 02:590E, 00:4C5Clike Input but auto-labels NAME=?
Menu(tMenu=E638:5A8A (CP E6), 02:555D, 06:4A17_DispMenuTitle (39:4D21) + branch on choice
PausetPause=D802:55E7, 02:6684, 39:6B8E, 3A:7E7Cdisplay then wait for [ENTER] via key loop
getKeytGetKey=AD37:6700 (a token-attribute table, not a keymap), 3A:7E8Anon-blocking _GetKey (bcall 0x4972, page 06); returns keycode→OP1
ClrHome(cmd token)clears text shadow + home cursor_ClrLCDFull / home-cursor reset

Details:

  • Disp / Disp expr_Disp (37:51D3): sets a “text in display” flag (IY+0x0D |= 4), and when the active context is the home/run context (cxCurApp == 'D') it clears graph-style state and cross-page-jumps into the paint routine; otherwise RST5 (bjump) to the generic display path. Numeric results format via _DispOP1A (04:7844) → _CkOP1Real; strings/lists route through their formatters. Each Disp item ends with _NewLine (01:5F4A): curCol=0, and if curRow+1 >= winBtm it triggers scroll, else curRow++. _DispDone (01:69B0) finishes. [confirmed for _Disp/_NewLine]

  • Output(row,col,value_OutputExpr (03:4AF2, cross-page) writes at an absolute (row,col) without scrolling. Handler parses three comma-separated args, range-checks row/col, then calls it. [confirmed]

  • Input / Prompt — these handlers (page 02) drop into the entry-line editor: show the prompt (? for Input, VAR= for Prompt), let the user type, tokenize the input, and feed it back through the parser (_ParseInp) to store into the target variable. The exact argument-parsing order (byte-traced):

    • Input dispatch is 02:54EF (CP 0xDC) and the body entry 02:54F6 reached via 02:641F (CP 0xDC → POP AF; CALL 0x649E; EX DE,HL; JP 0x54F6). Order: (1) check for an optional leading argument — a string/Str/"…" prompt or a (row,col) pair, comma-terminated; (2) parse the single store target variable; (3) print the prompt (? if no custom prompt was given); (4) run the entry-line editor; (5) tokenize + _ParseInp the typed text; (6) _MovFrOP1/store into the target. With no args at all, Input pauses on the graph screen with a free-moving cursor.
    • Prompt dispatch is 02:562F (CP 0xDD) → 02:6699. It is a loop over a comma-separated variable list (02:6699 LD DE,1; … ; 02:66BF CALL 0x1942; CP 0x04; JR NZ,error — each list item must classify as a storable real/var, type 4). For each variable: resolve its name (02:66AC CALL 0x1DF3 then cross-page CALL 0x3A89), auto-print “NAME=”, run the editor, parse the typed value, store it, then advance to the next comma item. [confirmed token sites + loop/validation bytes; entry-line editor internals dense]
  • Menu( — dispatched on page 02 at 02:555D (CP 0xE6, → handler pointer LD HL,0x6A16; JP 0x5676). Argument order: (1) parse the title string argument; (2) then parse (option-string, Lbl-name) pairs, up to 7. _DispMenuTitle (39:4D21) draws the title; the handler stores each branch-target Lbl, draws the option rows, blocks for a key, and on selection performs a Goto-style jump to the chosen Lbl. Token site also 38:5A8A. [confirmed]

  • Pause — displays (optionally an expression), then spins in a key-read loop until [ENTER]; Pause expr,N (2.55MP) scrolls a list/matrix. Sites at 02:55E7, 39:6B8E. [confirmed]

  • getKey — non-blocking: reads the current key and returns its code in OP1 (0 if none). Used as a value inside expressions, so it’s wired as an operand token (tGetKey=AD) in the evaluator, not a statement. The keycode read itself is the OS system call _GetKey (bcall 0x4972 → page 06 06:491E); the per-key numeric codes returned are the standard TI kXxx constants (e.g. kRight=1, kLeft=2, kUp=3, kDown=4, kEnter=5, kClear=9, k0..k9 = 0x8E…). 37:6700 is a fixed-width token-attribute / opcode-template table keyed by token, which Ghidra renders as code. Byte-decoding it (FE AD 1C 1B 18 EC 31 00 84 …) shows it begins CP 0xAD (tGetKey) / CP 0x55 / CP 0x54 and continues as records keyed by token (FE xx 1-byte, FB xx/FC xx/F4 89 2-byte tokens — getKey, stat/distribution and finance tokens), used by a (de)tokenizer/compiler rather than as a key→code map. The keycodes a getKey returns come from _GetKey on page 06, not this table. [confirmed: 37:6700 is a token-descriptor table; keycodes come from _GetKey on page 06]

  • ClrHome — clears the home-screen text shadow and resets the cursor to (0,0). [inferred — standard]

The _RunIndicOn/Off (01:6518/6531) busy indicator runs during execution: _RunIndicOn sets indicBusy=0xF0, indicCounter=1, enables interrupts, sets IY+0x12 |= 1. The interpreter turns it on while a program runs and off at Done. [confirmed]


6. Token dispatch & precedence — summary [confirmed]

  1. parse_cur_tok fetches a token at parsePtr.
  2. chk_tok_end (38:72E0) classifies it into a small class number (<=3 operand/expr, 4 = syntax error, others = operator/command). Flagged tokens reclassify via set_split_rows (ram:20A0) when IY+9 & 0x80.
  3. parse_cur_err_illegal (38:70F8) validates the current token; its caller (at 38:6FBE) then maps the token byte to a grammar/precedence class — tokens ≥0xF2 get +0x12 (38:6FBE: ADD A,0x12), folding the high token page into the class space.
  4. The precedence level (cVar4 = 1/2/3) selects the production handler base: 0x4000 (base term — the flat handler-pointer table, indexed by token class), 0x478C (postfix ^/!), or 0x7175 (leaf) — 0x478C and 0x7175 are raw code targets inside parse_eval_expr (not defined functions in the live DB), whereas 0x4000 is the pointer table itself. Nesting these realizes precedence.
  5. Binary ops fold operands via FP RSTs (RST 30h _FPAdd; _FPMult=00:238B, …) / _BinOPExec, leaving the result in OP1.
  6. Variable tokens become an OP1 name (type byte + name) and resolve via _FindSym/_RclVarSym (doc 05); store targets (→VAR) resolve through the 38:7600 name scanner (handles [A]/L1/Str1/Y-var/Ans classes, _JError(0x8F) on an attempt to store into Ans).
  7. Statement separators (: and EOL 0x3F) end a statement; the loop re-enters for the next.

The sub-tables 38:5110/38:5127 (doc 07) are small token-class lookups (38:5110 keys off tDisp(DE)/tOutput(E0) region; 38:5127 is a paired classifier) that the dispatch consults; both tail into RST5 (bjump) handlers.


7. Confident addresses (space:addr → name)

38:5987   _ParseInp                  ; parse/eval entry line or formula
38:5ab3   parse_eval_expr            ; recursive-descent statement/expr core
38:59c5   eval_stmt_entry            ; statement-loop variant (shared loop label 38:59C8)
38:5826   if_else_skip_handler       ; Else-branch skip via block matcher
38:6f63   if_isg_stmt_handler        ; per-statement If/IS>( dispatch
38:4130   blockmatch_end_else        ; nest-counting End/Else scanner
38:4180   parse_scan_tokens          ; skip-to-delimiter (2-byte aware)
38:4870   goto_lbl_name_scanner      ; scan label name, jump to search
38:7600   store_target_name_scanner  ; →VAR store-target name scanner (inferred; live DB auto-name is set_tblgraph_draw_xpage)
38:72da   parse_cur_tok
38:7248   parse_advance
38:5cd8   parse_expect_or_err
38:5b7b   parse_init
38:758a   _Find_Parse_Formula        ; FindSym + parse stored formula / run prog
38:6251   _StoAns
38:679f   _RclAns
38:74b7   _AnsName                   ; OP1 = Ans name (class 0x72)
38:623b   _StoSysTok
38:683e   _RclSysTok
37:51d3   _Disp                      ; home-screen text display
03:4af2   _OutputExpr                ; Output( absolute row,col
04:7844   _DispOP1A                  ; format+display OP1 (real)
39:4d21   _DispMenuTitle             ; Menu( title bar
01:5f4a   _NewLine                   ; cursor newline + scroll
01:69b0   _DispDone                  ; finish a Disp
01:6518   _RunIndicOn
01:6531   _RunIndicOff
01:66e5   _GetTokLen                 ; token byte-length (editor)
01:66ea   _Get_Tok_Strng             ; token -> display string (editor)

I/O command token-handler sites (page 02 = the command-exec page; dispatched from the page-38 evaluator via cross-page jump):

02:54ef   input_cmd_handler  (tInput DC)
02:562f   prompt_cmd_handler (tPrompt DD)
02:555d   menu_cmd_handler   (tMenu E6)
02:55e7   pause_cmd_handler  (tPause D8)
02:673e   output_cmd_handler (tOutput E0)
38:6ae6   output_dispatch    (CP E0 in evaluator)

8. Dynamic confirmation and traceable examples

The current headless trace workflow confirms the parser/evaluator path with both homescreen input and stored programs. A full-range TilEm trace of 2+3 on the homescreen reaches page-38 parser functions including eval_stmt_entry (38:59C5), parse_eval_expr (38:5AB3), parse_init (38:5B7B), parse_advance (38:7248), chk_tok_end (38:72E0), and _StoAns (38:6251).

The stored-program samples in tools/tibasic-samples/ are generated with tools/tibasic_samples.py --write-dir tools/tibasic-samples. Each sample has readable .bas, hex-text .tok, and loadable .8xp forms. The .8xp validation traces below were run on 2026-06-06/07 against OS 2.55MP in tools/rom.bin with a local headless TilEm patch that loads command-line .8xp files before running the macro; the traces therefore include startup link-transfer code as well as interpreter execution.

Hello world

ClrHome
Disp "HELLO, WORLD"

Body bytes:

E1 3F DE 2A 48 45 4C 4C 4F 2B 29 57 4F 52 4C 44 2A 3F

Observed run: HELLO.8xp displays HELLO, WORLD and then Done. The trace hits eval_stmt_entry, parse_refill, parse_advance, and _Disp (37:51D3).

Factorial loop

Prompt N
1->F
For(I,1,N)
F*I->F
End
Disp F

Body bytes:

DD 4E 3F 31 04 46 3F D3 49 2B 31 2B 4E 11 3F 46 82 49 04 46 3F D4 3F DE 46 3F

Observed run: FACTOR.8xp with prompt input 5 displays N=5, result 120, and then Done. The trace hits eval_stmt_entry, page-38 parser/refill paths, _FPMult (ram:238B), and _Disp (37:51D3).

List/data manipulation

{3,1,4,1,5}->L1
SortA(L1)
cumSum(L1)->L2
sum(L1)->S
Disp L1
Disp L2
Disp S

Body bytes:

08 33 2B 31 2B 34 2B 31 2B 35 09 04 5D 00 3F E3 5D 00 11 3F BB 29 5D 00 11 04 5D 01 3F B6 5D 00 11 04 53 3F DE 5D 00 3F DE 5D 01 3F DE 53 3F

Observed run: DATA.8xp displays sorted L1 as {1 1 3 4 5}, cumulative L2 as {1 2 5 9 14}, sum 14, and then Done. The trace hits 2-byte/list paths including resolve_2byte_var2, chk_list_type, store_list_elem*, list_var_index, and list_fold_dispatch (02:6104).

Asm( smoke test

Safe Asm( tracing uses a program that returns immediately:

AsmPrgm
C9

C9 is Z80 RET. A BASIC wrapper can show that control returns to TI-BASIC:

Disp "BEFORE"
Asm(prgmASMRET)
Disp "AFTER"

Raw bodies:

ASMRET:  BB 6C 3F 43 39 3F
ASMCALL: DE 2A 42 45 46 4F 52 45 2A 3F BB 6A 5F 41 53 4D 52 45 54 11 3F DE 2A 41 46 54 45 52 2A 3F

Asm( is the 2-byte token BB 6A; AsmPrgm is BB 6C; the displayed prgm prefix in Asm(prgmASMRET) is the program-name token 0x5F, followed by the name characters and the closing ) token. Observed run: loading ASMCALL.8xp and ASMRET.8xp displays BEFORE, executes Asm(prgmASMRET), displays AFTER, and then Done. The trace shows the Asm( handler parse the prgmASMRET token stream, bcall _ExecutePrgm (4E7C, target 07:5758), compile/copy the AsmPrgm body, hand off through 07:57B4, and execute the payload byte itself at ram:9D95 op=0x000000C9, returning to BASIC immediately after. This _ExecutePrgm route is the ASM executor; it is not the ordinary BASIC prgmNAME subprogram path.

ASMBRIDG.8xp demonstrates a cooperative ASM-directed BASIC callback that routes through a BASIC variable. The wrapper runs:

Disp "BEFORE"
Asm(prgmASMSIG)
If Ans
prgmZZBASIC
Disp "AFTER"

with ASMSIG.8xp:

AsmPrgm
EF9B41EFBF4AC9

and target:

Disp "CALLED"

The payload bytes are rst 28h; .dw 419B (_OP1Set1), rst 28h; .dw 4ABF (_StoAns), ret. Observed run: loading ASMBRIDG.8xp, ASMSIG.8xp, and ZZBASIC.8xp displays BEFORE, CALLED, AFTER, then Done. The trace hits the AsmPrgm payload at ram:9D95, _OP1Set1 (00:1B38), _StoAns (38:6251), _AnsName (38:74B7) during If Ans, and the ordinary prgmZZBASIC body evaluator path (38:691038:691438:778F). This is the practical callback convention: ASM returns a value through a BASIC variable, and BASIC performs the actual program call.

ASMRTN.8xp demonstrates the same Ans convention as a numeric return value:

Asm(prgmASMVAL)
Ans+3->A
Disp A

with ASMVAL.8xp:

AsmPrgm
EFA741EFBF4AC9

The payload bytes are rst 28h; .dw 41A7 (_OP1Set2), rst 28h; .dw 4ABF (_StoAns), ret. Observed run: loading ASMRTN.8xp and ASMVAL.8xp displays 5, then Done. The trace hits ram:9D95, _OP1Set2 (00:1B50), _StoAns (38:6251), _AnsName while evaluating Ans+3, _FPAdd, and _Disp. This confirms that Ans is a practical scalar return channel from an AsmPrgm back into BASIC.

Animation, graphing, and BASIC subprogram calls

Additional generated fixtures extend coverage beyond arithmetic/list samples:

ClrHome
For(I,1,8)
Output(1,I,"X")
End
Disp "DONE"

Observed run: ANIMTXT.8xp displays a row of X characters, DONE, and then Done. The trace hits _OutputExpr (03:4AF2), page-38 parser/loop paths, _Disp, and LCD text routines.

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 a diagonal line visible. The trace hits _GrBufClr, _StoSysTok, _ILine (04:4029), graph_pixel_op, _IPoint, and _PDspGrph (04:7904).

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

Observed run: GRAPHDFS.8xp draws the DFS sample’s four-node topology on the graph screen: nodes 1, 2, 3, and 4 with edges 1-2, 1-3, and 2-4. The trace hits _ILine (04:4029), graph_pixel_op, _IPoint, small-font glyph rendering, _PDspGrph (04:7904), _StoSysTok window stores, _RestoreDisp, and page-38 statement evaluation.

GRAPHLST.8xp draws the same topology from list data. It stores edge endpoint coordinates in L1L4, stores node centers in L5 and L6, then loops over those lists:

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: the final frame matches the GRAPHDFS topology and passes the same node/edge crop-region checks. The trace hits list_var_index, _GetLToOP1, _ILine, _IPoint, _PDspGrph, and _StoSysTok, confirming a list-driven graph visualization path rather than only hard-coded draw coordinates.

0->A
prgmSUBRT
Disp A

with 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 BASIC calling convention: caller and callee share variables, and Return resumes the caller. The trace hits VAT/name lookup, parser entry/refill paths, the program-body evaluator call at 38:6914 into eval_eqn_recursive (38:778F), shared A store/recall, and _Disp.

ABICALL.8xp and ABISUB.8xp exercise more of that convention:

{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: the caller displays 11, {2 4 9}, 11, then Done. The callee reads the caller’s Ans and L1, writes shared scalar A, mutates shared L1, leaves Ans as 11 by evaluating A, and returns. The trace hits the BASIC subprogram body path plus _AnsName and list store paths, so this is a confirmed scalar/list/Ans ABI fixture.

CALLSTOP.8xp and STOPSUB.8xp cover the non-returning subprogram case:

Disp "BEFORE"
prgmSTOPSUB
Disp "AFTER"

with callee:

Disp "STOP"
Stop

Observed run: the caller displays BEFORE, the callee displays STOP, and the homescreen displays Done; caller text AFTER does not appear. The smoke runner checks the BEFORE, STOP, and Done regions, plus a bounded low-pixel region where AFTER would appear if execution resumed. The trace reaches the BASIC subprogram body path and _Disp, so this confirms that Stop terminates the whole BASIC program chain instead of returning to the caller.

The ordinary BASIC subprogram path is separate from Asm(. In the validated trace it does not hit _ParsePrgmName, _ExecutePrgm, _Find_Parse_Formula, or _SetParseVarProg; it uses the page-38 parser/VAT/body evaluator path and then either resumes the caller through BASIC’s own Return handling or exits the caller chain through Stop.

ASMFIND.8xp and ZZFIND.8xp prove the narrower ASM-side lookup case. The payload builds OP1={ProgObj,"ZZBASIC"} and bcalls _ChkFindSym; the trace hits ram:9D95 and findsym_scan, then the BASIC wrapper displays AFTER and Done. ZZBASIC’s CALLED line does not display, so lookup is confirmed but execution is not.

ASMPARSE.8xp and ZZPARSE.8xp turn that boundary into a negative fixture. The payload uses the same OP1={ProgObj,"ZZBASIC"} setup as ZZFIND, but bcalls _ParseInpLastEnt instead. The trace reaches _ParseInpLastEnt, _ParseInp, parseinp_find_setup, findsym_scan, parse_init, and eval_stmt_entry; the final screen is ERR:INVALID / 1:Quit / 2:Goto, and ZZBASIC’s CALLED line does not display.

ASMFORM.8xp and ZZFORM.8xp turn the _Find_Parse_Formula boundary into the same kind of generated negative fixture. The payload again builds OP1={ProgObj,"ZZBASIC"}, but bcalls _Find_Parse_Formula (4AF2, target 38:758A). The trace reaches ram:9D95, _Find_Parse_Formula, parse_init_findsym, findsym_scan, and eval_stmt_entry; the final screen is ERR:UNDEFINED / 1:Quit / 2:Goto, and ZZBASIC’s CALLED line does not display.

The full CALLSUB smoke trace does hit _ParseInpLastEnt/_ParseInp once, because the macro starts the program by submitting prgmCALLSUB from the homescreen. That launch parse resolves the top-level program and seeds the private parser RAM. It does not make _ParseInpLastEnt a reusable ASM-to-BASIC ABI: ASMPARSE shows that calling _ParseInpLastEnt with only OP1={ProgObj,"ZZBASIC"} reaches ERR:INVALID instead of the target program.

The relevant page-38 evaluator transition is private state, not a bcall ABI: stmt_eval_body_entry (38:6910) calls the token scanner, then call_eval_eqn_recursive (38:6914) directly calls eval_eqn_recursive (38:778F). At the first observed hit in the CALLSUB trace, the parser cursor/end, OPS/temp-stack pointers, OP1, stack depth, and IY parser flags are already live. This is why ASMFIND can successfully _ChkFindSym a BASIC program name, but ASMFORM still reaches ERR:UNDEFINED instead of running that program through _Find_Parse_Formula.

The _ParseInpLastEnt fixture narrows the parser-entry boundary further: _ParseInp variants are not byte-stream program-call ABIs; they expect parser/FPS stack state that a live BASIC caller has already established.

The forced-command/edit-buffer path is another boundary, not an ABI. A temporary AsmPrgm that calls _JForceCmd(kEnter) reaches _JForceCmd (00:0747) but does not return to the BASIC wrapper; the final screen repeats the wrapper’s first line and Done instead of reaching the wrapper’s following statement. The reason is visible in page-0 disassembly: _JForceCmd reloads SP from the OS command-loop state at 85BC. A second payload that calls _PutTokString (06:46FD) for the bytes of prgmZZBASIC returns to BASIC, but only renders/inserts token text; combining _PutTokString with _JForceCmd still never displays the target program’s CALLED line. _rclToQueue (06:5F29) is an edit-buffer queue helper guarded by rclFlag.enableQueue, not a standalone program executor.

_ExecuteNewPrgm (00:265F) is also stateful. An AsmPrgm probe with OP1=ProgObj and HL → "ZZBASIC",0 enters _ExecuteNewPrgm and findsym_scan, then reaches ERR:SYNTAX. The same probe with ZZBASIC loaded as ProtProgObj and OP1=06 gets through the copy/jump tail at 00:268A and 00:268F, but still stops at ERR:SYNTAX without displaying the target program’s CALLED line.

{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-10*C->L3(I)
End
C->L3(6)
Disp L3
Disp L3(6)

Observed run: BIGADD.8xp displays the low-end digits of L3 as {0 1 1 1 1 ...}, then an explicit carry line 1, then Done, representing 12345 + 98765 = 111110 in little-endian decimal digits. The trace hits list_var_index, _AdrLEle, _GetLToOP1, _PutToL, store_list_elem*, fnint_body, _FPDiv, _FPAdd, _FPSub, and _FPMult.

{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-10*C->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, representing 123 * 45 = 5535 in little-endian decimal digits. The trace hits nested For( loop parsing, list element reads/stores, _FPMult, _FPAdd, _FPSub, _GetLToOP1, and _PutToL.

{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: DFS.8xp displays traversal 1, 3, 2, 4, then visited flags {1 1 1 1}, then Done. The trace hits nested control-flow scanners (blockmatch_end_else, parse_scan_tokens), eval_stmt_entry, parser refill/advance, and list stack read/write paths (list_var_index, _AdrLEle, _GetLToOP1, _PutToL, store_list_elem*).

See TI-BASIC programming patterns for performance rules and larger source-level examples.


9. Argument handling, loop dispatch, and getKey

Three argument-handling and dispatch details, grounded in the bytes (see §5 / §4):

  • Input/Prompt/Menu argument order (§5). Input (02:54EF54F6): optional leading prompt-string or (row,col) → single store var → editor → parse → store. Prompt (02:562F6699): loop over comma-separated type-4 storable vars, each “NAME=” → editor → parse → store. Menu( (02:555D): title string, then up to 7 (option-string, Lbl) pairs, then key-select → Goto-style jump.
  • For/While/Repeat/End dispatch (§4). Execution handlers live on page 0x33 (jump table 33:4381, entered via bcall 0x5140/0x513D = 33:435F from the page-0x02 dispatcher at 02:54BD/02:6400), not page 0x38. End re-seeds the parse cursor from the loop record’s saved top position.
  • getKey 37:6700 (§5). A fixed-width token-attribute / opcode-template table keyed by token (FE/FB/FC/F4-prefixed). The keycodes a getKey returns come from the OS _GetKey system call (bcall 0x4972, page 06), which returns the standard kXxx constants.

Residual (genuinely unverified — would need deeper page-0x33 paged tracing):

  • The exact byte layout of the For/While/Repeat loop-control record on the FPS (field order/sizes for loop var, limit, step, and saved parsePtr) is not yet field-mapped; only the dispatch chain into the 33:4381 handlers is confirmed.
  • The page-0x02 Input/Prompt entry-line editor internals (cursor/redraw, 2.55MP multi-line) remain dense and are only confirmed at the argument-parse boundary.

TI-BASIC programming patterns

TI-84 Plus OS 2.55MP — feature deep dive.

This page turns the interpreter traces into practical programming rules. It is a map from common TI-BASIC patterns to the OS paths they exercise.

Confidence follows Conventions: [confirmed] = observed in the disassembly or the headless TilEm traces; [standard] = matches TI-BASIC semantics and the traced interpreter shape; [hypothesis] = useful pattern not yet traced end to end in this repo.


Performance rules from traces

  1. Keep parser work out of hot loops. Every statement re-enters the page-38 evaluator (eval_stmt_entry, parse_refill, parse_advance, chk_tok_end). Tiny loop bodies can still spend most of their time walking tokens and rebuilding temporary parse state. The For( optional-paren trap is the sharpest example: with a first-line false If, dropping the closing ) made an N=100 benchmark grow from 521,723 to 885,912 marker-to-marker instructions. [confirmed]
  2. Prefer built-ins for list-wide work. SortA(, cumSum(, and sum( cross into OS routines that run one parser setup and then loop internally. The DATA.8xp trace hits list_fold_dispatch (02:6104) for sum( rather than reparsing an explicit BASIC accumulator loop for every element. [confirmed]
  3. Cache list elements and dimensions. List indexing resolves a variable name through the VAT, checks type/dimension, computes an element address, and shuttles a 9-byte TIFloat through OP registers. Repeated L1(I) inside a loop is much more expensive than storing the element into a scalar once when the value is reused. [confirmed path, standard rule]
  4. Avoid Goto in hot loops. Goto searches for a matching Lbl by scanning the program token stream, and escaping structured loops through Goto can leave loop bookkeeping behind. Use For(/While/Repeat plus End unless the jump is truly cold. [standard; scanner confirmed in sub-tibasic.md]
  5. Batch display and graph output. Disp and Output( reach display primitives and LCD update paths; graph drawing reaches graph-buffer and pixel routines before display copy. Draw into the graph buffer and call DispGraph once when possible. [confirmed]
  6. Write the optional syntax in loops. Closing For( with ) costs at most a small command-finalization path, but it avoids the pathological implicit-close/false-If interaction. [confirmed]

Trace-backed cost map

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.

Evidence manifest

This branch keeps each claimed behavior tied to a runnable fixture or a negative probe trace. The visualization fixtures render visible output and pass first-to-final changed-pixel checks plus named crop-region checks for text, axes, circle arcs, nodes, and edges. ANIMTXT also has a distinct-frame threshold, so the animation fixture must show multiple captured LCD states rather than only a final still. The smoke runner also checks final-screen regions for the main text, list, ASM interop, arbitrary-precision, and DFS outputs. The full tools/tibasic_smoke.py suite also passed on 2026-06-07 against the current branch state.

Goal areaFixture or probeEvidence
Hello worldHELLO.8xpDisplays HELLO, WORLD, then Done; reaches page-38 statement parsing and _Disp.
FactorialFACTOR.8xpPrompt input 5 displays 120; reaches loop parsing and _FPMult.
Data manipulationDATA.8xpSorts, cumulatively sums, and displays list data; reaches list element stores and sum(’s list fold path.
Text animationANIMTXT.8xpMoves/writes X characters with Output(, then displays DONE; reaches LCD text routines each loop.
Graph drawingGRAPHV.8xpRenders DFS, axes, a circle, and diagonal line on the graph screen; reaches _ILine, _IPoint, and _PDspGrph.
Graph visualizationGRAPHDFS.8xp, GRAPHLST.8xpRenders the four-node DFS topology with labels and edges; the list-driven fixture stores edge/node coordinates in lists and loops over them before DispGraph.
Arbitrary precision arithmeticBIGADD.8xp, BIGMUL.8xpAdds and multiplies digit lists with carry propagation; reaches list indexing and FP helper paths.
DFS / stack-style list algorithmDFS.8xpDisplays traversal 1, 3, 2, 4 and visited list {1 1 1 1}; reaches nested scanner/control-flow paths.
BASIC subprogram calling conventionCALLSUB.8xp + SUBRT.8xp; ABICALL.8xp + ABISUB.8xp; CALLSTOP.8xp + STOPSUB.8xpCaller and callee share scalar/list/Ans state; Return resumes the caller, while Stop terminates the caller chain.
BASIC to ASMASMCALL.8xp + ASMRET.8xpAsm( runs an AsmPrgm payload (C9) and returns to BASIC, displaying BEFORE then AFTER.
ASM-directed BASIC callbackASMBRIDG.8xp + ASMSIG.8xp + ZZBASIC.8xpASM sets Ans=1 with _OP1Set1/_StoAns, returns, and BASIC calls prgmZZBASIC through If Ans.
ASM return valueASMRTN.8xp + ASMVAL.8xpASM sets Ans=2 with _OP1Set2/_StoAns; BASIC reads Ans, computes Ans+3, and displays 5.
ASM-side BASIC lookupASMFIND.8xp + ZZFIND.8xp + ZZBASIC.8xpAsmPrgm can build OP1={ProgObj,"ZZBASIC"} and reach findsym_scan, then return to BASIC without running ZZBASIC.
Direct ASM to BASIC executionASMPARSE.8xp + ZZPARSE.8xp + ZZBASIC.8xp; ASMFORM.8xp + ZZFORM.8xp + ZZBASIC.8xp; probes_ParseInpLastEnt reaches parser setup but ends at ERR:INVALID; _Find_Parse_Formula reaches parser/find setup but ends at ERR:UNDEFINED; _ExecuteNewPrgm, _JForceCmd, _PutTokString, and _rclToQueue do not prove a standalone callable BASIC-program ABI.

Run-confirmed fixtures

The generator tools/tibasic_samples.py now emits these additional trace-ready fixtures.

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]

BASIC subprogram calling convention

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 parser RAM has already been populated:

RAM stateAddressRole in the private parser frame
basic_prog9652current OP1-style program/object name
basic_start965Bfirst token byte after the stored program size word
nextParseByte965Dcurrent parser cursor
basic_end965Fparser end pointer
numArguments9661argument count/state byte used by parser helpers
chkDelPtr3 / chkDelPtr4981C / 981Etemporary VAT/data pointers used during name and object setup
FPS / OPS / pTemp / progPtr9824 / 9828 / 982E / 9830live 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/copy the AsmPrgm body and hand off through 07:57B4, execute the payload byte at ram:9D95 op=0xC9, and return to BASIC. [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 ram:9D95, _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 ram:9D95, _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 ram:9D95.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; forced-command/edit-buffer probes did not call the target BASIC program.

ASM to BASIC

Direct ASM-initiated BASIC program execution is not yet run-confirmed in this repo. Two easy-looking bcalls 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 more OS state than just 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]

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 ram:9D95 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 ram:9D95, _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 ram:9D95 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 probes; _rclToQueue role from disassembly]

_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 current open item is therefore precise: trace a small ASM payload that successfully invokes a BASIC program, identify the required parser/VAT/error state, and compare it to the rejected public routes above plus both confirmed paths: Asm(_ExecutePrgmram:9D95, and BASIC prgmNAME38:6914/38:778F program-body evaluation.

TI-BASIC dynamic tracing

TI-84 Plus OS 2.55MP — feature deep dive.

TI-BASIC behavior in these notes is grounded by generated programs, headless TilEm runs, resolved instruction coverage, and screen captures. This page is the book-facing recipe for reproducing those traces; the lower-level tooling details live in tools/dynamic-tracing.md.

Fixture suite

The fixture generator emits readable source, token bytes, and .8xp link files:

tools/tibasic_samples.py --write-dir tools/tibasic-samples

The smoke runner executes the exported programs, records a GIF, extracts the final frame, resolves trace coverage through tools/tilem_trace_resolve.py, and checks each case’s expected anchors:

TILEM=~/Git/tilem-headless/result/bin/tilem2
tools/tibasic_smoke.py --tilem "$TILEM" --rom tools/rom.bin \
  --out-dir /tmp/tibasic-smoke-full

Use --case NAME to run a subset, and --keep-trace when the raw binary trace is needed for instruction-level inspection. The runner deletes trace files by default because several cases produce hundreds of MiB per run.

The full suite passed on 2026-06-07 with the command above against OS 2.55MP and the local patched TilEm runner. The output directory kept final PNGs, GIFs, and coverage text for all cases; raw trace files were deleted by default.

The current headless workflow relies on a local TilEm patch that loads command line .8xp files before the macro starts. Without that patch, load the target programs into calculator RAM first, then run the same macro and resolver steps.

Operation coverage

CaseProgram(s)Operations exercisedAnchor examples
helloHELLO.8xpClrHome, Disp, string scan, Doneeval_stmt_entry, _Disp
factorialFACTOR.8xpPrompt, scalar stores, For(/End, FP multiply_FPMult, _Disp
dataDATA.8xplist literal, SortA(, cumSum(, sum(store_list_elem, list_fold_dispatch
asmcallASMCALL.8xp + ASMRET.8xpBASIC Asm(prgmNAME) into AsmPrgm payload_ExecutePrgm, ram:9D95
asmbridgeASMBRIDG.8xp + ASMSIG.8xp + ZZBASIC.8xpASM return code through Ans, BASIC callback_OP1Set1, _StoAns, _AnsName, eval_eqn_recursive
asmreturnASMRTN.8xp + ASMVAL.8xpASM return value through Ans, then BASIC arithmetic_OP1Set2, _StoAns, _AnsName, _FPAdd
asmfindASMFIND.8xp + ZZFIND.8xp + ZZBASIC.8xpASM-side VAT lookup of a BASIC program without executing itram:9D95, findsym_scan, _Disp
asmparseASMPARSE.8xp + ZZPARSE.8xp + ZZBASIC.8xpASM parser-entry negative probe ending at ERR:INVALID_ParseInpLastEnt, _ParseInp, parseinp_find_setup
asmformulaASMFORM.8xp + ZZFORM.8xp + ZZBASIC.8xpASM formula-parser negative probe ending at ERR:UNDEFINED_Find_Parse_Formula, parse_init_findsym, findsym_scan
animtextANIMTXT.8xptext placement animation with Output(_OutputExpr, _Disp
graphvizGRAPHV.8xpgraph-buffer primitives and DispGraph_GrBufClr, _ILine, _IPoint, _PDspGrph
graphdfsGRAPHDFS.8xpgraph visualization from DFS topology_StoSysTok, _ILine, _IPoint, _PDspGrph
graphlistGRAPHLST.8xplist-driven graph visualization from edge/node coordinate listslist_var_index, _GetLToOP1, _ILine, _IPoint
callsubCALLSUB.8xp + SUBRT.8xpBASIC prgmNAME, shared globals, Returnstmt_eval_body_entry, call_eval_eqn_recursive
callabiABICALL.8xp + ABISUB.8xpBASIC subprogram ABI through Ans, scalar A, and list L1_AnsName, store_list_elem, eval_eqn_recursive
callstopCALLSTOP.8xp + STOPSUB.8xpBASIC subprogram Stop terminates the caller chainstmt_eval_body_entry, call_eval_eqn_recursive, _Disp
bigaddBIGADD.8xplist-digit arithmetic and carry propagationlist_var_index, _GetLToOP1, _PutToL, _FPMult
bigmulBIGMUL.8xplist-digit multiplication, nested loops, carry normalizationlist_var_index, _GetLToOP1, _PutToL, _FPMult
dfsDFS.8xplist-backed stack, nested While/If/Forblockmatch_end_else, parse_scan_tokens, eval_stmt_entry

The visualization cases also enforce visible output by thresholding the final frame and comparing it with the first recorded frame. ANIMTXT, GRAPHV, GRAPHDFS, and GRAPHLST must contain at least 100, 100, 200, and 200 dark pixels respectively, and must change by at least the same number of pixels from first to final frame. ANIMTXT must also produce at least five distinct captured frames. The smoke runner also checks named crop regions. Visual cases check home-screen text, graph labels, axes, circle arcs, and node/edge regions. Text/list cases check important final-screen lines such as HELLO, WORLD, factorial 120, DATA list outputs, BEFORE/CALLED/AFTER, SUB, big-integer digit lists, and the DFS traversal/visited-list output. ASMRTN checks the displayed 5, ABICALL checks the scalar line, mutated list line, returned Ans line, and Done, and CALLSTOP checks BEFORE, STOP, Done, and a bounded low-pixel region where caller text AFTER would appear. ASMFIND checks the wrapper’s BEFORE, AFTER, and Done output plus a bounded low-pixel region where an unexpected third line would appear. ASMPARSE checks the ERR:INVALID, 1:Quit, and 2:Goto error-screen regions. ASMFORM checks the corresponding ERR:UNDEFINED error-screen regions.

For the visual graph cases, the 2026-06-07 run measured 212, 619, 466, and 466 dark pixels, with matching first-to-final pixel changes.

Reading the evidence

Trace anchors prove control reached the relevant ROM path; they do not by themselves prove the final screen looked right. Use both the coverage file and the final PNG/GIF for display or graph claims. This distinction matters for GRAPHDFS, where _ILine and _IPoint coverage only proves drawing routines ran, while the final frame proves the graph-screen topology is visible.

For parser and calling-convention claims, prefer resolved coverage plus a narrow routine trace. For example, the BASIC subprogram case uses the private 38:691038:691438:778F body-evaluator path after the top-level homescreen parse has already seeded parser RAM. That is why the negative ASM-to-BASIC probes in TI-BASIC programming patterns are negative fixtures or probes: they reach useful ROM paths, but they do not display the target BASIC program.

TI-BASIC For( paren trap

TI-84 Plus OS 2.55MP — feature deep dive.

This note explains a TI-BASIC performance trap:

For(I,1,N
If 0
1
End

is much slower than the visually similar:

For(I,1,N)
If 0
1
End

The closing ) is syntactically optional, but it is not performance-neutral when the first loop-body statement is a single-line false If.

Confidence: [confirmed] = measured in TilEm instruction traces or read from ROM bytes; [standard] = consistent with the interpreter structure but not field-mapped down to every loop-frame byte.


Benchmark shape

The test program brackets only the loop with an AsmPrgm marker:

Asm(prgmZMARK)
For(I,1,100)
If 0
1
End
Asm(prgmZMARK)
Disp I

ZMARK is:

AsmPrgm
C9

C9 is Z80 RET. In both traces the marker payload executes at ram:9D95 op=0xC9, so the interval from the first marker RET to the second marker RET excludes boot, link transfer, menu navigation, and the final display.

The compared token streams differ by exactly one byte in the For( header:

For(I,1,25) / If 0:  D3 49 2B 31 2B 32 35 11 3F CE 30 ...
For(I,1,25  / If 0:  D3 49 2B 31 2B 32 35    3F CE 30 ...

D3 is tFor, 11 is tRParen, 3F is EOL, and CE is tIf.

Trace results

All runs completed and displayed the expected final loop variable (26 for N=25, 101 for N=100). Counts are marker-to-marker instruction records and Z80 clock deltas from the TilEm trace.

Loop bodyNFor(... )For(...Delta
If 0 / 125144,805 instr / 1,519,710 clocks156,292 instr / 1,604,282 clocks+7.9% instr / +5.6% clocks
If 0 / 1100521,723 instr / 5,498,347 clocks885,912 instr / 8,862,729 clocks+69.8% instr / +61.2% clocks
If 1 / 125185,032 instr / 1,920,085 clocks179,874 instr / 1,859,796 clocks-2.8% instr / -3.1% clocks

The true-condition control matters: omitting ) is not inherently slower. The large slowdown appears when the omitted close is followed immediately by a single-line false If.

Dispatch difference

The optional close is handled by the page-2 command-finalization gate 02:5676 [confirmed]:

02:5676  LD A,C
02:5677  CP 11h          ; explicit ")"
02:5679  JR Z,56C3h
02:5683  OR A            ; implicit close / statement end
02:5684  JP NZ,2708h     ; syntax/type error for other cases
02:5687  CALL 5675h      ; direct command handler path

The explicit ) path (02:56C3...) calls through the command cleanup path before returning to statement execution. The implicit-close path (C=0) calls the command handler directly. For For(, that handler is the page-2 stub 02:6A30, which calls bcall _grf_435f (33:435F) and indexes the control-flow jump table at 33:4381; the For entry is table index 0x29 - 0x20 = 9, and End is index 0x2A - 0x20 = 10 [confirmed].

That one-byte syntax difference therefore changes the parser state at exactly the point where the For loop frame records the body cursor.

What the slow case does

The marker interval profiler shows that the no-paren/false-If case spends its extra time in name/VAT scanning and pointer walking, not arithmetic:

false N=25, no paren minus explicit paren:
  +18,900 instr  07:565F  findsym_scan
   +3,600 instr  ram:1787      dec_hl_tail_1787
   +1,800 instr  ram:1785      dec_hl_tail_1785
     +900 instr  ram:1784      dec_hl_tail_1784

The parser-cursor writes explain why. In the explicit-paren trace, the single-line false If path repeatedly uses the same temporary parser buffer:

... write nextParseByte=9EA8, parseEnd=9EA8
... restore nextParseByte=9E3B, parseEnd=9E4C
... write nextParseByte=9EA8, parseEnd=9EA8
... restore nextParseByte=9E3B, parseEnd=9E4C

In the omitted-paren trace, the equivalent temporary buffer advances on each iteration:

iteration 1: nextParseByte=9EA7, parseEnd=9EA7
iteration 2: nextParseByte=9EB4, parseEnd=9EB4
iteration 3: nextParseByte=9EC1, parseEnd=9EC1
iteration 4: nextParseByte=9ECE, parseEnd=9ECE
iteration 5: nextParseByte=9EDB, parseEnd=9EDB

Those addresses come from writes to nextParseByte (0x965D) and basic_end (0x965F) inside the marker interval. The advancing high-water mark matches the growing findsym_scan cost: the interpreter keeps allocating/walking temporary expression storage instead of reusing one stable temporary range.

Mechanism

The false single-line If path is the amplifier [standard]:

  1. For( with explicit ) enters the 02:56C3 close/cleanup path before the control-flow handler records the loop continuation.
  2. For( without ) takes the C=0 direct path at 02:5687.
  3. With a first body line of If 0, the loop immediately enters the single-statement false-If skip path (if_isg_stmt_handler at 38:6F63, with skip/temporary-parser work through the page-38 statement evaluator).
  4. In the direct-path case, that skip work is performed with a temporary parse range that advances every iteration. More iterations mean longer VAT/name and pointer scans, so the cost grows much faster than the explicit-paren version.

This is why N=25 shows only a modest penalty while N=100 shows a large one. The omitted ) saves a little cleanup work in the If 1 control, but with If 0 first it changes the loop/skip interaction and leaks work into temporary parser storage.

Practical rule

When a For( loop body starts with a guard like If not(condition) or If 0, write the closing ):

For(I,1,N)
If A
...
End

Better still, avoid single-line false guards as the first statement of a hot loop. Use If ... Then blocks when the body is structured, or invert the loop so the common path does not repeatedly exercise the false-If skip scanner.

08 — Display & LCD

Deep dives: Graphing (graph buffer → LCD, transforms) · Table & Y= Variables (text grid).

The TI-84+ shows a 96×64 monochrome image — the OS only ever drives a 96×64 region (_ClrLCDFull clears all 768 bytes of it: 8 row-passes × 12 columns × 8 data bytes). The underlying controller (Toshiba T6K04 / later Novatek) has wider video RAM (up to 128 px), so 96×64 is the visible area, not the controller’s geometry. It is reached through I/O ports 0x10 (command) and 0x11 (data). The OS keeps a graph/screen buffer in RAM and renders text via a built-in font.

Controller [confirmed against code]

  • port_lcdCmd (0x10): commands — set row, set column, set Y, on/off, contrast.
  • port_lcdData (0x11): read/write a byte of pixels at the current address.
  • The panel is organized in 8-pixel-tall rows; _ClrLCDFull (01:60E4) loads A=0xB8, subtracts 8 each pass and stops at 0x80 → row commands 0xB8 … 0x80 (8 rows of 8 px = 64 px tall), calling _ClearRow per row with interrupts masked (DI). [confirmed]
  • Command bytes (all grounded in _ClrLCDFull/_ClearRow/lcd_set_col_cmd):
    • Row (page) select = 0xB8 − 8·row for row = 0…7 (i.e. 0xB8, 0xB0, … 0x80), sent to 0x10 via lcd_set_col_cmd (01:5A89), which only emits the byte when 0x80 ≤ A < 0xC0 (guards the row/Z-address range). _ClrLCDFull walks this by loading A=0xB8, calling _ClearRow, then SUB 0x8 and looping while A ≥ 0x80 (B=0x80).
    • Column select = 0x20 + col, sent raw to 0x10. _ClearRow (01:6934) walks E from 0x20 to 0x2B (CP 0x2C terminates) = 12 columns (12 bytes × 8 px = 96 px wide), writing 8 data bytes to 0x11 per column — the B=0x08 inner djnz loop writes one byte per pixel row (8 rows). [confirmed]
    • Contrast: lcd_set_contrast (01:5A59) writes the contrast level to the data port 0x11; lcd_get_contrast (01:5A60) reads it back from the controller with IN A,(0x11) (the standard dummy+real LCD read), not a command re-send. The level is also held in RAM at contrast (0x8447); the command-port form (contrast+0x18)|0xC0 is what _LCD_DRIVERON (page 06) and the _GetKey contrast keys send to 0x10. [confirmed]
    • Every port access is preceded by CALL ram:0CC3 (lcd_wait), the controller-busy delay. [confirmed]

Text output [confirmed]

  • curRow/curCol (0x844B/844C) — the homescreen text cursor (16 columns wide; _PutC wraps at col 16, calls _NewLine).
  • _PutMap (01:5A98) draws one large-font character at the cursor: it clamps invalid codes to 0xD0, computes an initial char * 8 offset, then bjumps to the page-7 large-font blitter, which adjusts that offset to the actual 7-byte-stride glyph table before copying an 8-byte render record. [confirmed]
  • _PutC (01:5B4C) = _PutMap + advance cursor + newline handling; _PutS prints a string; _NewLine scrolls.
  • _DispHL (01:5BF6) prints HL as a right-justified 5-digit decimal: repeated _DivHLBy10, digits +0x30, leading zeros → spaces, writing the digits backward from 0x847C into the OP1 scratch area, then _PutC/_PutMap each digit. [confirmed]

Screen buffers [standard]

  • plotSScreen (0x9340, 768 bytes = 96×64/8) — the main graph/back buffer.
  • saveSScreen (0x86EC, 768 bytes) — saved copy (e.g. for menus over the graph).
  • _GrBufCpy (04:60A3) blits plotSScreen to the LCD; _GrBufClr (04:6071) zero-fills the 768-byte graph buffer (LD HL,0x9340; LD (HL),0; LDIR) and does not touch the LCD.

Fonts [confirmed]

  • Large font: glyph table is on page 7, base 07:45FF, with a 7-byte stride per glyph (not 8). _PutMap (01:5A98) clamps the code (0 or ≥0xF80xD0), computes HL = char*8 (three ADD HL,HL), then bjumps via trampoline ram:3B3D to the blitter put_glyph_large (07:4588). The blitter does HL = 07:45FF + char*8, then lgfont_glyph_ptr_adjust (07:45EB) subtracts char (it shifts char*8 right by 3 → char, then SBC HL,DE), yielding the real glyph pointer 07:45FF + char*7. It then copies an 8-byte record via _Mov8B (ram:1A94, 8× LDI) into RAM at 0x845A (lFont_record), which the renderer blits. [confirmed] (The stride is 7 bytes while the copy is 8 bytes — the 8th byte overlaps the next glyph’s first row — so the table packs glyphs at 07:45FF + char*7.)
  • Alternate large fonts: two bits in (IY+0x35) select a replacement glyph source before the page-7 table read — bit 5 loads A=0x01 and calls ram:36E7 (bjump to 3B:7BFB), bit 1 loads A=0x76 and calls ram:3E1F (bjump to 3B:7B9C); both are font-hook routines on page 3B that take the A value as a selector. When neither bit is set, the page-7 table at 07:45FF is used. [confirmed]
  • Small/variable-width font: _VPutMap/_VPutS (graph screen, pixel-addressed via penCol/penRow).

Indicators

  • flags.indicFlags bit 0 = the run/busy indicator (the moving dashes top-right); _ClrLCDFull preserves it across a clear; _RunIndicOn / _RunIndicOff toggle it. [confirmed]

LCD command bytes and glyph table

  • LCD command bytes confirmed by tracing _ClrLCDFull (01:60E4), _ClearRow (01:6934) and lcd_set_col_cmd (01:5A89): row (page) select = 0xB8 − 8·row (range 0xB8 … 0x80, stepping down by 8), column select = 0x20 + col (range 0x20 … 0x2B, 12 columns = 96 px), command port 0x10 / data port 0x11, busy-wait via ram:0CC3. Contrast is held at RAM contrast (0x8447) and written to the data port 0x11 by lcd_set_contrast (01:5A59). See Controller. [confirmed]
  • Large-font glyph table pinned: page 0x07, base 07:45FF, 7-byte stride (put_glyph_large @ 07:4588 → glyph ptr 07:45FF + char*7 via 07:45EB, then _Mov8B copies an 8-byte record to RAM 0x845A). See Fonts and Flash Page Map. [confirmed]

Graphing

TI-84 Plus OS 2.55MP — feature deep dive.

What a college student touches when they press Y=, WINDOW, GRAPH, TRACE, or run a DRAW menu command. This traces the path real-coordinate → screen pixel → plotSScreen → LCD, plus the window variables, Y= equation storage, and DRAW primitives.

Address form is page:addr (flash page hex : logical offset, routines run mapped at 0x4000). Confidence: [confirmed] = read the code/data directly, or cross-checked against equate/convention; [standard] = matches documented TI behavior but not byte-verified here; [hypothesis] = inferred, not yet verified.


1. Window variables (RAM) [confirmed addresses, from ti83plus.inc + code refs]

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
0x9164shortXscratch/divisor float for the X transform (per-pixel ΔX)
0x916DshortYscratch/divisor float for the Y transform (per-pixel ΔY)
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]

deltaX/deltaY are derived from the window when the graph is set up and feed both the forward (real→pixel) and the circle/draw routines. The LCD is 96×64, but the graph area is 95 columns wide (0..94) and 63 tall (0..62), hence the /94 and /62. [standard]


2. Coordinate ↔ pixel transforms

Forward: real coordinate → pixel index

_XftoI (37:41EB) and _YftoI (37:41DF) convert an OP1 real coordinate to a pixel index. Both are thin shims around the shared engine at 37:41F2: [confirmed]

_XftoI (37:41EB):  BC = 0x8E6A (X working float),  HL = shortX (0x9164),  SCF  → 41F2
_YftoI (37:41DF):  BC = Ymin   (0x8F6B),            HL = shortY (0x916D),  OR A → 41F2; INC A

Shared engine 37:41F2 computes $\mathrm{pixel}=\dfrac{\mathrm{value}-\mathrm{min}}{\mathrm{pixelDelta}}$:

  • RST 20h pushes/loads OP1 (the input value),
  • CALL 228F moves the min operand in and subtracts it (value − min),
  • CALL 2385 divides by the per-pixel delta (shortX/shortY),
  • the X path additionally adds the 0x8E73 X-origin term, the Y path negates so that larger Y maps to a smaller row (screen Y grows downward),
  • CALL 4229 clamps/handles the float→integer exponent (reads OP1.exp at 0x8479, bias 0x7F) and rounds to an integer pixel; out-of-range loads ±large sentinel. _YftoI returns pixel row +1 (INC A) so callers get a 1-based / inverted row. [confirmed]

So a student’s function value y at sample x becomes a (col,row) pair via two subtract-then-divide float ops against the window. This is the heart of plotting and TRACE coordinate readout.

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.


3. The graph buffer plotSScreen 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 "set row page" command — the rotated TI panel pages by X
(0x8451) = (0x3F - y) | 0x80   ; LCD "set column" command (Y, mirrored)
returns (table_42E4)[x & 7]    ; the 1-of-8 bit mask within the byte (bit = x mod 8)

This is the bridge from a (x,y) pixel to a byte+bit in the buffer and the matching LCD command bytes. [confirmed]

_IPoint (04:4157): set/clear/test one pixel in plotSScreen. Honors the current pen mode / plot style: reads style at (IY+0x14) and a style selector at 0x9775 (0x9775 = 1 selects the “thick/line connect” branch that draws an extra adjacent pixel; 1..3 select dotted/animated styles), clips against the X-offset (XOffset) and the buffer bounds (_IBounds), then OR/AND/XORs the mask from _IOffset. _PointOn (04:4155) is the plain set-pixel entry. [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]


4. Drawing primitives (page 0x04 / 0x33)

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

_GrphCirc (33:758D) — draws a circle in real coordinates. [confirmed] Allocates a 0x5A-byte FPS scratch frame (EQS), snapshots the working float and the window (Xmin, Ymin, deltaX), zeroes accumulators, seeds the X/Y center and the radius-stepped parametric state, then iterates plotting points via the integer line/point primitives (cross-page into the page-3B _DrawCirc2 plotter at 3B:7171). It accounts for the X/Y pixel-aspect via deltaX/deltaY so a Circle( looks round only after a ZSquare. [confirmed] _CircCmd (33:74CE) is the parser-facing Circle( command wrapper (cross-page jump into the argument grabber). [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.mantissa[0] & 0x20, dispatches set/clear/toggle.
_DrawCmd04:7B8Btop-level DRAW dispatch — grabs the pending count and cross-jumps to the per-command handler.
_DrawZeroOP104: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]


5. Rendering the graph to the LCD

_PDspGrph (04:7904, “possibly-display graph”) is the gatekeeper between buffer and screen. [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 column/row 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) re-evaluates and re-plots every selected Y= equation from scratch (cross-page jump into the plot driver). This is what runs when you change a window var or turn SmartGraph off; SmartGraph (grfModeFlags bit smartGraph) lets the OS skip the re-plot and _GrBufCpy the existing buffer when nothing changed. [confirmed]


6. Y= equations: 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 — selection/style byte values match the TI link-protocol guide]

Parsing / pre-scan

_GraphParseTok (33:5023) walks an equation’s token stream to classify it before plotting: it reads tokens via the paged-pointer reader (_SetupPagedPtr/_PagedGet), recognizes 2-byte tokens (_IsA2ByteTok), and sets feature bits (e.g. token 0xEF… ranges → returns a category in A) used to decide draw mode and whether the equation is graphable. [confirmed]

Evaluation → points

Plotting (driven by _Regraph → the page-04/38 plot loop) walks pixel columns left→right:

  1. compute the real X for the column from Xmin + col*deltaX (the inverse of _XftoI),
  2. store it into the X system variable,
  3. _ParseInp (38:5987) parses+evaluates the selected equation’s tokens against the current X (it resets the parser state, clears a status bit at (IY+0x1F), and runs the formula evaluator _ChkFindSym/Find_Parse_Formula), leaving the result Y in OP1,
  4. _YftoI maps that Y to a pixel row,
  5. _ILine connects this point to the previous column’s point (or _IPoint for dotted style), drawing into plotSScreen. Xres (XresO/Xres_int) controls the column step: Xres=1 evaluates every pixel column, higher Xres skips columns (faster, coarser). [confirmed]

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.

Graph table [confirmed]

_GraphTblFind (33:7097) / _GraphTblNext (33:707A) index the in-RAM table of equation pointers (iMathPtr4-based, 2 bytes/entry) used to iterate the selected functions during a regraph or TABLE build.


7. Graph screen vs. home screen; TRACE

  • The home screen uses the large font and curRow/curCol text cursor (see 08-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 addresses]
  • TRACE moves a cursor along a selected function: it steps the column, evaluates the function (_ParseInp) 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. [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]

8. Confidence summary / open items

  • Forward transform (value−min)/pixelDelta: structure [confirmed] from the 37:41F2 disassembly (subtract 228F, divide 2385); the exact rounding in 4229 is read but the ±sentinel constants are summarized, not exhaustively byte-traced.
  • _HorizCmd/_VertCmd endpoint build: 7933 allocates a 0x24-byte FPS frame, and the commands _Mov9B the window edges (Xmin/Xmax or Ymin/Ymax) plus _MovFrOP1 the line’s coordinate (OP1) into that frame, reading the live window variables only.
  • Circle parametric stepping in 3B:7171 (_DrawCirc2) not decompiled here (lives on page 3B); the _GrphCirc setup is confirmed.
  • Y= selection bit (0x20; flags byte 0x23 selected / 0x03 deselected) and the style byte values (06) are [confirmed] against the TI link-protocol var guide.

Table & Y= variables

TI-84 Plus OS 2.55MP — feature deep dive.

What a college student touches when they enter functions in Y=, configure TBLSET (2nd WINDOW), and read the TABLE (2nd GRAPH) to tabulate Y1(X), Y2(X), … over a range of X. Traces: where the table-setup settings live → how the OS, per X row, sets the X variable, evaluates each selected Y= function through the parser into OP1, formats the result, and lays the values out as a text grid → how Y= equations are stored, selected, and styled → and the Table ↔ Y= ↔ parser ↔ display interaction.

Builds on sub-graphing.md (Y= storage, the regraph/eval path, plotSScreen), sub-tibasic.md (the page-38 parser, _Find_Parse_Formula, _ParseInp), 05-variables-vat.md (EquObj, _FindSym), 08-display-lcd.md (text grid via _PutMap/_PutC), and 11-boot-contexts-errors.md (the context/cxMain mechanism that selects the TABLE editor vs TABLE-setup screens).

Address form is page:addr (flash-page hex : logical offset; flash routines run mapped at 0x4000). RAM addresses are absolute. Confidence: [confirmed] = decompiled/byte-verified here, or multiple consistent signals (flag/token compares, call shape) pin it even where the dense Z80 handler bodies don’t fully reduce in the decompiler; [standard] = documented TI-83+/84+ behavior consistent with what was seen, not byte-pinned here; [hypothesis] = inferred, not yet verified.


0. The three pieces and how they connect

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 · _StoX sets the X system var"]
      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 exact same 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 for structure]


1. Table setup — where the settings live & how they’re read

System variables (RAM TIFloats) [confirmed addresses, ti83plus.inc + code]

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 token+addr]

Mode flags — tblFlags (IY+19 = IY+0x13) [confirmed bit layout]

From ti83plus.inc and verified by the bit-ops below:

BitNameMeaning
4 (0x10)autoFillIndpnt: 0 = Auto (fill X from TblStart/ΔTbl), 1 = Ask (prompt the student 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/edit handler — tblsetup_handler (02:7B20) [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/validation context — tblset_cx_display (37:5F10) [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]


2. Y= equation variables — storage, selection, 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 tokens]

Selection & 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 against the TI link-protocol var guide]

The selected-equation list — iMathPtr4 (0x84D9) [confirmed]

The OS keeps an in-RAM table of 2-byte pointers to the selected equations’ VAT entries, based at iMathPtr4 = 0x84D9. Three bcalls index it (verified by decompile — they compute 0x84D9 + 2·n):

bcallAddrRole
_GraphTblFind33:7097(re)build the list of selected-equation pointers
_GraphTblNext33:707A_LdHLind(0x84D9 + 2·n) — fetch the n-th equation pointer
_grf_706633:7066store a pointer into slot n (0x84D9 + 2·n)

This is the shared iterator the regraph driver and the table builder both walk to visit each selected Yn. [confirmed]

Resolving & evaluating a Y-var — _Find_Parse_Formula (38:758A) [confirmed]

_Find_Parse_Formula (bcall id 0x4AF2) is the universal “find a named var and parse/evaluate its stored formula” entry (see sub-tibasic.md §3). 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]


3. Table generation — the page-05 subsystem

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  (see §3.1)
         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 bytes]

3.1 Seeding the running independent value [confirmed]

05:774B initialises the per-table running X. It first clears a working float (05:774B LD DE,0x8622 ; CALL 0x1920), then at 05:7751 copies TblMin into the running-X slot:

05:7751  LD HL,0x92B3 (TblMin) ; LD DE,0x862B ; JP 0x1A92   ; running-X (0x862B) ← TblMin

i.e. the first row’s independent value is TblStart. A working float at 0x8622/0x862B holds the current row’s X. The row index is bounds-checked at 05:65DC (LD A,(0x91E0); LD HL,0x91DC; CP (HL); RET — current row vs the last row), and 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 student types each X; see §3.4.) [confirmed structure]

3.2 The per-row evaluation: set X, evaluate each selected Y [confirmed]

For each row the recompute fills the cache by, per selected equation:

  1. store the running-X into the X system variable (_StoX, 38:62A3),
  2. evaluate that equation’s tokens against the current X — the table walks the selected list via _GraphTblNext (33:707A) and runs each formula through the page-38 evaluator (_ParseInp 38:5987 / the _Find_Parse_Formula path), leaving Y in OP1 (exactly the grapher’s per-column eval in sub-graphing.md §6),
  3. format OP1 and stash the result string/value into the row’s cache slot.

The fill loop is 05:5EE1: it strides the cell buffer 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]

3.3 The value cache & scrolling [confirmed buffers]

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
0x91DB / 0x91DC / 0x91E0row indices / top-row index / column count
0x91E2 … 0x9221 …the per-cell computed-value buffers (9-byte floats)

05:6014 performs the scroll: LD HL,0x9221 ; LD DE,0x9260 ; LDIR (copy a 0x3F-byte block) and an LDDR shift of a 0xB4-byte region — i.e. when you press ↑/↓ past the cached window it slides the cache and only computes the one new row (or recomputes if reTable). [confirmed bytes]

3.4 Indpnt = Auto vs Ask, Depend = Auto vs Ask [confirmed test sites]

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 (§3.1).
  • Indpnt = Ask (bit4=1): the X column starts empty; the editor prompts the student to type each X, parses it (entry-line editor → _ParseInp), stores it to X, then evaluates the Y columns for only that row.
  • Depend = Auto (bit5=0): Y cells compute immediately during the fill.
  • Depend = Ask (bit5=1): Y cells show blank until the cursor lands on one and [ENTER] requests it, at which point that single cell is evaluated. [confirmed]

3.5 Rendering the grid to the LCD — 05:7E45 (column/row paint loop) [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, 08-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 text buffer (0x9221 vs 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 loop + buffers]

3.6 Split graph-table mode — _ScreenSplit (05:7712) [confirmed]

The G-T mode (graph on the left half, table on the right) is set up by _ScreenSplit (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. So G-T mode shares the very same table cache + running-X driver, rendered into the right columns alongside the plot. [confirmed]


4. What marks the table dirty (reTable = 1) [confirmed sites]

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]


5. End-to-end: a student tabulates 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),
    • _GraphTblFind/_GraphTblNext (33:7097/707A) walk the selected equation list at iMathPtr4 (0x84D9) — here only Y1,
    • per row: _StoX the running-X, evaluate Y1’s tokens via the page-38 evaluator (_Find_Parse_Formula / _ParseInp) → OP1 = X²+1, format and stash into the cell cache (0x91E2/0x9221),
    • 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.

6. Confident addresses (space:addr → name)

; --- 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            ; per-row value-cache fill (0x91E2)
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 cell text buffer 0x9221/0x91E2
05:7712  _ScreenSplit                     ; 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  91DB/91DC/91E0  row idx / row idx / top-row & col count
RAM  91E2 / 9221     per-cell computed-value buffers (9-byte floats)
RAM  8622 / 862B     running independent value (current row's X)

; --- 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                          ; base of selected-equation pointer list
33:7097  _GraphTblFind                    ; (re)build selected-equation list
33:707a  _GraphTblNext                    ; fetch n-th equation ptr (0x84D9+2n)
33:7066  _grf_7066                        ; store n-th equation ptr
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 (per row)
38:67ae  _RclX  / 38:67a4 _RclY / 38:626c _StoY
33:5023  _GraphParseTok                   ; pre-scan equation tokens (graphable?)

; --- 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)

7. Confidence summary / 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 exact per-row _StoX + selected-Y eval calls inside 05:5EE1/the fill loop are [hypothesis] — the loop, buffers, and the shared selected-equation iterator (iMathPtr4 / _GraphTblNext) are confirmed; the individual on-page direct CALLs to _StoX/the evaluator were inferred from the identical grapher per-column path rather than each byte-traced.
  • 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 (entry-line editor → _ParseInp_StoX for a typed X, single-cell Depend:Ask compute) is [hypothesis]: the mode bit tests (05:6D40) are confirmed; the interactive prompt body overlaps the page-02 Input/entry handlers and was not fully reduced.
  • _Find_Parse_Formula’s TblRng (type 0x11) special-case is [confirmed] in the header switch but its full body (cross-page) was not traced here.

Equation display (MathPrint)

TI-84 Plus OS 2.55MP — feature deep dive.

MathPrint is the OS subsystem that turns a tokenized expression into a two-dimensional screen layout. It is used by the homescreen entry line, the Y= editor, the Solver equation line, and the template menus. The implementation is concentrated on flash page 39 and drives the display services described in Display & LCD. It consumes the token stream described in Tokenizer & TI-BASIC and preserves the OP registers described in Floating-point engine.

The useful mental model is a cell-grid typesetter. The OS classifies each token, selects a compact handler record, walks the expression into rows and slots, maps each cell to pixel coordinates, and emits glyphs or graph-buffer rules. The live state is a small RAM block, a few display variables, and ROM tables — a flat cell grid, not a recursively allocated box tree.

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 / 00:3555<br/>rules and rectangles"]

Core state

The layout engine keeps most of its state in 0x85DE..0x85F2. The table below names the fields that matter for reading the page-39 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.
0x85E3..0x85E6saved 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]

Handler records

A visible expression is driven by handler records reached through the class table at 39:5E45. Each class has one word entry:

handler = word(39:5E45 + 2 * 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 Lroot 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]

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]

Layout pass

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 argument walker at 39:5167 manages multi-argument operators. It keeps the parser argument index in 0x85E0 and uses 0x85E2 as the argument count. Normal operands pass through 39:59E0; variable operands pass through 39:59F9. Both routes delegate token scanning to page 7, reusing its single field order. [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 is the tall-template row compositor. eqdisp_layout_main reaches 39:5167 from the action-0x08 window-advance path at 39:50A4 and the action-0x04 drain 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. The result is a row composition around fixed structural cells; the ROM does not use a separate stretch-bitmap routine for the final MathPrint operator form. [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. The +7 loop (DEC B; ADD A,7) builds the high byte and the +(rowHeight+2) loop builds the low byte; the caller stores HL to penCol(0x86D7, low→x) / penRow(0x86D8, high→y), so the two products land as:

$$x\ (\mathit{penCol}) = \mathit{base}_x + \mathit{row} \cdot (\mathit{rowHeight} + 2)$$

$$y\ (\mathit{penRow}) = \mathit{base}_y + 7 \cdot \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.

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 visible fraction bar in the generic expression layout is a rule drawn through the graph-buffer drawing machinery, not a character cell. [confirmed]

Exponents and raised rows

Superscripts are represented as row placement, not as a font attribute. The helper at 39:4CE9 raises classes in the 0x24..0x28 family and class 0x39 by forcing 0x844B to a higher display row before emitting the selected cell. The per-row height accounting then folds that raised row into the parent layout. [confirmed]

This means X^2 is stored and walked as ordinary cells in different rows. The row selection does the work; the glyph for 2 is the ordinary one.

Radicals

The radical mark starts with the fixed large-font Lroot glyph (0x10). Its glyph bytes live at 07:466F, and the root/power records contain the payload cell 00 10 in class 0x2A and class 0x31. [confirmed]

The same records also contain low-byte E=1F cells for related power/root pieces. Those cells follow the ordinary token-string path; they are not the special high-byte D=1F cell form used by the 39:4E8E IX-backed branch. [confirmed]

A tall radical is therefore a composition:

  1. A fixed Lroot glyph supplies the hook and top shape.
  2. The radicand is laid out as a recursive operand window.
  3. 39:5167 advances the operand row window when the radicand or degree spans rows.
  4. Parentheses or other delimiters are chosen around the resulting height when needed.

The root mark is ROM-backed by fixed records and the generic cell emitter. The variable piece is operand placement, not a synthesized root bitmap: 39:5167 owns the recursive row-window composition, while 39:4E8E/39:4F1A and the page-7 glyph blitter emit the fixed structural cells. [confirmed]

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-39 handler cell has been found.

The fixed Lintegral glyph is emitted by the ordinary structural-glyph path: 39:4E8E calls the delimiter classifier, falls through to 39:4F1A, maps the cell to large-font code 0x08, and emits it. [confirmed]

The tall definite-integral layout is a higher-level composition around that glyph:

  1. Place the tall integral glyph on the main axis.
  2. Use 39:5167 to 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 display compositor are both identified. 39:5167 is the ROM routine that turns the measured argument slots into the visible row placement around the fixed integral cell. The final pixels still come from the ordinary output paths: 39:4E8E/39:4F1A for fixed glyph cells, page 07:4588 for the large-font record copy, and the rectangle helpers for rule-like UI surfaces. [confirmed]

Delimiters

The fixed delimiter families are handler records. 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 those cells to output families 61 00..61 09, 60 00..60 09, and AA 00..AA 09. [confirmed]

This covers the fixed delimiter surface. The dynamic choice of delimiter height is part of the same row-window composition used by radicals and integrals: 39:5167 advances or backs the visible argument slot, while the delimiter families themselves remain fixed handler records and page-7 display-byte mappings. [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 FC3C..FC40, FE7D..FE81, 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, 00:3555Draw fraction UI rectangles, boxes, and fixed chrome lines.

The page-7 large-font service copies fixed glyph rows. It does not measure a radicand or stretch a glyph by itself. [confirmed]

Algorithm summary

\begin{algorithm}
\caption{MathPrint layout}
\begin{algorithmic}
\STATE save display flags and OP scratch registers
\FOR{each visible token or template action}
  \STATE classify the token into a layout class
  \STATE load the class handler record from \texttt{39:5E45}
  \STATE read row count, row actions, and row cell counts
  \IF{the selected cell is an operand slot}
    \STATE recurse through the argument walker
  \ELSIF{the selected cell is a descriptor-backed template cell}
    \STATE map descriptor row/column to pixel coordinates
    \STATE emit the descriptor cell or marker action
  \ELSIF{the selected cell is a structural glyph}
    \STATE map it through the direct glyph path and emit the fixed glyph
  \ELSE
    \STATE convert it through the string or display-byte path
  \ENDIF
  \STATE update row, column, and overflow state
\ENDFOR
\STATE restore display flags and OP scratch registers
\end{algorithmic}
\end{algorithm}

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: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-operand emitters used by forward and reverse placement.
39:59E0 / 39:59F9Normal and variable operand emitters.
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.
07:44DEDisplay-byte remapper.
07:4588Large-font fixed glyph blitter.
01:6293_VPutMap small-font pixel output.

Dynamic confirmation

The static map above was checked against live execution. The TI-84 Plus OS was run under headless TilEm, the entry line was driven to render each construct, and page-39 execution was rolled up to function level with tools/tilem_trace_resolve.py --funcs --only-space page_39 (see dynamic tracing). The macros are tools/macros/mathprint-{power,fraction,fnint}.macro. [confirmed]

The trace cleanly separates the two documented rendering mechanisms by which routines actually run for each construct:

Rendered (entry line)Page-39 routines exercisedPath
X^2 (raised exponent)eqdisp_emit_subexpr2 4CA4, eqdisp_menu_or_emit 53ADlight entry-line emit (eqdisp_set_row_for_tok 4CE9 is the static superscript-row helper but did not execute in this trace — it does run in the 1/2 trace)
1/2 (n/d template)eqdisp_compute_dims 69C8, eqdisp_layout_token_geom 68AE, the 683D cell-to-pixel mapper, eqdisp_draw_fraction_bar 6ABF, eqdisp_draw_box_jp 6AF5, eqdisp_load_glyph18b2 6B66, eqdisp_dispatch_token 4A74descriptor / geometry
fnInt( (MATH ▸ 9)eqdisp_emit_glyph 4E8E, eqdisp_map_token_glyph 4F1A, eqdisp_emit_arglist 4DE6, eqdisp_sum_arg_widths 4DCA, eqdisp_emit_digit_chk 4E0Ahandler record / multi-arg

This confirms the headline static result: the descriptor path (69C8/68AE/683D/6ABF) and the handler-record path (4DCA/4DE6/4E8E/4F1A) are mutually exclusive per construct, exactly as the two-mechanism model predicts. [confirmed]

39:5167 (eqdisp_layout_multiarg) statically owns the multi-arg row composition, but it did not execute in this trace: the fnInt( template was inserted empty (∫(0)dV), so the operand-recursion branch was never driven — 5167 and its body (5949/5B10) show 0 hits, and the --funcs “5167” rollup bucket is a nearest-name artifact (only a 51F1/51F3 fragment ran). A filled integrand would be needed to drive 5167 under a trace. [confirmed] (static); the dynamic path is open.

The live state block matches the field map. Reading 0x85DE..0x85F2 from a RAM dump right after each render (memdump … ram-logical):

Field1/2fnInt(Confirms
0x85E8 template kind0x100x000x10 = the fraction descriptor kind; 0x00 = handler-record (no descriptor).
0x85EE/0x85EF fraction width2/20/0Measured numerator/denominator widths (each 1 cell ≈ 2 px wide).
0x85E1 row count4Four rows for the tall integral (upper limit, body, lower limit, baseline).

eqdisp_compute_dims (39:69C8) decompiles consistently: it switches on 0x85E8 & 0x0F, picks descriptor 686F/6880/… by kind, and for the kind-2 measured-fraction path calls eqdisp_draw_box_jp with 0x85EE. [confirmed]

Decompiler caution. The cell-to-pixel mapper 39:683D and the fraction endpoint helper 39:6B1C are named eqdisp_decr_counters / eqdisp_advance_col6 in the symbol table, and the Ghidra decompiler mis-analyzes both (it shows bare decrement loops). The raw disassembly matches this page instead: 683D does A = base; loop: add a,7 into the high byte (y = base_y + 7·colpenRow) and 6B1C does ld a,1Bh; … add a,7 (×n); add a,4 (x_left = 0x1B + 7n, x_right = x_left + 4). Trust z80dasm over the decompiler for these tight register-passing routines, as the README advises. [confirmed]

MathPrint pipeline coverage

The static MathPrint pipeline is recovered: token classification, handler records, recursive operand order, descriptor templates, fraction UI geometry, fixed structural glyphs, display-byte remaps, generic output services, and multi-argument row composition. The row compositor is 39:5167; the pixel emitters are the fixed glyph and rule paths listed above. Dynamic traces can still refine the exact runtime path for a specific expression and cursor state, but the static map now has the ROM routine that owns tall-template row composition.

09 — Keyboard & link port

Deep dive: Link / Data Transfer — the silent-link packet protocol and variable send/receive.

Keyboard

The keypad is a matrix read through port 1 (port_keypad): write a group-select mask, read back the active columns. The interrupt triggers periodic scans; the result is debounced into kbdScanCode (0x843F).

  • _GetCSC (00:04B2) — “Get Cursor/Scan Code”: with interrupts masked, returns kbdScanCode and clears it (one key per call, no repeat). Raw scan codes (skXxx). [confirmed]
  • _GetKey (06:491E) — the cooked key API: blocks, handles 2nd/ALPHA modifier state, key repeat, and APD; returns a TIKeyCode (kXxx; a single byte, ~255 distinct values exposed under many named aliases). Also runs the cursor blink. Drives menus/homescreen. [confirmed entry; body large]
  • _KeyToString (01:6D10) — map a key code to its display token/string (for text entry).

Scan codes (skEnter, hardware matrix position) differ from key codes (kEnter=5, post-modifier). _GetCSC returns the former; _GetKey the latter.

Matrix scan — port 1 group masks [confirmed from disassembly]

The low-level read primitive is kbd_reset_port (ram:0480): it writes a group-select mask to port 1 (active-low — 0 bits select rows to drive), waits 4 NOPs for the lines to settle, reads the column bits back, then writes 0xFF to release. (On 84+ hardware, IN A,(0x02) bit7 + IN A,(0x20) gate the read against link/clock activity first.)

0480: PUSH AF                         ; save the group-select mask
0481: IN A,(0x2) ; AND 0x80           ; 84+ present?
0485: JR NZ,0x0497                    ; 84+: gate the read on link/clock first (IN A,(0x20))
0487: POP AF                          ; restore the group-select mask
0488: OUT (0x1),A                     ; A = group-select mask (active-low)
048a: NOP×4                           ; let the matrix settle
048e: IN A,(0x1) ; LD B,A             ; read columns (active-low: 0 = key down)
0491: LD A,0xFF ; OUT (0x1),A         ; release all groups
0495: LD A,B ; RET                    ; return raw column byte

The scanner kbd_scan_autorepeat (ram:0406) walks the matrix one group at a time. It seeds the mask in C = 0xFE (only bit0 low → group 0) and rotates it left (RLC C @ 044C) after each column read, so successive iterations drive groups 0..7:

port-1 mask (C)group / key row driven
0xFE (bit0 low)group 0 — arrow keys (skDown/skLeft/skRight/skUp = 0x010x04; the special 0xF5/0xF3/0xFA/0xFC direction codes). GRAPH lives in group 6 (skGraph=0x31).
0xFD (bit1 low)group 1
0xFB (bit2 low)group 2
0xF7 (bit3 low)group 3
0xEF (bit4 low)group 4
0xDF (bit5 low)group 5
0xBF (bit6 low)group 6
0x7F (bit7 low)group 7
0x00 (all low)“any key down?” probe used by 0x0406’s initial SUB A call; selects all groups
0xFF (all high)release all groups after a read (kbd_reset_port writes this at 0491)

Forming a raw scan code. For each driven group the routine reads the column byte, then finds the single low (pressed) column bit by an 8-iteration RLA/DJNZ loop (0435043C) that counts the cleared (pressed, active-low) column bits into E (RLAJR C skips INC E on a 1 bit, so only 0 bits are counted) and records the 1-based column position (the loop counter B, which runs 8→1) in L. If exactly one key is down it builds the scan code as group × 8 + L (L = 1…8, the pressed column’s bit index plus one), where group is the 0-based group from the table above. The code forms it at 0453: DEC A; RLA;RLA;RLA; ADD A,L: the iteration counter in A is 1-based, so DEC A converts it to the 0-based group, the three RLAs multiply by 8, and ADD A,L adds the 1-based column position in L; it then returns the byte. multiple simultaneous keys set carry (SCF @ 0459) so the press is rejected (debounce / ghost-key reject). The single resulting byte is the raw scan code that lands in kbdScanCode (0x843F), which _GetCSC (00:04B2) then reads-and-clears under DI. The scan-enable gate BIT 0,(IY+0x2C) @ 0415 lets the ISR/_GetKey path special-case the arrow group.

2nd / ALPHA modifier state machine [confirmed]

While cooking raw scan codes into the kXxx key constants, _GetKey (06:491E) runs a small modifier state machine in shiftFlags (IY+0x12 = 0x8A02, the [2nd]/[ALPHA] flag byte):

BitNameMeaning
3shift2nd[2nd] is pending — the next key is 2nd-shifted
4shiftAlphaalpha mode active — the next key is a letter
5shiftLwrAlph1 = lowercase, 0 = uppercase
6shiftALockalpha lock — alpha survives across keys
7shiftKeepAlphthe alpha shift cannot be cancelled

The low three bits of the same IY+0x12 byte are indicFlags (bit 0 indicRun, the run/busy indicator set by _RunIndicOn; see 04); shiftFlags occupies bits 3–7.

_GetKey first dispatches on the pending modifier (06:4AC3 BIT 3 → the 2nd handler at 06:4B87; 06:4ACA BIT 4 → the alpha handler at 06:4BFD), then on the key. The transitions, with the SET/RES sites:

  • [2nd] (0x36) from idleSET shift2nd (06:4AD5); loop without returning a key, lighting the 2nd cursor (IY+0x1F bit 6).
  • [2nd] again while pending → cancel (06:4B8E CP 0x36, clear the cursor, loop).
  • any other key while 2nd-pending → the 2nd handler clears the flag first (06:4B87 RES 3) then returns the 2nd-shifted code, so [2nd] is one-shot.
  • [ALPHA] (0x30) from idleSET shiftAlpha, RES shiftLwrAlph (uppercase) (06:4AE8/4AEC); loop, lighting the alpha cursor (IY+0x1F bit 7).
  • [2nd] then [ALPHA] (alpha pressed while 2nd is pending) → the 2nd handler reaches 06:4B92 CP 0x30 and sets SET shiftALock + SET shiftAlpha (06:4B96/4B9A): alpha LOCK.
  • [ALPHA] while already in alpha (06:4BFD) → in a lowercase-capable context (IY+0x24 bit 3) and still uppercase, SET shiftLwrAlph (06:4C0D) cycles upper → lower; otherwise it cancels alpha. Repeated [ALPHA] walks uppercase → lowercase → off.
  • [2nd] while in alpha (06:4C1D) → SET shift2nd (06:4C25): a 2nd press stacks on top of alpha (alpha is retained).

shift2nd clears the moment a 2nd-combo key is consumed (06:4B87), and the IM1 timer ISR also clears it at ram:01E0 (RES 3,(IY+0x12)), so a pending [2nd] does not linger. _GetKey makes alpha one-shot itself: after an alpha-mode key is consumed it calls the page-0 helper ram:04BF, which clears shiftAlpha (RES 4,(IY+0x12)) unless shiftALock (bit 6) or shiftKeepAlph (bit 7) is set (BIT 6 → RET NZ; BIT 7 → RET NZ; RES 4). So a plain [ALPHA] shifts only the next key, while [2nd]+[ALPHA] — which sets shiftALock — keeps alpha across keys until a later [ALPHA] cancels it.

stateDiagram-v2
    [*] --> Idle
    Idle --> Second: 2nd, SET b3 @4AD5
    Second --> Idle: 2nd again, cancel @4B8E
    Second --> Idle: any key, 2nd-shifted then RES b3 @4B87
    Second --> AlphaLock: ALPHA, SET b6 @4B96 + b4 @4B9A
    Idle --> Alpha: ALPHA, SET b4 + RES b5 @4AE8
    Alpha --> Idle: letter, alpha code, then RES b4 @04BF (one-shot)
    Alpha --> AlphaLower: ALPHA, SET b5 @4C0D
    AlphaLower --> Idle: ALPHA, cancel @4C13
    AlphaLock --> AlphaLock: letter, alpha code, locked
    AlphaLock --> Idle: ALPHA, cancel
    Alpha --> AlphaSecond: 2nd, SET b3 @4C25
    AlphaLock --> AlphaSecond: 2nd, SET b3 @4C25
    AlphaSecond --> Idle: key, 2nd-shifted, RES b3, RES b4 @04BF (b6 clear)
    AlphaSecond --> AlphaLock: key, 2nd-shifted, RES b3 (b6 set)

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 11), 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 (07).

The 2.5 mm I/O link has two open-collector lines (tip/ring), driven via port 0 (port_link), with an 84+ hardware link-assist / USB path via ports 0x080x0D. See USB ASIC and link assist for the ASIC-facing port map and the _LinkXferOP USB-selection gate.

_SendAByte (3C:420D) shows both paths [confirmed]:

  • Hardware-assisted (when enabled): poll status port 0x09 bit 5 (ready), then write the byte to port 0x0D; helper routines on page 3C manage the assist FIFO/timing.
  • Legacy bit-bang: to send a bit, pull one line low (write 1 to port_link for a 0-bit, 2 for a 1-bit), wait for the receiver to mirror it, release, wait for idle — with a timeout that raises E_LnkErr (0x9F, “ERR:LINK”) via _JErrorNo on failure (matching sub-link-transfer.md). Repeats per bit of the byte.

_RecAByteIO (3C:443F) is the matching receive. Higher-level link commands (_SendCmd (bcall 4F3F), variable transfer, plus screen-shot / remote-control commands) sit on top. (Note: the names _CircCmd/_VertCmd are documented in sub-graphing.md as the graphing draw commands Circle( (33:74CE) and Vertical (04:7955); the link-layer command routines referenced here are distinct and were not separately traced — treat this as a [hypothesis].)

Variable-transfer command/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. The full framing, the silent-link send/receive engine (_LinkXferOP, _SendVarCmd), checksum/ACK handling, and the 16-byte Flash-batched receive path are all reverse-engineered in sub-link-transfer.md — see §3 (framing) and §5 (variable send). [confirmed]

Link / data transfer

TI-84 Plus OS 2.55MP — feature deep dive.

Deep-dive companion to 09-keyboard-link.md, focused on what a student transferring data touches: pushing a program/list/etc. to a computer (TI-Connect) or another calculator over the 2.5 mm I/O link or the 84+ USB/hardware link-assist. Builds the full stack on top of doc 09’s byte primitives _SendAByte (3C:420D) and _RecAByteIO (3C:443F); the ASIC-facing assist/USB ports are covered separately in USB ASIC and link assist.

Addresses here are read from the raw Z80 disassembly. The decompiler mis-renders this subsystem — it passes arguments in registers and does its state work with SET/RES/BIT b,(IY+d) flag ops that the C view shows as bogus *(param+0xNN) stores — so the notes follow the disassembly, not the C view.

Page numbers are the masked flash page (rawpage & 0x3F). The whole silent-link engine lives on flash page 3C (shared with the flash/archive command code — see sub-vat-archive.md).


0. The three layers

flowchart TB
    SRC(["user 'Send…' / TI-Connect"])
    subgraph VAR["Variable layer · page 3C"]
      LX["_LinkXferOP 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 · doc 09"]
      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

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: 0x0A=backup, 0x15=var DATA, 0x0B=request/dir
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
9834pagedCountbytes buffered in the 16-byte staging block (Flash-write batching)
9836pagedGetPtrwrite cursor into pagedBuf
983ApagedBuf16-byte staging block (received data flushed to RAM/Flash 16 at a time)
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.


2. The byte layer (recap + receive internals) [confirmed]

Doc 09 covers _SendAByte. Two new things pinned here:

2a. Hardware-assist send 6BB2 [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), it jumps to 3C:6BB2:

6BB2: setup line / 2× short delay (6BD2 seeds 9CAC from port 0x20 = CPU speed)
6BBB: LD A,0xFA ; (9C86)=A           ; reload inner timeout
      IN A,(0x9) ; BIT 5,A           ; port 0x09 bit5 = TX buffer empty/ready
      JR Z,6BCA                       ; not ready → spin
      LD A,C ; OUT (0x0D),A ; RET     ; *** write the byte to port 0x0D (assist FIFO) ***
6BCA: CALL 6BE4 (decrement 9CAC) ; JR Z,6BBB (retry) ; else JP 4434 (timeout)

So the assist path is: poll port 0x09 bit 5, then OUT (0x0D),byte — exactly the “FIFO” doc 09 mentioned, with a CPU-speed-scaled timeout. The legacy bit-bang fall-through (port 0, send 1/2, wait for echo, DE-timeout → _JErrorNo) is unchanged from doc 09.

2b. Receive _RecAByteIO 443F and decoder 444A [confirmed]

443F: DI ; CALL 447E (arm/clock the line) ; CALL 444A (get-status) ; RET C/NZ ; loop if Z
444A:  CP 1                              ; result==1?  (got a real byte)
       LD A,C                            ; A = the byte
       JR NZ,4456
       CP 0xE0 ; JP NZ,_ErrLinkXmit      ; in 'must-get-byte' mode, anything else = link error
       JR 4470
4456:  CP 0xE0 ; RET NZ                  ; caller passed sentinel 0xE0 = "non-blocking probe"
       IN A,(0x2) ; AND 0x80             ; port 0x02 bit7 set = non-83+-Basic (has assist HW on 84+)
       JR Z,4469                         ;   legacy:  6CC1 polls the bit-bang lines
       IN A,(0x9) ; BIT 6,A ; JR NZ,4470 ; port 0x09 bit6 = transmission error → abort
       AND 0x19 ; JR NZ,4475             ; port 0x09 bits 0x19 = 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 sentinel 0xE0 passed in A selects non-blocking (“peek-only”) vs. blocking-with-error behaviour. _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 machineID;   /* 0x8674: machine-ID (peer/local device class) */
    uint8_t commandID;   /* 0x8675: command-ID                           */
    uint8_t lenLo;       /* 0x8676: data length, low byte                */
    uint8_t lenHi;       /* 0x8677: data length, high byte               */
} LinkPacketHeader;                                  /* 4 bytes (0x8674..0x8677) */

3a. Send a header — 41C3 [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; failure → _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).

3b. Receive a header — 4338 [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  (cmd)     ; (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).

3c. Machine-ID selector — 620A [confirmed]

The local machine-ID advertised in outgoing packets depends on the peer-type bits in IY+0x1B:

620A: L=0x82 ; BIT 2,(IY+0x1B) ; RET NZ     ; 0x82 = default / TI-84+ silent
      L=0x95 ; BIT 1,(IY+0x1B) ; RET NZ     ; 0x95 = computer / TI-Connect (USB host)
      L=0x83 ; BIT 3,(IY+0x1B) ; RET NZ
      L=0x03 ; BIT 4,(IY+0x1B) ; RET NZ     ; 0x03 = TI-83
      L=0x73 ; RET                          ; 0x73 = TI-73 / fallback

3d. Command-ID byte cheat-sheet [hypothesis]

Confirmed in the code; semantics are the standard TI link protocol:

cmdnameseen atmeaning
0x06VAR_LinkXferOP 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/EXIT_LinkXferOP 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/0xB7request_LinkXferOP 4E2B/4E2Frequest var (A2=DATA-type, B7=other)

3e. Checksum / ACK tail [confirmed]

After the data payload, the sender appends the 16-bit sum and waits for the ACK:

4167 (send tail): LD HL,(8678) ; A=L ; CALL _SendAByte ; A=H ; CALL _SendAByte   ; chk lo, 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) and 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.


4. Receive DATA payload — 4292 [confirmed]

4261/4292 is the data-payload receiver. It streams len (8676) bytes from _RecAByteIO, buffering 16 at a time into pagedBuf (983A) and flushing the block (so an incoming archived variable is written straight to Flash via 6AB1, which runs the port-0x14 flash-program stub — identical prologue to the archive writer in sub-vat-archive.md §6):

4292: BC=(8676) len ; (8678)=0
      loop: HL=(84DB) dest ; 1FD6 (clock) ; _RecAByteIO → A
            store A via pagedGetPtr (9836); INC pagedCount (9834)
            when pagedCount==0x10 → CALL 6AB1 (flush 16 bytes to RAM/Flash)
            (8678) += received_byte ; DEC BC ; loop while BC
      flush remainder (6AB1)
42EF: _RecAByteIO ×2 → received checksum ; CALL 6356 (verify len/sum, NAK 0x5A on mismatch)
42FB: send ACK (cmd 0x56)

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 headline path a student’s “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.

_LinkXferOP (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 (see §7)
  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)

5a. Resolve the variable for sending — 4763 [confirmed]

4763 reads the var-header type byte at 867F and branches by class. For graph/equation types (0x0F‥0x14) it uses a cross-page helper; otherwise 47AB: _CkOP1Real, checks size, then _ChkFindSym (0E60) to locate the VAT entry, and for an archived var it routes through the flash path (_Chk_Batt_Low, 83F7 size save). The actual data ptr/page/length come from _SetupPagedPtr inside the DATA sender.

5b. Send the DATA payload — 40DA [confirmed]

40DA: CALL _SetupPagedPtr (17AC)            ; HL=data ptr, DE=len, A=page  ←  VAT entry resolution
      (84DB)=ptr ; (8676)=len               ; iMathPtr5, packet length
      6971 ; 620A (machine-ID) ; (8674)=ID
      (special-case sndRecState==0x08 backup w/ varClass 0x0A: clamp len 0x37D, prepend 0x63 00)
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

_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 (sub-vat-archive.md §5).


6. _SendVarCmd (3C:4A14 → body 3C:4EDD) [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 ; (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 (→ SET 1,(IY+0x24) ; _ChkFindSym)  ; locate the var
      JR 4EAD (shared tail with _LinkXferOP: RES 1,(IY+0x24); 2800; JP 4F3E)

Note 4EDD physically overlaps / shares the tail (4EAD) with _LinkXferOP; they are two entry points into one routine body. _SendVarCmd is the “send by name from the running context” door; _LinkXferOP is the “OP1 already set up, do the silent transfer” door.


7. APD, cleanup, and the line idle wait [confirmed]

  • 27DA (FUN_ram_27da) installs an abort/cleanup callback (always 3C:4F3E) so that if the transfer errors out via _JError, the link state, APD timer and IY+0xC APD bit are restored. 4F3E: POP AF ; BIT 2,A ; (restore IY+0xC bit2) ; → 4F31 (RES 2,(IY+0x12); re-enable timers; EI).
  • _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).

8. Error handling [confirmed]

TriggerAddressError
send/receive line timeout, bad echo, unexpected reply cmd_JErrorNo 00:2799E_LnkErr 0x9F “ERR:LINK”
_RecAByteIO got non-byte in must-get mode; 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)_LinkXferOP 4E80/4E83E_LnkErr 0x9F

The OS collapses the link failures into the single user-visible E_LnkErr (0x9F) “ERR:LINK”. The finer-grained codes E_LinkIOChkSum 0x22, E_LinkIOTimeOut 0x23, E_LinkIOBusy 0x24, E_LinkIOVer 0x25 exist in the error table (ty_error.txt) and are used by the higher-level assembly-callable file-transfer API (e.g. OpenSendFlag/Send/Receive style), not by the raw silent-link engine documented here. [confirmed for 0x9F path; standard for the 0x22-25 mapping.]


9. End-to-end: “Send PRGM to computer” [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: _LinkXferOP/_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 (buffered 16 bytes → RAM/Flash via 6AB1) → checksum verify (6356, NAK 0x5A on error) → ACK out → VAT store (RST5).

10. Confident address 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_statusRX status decode (port 0x09 bit6 = TX error / bit4 = byte received / 0x19 err; sentinel 0xE0)
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:6AB1lnk_flush_blockflush 16-byte staging block to RAM/Flash (port 0x14)
3C:4DD2_LinkXferOPsilent-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: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 = bit-bang link (tip/ring); 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.

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.

11. Open items

  • The 0x22–0x25 fine-grained link error codes: which higher API (Send/Receive bcalls) emits them vs. the blanket 0x9F here. [standard]
  • Backup (sndRecState 0x08 / varClass 0x0A) framing detail in 40DA (the 0x37D clamp + 0x63 00 prefix) — full backup-packet layout. [standard]
  • The prior USB target gap is now mapped in sub-usb-asic.md: _LinkXferOP calls ram:2E0B, a cross_page_jump thunk to 35:4280, after sampling port 0x4D.

USB ASIC and link assist

TI-84 Plus OS 2.55MP — feature deep dive.

This page covers the OS-visible USB/link-assist hardware interface: the Z80 I/O ports the ROM uses, the byte FIFO path used by the link layer, and the places where _LinkXferOP chooses USB before falling back to the 2.5 mm link. It complements Link / data transfer, which covers the TI link 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 bridge. 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 tip/ring open-collector byte path. [confirmed: 3C:6C99, 3C:6CF3]
Link-assist FIFO0x080x0DHardware byte send/receive assist used below _SendAByte and _RecAByteIO. [confirmed: 3C:6BB16D53]
USB line / interrupt gates0x4D, 0x55, 0x56Line-state and event/status gates used before and during link handling. [confirmed: 3C:4E4A, 00:006F]
USB controller / endpoints0x4A0x5B, 0x800xA2Page-35 USB host/device stack, including setup, endpoint FIFOs, callbacks, and data transfer. [confirmed: 35:40315B9B]

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 unless marked]

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
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. _LinkXferOP 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
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
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 project-local tools/ports.txt now names the confirmed assist and USB interrupt ports so future Ghidra rebuilds show the same surface in the database. These labels describe the observed OS use, not a complete vendor register map.

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.

lnk_rec_status also uses the sentinel byte 0xE0: callers pass it for a nonblocking/probe style receive check. If the caller requires a byte and the status path reports anything else, the code raises E_LnkErr through _JError(0x9F). [confirmed]

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. [ROM-confirmed writes; WikiTI field names; TilEm storage-only]

USB selection in _LinkXferOP [confirmed]

_LinkXferOP (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 _LinkXferOP 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, _LinkXferOP 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 _LinkXferOP 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) treats the USB interrupt status as its first source gate:

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.

[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), 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: 00:01B101DB]

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. [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 / _LinkXferOP3C: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.)

How to use it in code [grounded by OS calls]

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/_LinkXferOP (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 sub-link-transfer.md.

Practical rules:

  • Set up IY+0x1B consistently before calling _LinkXferOP. 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 are mapped to bodies and sampled above. The boot-page 0x8xxx USB names (_InitUSB, _KillUSB, _AttemptUSBOSReceive, _ReceiveOS_USB, _USBErrorCleanup) remain part of the repository-wide 0x8xxx bcall-table reconciliation problem, not a page-3C link-transfer gap.
  • The ROM does not give bit names for every page-35 controller register, and TilEm does not model physical timing from the assist setup values. This page therefore treats the 0x97/0xB4 field names as WikiTI-supported timing configuration, while ROM-confirmed claims remain limited to the written constants, status/data port use, comparisons, branch bits, RAM state, and FIFO direction.

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)
_AbsO1O2Cp410E00:1987
_AbsO1PAbsO2405A00:225B
_ACos40DE02:76DF
_ACosH40F002:7964
_ACosRad40D202:76C9
_AdrLEle462D02:47C5
_AdrMEle460902:4002
_AdrMRow460602:4000
_AllEq487604:6218
_AllocFPS43A500:1534
_AllocFPS143A800:1537
_Angle410202:6A38
_AnsName4B5238:74B7
_ApdSetup4C9300:03AE
_app_5de753263D:5DE7
_AppGetCalc4C783B:54EC
_AppGetCbl4C753B:54C3
_AppInit404B00:0936
_arc_593651AC07:5936
_arc_59f14A6807:59F1
_Arc_Unarc4FD807:6248
_ASin40E402:76F1
_ASinH40ED02:7956
_ASinRad40DB02:76DA
_ATan40E102:76E9
_ATan240E702:7749
_ATan2Rad40D802:76D4
_ATanH40EA02:7909
_ATanRad40D502:76CF
_BinOPExec466302:53DD
_Bit_VertSplit4FA800:215D
_BufClr507404:6074
_BufCpy507104:60A6
_CAbs4E9702:6C47
_CAdd4E8802:6BA5
_CanAlphIns4C6900:04C6
_CDiv4E9402:6BF3
_CDivByReal4EBB02:6DAC
_CEtoX4EA902:6D1D
_CFrac4EC102:6DCF
_CheckSplitFlag49F000:2060
_Chk_Batt_Low50B300:0D07
_ChkFindSym42F100:0E60
_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
_ClearRect4D5C3B:6935
_ClearRow4CED01:6934
_CLine479833:6028
_CLineS479B33:6034
_CLN4EA002:6CCA
_CLog4EA302:6CE7
_CLogPrep50FE02:6F1B
_CloseEditBuf48D305:5675
_CloseEditBufNoR476E03:4743
_CloseEditEqu496C06:4771
_CloseProg4A3507:4FB4
_ClrGraphRef4A3807:4FD8
_ClrLCD454301:60F5
_ClrLCDFull454001:60E4
_ClrLp41D100:1BC4
_ClrOP1S425E00:1E68
_ClrOP2S425B00:1E63
_ClrScrn454901:6167
_ClrScrnFull454601:6162
_ClrTxtShd454C01:616F
_CMltByReal4EB802:6D94
_CmpSyms4A4A07:519E
_CMult4E8E02:6BB7
_Conj4EB502:6D8F
_ConvDim4B4338:741F
_ConvDim004B4638:7422
_ConvLcToLr4A2307:4CFF
_ConvLrToLc4A5607:5368
_ConvOP14AEF38:7433
_COP1Set0410500:195F
_Cos40C002:7346
_CosH40CC02:762E
_CpHLDE400C00:21BB
_CplxOPArrange464802:494F
_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
_CrystalTimerA4B4934:5A99
_CrystalTimerB4B4C34:5A9D
_CrystalTimerC4B4F34:5AA1
_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
_DispPagedStr51CA01:7C4D
_DivHLBy10400F00:0269
_DivHLByA401200:026B
_DrawCirc24C663B:7171
_DrawCmd48C104:7B8B
_DrawRectBorder4D7D3B:68F5
_DrawRectBorderClear4D8C3B:692A
_DrawZeroOP1487304:620B
_drw_5df153F504:5DF1
_drw_5df4482504:5DF4
_drw_638e487C04:638E
_dsp_62404C4201:6240
_dsp_65ea4D6B01:65EA
_DToR407500:236B
_EditProg4A3207:4F6B
_edt_5d6f546403:5D6F
_edt_69f8546103:69F8
_edt_6bd1545803:6BD1
_EnableApd4C873B:7AAD
_EnoughMem42FD00:0FA6
_EOP1NotReal427900:1F06
_Equ_or_NewEqu42C400:20FD
_EraseEOL455201:61C5
_EraseRectBorder4D863B:68F1
_ErrArgument44AD00:2711
_ErrBadGuess44CB00:2751
_ErrBreak44BF00:273D
_ErrD_OP1_042D300:212D
_ErrD_OP1_LE_042D000:212A
_ErrD_OP1Not_R42CA00:2120
_ErrD_OP1NotPos42C700:2119
_ErrD_OP1NotPosInt42CD00:2125
_ErrDataType44AA00:2708
_ErrDimension44B300:2719
_ErrDimMismatch44B000:2715
_ErrDivBy0449800:26EC
_ErrDomain449E00:26F4
_ErrIncrement44A100:26F8
_ErrInvalid44BC00:2729
_ErrIterations44C800:274D
_ErrLinkXmit44D400:278D
_ErrMemory44B900:2721
_ErrNon_Real44A400:26FC
_ErrNonReal4A8C38:42E1
_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
_FillRect4D623B:6939
_FillRectPattern4D893B:6814
_Find_Parse_Formula4AF238:758A
_FindAlphaDn4A4707:50B8
_FindAlphaUp4A4407:50B5
_FindApp4C4E3D:5EE3
_FindAppDn4C4B3D:5DE6
_FindAppNumPages509B3D:4AA3
_FindAppUp4C483D:5DDA
_FindSym42F400:0E65
_FiveExec467E02:69BC
_FixTempCnt4A3B07:4FEC
_FlashToRam50173D:6745
_ForceFullScreen508F39:66D2
_FormBase50AA06:57C0
_FormDCplx499606:59D3
_FormEReal499006:5799
_FormReal499906:5ACF
_FourExec467B02:6889
_FPAdd407200:229E
_FPDiv409900:2541
_FPMult408400:238B
_FPRecip409600:253D
_FPSPushReal4A8307:6365
_FPSquare408100:238A
_FPSub406F00:2297
_fpx_4a7b518502:4A7B
_fpx_5d70466902:5D70
_fpx_5dbb466C02:5DBB
_fpx_7069510102:7069
_fpx_7d9d533B02:7D9D
_fpx_7dfe533802:7DFE
_Frac409300:24E3
_Get_Tok_Strng459401:66EA
_GetBaseVer4C6F00:0284
_GetCSC401800:04B2
_GetKey497206:491E
_GetLToOP1463602:47EA
_GetMToOP1461502:4044
_GetPosListElem466602:5BBB
_GetSysInfo50DD07:7345
_GetTokLen459101:66E5
_GraphParseTok510A33:5023
_GraphTblFind47CB33:7097
_GraphTblNext47C833:707A
_GrBufClr4BD004:6071
_GrBufCpy486A04:60A3
_grc_454b526337:454B
_grc_4556526637:4556
_grc_4575526937:4575
_grc_461152FF37:4611
_grc_51c2517F37:51C2
_grc_522351A037:5223
_grc_5d4451D637:5D44
_grc_5f42520037:5F42
_grc_60cb51FA37:60CB
_grf_435f514033:435F
_grf_5e06547633:5E06
_grf_706647C533:7066
_GrphCirc47D733:758D
_HLTimes940F900:1930
_homeup455801:6216
_HorizCmd48A604:793E
_HTimesL427600:1EF6
_IBounds4C6004:42EC
_IBoundsFull4D9804:4306
_ILine47E004:4029
_IncLstSize4A2907:4EF4
_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
_JError44D700:2793
_JErrorNo400000:2799
_JForceCmd402A00:0747
_JForceCmdNoChar402700:0746
_JForceGraphKey500501:6BFD
_JForceGraphNoKey500201:6BFB
_KeyToString45CA01:6D10
_LCD_DRIVERON497806:4D02
_LcdBlitRegion4D2607:5431
_LdHLind400900:0033
_LineCmd48AC04:796A
_LinkXferOP50FB3C:4DD2
_ListIdxTimes953D135:79E9
_lnk_62b051823C:62B0
_LnX40AB02:6EFD
_Load_SFont478303:4A8F
_LoadCIndPaged501D00:029F
_LoadDEIndPaged501A3C:6B36
_LoadPattern4CB101:6267
_LogX40AE02:6F16
_Max405700:224D
_mde_7da949DB36:7DA9
_MemChk42E500:0E20
_MemClear4C303B:7138
_MemSet4C333B:7139
_Min405400:2244
_Minus1406C00:2294
_mnu_6ddb546739:6DDB
_Mov10B415C00:1A90
_Mov18B47DA00:192B
_Mov7B416800:1A96
_Mov8B416500:1A94
_Mov9B415F00:1A92
_Mov9OP1OP2417D00:1B06
_Mov9OP2Cp410B00:1982
_Mov9ToOP1417A00:1B01
_Mov9ToOP2418000:1B07
_MovFrOP1418300:1B0C
_NewLine452E01:5F4A
_OneVar4BA33A:6420
_OP1ExOP2421F00:1DD2
_OP1ExOP3421900:1DB7
_OP1ExOP4421C00:1DBC
_OP1ExOP5420D00:1DA0
_OP1ExOP6421000:1DA5
_OP1ExpToDec425200:1E77
_OP1IntPartNeg489A04:74E8
_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
_ParseInp4A9B38:5987
_ParseInpLastEnt4B0738:5984
_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
_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
_PushZeroOP1465102:49C0
_PutAway403900:08AF
_PutC450401:5B4C
_PutMap450101:5A98
_PutPS451001:5C73
_PutPSB450D01:5C52
_PutS450A01:5C39
_PutTokString496006:46FD
_PutToL464502:4829
_PutToMat461E02:406C
_RandInit4B7F36:7E8A
_Random4B7936:7DC9
_Rcl_StatVar42DC00:2149
_RclAns4AD738:679F
_RclCListElem464B02:49A7
_RclCListElemB464E02:49B5
_RclGDB247D133:72D9
_RclListElemB463C02:47FE
_RclListElemToOP1463902:47FB
_RclN4ADD38:67A9
_RclSysTok4AE638:683E
_RclVarPush4B9A3A:5D07
_RclVarSym4AE338:67B1
_RclX4AE038:67AE
_RclY4ADA38:67A4
_Rec1stByte4EFA3C:439C
_Rec1stByteNC4EFD3C:43A3
_RecAByteIO4F033C:443F
_RedimMat4A2607:4D3B
_Regraph488E04:6764
_ReleaseBuffer477103:47AC
_ReloadAppEntryVecs4C363B:73E4
_RestoreDisp487004:6176
_RName427F00:1F4C
_RndGuard409F02:6A57
_RnFx40A202:6A71
_Round40A800:2623
_RToD407800:2374
_RToP40F602:50DB
_RunIndicOff457001:6531
_RunIndicOn456D01:6518
_SaveDisp4C7B39:5DD8
_scr_405651F105:4056
_scr_461951E505:4619
_ScreenSplit522705:7712
_SendAByte4EE53C:420D
_SendVarCmd4A143C:4EDD
_SetAllPlots4FCC38:49C7
_SetExSpeed50BF00:0DCA
_SetFuncM484036:7D11
_SetNorm_Vals49FC00:220F
_SetParM484936:7D39
_SetPolM484636:7D2C
_SetSeqM484336:7D1F
_SetTblGraphDraw4C0000:00F5
_SetupPagedPtr502000:17AC
_SetXXOP1478C33:5F7E
_SetXXOP2478F33:5F83
_SetXXXXOP2479233:5F9E
_SFont_Len478603:4ABD
_Sin40BD02:7342
_SinCosRad40BA02:733E
_SinH40CF02:7632
_SinHCosH40C602:7626
_SqRoot409C02:6E38
_SrchVLstDn4F1207:71D7
_SrchVLstUp4F0F07:707F
_SStringLength4CB43B:61A6
_sta_5d3c520335:5D3C
_sta_5eef4B9D3A:5EEF
_sta_760f4BA93A:760F
_StMatEl4AE938:6C8F
_StoAns4ABF38:6251
_StoGDB247CE33:71AC
_StoN4ACB38:6274
_StoOther4AD438:62A9
_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
_Times2406600:2282
_TimesPt5407E00:2382
_TName428E00:1F69
_ToFrac465702:4BBE
_Trunc406000:2279
_UCLineS479533:6010
_UnLineCmd48AF04:797C
_UnOPExec467202:5E14
_VertCmd48A904:7955
_VertSplitDraw48DC05:5D88
_VPutMap455E01:6293
_VPutS456101:646D
_VPutSN456401:644D
_VtoWHLDE47FB04:4410
_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

Retail boot (0x8xxx) bcalls

These 0x8xxx IDs are defined in the full 2007 ti83plus.inc and resolved from the retail boot table. tools/resolve_bcalls.py emits this table only when page 3F has the retail boot prefix, not when it sees BootFree.

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
_LoadAIndPaged80513F:486E
_FlashToRam280543F:4888
_GetCertificateStart80573F:4D46
_GetFieldSize805A3F:4DB8
_FindSubField805D3F:4DFB
_EraseCertificateSector80603F:4E3F
_CheckHeaderKey80633F:4B4A
_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
_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 07.)

This file is generated by tools/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 / 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 & 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 & 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+-era extended tokens (date/time, clock, GarbageCollect…)

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+

99 — Open questions & roadmap

The structural reverse-engineering is comprehensive (every subsystem mapped, both cross-page mechanisms resolved, full input→parse→eval→display pipeline documented). What remains is depth. Each item below is self-contained for a future session.

Still open

  1. Flash sector write/erase primitive map — the live DB confirms anchors flash_program_buf@3D:678C, flash_erase_wait@3D:5ED3, flash_cmd_base@3D:738B, flash_set_sector_cnt@3D:727D, flash_page_select@3D:726E, flash_find_nonff@3D:7DEA, and flash_op_fd/fb/fe@3D:7C8F/7C93/7C97. The public bcall entry points for these primitives are named in ti83plus.inc: _WriteAByte (8021), _WriteAByteSafe (80C6), and _FlashToRam2 (8054); the retail boot table maps them to 3F:4C9F, 3F:4C9A, and 3F:4888. The candidate addresses 3D:61AF, 3D:64AA, 3D:62C2, 3D:6413, and 3D:6B9B are still undisassembled (not defined functions) in the live DB; their flash_* names are project-local inferred labels, not WikiTI or ti83plus.inc equates. See sub-vat-archive.md / 12.
  2. Flash archive garbage collector — the behavior is documented. The GC-path candidates flash_gc_relocate@3C:7BD0, gc_show_screen@3C:7E0D, and flash_cmd_dispatch@3C:7121 are still undisassembled (not defined functions) in the live DB; those flash_*/gc_* names are project-local inferred labels, not WikiTI or ti83plus.inc equates.
  3. Enum equates. Apply TIKeyCode/TIError/TIVarType to scalar operands in the relevant handlers (conservative, scoped).
  4. Smaller residuals (in each doc’s local TODO): absolute APD timeout/blink period (page-0x35 crystal-timer handler is unanalyzed data), the For/While/Repeat FPS loop-frame byte layout (page-0x33 dispatch confirmed), the Asm(/AsmPrgm compile/setup body before the traced payload handoff (ram:9D95 op=0xC9 is confirmed), direct ASM-initiated BASIC program execution beyond VAT lookup and cooperative Ans callback (_ChkFindSym works from AsmPrgm; the ASMFORM.8xp/ZZFORM.8xp _Find_Parse_Formula fixture fails with ERR:UNDEFINED; the ASMPARSE.8xp/ZZPARSE.8xp _ParseInpLastEnt fixture fails with ERR:INVALID; _ExecuteNewPrgm reaches ERR:SYNTAX; _JForceCmd abandons the caller stack; _PutTokString/_rclToQueue are edit-buffer paths, not proven program-call entries), and the group-archive member walk (_Arc_Unarc’s CP 0x17 reject routes elsewhere; body fragmented by cross-page calls).

How to continue

Reopen ti84.gpr (the GhidraMCP plugin reconnects for interactive work), or extend the headless pipeline in tools/ and rebuild with tools/build.sh. The remaining items mostly need a headless raw-byte dump of regions the live decompiler leaves as unanalyzed data (the page-0x35 timer handler, the page-0x38 0xBB/class-3 dispatch tables).